fastlifeweb 0.6.0__py3-none-any.whl → 0.7.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 (34) hide show
  1. fastlife/configurator/__init__.py +1 -0
  2. fastlife/configurator/configurator.py +51 -3
  3. fastlife/configurator/registry.py +14 -13
  4. fastlife/configurator/route_handler.py +30 -0
  5. fastlife/configurator/settings.py +52 -0
  6. fastlife/routing/__init__.py +0 -0
  7. fastlife/routing/router.py +13 -0
  8. fastlife/security/csrf.py +16 -1
  9. fastlife/security/policy.py +5 -1
  10. fastlife/templates/Button.jinja +6 -3
  11. fastlife/templates/pydantic_form/Boolean.jinja +7 -2
  12. fastlife/templates/pydantic_form/Checklist.jinja +2 -0
  13. fastlife/templates/pydantic_form/Dropdown.jinja +1 -0
  14. fastlife/templates/pydantic_form/Model.jinja +1 -0
  15. fastlife/templates/pydantic_form/Sequence.jinja +1 -0
  16. fastlife/templates/pydantic_form/Union.jinja +4 -4
  17. fastlife/templating/__init__.py +4 -0
  18. fastlife/templating/binding.py +7 -0
  19. fastlife/templating/renderer/abstract.py +60 -13
  20. fastlife/templating/renderer/jinjax.py +13 -16
  21. fastlife/templating/renderer/widgets/boolean.py +7 -1
  22. fastlife/templating/renderer/widgets/checklist.py +9 -2
  23. fastlife/templating/renderer/widgets/dropdown.py +7 -1
  24. fastlife/templating/renderer/widgets/factory.py +13 -1
  25. fastlife/templating/renderer/widgets/model.py +7 -1
  26. fastlife/templating/renderer/widgets/sequence.py +7 -1
  27. fastlife/templating/renderer/widgets/union.py +7 -1
  28. fastlife/testing/__init__.py +1 -0
  29. fastlife/testing/testclient.py +12 -1
  30. fastlife/views/pydantic_form.py +3 -0
  31. {fastlifeweb-0.6.0.dist-info → fastlifeweb-0.7.0.dist-info}/METADATA +4 -4
  32. {fastlifeweb-0.6.0.dist-info → fastlifeweb-0.7.0.dist-info}/RECORD +34 -31
  33. {fastlifeweb-0.6.0.dist-info → fastlifeweb-0.7.0.dist-info}/LICENSE +0 -0
  34. {fastlifeweb-0.6.0.dist-info → fastlifeweb-0.7.0.dist-info}/WHEEL +0 -0
@@ -1,3 +1,4 @@
1
+ """Configure fastlife app for dependency injection."""
1
2
  from .configurator import Configurator, configure
2
3
 
3
4
  __all__ = ["Configurator", "configure"]
@@ -2,6 +2,7 @@
2
2
  The configurator is here to register routes in a fastapi app,
3
3
  with dependency injection.
4
4
  """
5
+
5
6
  import importlib
6
7
  import inspect
7
8
  import logging
@@ -26,6 +27,7 @@ from fastapi.params import Depends as DependsType
26
27
  from fastapi.staticfiles import StaticFiles
27
28
 
28
29
  from fastlife.configurator.base import AbstractMiddleware
30
+ from fastlife.configurator.route_handler import FastlifeRoute
29
31
  from fastlife.security.csrf import check_csrf
30
32
 
31
33
  from .settings import Settings
@@ -38,6 +40,14 @@ VENUSIAN_CATEGORY = "fastlife"
38
40
 
39
41
 
40
42
  class Configurator:
43
+ """
44
+ Configure and build an application.
45
+
46
+ Initialize the app from the settings.
47
+
48
+ :param settings: Application settings.
49
+ """
50
+
41
51
  registry: "AppRegistry"
42
52
 
43
53
  def __init__(self, settings: Settings) -> None:
@@ -49,14 +59,37 @@ class Configurator:
49
59
  docs_url=None,
50
60
  redoc_url=None,
51
61
  )
62
+ FastlifeRoute.registry = self.registry
63
+ self._app.router.route_class = FastlifeRoute
52
64
  self.scanner = venusian.Scanner(fastlife=self)
53
65
  self.include("fastlife.views")
54
66
  self.include("fastlife.session")
55
67
 
56
68
  def get_app(self) -> FastAPI:
69
+ """
70
+ Get the app after configuration in order to start after beeing configured.
71
+
72
+ :return: FastAPI application
73
+ """
57
74
  return self._app
58
75
 
59
76
  def include(self, module: str | ModuleType) -> "Configurator":
77
+ """
78
+ Include a module in order to load its views.
79
+
80
+ Here is an example.
81
+
82
+ ::
83
+
84
+ from fastlife import Configurator, configure
85
+
86
+ @configure
87
+ def includeme(config: Configurator) -> None:
88
+ config.include(".views")
89
+
90
+
91
+ :param module: a module to include.
92
+ """
60
93
  if isinstance(module, str):
61
94
  package = None
62
95
  if module.startswith("."):
@@ -70,7 +103,10 @@ class Configurator:
70
103
  def add_middleware(
71
104
  self, middleware_class: Type[AbstractMiddleware], **options: Any
72
105
  ) -> Self:
73
- self._app.add_middleware(middleware_class, **options)
106
+ """
107
+ Add a starlette middleware to the FastAPI app.
108
+ """
109
+ self._app.add_middleware(middleware_class, **options) # type: ignore
74
110
  return self
75
111
 
76
112
  def add_route(
@@ -105,6 +141,7 @@ class Configurator:
105
141
  # generate_unique_id
106
142
  # ),
107
143
  ) -> "Configurator":
144
+ """Add a route to the app."""
108
145
  dependencies: List[DependsType] = []
109
146
  if permission:
110
147
  dependencies.append(Depends(self.registry.check_permission(permission)))
@@ -140,13 +177,22 @@ class Configurator:
140
177
  def add_static_route(
141
178
  self, route_path: str, directory: Path, name: str = "static"
142
179
  ) -> "Configurator":
143
- """Mount a directory to an http endpoint."""
180
+ """
181
+ Mount a directory to an http endpoint.
182
+
183
+ :param route_path: the root path for the statics
184
+ :param directory: the directory on the filesystem where the statics files are.
185
+ :param name: a name for the route in the starlette app.
186
+ :return: the configurator
187
+
188
+ """
144
189
  self._app.mount(route_path, StaticFiles(directory=directory), name=name)
145
190
  return self
146
191
 
147
192
  def add_exception_handler(
148
193
  self, status_code_or_exc: int | Type[Exception], handler: Any
149
194
  ) -> "Configurator":
195
+ """Add an exception handler the application."""
150
196
  self._app.add_exception_handler(status_code_or_exc, handler)
151
197
  return self
152
198
 
@@ -154,7 +200,9 @@ class Configurator:
154
200
  def configure(
155
201
  wrapped: Callable[[Configurator], None]
156
202
  ) -> Callable[[Configurator], None]:
157
- """Decorator used to attach route in a submodule while using the confirator.scan"""
203
+ """
204
+ Decorator used to attach route in a submodule while using the configurator.include.
205
+ """
158
206
 
159
207
  def callback(
160
208
  scanner: venusian.Scanner, name: str, ob: Callable[[venusian.Scanner], None]
@@ -2,6 +2,7 @@ from typing import TYPE_CHECKING, Annotated
2
2
 
3
3
  from fastapi import Depends
4
4
 
5
+ from fastlife.configurator.route_handler import FastlifeRequest
5
6
  from fastlife.security.policy import CheckPermission
6
7
  from fastlife.shared_utils.resolver import resolve
7
8
 
@@ -14,6 +15,11 @@ from .settings import Settings
14
15
 
15
16
 
16
17
  class AppRegistry:
18
+ """
19
+ The application registry got fastlife dependency injection.
20
+ It is initialized by the configurator and accessed by the `fastlife.Registry`.
21
+ """
22
+
17
23
  settings: Settings
18
24
  renderer: "AbstractTemplateRendererFactory"
19
25
  check_permission: CheckPermission
@@ -27,22 +33,17 @@ class AppRegistry:
27
33
  self.check_permission = resolve(settings.check_permission)
28
34
 
29
35
 
30
- DEFAULT_REGISTRY: AppRegistry = None # type: ignore
31
-
32
-
33
36
  def initialize_registry(settings: Settings) -> AppRegistry:
34
- global DEFAULT_REGISTRY
35
- if DEFAULT_REGISTRY is not None:
36
- raise ValueError("Registry is already set")
37
+ # global DEFAULT_REGISTRY
38
+ # if DEFAULT_REGISTRY is not None: # type: ignore
39
+ # raise ValueError("Registry is already set")
37
40
  AppRegistryCls = resolve(settings.registry_class)
38
- DEFAULT_REGISTRY = AppRegistryCls(settings) # type: ignore
39
- return DEFAULT_REGISTRY
41
+ return AppRegistryCls(settings) # type: ignore
40
42
 
41
43
 
42
- def cleanup_registry() -> None:
43
- """ "Method to cleanup the registry, used for testing"""
44
- global DEFAULT_REGISTRY
45
- DEFAULT_REGISTRY = None # type: ignore
44
+ def get_registry(request: FastlifeRequest) -> AppRegistry:
45
+ return request.registry
46
46
 
