fastlifeweb 0.6.1__py3-none-any.whl → 0.7.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.
@@ -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
@@ -21,14 +22,16 @@ from typing import (
21
22
  )
22
23
 
23
24
  import venusian # type: ignore
24
- from fastapi import Depends, FastAPI, Response
25
+ from fastapi import Depends, FastAPI, Request, Response
25
26
  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
34
+ from .route_handler import FastlifeRequest
32
35
 
33
36
  if TYPE_CHECKING:
34
37
  from .registry import AppRegistry # coverage: ignore
@@ -38,6 +41,14 @@ VENUSIAN_CATEGORY = "fastlife"
38
41
 
39
42
 
40
43
  class Configurator:
44
+ """
45
+ Configure and build an application.
46
+
47
+ Initialize the app from the settings.
48
+
49
+ :param settings: Application settings.
50
+ """
51
+
41
52
  registry: "AppRegistry"
42
53
 
43
54
  def __init__(self, settings: Settings) -> None:
@@ -49,14 +60,37 @@ class Configurator:
49
60
  docs_url=None,
50
61
  redoc_url=None,
51
62
  )
63
+ FastlifeRoute.registry = self.registry
64
+ self._app.router.route_class = FastlifeRoute
52
65
  self.scanner = venusian.Scanner(fastlife=self)
53
66
  self.include("fastlife.views")
54
67
  self.include("fastlife.session")
55
68
 
56
69
  def get_app(self) -> FastAPI:
70
+ """
71
+ Get the app after configuration in order to start after beeing configured.
72
+
73
+ :return: FastAPI application
74
+ """
57
75
  return self._app
58
76
 
59
77
  def include(self, module: str | ModuleType) -> "Configurator":
78
+ """
79
+ Include a module in order to load its views.
80
+
81
+ Here is an example.
82
+
83
+ ::
84
+
85
+ from fastlife import Configurator, configure
86
+
87
+ @configure
88
+ def includeme(config: Configurator) -> None:
89
+ config.include(".views")
90
+
91
+
92
+ :param module: a module to include.
93
+ """
60
94
  if isinstance(module, str):
61
95
  package = None
62
96
  if module.startswith("."):
@@ -70,7 +104,10 @@ class Configurator:
70
104
  def add_middleware(
71
105
  self, middleware_class: Type[AbstractMiddleware], **options: Any
72
106
  ) -> Self:
73
- self._app.add_middleware(middleware_class, **options)
107
+ """
108
+ Add a starlette middleware to the FastAPI app.
109
+ """
110
+ self._app.add_middleware(middleware_class, **options) # type: ignore
74
111
  return self
75
112
 
76
113
  def add_route(
@@ -105,6 +142,7 @@ class Configurator:
105
142
  # generate_unique_id
106
143
  # ),
107
144
  ) -> "Configurator":
145
+ """Add a route to the app."""
108
146
  dependencies: List[DependsType] = []
109
147
  if permission:
110
148
  dependencies.append(Depends(self.registry.check_permission(permission)))
@@ -140,21 +178,37 @@ class Configurator:
140
178
  def add_static_route(
141
179
  self, route_path: str, directory: Path, name: str = "static"
142
180
  ) -> "Configurator":
143
- """Mount a directory to an http endpoint."""
181
+ """
182
+ Mount a directory to an http endpoint.
183
+
184
+ :param route_path: the root path for the statics
185
+ :param directory: the directory on the filesystem where the statics files are.
186
+ :param name: a name for the route in the starlette app.
187
+ :return: the configurator
188
+
189
+ """
144
190
  self._app.mount(route_path, StaticFiles(directory=directory), name=name)
145
191
  return self
146
192
 
147
193
  def add_exception_handler(
148
194
  self, status_code_or_exc: int | Type[Exception], handler: Any
149
195
  ) -> "Configurator":
150
- self._app.add_exception_handler(status_code_or_exc, handler)
196
+ """Add an exception handler the application."""
197
+
198
+ def exception_handler(request: Request, exc: Exception) -> Any:
199
+ req = FastlifeRequest(self.registry, request)
200
+ return handler(req, exc)
201
+
202
+ self._app.add_exception_handler(status_code_or_exc, exception_handler)
151
203
  return self
152
204
 
153
205
 
154
206
  def configure(
155
207
  wrapped: Callable[[Configurator], None]
156
208
  ) -> Callable[[Configurator], None]:
157
- """Decorator used to attach route in a submodule while using the confirator.scan"""
209
+ """
210
+ Decorator used to attach route in a submodule while using the configurator.include.
211
+ """
158
212
 
