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.
- fastlife/configurator/__init__.py +1 -0
- fastlife/configurator/configurator.py +59 -5
- fastlife/configurator/registry.py +14 -13
- fastlife/configurator/route_handler.py +29 -0
- fastlife/configurator/settings.py +52 -0
- fastlife/routing/__init__.py +0 -0
- fastlife/routing/router.py +13 -0
- fastlife/security/csrf.py +16 -1
- fastlife/security/policy.py +5 -1
- fastlife/templates/Button.jinja +6 -3
- fastlife/templating/__init__.py +4 -0
- fastlife/templating/binding.py +7 -0
- fastlife/templating/renderer/abstract.py +60 -13
- fastlife/templating/renderer/jinjax.py +13 -16
- fastlife/templating/renderer/widgets/factory.py +1 -1
- fastlife/testing/__init__.py +1 -0
- fastlife/testing/testclient.py +12 -1
- fastlife/views/pydantic_form.py +3 -0
- {fastlifeweb-0.6.1.dist-info → fastlifeweb-0.7.1.dist-info}/METADATA +3 -3
- {fastlifeweb-0.6.1.dist-info → fastlifeweb-0.7.1.dist-info}/RECORD +22 -19
- {fastlifeweb-0.6.1.dist-info → fastlifeweb-0.7.1.dist-info}/LICENSE +0 -0
- {fastlifeweb-0.6.1.dist-info → fastlifeweb-0.7.1.dist-info}/WHEEL +0 -0
@@ -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
|
-
|
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
|
-
"""
|
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
|
-
|
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
|
-
"""
|
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
|
-
|
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
|
-
|
39
|
-
return DEFAULT_REGISTRY
|
41
|
+
return AppRegistryCls(settings) # type: ignore
|
40
42
|
|
41
43
|
|
42
|
-
def
|
43
|
-
|
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(
|
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"
|
fastlife/security/policy.py
CHANGED
@@ -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
|
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:
|
fastlife/templates/Button.jinja
CHANGED
@@ -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
|
37
|
-
endif %}{% if hx_after_request %}hx-on::after-request="{{hx_after_request}}" {% endif %} {%
|
38
|
-
%}hx-vals='{{hx_vals|safe}}' {% endif %} {% if
|
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>
|
fastlife/templating/__init__.py
CHANGED
fastlife/templating/binding.py
CHANGED
@@ -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:
|
50
|
-
token:
|
51
|
-
removable: bool
|
52
|
-
field: FieldInfo | 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:
|
91
|
-
token:
|
92
|
-
removable: bool
|
93
|
-
field: FieldInfo | 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,
|
fastlife/testing/__init__.py
CHANGED
fastlife/testing/testclient.py
CHANGED
@@ -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"],
|
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,
|
fastlife/views/pydantic_form.py
CHANGED
@@ -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.
|
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.
|
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.
|
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=
|
2
|
+
fastlife/configurator/__init__.py,sha256=vMV25HEfuXjCL7DhZukhlB2JSfxwzX2lGbErcZ5b7N0,146
|
3
3
|
fastlife/configurator/base.py,sha256=2ahvTudLmD99YQjnIeGN5JDPCSl3k-mauu7bsSEB5RE,216
|
4
|
-
fastlife/configurator/configurator.py,sha256=
|
5
|
-
fastlife/configurator/registry.py,sha256=
|
6
|
-
fastlife/configurator/
|
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=
|
13
|
-
fastlife/security/policy.py,sha256=
|
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=
|
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=
|
51
|
-
fastlife/templating/binding.py,sha256=
|
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=
|
54
|
-
fastlife/templating/renderer/jinjax.py,sha256=
|
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=
|
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=
|
67
|
-
fastlife/testing/testclient.py,sha256=
|
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=
|
70
|
-
fastlifeweb-0.
|
71
|
-
fastlifeweb-0.
|
72
|
-
fastlifeweb-0.
|
73
|
-
fastlifeweb-0.
|
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,,
|
File without changes
|
File without changes
|