47
47
 
48
- Registry = Annotated[AppRegistry, Depends(lambda: DEFAULT_REGISTRY)]
48
+ Registry = Annotated[AppRegistry, Depends(get_registry)]
49
+ """FastAPI dependency to access to the registry."""
@@ -0,0 +1,30 @@
1
+ from typing import TYPE_CHECKING, Any, Callable, Coroutine
2
+
3
+ from fastapi import Request as BaseRequest
4
+ from fastapi.routing import APIRoute
5
+ from starlette.responses import Response
6
+ from starlette.types import Receive, Scope
7
+
8
+ if TYPE_CHECKING:
9
+ from .registry import AppRegistry # coverage: ignore
10
+
11
+
12
+ class FastlifeRequest(BaseRequest):
13
+ def __init__(self, registry: "AppRegistry", scope: Scope, receive: Receive) -> None:
14
+ super().__init__(scope, receive)
15
+ self.registry = registry
16
+
17
+
18
+ class FastlifeRoute(APIRoute):
19
+ registry: "AppRegistry" = None # type: ignore
20
+
21
+ def get_route_handler( # type: ignore
22
+ self,
23
+ ) -> Callable[[FastlifeRequest], Coroutine[Any, Any, Response]]:
24
+ orig_route_handler = super().get_route_handler()
25
+
26
+ async def route_handler(request: BaseRequest) -> FastlifeRequest:
27
+ req = FastlifeRequest(self.registry, request.scope, request.receive)
28
+ return await orig_route_handler(req) # type: ignore
29
+
30
+ return route_handler # type: ignore
@@ -1,3 +1,4 @@
1
+ """Settings for the fastlife."""
1
2
  from datetime import timedelta
2
3
  from typing import Literal
3
4
 
@@ -6,31 +7,82 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
6
7
 
7
8
 
8
9
  class Settings(BaseSettings):
10
+ """
11
+ Settings based on pydantic settings to configure the app.
12
+
13
+ It can be overriden in order to inject application own settings.
14
+ """
15
+
9
16
  model_config = SettingsConfigDict(env_prefix="fastlife_")
17
+ """Set the prefix fastlife_ for configuration using operating system environment."""
10
18
 
11
19
  fastlife_route_prefix: str = Field(default="/_fl")
20
+ """Route prefix used for fastlife internal views."""
12
21
  template_search_path: str = Field(default="fastlife:templates")
22
+ """
23
+ list of directories where templates could be found by the template engine.
24
+
25
+ the list is a comma separated string. The directory resolution is made from
26
+ a python module name. for instance `fastlife:templates` is the direcotry templates
27
+ found in the fastlife package.
28
+ """
13
29
  registry_class: str = Field(default="fastlife.configurator.registry:AppRegistry")
30
+ """Implementation class for the application regitry."""
14
31
  template_renderer_class: str = Field(
15
32
  default="fastlife.templating.renderer:JinjaxTemplateRenderer"
16
33
  )
34
+ """
35
+ Implementation class for the :class:`fastlife.templatingAbstractTemplateRenderer`.
36
+ """
17
37
  form_data_model_prefix: str = Field(default="payload")
38
+ """
39
+ Pydantic form default model prefix for serialized field in www-urlencoded-form.
40
+ """
18
41
  csrf_token_name: str = Field(default="csrf_token")
42
+ """Name of the html input field for csrf token."""
19
43
 
20
44
  jinjax_use_cache: bool = Field(default=True)
45
+ """
46
+ JinjaX (the default template engine) setting use_cache.
47
+
48
+ Could be disabled while developping, leave value true for production.
49
+ """
21
50
  jinjax_auto_reload: bool = Field(default=False)
51
+ """
52
+ JinjaX (the default template engine) setting auto_reload.
53
+
54
+ Set to true while developing, set false for production.
55
+ """
22
56
 
23
57
  session_secret_key: str = Field(default="")
58
+ """
59
+ A secret key, that could not changes for life.
60
+
61
+ Used to securize the session with itsdangerous.
62
+ """
24
63
  session_cookie_name: str = Field(default="flsess")
64
+ """Cookie name for the session cookie."""
25
65
  session_cookie_domain: str = Field(default="")
66
+ """Cookie domain for the session cookie."""
26
67
  session_cookie_path: str = Field(default="/")
68
+ """Cookie path for the session cookie."""
27
69
  session_duration: timedelta = Field(default=timedelta(days=14))
70
+ """Cookie duration for the session cookie."""
28
71
  session_cookie_same_site: Literal["lax", "strict", "none"] = Field(default="lax")
72
+ """Cookie same-site for the session cookie."""
29
73
  session_cookie_secure: bool = Field(default=False)
74
+ """
75
+ Cookie secure for the session cookie,
76
+
77
+ should be true while using https on production.
78
+ """
30
79
  session_serializer: str = Field(
31
80
  default="fastlife.session.serializer:SignedSessionSerializer"
32
81
  )