159
213
  def callback(
160
214
  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,29 @@
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
+
7
+ if TYPE_CHECKING:
8
+ from .registry import AppRegistry # coverage: ignore
9
+
10
+
11
+ class FastlifeRequest(BaseRequest):
12
+ def __init__(self, registry: "AppRegistry", request: BaseRequest) -> None:
13
+ super().__init__(request.scope, request.receive)
14
+ self.registry = registry
15
+
16
+
17
+ class FastlifeRoute(APIRoute):
18
+ registry: "AppRegistry" = None # type: ignore
19
+
20
+ def get_route_handler( # type: ignore
21
+ self,
22
+ ) -> Callable[[FastlifeRequest], Coroutine[Any, Any, Response]]:
23
+ orig_route_handler = super().get_route_handler()
24
+
25
+ async def route_handler(request: BaseRequest) -> FastlifeRequest:
26
+ req = FastlifeRequest(self.registry, request)
27
+ return await orig_route_handler(req) # type: ignore
28
+
29
+ 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,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,
@@ -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(
@@ -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.1
3
+ Version: 0.7.1
4
4
  Summary: High-level web framework
5
5
  Home-page: https://github.com/mardiros/fastlife
6
6
  License: BSD-derived
@@ -19,9 +19,9 @@ 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
27
  Requires-Dist: pydantic (>=2.5.3,<3.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=3VnwA-ES9KumYc_XOJZq7O--qHeRceUldH66RaIy3bQ,7234
5
+ fastlife/configurator/registry.py,sha256=FKXCxWlMMGNN1-l5H6QLTCoqP_tWENN84RjkConejWI,1580
6
+ fastlife/configurator/route_handler.py,sha256=TRsiGM8xQxJvRFbdKZphLK7l37DSfKvvmkEbg-h5EoU,977
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
@@ -47,27 +50,27 @@ fastlife/templates/pydantic_form/Sequence.jinja,sha256=RU19X6FwNhoFhQ0fBpTrfRsS_
47
50
  fastlife/templates/pydantic_form/Text.jinja,sha256=DCZNAly_YjYQcGdz-EiCbX5DThHHTDq66KySpiJe3Ik,450
48
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
60
  fastlife/templating/renderer/widgets/boolean.py,sha256=1Cnrs6Sqf7CJURrCRkDL2NDQZWO9dvac350w0PcZtDg,544
58
61
  fastlife/templating/renderer/widgets/checklist.py,sha256=Qit-k4RnW9Y3xOyE9BevBRuFZ8XN5jH3x_sI5fWt43Y,1009
59
62
  fastlife/templating/renderer/widgets/dropdown.py,sha256=FyPZzrTprLff6YRqZ031J8-KZpBgidTw0BsKY1Qt7Ts,1009
60
- fastlife/templating/renderer/widgets/factory.py,sha256=BVz2iNb5GMbjGlTaWndcUIitQHBb88ZSp39ays2-Os0,14779
63
+ fastlife/templating/renderer/widgets/factory.py,sha256=iOBuxWlY1d9WfCqcm5f5Ck5NwoK9uDkioCSPyu5HsQo,14787
61
64
  fastlife/templating/renderer/widgets/hidden.py,sha256=2fsbTQKsACV0JVYpCjXaQAV7VnQTIBPCi4lJPdWCRHc,308
62
65
  fastlife/templating/renderer/widgets/model.py,sha256=BNM6dPPN9jzM26LreeOw7wiCsZun1uSMMFXMNcO2hII,1094
63
66
  fastlife/templating/renderer/widgets/sequence.py,sha256=rYXsUZokV3wnKa-26BmgAu7sVCtFf8FdBmhvrnqR-gM,1455
64
67
  fastlife/templating/renderer/widgets/text.py,sha256=OWFFA0muZCznrlUrBRRUKVj60TdWtsdgw0bURdOA3lE,879
65
68
  fastlife/templating/renderer/widgets/union.py,sha256=xNDctq0SRXfRyMHXL8FgRKyUOUreR1xENnt6onkZJ9I,1797
66
- fastlife/testing/__init__.py,sha256=KgTlRI0g8z7HRpL7mD5QgI__LT9Y4QDSzKMlxJG3wNk,67
67
- fastlife/testing/testclient.py,sha256=o7Yc-_At0S23lmV8-vbGGPm-s0xRTAV7OhBri2rqqrU,20220
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.1.dist-info/LICENSE,sha256=F75xSseSKMwqzFj8rswYU6NWS3VoWOc_gY3fJYf9_LI,1504
71
- fastlifeweb-0.6.1.dist-info/METADATA,sha256=a76c4GR6xUogf6ORnzywvcl2miKzGQFawFOhLCXeb8k,1833
72
- fastlifeweb-0.6.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
73
- fastlifeweb-0.6.1.dist-info/RECORD,,
72
+ fastlife/views/pydantic_form.py,sha256=KJtH_DK8em0czGPsv0XpzGUFhtycyXdeRldwiU7d_j4,1257
73
+ fastlifeweb-0.7.1.dist-info/LICENSE,sha256=F75xSseSKMwqzFj8rswYU6NWS3VoWOc_gY3fJYf9_LI,1504
74
+ fastlifeweb-0.7.1.dist-info/METADATA,sha256=-dGK9zmf4PUfvKmBZmF5zCIbyJb_QmmgFI70rvOoDrk,1833
75
+ fastlifeweb-0.7.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
76
+ fastlifeweb-0.7.1.dist-info/RECORD,,