82
+ """Cookie serializer for the session cookie."""
33
83
 
34
84
  domain_name: str = Field(default="", title="domain name where the app is served")
85
+ """Domain name whre the app is served."""
35
86
 
36
87
  check_permission: str = Field(default="fastlife.security.policy:check_permission")
88
+ """Handler for checking permission set on any views using the configurator."""
File without changes
@@ -0,0 +1,13 @@
1
+ from typing import Any
2
+
3
+ from fastapi import APIRouter
4
+
5
+ from fastlife.configurator.route_handler import FastlifeRoute
6
+
7
+
8
+ class FastLifeRouter(APIRouter):
9
+ """The router used split your app in many routes."""
10
+
11
+ def __init__(self, **kwargs: Any) -> None:
12
+ kwargs["route_class"] = FastlifeRoute
13
+ super().__init__(**kwargs)
fastlife/security/csrf.py CHANGED
@@ -1,3 +1,8 @@
1
+ """
2
+ Prevents CSRF attack using cookie and html hidden field comparaison.
3
+
4
+ Fast life did not reinvent the wheel on CSRF Protection. It use the good old method.
5
+ """
1
6
  import secrets
2
7
  from typing import TYPE_CHECKING, Any, Callable, Coroutine
3
8
 
@@ -8,14 +13,24 @@ if TYPE_CHECKING:
8
13
 
9
14
 
10
15
  class CSRFAttack(Exception):
11
- ...
16
+ """
17
+ An exception raised if the cookie and the csrf token hidden field did not match.
18
+ """
12
19
 
13
20
 
14
21
  def create_csrf_token() -> str:
22
+ """A helper that create a csrf token."""
15
23
  return secrets.token_urlsafe(5)
16
24
 
17
25
 
18
26
  def check_csrf(registry: "Registry") -> Callable[[Request], Coroutine[Any, Any, bool]]:
27
+ """
28
+ A global application dependency, that is always active.
29
+
30
+ If you don't want csrf token, its simple don't use the
31
+ application/x-www-form-urlencoded on a POST method.
32
+ """
33
+
19
34
  async def check_csrf(request: Request) -> bool:
20
35
  if (
21
36
  request.method != "POST"
@@ -1,3 +1,4 @@
1
+ """Security policy."""
1
2
  from typing import Any, Callable, Coroutine
2
3
 
3
4
  CheckPermissionHook = Callable[..., Coroutine[Any, Any, None]] | Callable[..., None]
@@ -6,10 +7,13 @@ CheckPermission = Callable[[str], CheckPermissionHook]
6
7
 
7
8
  def check_permission(permission_name: str) -> CheckPermissionHook:
8
9
  """
9
- A closure that check that a user as the given username.
10
+ A closure that check that a user as the given permission_name.
10
11
 
11
12
  This method has to be overriden using the setting check_permission
12
13
  to implement it.
14
+
15
+ :param permission_name: a permission name set in a view to check access.
16
+ :return: a function that raise http exceptions or any configured exception here.
13
17
  """
14
18
 
15
19
  def depencency_injection() -> None:
@@ -12,6 +12,8 @@ hx_get="",
12
12
  hx_select="",
13
13
  hx_after_request="",
14
14
  hx_vals="",
15
+ hx_confirm="",
16
+ hx_delete="",
15
17
  full_width=false,
16
18
  class_="""
17
19
  bg-primary-600
@@ -33,9 +35,10 @@ dark:hover:bg-primary-700
33
35
  #}
34
36
  <button type="{{type}}" {%if id %}id="{{id}}" {%endif%} name="{{name}}" value="{{value}}" {% if
35
37
  hx_target%}hx-target="{{hx_target}}" {% endif %} {% if hx_swap %}hx-swap="{{hx_swap}}" {% endif %} {% if hx_select
36
- %}hx-select="{{hx_select}}" {% endif %} {% if hx_get %}hx-get="{{hx_get}}" {% endif %} {% if onclick %}onclick="{{onclick}}" {%
37
- endif %}{% if hx_after_request %}hx-on::after-request="{{hx_after_request}}" {% endif %} {% if hx_vals
38
- %}hx-vals='{{hx_vals|safe}}' {% endif %} {% if aria_label %}aria-label="{{aria_label}}" {% endif %}
38
+ %}hx-select="{{hx_select}}" {% endif %} {% if hx_get %}hx-get="{{hx_get}}" {% endif %} {% if onclick
39
+ %}onclick="{{onclick}}" {% endif %}{% if hx_after_request %}hx-on::after-request="{{hx_after_request}}" {% endif %} {%
40
+ if hx_vals %}hx-vals='{{hx_vals|safe}}' {% endif %} {% if hx_confirm %}hx-confirm="{{hx_confirm}}" {% endif %} {% if
41
+ hx_delete %}hx-delete="{{hx_delete}}" {% endif %} {% if aria_label %}aria-label="{{aria_label}}" {% endif %}
39
42
  class="{% if full_width %}w-full {% endif %}{{attrs.class or class_}}" {% if hidden %}hidden{% endif %}>
40
43
  {{- content -}}
41
44
  </button>
@@ -1,7 +1,12 @@
1
1
  {# def widget #}
2
2
  <pydantic_form.Widget widget={widget} removable={widget.removable}>
3
3
  <div class="pt-4">
4
- <Label for={widget.id}>{{widget.title}}</Label>
5
- <Checkbox name={widget.name} type="checkbox" id={widget.id} checked={widget.value} />
4
+ <div class="flex items-center">
5
+ <Checkbox name={widget.name} type="checkbox" id={widget.id} checked={widget.value} value="1" />
6
+ <Label for={widget.id} class="ms-2 text-base text-neutral-900 dark:text-white">
7
+ {{widget.title|safe}}
8
+ </Label>
9
+ </div>
10
+ <pydantic_form.Error text={widget.error} />
6
11
  </div>
7
12
  </pydantic_form.Widget>
@@ -7,12 +7,14 @@ widget,
7
7
  <details open>
8
8
  <summary class="justify-between items-center font-medium cursor-pointer">
9
9
  <H3>{{widget.title}}</H3>
10
+ <pydantic_form.Error text={widget.error} />
10
11
  </summary>
11
12
  <div>
12
13
  {% for value in widget.value %}
13
14
  <div class="flex items-center mb-4">
14
15
  <Checkbox name={value.field_name} type="checkbox" id={value.id} value={value.value} checked={value.checked} />
15
16
  <Label for={value.id} class="ms-2 text-base text-neutral-900 dark:text-white">{{value.label}}</Label>
17
+ <pydantic_form.Error text={value.error} />
16
18
  </div>
17
19
  {% endfor %}
18
20
  </div>
@@ -10,6 +10,7 @@
10
10
  </Option>
11
11
  {%- endfor -%}
12
12
  </Select>
13
+ <pydantic_form.Error text={widget.error} />
13
14
  <pydantic_form.Hint text={widget.hint} />
14
15
  </div>
15
16
  </pydantic_form.Widget>
@@ -5,6 +5,7 @@
5
5
  <details open>
6
6
  <summary class="justify-between items-center font-medium cursor-pointer">
7
7
  <H3>{{widget.title}}</H3>
8
+ <pydantic_form.Error text={widget.error} />
8
9
  </summary>
9
10
  <div>
10
11
  {% for child in children_widget %}
@@ -4,6 +4,7 @@
4
4
  <details id="{{widget.id}}" open>
5
5
  <summary class="justify-between items-center font-medium cursor-pointer">
6
6
  <H3>{{widget.title}}</H3>
7
+ <pydantic_form.Error text={widget.error} />
7
8
  </summary>
8
9
  <div>
9
10
  {% set fnGetName = "get" + widget.id.replace("-", "_") %}
@@ -5,20 +5,20 @@
5
5
  <details open>
6
6
  <summary class="justify-between items-center font-medium cursor-pointer">
7
7
  <H3>{{widget.title}}</H3>
8
+ <pydantic_form.Error text={widget.error} />
8
9
  </summary>
9
10
  <div hx-sync="this" id="{{widget.id}}-child">
10
11
  {% if child %}
11
12
  {{ child }}
12
13
  {% else %}
13
14
  {% for typ in types %}
14
- <Button type="button" hx-target="closest div" hx-get={typ.url} hx-vals={typ.params|tojson}
15
- id={typ.id}
15
+ <Button type="button" hx-target="closest div" hx-get={typ.url} hx-vals={typ.params|tojson} id={typ.id}
16
16
  onclick={"document.getElementById('" + widget.id +"-remove-btn').hidden=false"}>{{typ.title}}</Button>
17
17
  {% endfor %}
18
18
  {% endif %}
19
19
  </div>
20
- <Button type="button" id={widget.id + "-remove-btn" } hx-target={"#" + widget.id} hx-vals={parent_type.params|tojson}
21
- hx-get={parent_type.url} hidden={not child}>
20
+ <Button type="button" id={widget.id + "-remove-btn" } hx-target={"#" + widget.id}
21
+ hx-vals={parent_type.params|tojson} hx-get={parent_type.url} hidden={not child}>
22
22
  Remove
23
23
  </Button>
24
24
  </details>
@@ -1,3 +1,7 @@
1
+ """
2
+ Utilities for rendering HTML templates for page and components.
3
+ """
4
+
1
5
  from .binding import Template, template
2
6
  from .renderer import AbstractTemplateRendererFactory, JinjaxTemplateRenderer
3
7
 
@@ -38,4 +38,11 @@ def get_template(template: str, *, content_type: str = "text/html") -> TemplateE
38
38
 
39
39
 
40
40
  def template(template_path: str) -> Template:
41
+ """
42
+ Return a FastAPI dependency template engine ready to render the template.
43
+
44
+
45
+ :param template_path: path to template to render by the engine setup in the regitry.
46
+ :return: A callable accepting kwargs to pass as the context, returning a string.
47
+ """
41
48
  return Depends(get_template(template_path))
@@ -9,6 +9,11 @@ from fastlife.request.model_result import ModelResult
9
9
 
10
10
 
11
11
  class AbstractTemplateRenderer(abc.ABC):
12
+ """
13
+ An object that will be initialized by an AbstractTemplateRendererFactory,
14
+ passing the request to process.
15
+ """
16
+
12
17
  route_prefix: str
13
18
  """Used to buid pydantic form"""
14
19
 
@@ -27,18 +32,43 @@ class AbstractTemplateRenderer(abc.ABC):
27
32
  renderer and sent to every rendering made by the request.
28
33
  This is used by the pydantic form method that will render other templates
29
34
  for the request.
35
+ In traditional frameworks, only one template is rendered containing the whole
36
+ pages. But, while rendering a pydantic form, every field is rendered in its
37
+ distinct template. The template renderer keep the globals and git it back
38
+ to every templates. This can be used to fillout options in a select without
39
+ performing an ajax request for example.
40
+
41
+ :param template: name of the template to render
42
+ :param globals: some variable that will be passed to all rendered templates.
43
+ :param params: paramaters that are limited to the main rendered templates.
44
+ :return: The template rendering result.
30
45
  """
31
46
 
32
47
  @abc.abstractmethod
33
48
  def pydantic_form(
34
- self,
35
- model: ModelResult[Any],
36
- *,
37
- name: str | None = None,
38
- token: str | None = None,
39
- removable: bool = False,
40
- field: FieldInfo | None = None,
49
+ self, model: ModelResult[Any], *, token: Optional[str] = None
41
50
  ) -> Markup:
51
+ """
52
+ Render an http form from a given model.
53
+
54
+ Because template post may be give back to users with errors,
55
+ the model is wrap in an object containing initial model, or validated model
56
+ and a set of errors.
57
+
58
+ this function is used inside the template directly. And it will not render the
59
+ <form> tag so the action/httpx post is not handled byu the method..
60
+ Somethinging like this
61
+
62
+ ::
63
+
64
+ <form action="" method="post">
65
+ {{ pydantic_form(model) }}
66
+ </form>
67
+
68
+
69
+ :param model: model to render
70
+ :param token: a random string that can be passed for testing purpose.
71
+ """
42
72
  ...
43
73
 
44
74
  @abc.abstractmethod
@@ -46,15 +76,32 @@ class AbstractTemplateRenderer(abc.ABC):
46
76
  self,
47
77
  model: Type[Any],
48
78
  *,
49
- name: Optional[str] = None,
50
- token: Optional[str] = None,
51
- removable: bool = False,
52
- field: FieldInfo | None = None,
79
+ name: str | None,
80
+ token: str | None,
81
+ removable: bool,
82
+ field: FieldInfo | None,
53
83
  ) -> Markup:
54
- ...
84
+ """
85
+ Render a field of a model inside a pydantic_form.
86
+
87
+ Models that contains field of type Union, for instance may have
88
+ many types of children and the form have a user interaction to choose
89
+ which sub type to select. When the user choose the type, the sub model is
90
+ rendered using this helper under the hood.
91
+ """
55
92
 
56
93
 
57
94
  class AbstractTemplateRendererFactory(abc.ABC):
95
+ """
96
+ The template render factory.
97
+ """
98
+
58
99
  @abc.abstractmethod
59
100
  def __call__(self, request: Request) -> AbstractTemplateRenderer:
60
- ...
101
+ """
102
+ While processing an HTTP Request, a renderer object is created giving
103
+ isolated context per request.
104
+
105
+ :param Request: the HTTP Request to process.
106
+ :return: The renderer object that will process that request.
107
+ """
@@ -69,28 +69,18 @@ class JinjaxRenderer(AbstractTemplateRenderer):
69
69
  )
70
70
 
71
71
  def pydantic_form(
72
- self,
73
- model: ModelResult[Any],
74
- *,
75
- name: Optional[str] = None,
76
- token: Optional[str] = None,
77
- removable: bool = False,
78
- field: FieldInfo | None = None,
72
+ self, model: ModelResult[Any], *, token: Optional[str] = None
79
73
  ) -> Markup:
80
- return WidgetFactory(self, token).get_markup(
81
- model,
82
- removable=removable,
83
- field=field,
84
- )
74
+ return WidgetFactory(self, token).get_markup(model)
85
75
 
86
76
  def pydantic_form_field(
87
77
  self,
88
78
  model: Type[Any],
89
79
  *,
90
- name: Optional[str] = None,
91
- token: Optional[str] = None,
92
- removable: bool = False,
93
- field: FieldInfo | None = None,
80
+ name: str | None,
81
+ token: str | None,
82
+ removable: bool,
83
+ field: FieldInfo | None,
94
84
  ) -> Markup:
95
85
  return (
96
86
  WidgetFactory(self, token)
@@ -107,6 +97,12 @@ class JinjaxRenderer(AbstractTemplateRenderer):
107
97
 
108
98
 
109
99
  class JinjaxTemplateRenderer(AbstractTemplateRendererFactory):
100
+ """
101
+ The default template renderer factory. Based on JinjaX.
102
+
103
+ :param settings: setting used to configure jinjax.
104
+ """
105
+
110
106
  route_prefix: str
111
107
  """Used to prefix url to fetch fast life widgets."""
112
108
 
@@ -123,6 +119,7 @@ class JinjaxTemplateRenderer(AbstractTemplateRendererFactory):
123
119
  self.catalog.add_folder(path)
124
120
 
125
121
  def __call__(self, request: Request) -> AbstractTemplateRenderer:
122
+ """Build the renderer to render request for template."""
126
123
  return JinjaxRenderer(
127
124
  self.catalog,
128
125
  request,
@@ -8,11 +8,17 @@ class BooleanWidget(Widget[bool]):
8
8
  *,
9
9
  title: str | None,
10
10
  value: bool = False,
11
+ error: str | None = None,
11
12
  removable: bool = False,
12
13
  token: str,
13
14
  ) -> None:
14
15
  super().__init__(
15
- name, title=title, value=value, removable=removable, token=token
16
+ name,
17
+ title=title,
18
+ value=value,
19
+ error=error,
20
+ removable=removable,
21
+ token=token,
16
22
  )
17
23
 
18
24
  def get_template(self) -> str:
@@ -1,6 +1,6 @@
1
1
  from typing import Sequence
2
2
 
3
- from pydantic import BaseModel
3
+ from pydantic import BaseModel, Field
4
4
 
5
5
  from .base import Widget
6
6
 
@@ -11,6 +11,7 @@ class Checkable(BaseModel):
11
11
  value: str
12
12
  token: str
13
13
  checked: bool
14
+ error: str | None = Field(default=None)
14
15
 
15
16
  @property
16
17
  def id(self) -> str:
@@ -29,11 +30,17 @@ class ChecklistWidget(Widget[Sequence[Checkable]]):
29
30
  *,
30
31
  title: str | None,
31
32
  value: Sequence[Checkable],
33
+ error: str | None = None,
32
34
  token: str,
33
35
  removable: bool,
34
36
  ) -> None:
35
37
  super().__init__(
36
- name, value=value, token=token, title=title, removable=removable
38
+ name,
39
+ value=value,
40
+ error=error,
41
+ token=token,
42
+ title=title,
43
+ removable=removable,
37
44
  )
38
45
 
39
46
  def get_template(self) -> str:
@@ -10,13 +10,19 @@ class DropDownWidget(Widget[str]):
10
10
  *,
11
11
  title: Optional[str],
12
12
  value: Optional[str] = None,
13
+ error: str | None = None,
13
14
  options: Sequence[Tuple[str, str]] | Sequence[str],
14
15
  removable: bool = False,
15
16
  token: Optional[str] = None,
16
17
  hint: Optional[str] = None,
17
18
  ) -> None:
18
19
  super().__init__(
19
- name, value=value, title=title, token=token, removable=removable
20
+ name,
21
+ value=value,
22
+ error=error,
23
+ title=title,
24
+ token=token,
25
+ removable=removable,
20
26
  )
21
27
  self.options: list[dict[str, str]] = []
22
28
  for opt in options:
@@ -35,7 +35,7 @@ class WidgetFactory:
35
35
  self,
36
36
  model: ModelResult[Any],
37
37
  *,
38
- removable: bool,
38
+ removable: bool = False,
39
39
  field: FieldInfo | None = None,
40
40
  ) -> Markup:
41
41
  return self.get_widget(
@@ -88,6 +88,7 @@ class WidgetFactory:
88
88
  title=field.title if field else "",
89
89
  aria_label=field.description if field else None,
90
90
  token=self.token,
91
+ error=form_errors.get(name),
91
92
  ),
92
93
  )
93
94
 
@@ -175,6 +176,7 @@ class WidgetFactory:
175
176
  removable=removable,
176
177
  title=get_title(typ),
177
178
  token=self.token,
179
+ error=form_errors.get(field_name),
178
180
  )
179
181
 
180
182
  def build_union(
@@ -233,6 +235,7 @@ class WidgetFactory:
233
235
  title=field.title if field else "",
234
236
  token=self.token,
235
237
  removable=removable,
238
+ error=form_errors.get(field_name),
236
239
  )
237
240
 
238
241
  return widget
@@ -267,6 +270,7 @@ class WidgetFactory:
267
270
  item_type=typ, # type: ignore
268
271
  token=self.token,
269
272
  removable=removable,
273
+ error=form_errors.get(field_name),
270
274
  )
271
275
 
272
276
  def build_set(
@@ -291,6 +295,7 @@ class WidgetFactory:
291
295
  checked=c in value if value else False, # type: ignore
292
296
  name=field_name,
293
297
  token=self.token,
298
+ error=form_errors.get(f"{field_name}-{c}"),
294
299
  )
295
300
  for c in litchoice
296
301
  ]
@@ -305,6 +310,7 @@ class WidgetFactory:
305
310
  checked=e.name in value if value else False, # type: ignore
306
311
  name=field_name,
307
312
  token=self.token,
313
+ error=form_errors.get(f"{field_name}-{e.name}"),
308
314
  )
309
315
  for e in choice_wrapper
310
316
  ]
@@ -317,6 +323,7 @@ class WidgetFactory:
317
323
  token=self.token,
318
324
  value=choices,
319
325
  removable=removable,
326
+ error=form_errors.get(field_name),
320
327
  )
321
328
 
322
329
  def build_boolean(
@@ -334,6 +341,7 @@ class WidgetFactory:
334
341
  title=field.title if field else "",
335
342
  token=self.token,
336
343
  value=value,
344
+ error=form_errors.get(field_name),
337
345
  )
338
346
 
339
347
  def build_emailtype(
@@ -354,6 +362,7 @@ class WidgetFactory:
354
362
  title=field.title if field else "",
355
363
  token=self.token,
356
364
  value=str(value),
365
+ error=form_errors.get(field_name),
357
366
  )
358
367
 
359
368
  def build_secretstr(
@@ -374,6 +383,7 @@ class WidgetFactory:
374
383
  title=field.title if field else "",
375
384
  token=self.token,
376
385
  value=value.get_secret_value() if isinstance(value, SecretStr) else value,
386
+ error=form_errors.get(field_name),
377
387
  )
378
388
 
379
389
  def build_literal(
@@ -399,6 +409,7 @@ class WidgetFactory:
399
409
  title=field.title if field else "",
400
410
  token=self.token,
401
411
  value=str(value),
412
+ error=form_errors.get(field_name),
402
413
  )
403
414
 
404
415
  def build_enum(
@@ -418,6 +429,7 @@ class WidgetFactory:
418
429
  title=field.title if field else "",
419
430
  token=self.token,
420
431
  value=str(value),
432
+ error=form_errors.get(field_name),
421
433
  )
422
434
 
423
435
  def build_simpletype(
@@ -13,12 +13,18 @@ class ModelWidget(Widget[Sequence[Widget[Any]]]):
13
13
  name: str,
14
14
  *,
15
15
  value: Sequence[Widget[Any]],
16
+ error: str | None = None,
16
17
  removable: bool,
17
18
  title: str,
18
19
  token: str,
19
20
  ):
20
21
  super().__init__(
21
- name, title=title, value=value, removable=removable, token=token
22
+ name,
23
+ title=title,
24
+ value=value,
25
+ error=error,
26
+ removable=removable,
27
+ token=token,
22
28
  )
23
29
 
24
30
  def get_template(self) -> str:
@@ -15,12 +15,18 @@ class SequenceWidget(Widget[Sequence[Widget[Any]]]):
15
15
  title: Optional[str],
16
16
  hint: Optional[str],
17
17
  value: Optional[Sequence[Widget[Any]]],
18
+ error: str | None = None,
18
19
  item_type: Type[Any],
19
20
  token: str,
20
21
  removable: bool,
21
22
  ):
22
23
  super().__init__(
23
- name, value=value, title=title, token=token, removable=removable
24
+ name,
25
+ value=value,
26
+ error=error,
27
+ title=title,
28
+ token=token,
29
+ removable=removable,
24
30
  )
25
31
  self.item_type = item_type
26
32
  self.hint = hint
@@ -15,12 +15,18 @@ class UnionWidget(Widget[Widget[Any]]):
15
15
  *,
16
16
  title: Optional[str],
17
17
  value: Optional[Widget[Any]],
18
+ error: str | None = None,
18
19
  children_types: Sequence[Type[BaseModel]],
19
20
  token: str,
20
21
  removable: bool,
21
22
  ):
22
23
  super().__init__(
23
- name, value=value, title=title, token=token, removable=removable
24
+ name,
25
+ value=value,
26
+ error=error,
27
+ title=title,
28
+ token=token,
29
+ removable=removable,
24
30
  )
25
31
  self.children_types = children_types
26
32
  self.parent_name = name
@@ -1,3 +1,4 @@
1
+ """Testing fastlife client."""
1
2
  from .testclient import WebTestClient
2
3
 
3
4
  __all__ = ["WebTestClient"]
@@ -342,6 +342,9 @@ class WebForm:
342
342
  """Test if a field exists in the form."""
343
343
  return key in self._formdata
344
344
 
345
+ def __repr__(self) -> str:
346
+ return repr(self._formdata)
347
+
345
348
 
346
349
  class WebResponse:
347
350
  """Represent an http response made by the WebTestClient browser."""
@@ -511,7 +514,7 @@ class WebTestClient:
511
514
 
512
515
  def request(
513
516
  self,
514
- method: Literal["GET", "POST"], # I am a browser
517
+ method: Literal["GET", "POST", "DELETE"],
515
518
  url: str,
516
519
  *,
517
520
  content: str | None = None,
@@ -562,6 +565,14 @@ class WebTestClient:
562
565
  max_redirects=int(follow_redirects) * 10,
563
566
  )
564
567
 
568
+ def delete(self, url: str, follow_redirects: bool = True) -> WebResponse:
569
+ """Perform http DELETE request."""
570
+ return self.request(
571
+ "DELETE",
572
+ url,
573
+ max_redirects=int(follow_redirects) * 10,
574
+ )
575
+
565
576
  def post(
566
577
  self,
567
578
  url: str,
@@ -17,6 +17,9 @@ async def show_widget(
17
17
  token: Optional[str] = Query(None),
18
18
  removable: bool = Query(False),
19
19
  ) -> Response:
20
+ """
21
+ This views is used by pydantic_form to generate a nested field asynchronously.
22
+ """
20
23
  model_cls = resolve_extended(typ)
21
24
  field = None
22
25
  if title:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fastlifeweb
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: High-level web framework
5
5
  Home-page: https://github.com/mardiros/fastlife
6
6
  License: BSD-derived
@@ -19,12 +19,12 @@ Classifier: Topic :: Internet :: WWW/HTTP
19
19
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
20
  Requires-Dist: beautifulsoup4[testing] (>=4.12.2,<5.0.0)
21
21
  Requires-Dist: behave (>=1.2.6,<2.0.0)
22
- Requires-Dist: fastapi (>=0.110.0,<0.111.0)
22
+ Requires-Dist: fastapi (>=0.112.0,<0.113.0)
23
23
  Requires-Dist: itsdangerous (>=2.1.2,<3.0.0)
24
- Requires-Dist: jinjax (>=0.34,<0.35)
24
+ Requires-Dist: jinjax (>=0.37,<0.38)
25
25
  Requires-Dist: markupsafe (>=2.1.3,<3.0.0)
26
26
  Requires-Dist: multidict (>=6.0.5,<7.0.0)
27
- Requires-Dist: pydantic (==2.5.1)
27
+ Requires-Dist: pydantic (>=2.5.3,<3.0.0)
28
28
  Requires-Dist: pydantic-settings (>=2.0.3,<3.0.0)
29
29
  Requires-Dist: python-multipart (>=0.0.6,<0.0.7)
30
30
  Requires-Dist: venusian (>=3.0.0,<4.0.0)
@@ -1,16 +1,19 @@
1
1
  fastlife/__init__.py,sha256=nAeweHnTvWHgDZYuPIWawZvT4juuhvMp4hzF2B-yCJU,317
2
- fastlife/configurator/__init__.py,sha256=2EPjM1o5iHJIViPwgJjaPQS3pMhE-9dik_mm53eX2DY,91
2
+ fastlife/configurator/__init__.py,sha256=vMV25HEfuXjCL7DhZukhlB2JSfxwzX2lGbErcZ5b7N0,146
3
3
  fastlife/configurator/base.py,sha256=2ahvTudLmD99YQjnIeGN5JDPCSl3k-mauu7bsSEB5RE,216
4
- fastlife/configurator/configurator.py,sha256=6BaB7SR24Q4Qvs8NrCpatRkkZiPXf9mKLID6RxOKxDg,5740
5
- fastlife/configurator/registry.py,sha256=1sOicKvwIvLbrzRk9z8yb65YUXxxagJK9AK-2gGDpOQ,1463
6
- fastlife/configurator/settings.py,sha256=ftv5MkNXeyBrvcqxnt2WKtLuzo7ge2_BNx1gX4CcSOE,1489
4
+ fastlife/configurator/configurator.py,sha256=yy7etOLqgm7KfybJMz0fFYwDSsbv81daV3W9wBugswA,7003
5
+ fastlife/configurator/registry.py,sha256=FKXCxWlMMGNN1-l5H6QLTCoqP_tWENN84RjkConejWI,1580
6
+ fastlife/configurator/route_handler.py,sha256=kFi-fPzt2t3NFZLtj-rY0219iSCpPzj4daeQ65wUcYU,1037
7
+ fastlife/configurator/settings.py,sha256=uyIZFLqzbwDAUkl8LWNYdlPifMR3ZItyC4y9ci_3LrM,3376
7
8
  fastlife/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
9
  fastlife/request/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
10
  fastlife/request/form_data.py,sha256=mP8ilwRUY2WbktIkRgaJJ2EUjwUMPbSPg29GzwZgT18,3713
10
11
  fastlife/request/model_result.py,sha256=TRaVkyIE50IzVprncoWUUZd15-y4D3ywyZdx7eh6nFE,3237
12
+ fastlife/routing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ fastlife/routing/router.py,sha256=hHK4NumgTTXI330ZxQeZBiEfGtBRn4_k_fIh_xaaVtg,338
11
14
  fastlife/security/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- fastlife/security/csrf.py,sha256=47epJVJtr5X6j-Hog54WCGOoiRLQQHvgBU71iqR1N0A,1025
13
- fastlife/security/policy.py,sha256=5jV5nypy5O8XPFFRJ_bzG8Ltk5xcPWclkz23qiG1_I8,509
15
+ fastlife/security/csrf.py,sha256=yDBF8X5jna-EZm7PPNvoaS_RJTYfTapwbghm506O9wM,1519
16
+ fastlife/security/policy.py,sha256=7gnwooTz7iCDZ26hzulUK0vUz9Ncd7y1Lq_6Lr7CJ98,702
14
17
  fastlife/session/__init__.py,sha256=OnzRCYRzc1lw9JB0UdKi-aRLPNT2n8mM8kwY1P4w7uU,838
15
18
  fastlife/session/middleware.py,sha256=JgXdBlxlm9zIEgXcidbBrMAp5wJVPsZWtvCLVDk5h2s,3049
16
19
  fastlife/session/serializer.py,sha256=qpVnHQjYTxw3aOnoEOKIjOFJg2z45KjiX5sipWk2gws,1458
@@ -18,7 +21,7 @@ fastlife/shared_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
18
21
  fastlife/shared_utils/infer.py,sha256=0jNPY5vqKvDlNCmVPnRAXbTcQnmbuOIOIGAeGcxDPok,472
19
22
  fastlife/shared_utils/resolver.py,sha256=wXQQTB4jf86m4qENhMOkHkWpLJj_T4-_eND_ItTLnTE,1410
20
23
  fastlife/templates/A.jinja,sha256=q71nu4Rq874LG6SykCKv8W-EZeX13NMF0AsLc9FFAP0,677
21
- fastlife/templates/Button.jinja,sha256=535UIK4Prunj4f0YZXBmCI0rfOiTr5GJTQkM0XDmtNA,1137
24
+ fastlife/templates/Button.jinja,sha256=sHJtgOTJmL2Ttpy-SNKdq3BcoTe6NI0MSKG2N44Z-iI,1283
22
25
  fastlife/templates/Checkbox.jinja,sha256=XdgtI7GTVjPkxTje9KMILzHqjMFRmTpubtKM8nL7K1o,447
23
26
  fastlife/templates/CsrfToken.jinja,sha256=X2-H0VmukyUV448blh9M8rhr_gN82ar51TnRAwfV3OY,59
24
27
  fastlife/templates/Form.jinja,sha256=h8Y2yrlrTRuLnwqCTDv8eeFgaej2eQiXDvFOx5eEcGg,367
@@ -36,38 +39,38 @@ fastlife/templates/P.jinja,sha256=xEcHIv9dJRpELu_SdqQcftvKrU8z1i_BHTEVO5Mu5dU,16
36
39
  fastlife/templates/Radio.jinja,sha256=51I5n1yjWQ_uLZfzuUUf3C8ngo-KT4dPw29-pjP9uSU,723
37
40
  fastlife/templates/Select.jinja,sha256=GoK2oh4Jl5dAfL2wN718RCwtHaQIslzDCN_nwy6a9oY,480
38
41
  fastlife/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
- fastlife/templates/pydantic_form/Boolean.jinja,sha256=tzEZpgv3t9vEuQr2_NzLvhqyusGm2w7kEkehCRJyG2c,281
40
- fastlife/templates/pydantic_form/Checklist.jinja,sha256=DbLYICI5maEc6hPmexBDut6DZluR8D3gQNHC32Cc7DM,670
41
- fastlife/templates/pydantic_form/Dropdown.jinja,sha256=JBkQp7LYtz6vvW6MyAXyW-6UDZ_xY0Fgd4mpDCQA-wM,500
42
+ fastlife/templates/pydantic_form/Boolean.jinja,sha256=gf8j1Wyy8C95tYsjDjxKL_ivSJPsUHBoRaK-4AiBiBA,467
43
+ fastlife/templates/pydantic_form/Checklist.jinja,sha256=s55FqHJGNryTT3h6VWFx6mEgR8lIVBoqo7NGYUTM_rE,775
44
+ fastlife/templates/pydantic_form/Dropdown.jinja,sha256=_dBSf7-Ba1Yr4BPXu08yJBSZjUW4Jo8t-s3R6TgN-oc,548
42
45
  fastlife/templates/pydantic_form/Error.jinja,sha256=Wb5NnVRc4U7ZGKmYV7s4eGenWEug8WK9li48iTlX4cQ,121
43
46
  fastlife/templates/pydantic_form/Hidden.jinja,sha256=n6CbTSwZr2E_oY8TO2WPbnrLHBaWfe_CXVCYOYmCfts,83
44
47
  fastlife/templates/pydantic_form/Hint.jinja,sha256=O0ZsAQnATcG0a_qLQfrwM6VZHmAw3k1W33WYlEBUas8,123
45
- fastlife/templates/pydantic_form/Model.jinja,sha256=wxEKEO8Kktxkrz-LZ9m_v_NPjKV-C8RFQ9wyEyrRKsI,449
46
- fastlife/templates/pydantic_form/Sequence.jinja,sha256=eA8GYhJC1KokWbU3zqmqCIuG8mR-yhvlEAzt0ds2RTM,1380
48
+ fastlife/templates/pydantic_form/Model.jinja,sha256=1t3eOxYjT_Kl85En_w27gmRuZ3xrET1x87UQfDw0Os4,501
49
+ fastlife/templates/pydantic_form/Sequence.jinja,sha256=RU19X6FwNhoFhQ0fBpTrfRsS_gMPkhpDhzyhFShwoHQ,1430
47
50
  fastlife/templates/pydantic_form/Text.jinja,sha256=DCZNAly_YjYQcGdz-EiCbX5DThHHTDq66KySpiJe3Ik,450
48
- fastlife/templates/pydantic_form/Union.jinja,sha256=Ss4dwNoHqmdAa4O7cvS2vwuxBPspQINBxq4hnkm_GVY,950
51
+ fastlife/templates/pydantic_form/Union.jinja,sha256=aH9Cj9IycCkMkL8j_yb5Ux5ysVZLVET9_AKTndQQX-Q,992
49
52
  fastlife/templates/pydantic_form/Widget.jinja,sha256=8raoMjtO4ARBfbz8EG-HKT112KkrWG82BbUfbXpAmZs,287
50
- fastlife/templating/__init__.py,sha256=QdrTmxt-K7UcJ_o9LY1n-QZAvk5foct5MNFt02zXr2w,234
51
- fastlife/templating/binding.py,sha256=n-MDg98Bne49BmWWWCopZsI6ef7h6PIdcszM_pRutYA,1378
53
+ fastlife/templating/__init__.py,sha256=UY_hSTlJKDZnkSIK-BzRD4_AXOWgHbRuvJsKAS9ljgE,307
54
+ fastlife/templating/binding.py,sha256=noY9QrArJGqB1X80Ny-0zk1Dg6T9mMXahjEcIiHvioo,1648
52
55
  fastlife/templating/renderer/__init__.py,sha256=UJUX3T9VYjPQUhS5Enz3P6OtwftimKoGGpoQEpewVFA,181
53
- fastlife/templating/renderer/abstract.py,sha256=Ljzll05kI9KIMRmBeZ9fIVs4PcIi6FE1DB_YmMiBGVY,1552
54
- fastlife/templating/renderer/jinjax.py,sha256=pCaBLqboFUuIKZcSuksNxJngzw8_zFLqLgBYuIEMytE,3977
56
+ fastlife/templating/renderer/abstract.py,sha256=9eQgshUMiVTaMcICr-RtP3Zqpu7Bji_sLif2InrxMek,3519
57
+ fastlife/templating/renderer/jinjax.py,sha256=Ue6YV1XQFT59qJBJsKwvBu9OZbVbEZWH8hvTskSvw9M,3925
55
58
  fastlife/templating/renderer/widgets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
56
59
  fastlife/templating/renderer/widgets/base.py,sha256=XtD-NRacHMn9Xt_dSfWb1Emk3XEXz5jExglx23Rzzpw,2808
57
- fastlife/templating/renderer/widgets/boolean.py,sha256=UPQOxIpydP2NzDVUQuNau1DewS2FkRqx5WkWSwEu1kk,436
58
- fastlife/templating/renderer/widgets/checklist.py,sha256=gBDeZXi4JdD9jeVDlC-5GOrM5C9uYaQ-wQuWuCB6HYY,850
59
- fastlife/templating/renderer/widgets/dropdown.py,sha256=o3BfD9gDiRPsCPB93xCZSYRo50LRAb_tZOmK4x7zkzY,901
60
- fastlife/templating/renderer/widgets/factory.py,sha256=9s0DZ2FFdjy8DvaCMK3u9LW_5SCKcrMAxHhHqLWeO2E,14162
60
+ fastlife/templating/renderer/widgets/boolean.py,sha256=1Cnrs6Sqf7CJURrCRkDL2NDQZWO9dvac350w0PcZtDg,544
61
+ fastlife/templating/renderer/widgets/checklist.py,sha256=Qit-k4RnW9Y3xOyE9BevBRuFZ8XN5jH3x_sI5fWt43Y,1009
62
+ fastlife/templating/renderer/widgets/dropdown.py,sha256=FyPZzrTprLff6YRqZ031J8-KZpBgidTw0BsKY1Qt7Ts,1009
63
+ fastlife/templating/renderer/widgets/factory.py,sha256=iOBuxWlY1d9WfCqcm5f5Ck5NwoK9uDkioCSPyu5HsQo,14787
61
64
  fastlife/templating/renderer/widgets/hidden.py,sha256=2fsbTQKsACV0JVYpCjXaQAV7VnQTIBPCi4lJPdWCRHc,308
62
- fastlife/templating/renderer/widgets/model.py,sha256=L7tdmRj0cetM_UusaD2zk5YJtIGBYizCMBKQzFyN_Ko,986
63
- fastlife/templating/renderer/widgets/sequence.py,sha256=x6o1L_hOh3WRlW16ZQaeO6H_qtnIitALuK-w7ck0RAw,1347
65
+ fastlife/templating/renderer/widgets/model.py,sha256=BNM6dPPN9jzM26LreeOw7wiCsZun1uSMMFXMNcO2hII,1094
66
+ fastlife/templating/renderer/widgets/sequence.py,sha256=rYXsUZokV3wnKa-26BmgAu7sVCtFf8FdBmhvrnqR-gM,1455
64
67
  fastlife/templating/renderer/widgets/text.py,sha256=OWFFA0muZCznrlUrBRRUKVj60TdWtsdgw0bURdOA3lE,879
65
- fastlife/templating/renderer/widgets/union.py,sha256=GntHssMjcgpD6S5h0JVr9_aWy3pSbMhYZ9QU-ugBH-Y,1689
66
- fastlife/testing/__init__.py,sha256=KgTlRI0g8z7HRpL7mD5QgI__LT9Y4QDSzKMlxJG3wNk,67
67
- fastlife/testing/testclient.py,sha256=o7Yc-_At0S23lmV8-vbGGPm-s0xRTAV7OhBri2rqqrU,20220
68
+ fastlife/templating/renderer/widgets/union.py,sha256=xNDctq0SRXfRyMHXL8FgRKyUOUreR1xENnt6onkZJ9I,1797
69
+ fastlife/testing/__init__.py,sha256=vuqwoNUd3BuIp3fm7nkvmYkIGjIimf5zUGhDkeWrg2s,98
70
+ fastlife/testing/testclient.py,sha256=wJtWXxn5cew8PlxweYvw7occnoD5Ktr_7IxEXi-Ieg0,20534
68
71
  fastlife/views/__init__.py,sha256=nn4B_8YTbTmhGPvSd20yyKK_9Dh1Pfh_Iq7z6iK8-CE,154
69
- fastlife/views/pydantic_form.py,sha256=uUSw9Wrpx2XAiep26L6fViXv9p6KYi-DFQTYymrbBMk,1158
70
- fastlifeweb-0.6.0.dist-info/LICENSE,sha256=F75xSseSKMwqzFj8rswYU6NWS3VoWOc_gY3fJYf9_LI,1504
71
- fastlifeweb-0.6.0.dist-info/METADATA,sha256=uLb0u6q1O_ZiO0hgv9BMUV0OnNRObYQAaEVAe5ZlpS4,1826
72
- fastlifeweb-0.6.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
73
- fastlifeweb-0.6.0.dist-info/RECORD,,
72
+ fastlife/views/pydantic_form.py,sha256=KJtH_DK8em0czGPsv0XpzGUFhtycyXdeRldwiU7d_j4,1257
73
+ fastlifeweb-0.7.0.dist-info/LICENSE,sha256=F75xSseSKMwqzFj8rswYU6NWS3VoWOc_gY3fJYf9_LI,1504
74
+ fastlifeweb-0.7.0.dist-info/METADATA,sha256=Of-0L69qHEgTqacKnSKduuEYiqho2RkH7OHjHUDgVKo,1833
75
+ fastlifeweb-0.7.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
76
+ fastlifeweb-0.7.0.dist-info/RECORD,,