fastlifeweb 0.10.0__py3-none-any.whl → 0.11.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.
- fastlife/__init__.py +2 -2
- fastlife/config/__init__.py +13 -0
- fastlife/{configurator → config}/configurator.py +62 -39
- fastlife/{configurator → config}/registry.py +2 -10
- fastlife/{configurator → config}/settings.py +7 -3
- fastlife/middlewares/__init__.py +7 -0
- fastlife/middlewares/base.py +24 -0
- fastlife/middlewares/reverse_proxy/__init__.py +16 -0
- fastlife/middlewares/reverse_proxy/x_forwarded.py +1 -14
- fastlife/middlewares/session/__init__.py +16 -0
- fastlife/middlewares/session/middleware.py +6 -1
- fastlife/middlewares/session/serializer.py +21 -0
- fastlife/request/__init__.py +5 -0
- fastlife/request/form.py +3 -1
- fastlife/request/form_data.py +28 -3
- fastlife/request/request.py +18 -0
- fastlife/routing/__init__.py +7 -0
- fastlife/routing/route.py +45 -0
- fastlife/routing/router.py +12 -4
- fastlife/security/__init__.py +1 -0
- fastlife/security/csrf.py +29 -11
- fastlife/security/policy.py +6 -2
- fastlife/shared_utils/__init__.py +1 -0
- fastlife/shared_utils/infer.py +7 -0
- fastlife/shared_utils/resolver.py +10 -2
- fastlife/templates/A.jinja +33 -9
- fastlife/templates/Button.jinja +55 -32
- fastlife/templates/Checkbox.jinja +20 -6
- fastlife/templates/CsrfToken.jinja +4 -0
- fastlife/templates/Details.jinja +31 -3
- fastlife/templates/Form.jinja +45 -8
- fastlife/templates/H1.jinja +14 -1
- fastlife/templates/H2.jinja +14 -1
- fastlife/templates/H3.jinja +14 -1
- fastlife/templates/H4.jinja +14 -1
- fastlife/templates/H5.jinja +14 -1
- fastlife/templates/H6.jinja +14 -1
- fastlife/templates/Hidden.jinja +3 -3
- fastlife/templates/Input.jinja +21 -8
- fastlife/templates/Label.jinja +18 -2
- fastlife/templates/Option.jinja +14 -2
- fastlife/templates/P.jinja +14 -2
- fastlife/templates/Radio.jinja +34 -12
- fastlife/templates/Select.jinja +15 -4
- fastlife/templates/Summary.jinja +13 -2
- fastlife/templates/Table.jinja +12 -1
- fastlife/templates/Tbody.jinja +11 -1
- fastlife/templates/Td.jinja +12 -1
- fastlife/templates/Textarea.jinja +15 -8
- fastlife/templates/Tfoot.jinja +11 -1
- fastlife/templates/Th.jinja +12 -1
- fastlife/templates/Thead.jinja +11 -1
- fastlife/templates/Tr.jinja +11 -1
- fastlife/templates/pydantic_form/Boolean.jinja +3 -2
- fastlife/templates/pydantic_form/Checklist.jinja +3 -5
- fastlife/templates/pydantic_form/Dropdown.jinja +3 -2
- fastlife/templates/pydantic_form/Error.jinja +4 -3
- fastlife/templates/pydantic_form/Hidden.jinja +2 -1
- fastlife/templates/pydantic_form/Hint.jinja +2 -1
- fastlife/templates/pydantic_form/Model.jinja +9 -2
- fastlife/templates/pydantic_form/Sequence.jinja +10 -2
- fastlife/templates/pydantic_form/Text.jinja +2 -2
- fastlife/templates/pydantic_form/Textarea.jinja +24 -2
- fastlife/templates/pydantic_form/Union.jinja +7 -1
- fastlife/templates/pydantic_form/Widget.jinja +5 -2
- fastlife/templating/binding.py +18 -4
- fastlife/templating/renderer/__init__.py +3 -1
- fastlife/templating/renderer/abstract.py +19 -6
- fastlife/templating/renderer/constants.py +82 -0
- fastlife/templating/renderer/jinjax.py +267 -4
- fastlife/templating/renderer/widgets/base.py +40 -8
- fastlife/templating/renderer/widgets/boolean.py +17 -0
- fastlife/templating/renderer/widgets/checklist.py +19 -0
- fastlife/templating/renderer/widgets/dropdown.py +18 -0
- fastlife/templating/renderer/widgets/factory.py +23 -13
- fastlife/templating/renderer/widgets/hidden.py +14 -0
- fastlife/templating/renderer/widgets/model.py +1 -1
- fastlife/templating/renderer/widgets/sequence.py +1 -1
- fastlife/templating/renderer/widgets/text.py +50 -4
- fastlife/templating/renderer/widgets/union.py +21 -2
- fastlife/testing/testclient.py +3 -3
- fastlife/views/pydantic_form.py +2 -2
- {fastlifeweb-0.10.0.dist-info → fastlifeweb-0.11.0.dist-info}/METADATA +4 -9
- {fastlifeweb-0.10.0.dist-info → fastlifeweb-0.11.0.dist-info}/RECORD +86 -86
- fastlife/configurator/__init__.py +0 -4
- fastlife/configurator/base.py +0 -9
- fastlife/configurator/route_handler.py +0 -29
- fastlife/templates/__init__.py +0 -0
- {fastlifeweb-0.10.0.dist-info → fastlifeweb-0.11.0.dist-info}/LICENSE +0 -0
- {fastlifeweb-0.10.0.dist-info → fastlifeweb-0.11.0.dist-info}/WHEEL +0 -0
fastlife/__init__.py
CHANGED
@@ -1,5 +1,4 @@
|
|
1
|
-
from .
|
2
|
-
from .configurator.registry import Registry
|
1
|
+
from .config import Configurator, Registry, Settings, configure
|
3
2
|
|
4
3
|
# from .request.form_data import model
|
5
4
|
from .templating import Template, template
|
@@ -11,6 +10,7 @@ __all__ = [
|
|
11
10
|
"template",
|
12
11
|
"Template",
|
13
12
|
"Registry",
|
13
|
+
"Settings",
|
14
14
|
# Model
|
15
15
|
# "model",
|
16
16
|
]
|
@@ -0,0 +1,13 @@
|
|
1
|
+
"""Configure fastlife app for dependency injection."""
|
2
|
+
|
3
|
+
from .configurator import Configurator, configure
|
4
|
+
from .registry import AppRegistry, Registry
|
5
|
+
from .settings import Settings
|
6
|
+
|
7
|
+
__all__ = [
|
8
|
+
"Configurator",
|
9
|
+
"configure",
|
10
|
+
"Registry",
|
11
|
+
"AppRegistry",
|
12
|
+
"Settings",
|
13
|
+
]
|
@@ -1,6 +1,14 @@
|
|
1
1
|
"""
|
2
|
-
The configurator
|
3
|
-
|
2
|
+
The configurator registers routes in a FastAPI application while
|
3
|
+
adding support for dependency injection during the configuration phase.
|
4
|
+
|
5
|
+
FastAPI does not provide any built-in support for dependency injection
|
6
|
+
during the configuration phase.
|
7
|
+
Instead, it only resolves dependencies at request time, ensuring they
|
8
|
+
are dynamically handled per request.
|
9
|
+
|
10
|
+
The configurator is designed to handle the setup during the configuration
|
11
|
+
phase.
|
4
12
|
"""
|
5
13
|
|
6
14
|
import importlib
|
@@ -9,28 +17,20 @@ import logging
|
|
9
17
|
from enum import Enum
|
10
18
|
from pathlib import Path
|
11
19
|
from types import ModuleType
|
12
|
-
from typing import
|
13
|
-
TYPE_CHECKING,
|
14
|
-
Any,
|
15
|
-
Callable,
|
16
|
-
Coroutine,
|
17
|
-
List,
|
18
|
-
Optional,
|
19
|
-
Self,
|
20
|
-
Type,
|
21
|
-
Union,
|
22
|
-
)
|
20
|
+
from typing import TYPE_CHECKING, Any, Callable, List, Optional, Self, Type, Union
|
23
21
|
|
24
22
|
import venusian
|
25
|
-
from fastapi import Depends, FastAPI
|
23
|
+
from fastapi import Depends, FastAPI
|
24
|
+
from fastapi import Request as BaseRequest
|
26
25
|
from fastapi.params import Depends as DependsType
|
27
26
|
from fastapi.staticfiles import StaticFiles
|
28
27
|
|
29
|
-
from fastlife.
|
30
|
-
from fastlife.
|
28
|
+
from fastlife.middlewares.base import AbstractMiddleware
|
29
|
+
from fastlife.request.request import Request
|
30
|
+
from fastlife.routing.route import Route
|
31
31
|
from fastlife.security.csrf import check_csrf
|
32
|
+
from fastlife.shared_utils.resolver import resolve
|
32
33
|
|
33
|
-
from .route_handler import FastlifeRequest
|
34
34
|
from .settings import Settings
|
35
35
|
|
36
36
|
if TYPE_CHECKING:
|
@@ -45,49 +45,53 @@ class Configurator:
|
|
45
45
|
Configure and build an application.
|
46
46
|
|
47
47
|
Initialize the app from the settings.
|
48
|
-
|
49
|
-
:param settings: Application settings.
|
50
48
|
"""
|
51
49
|
|
52
50
|
registry: "AppRegistry"
|
53
51
|
|
54
52
|
def __init__(self, settings: Settings) -> None:
|
55
|
-
|
56
|
-
|
57
|
-
|
53
|
+
"""
|
54
|
+
:param settings: Application settings.
|
55
|
+
"""
|
56
|
+
registry_cls = resolve(settings.registry_class)
|
57
|
+
self.registry = registry_cls(settings)
|
58
58
|
self._app = FastAPI(
|
59
|
-
dependencies=[Depends(check_csrf(
|
59
|
+
dependencies=[Depends(check_csrf())],
|
60
60
|
docs_url=None,
|
61
61
|
redoc_url=None,
|
62
62
|
)
|
63
|
-
|
64
|
-
self._app.router.route_class =
|
63
|
+
Route._registry = self.registry # type: ignore
|
64
|
+
self._app.router.route_class = Route
|
65
65
|
self.scanner = venusian.Scanner(fastlife=self)
|
66
66
|
self.include("fastlife.views")
|
67
67
|
self.include("fastlife.middlewares")
|
68
68
|
|
69
|
-
def
|
69
|
+
def get_asgi_app(self) -> FastAPI:
|
70
70
|
"""
|
71
71
|
Get the app after configuration in order to start after beeing configured.
|
72
72
|
|
73
|
-
:return: FastAPI application
|
73
|
+
:return: FastAPI application.
|
74
74
|
"""
|
75
75
|
return self._app
|
76
76
|
|
77
77
|
def include(self, module: str | ModuleType) -> "Configurator":
|
78
78
|
"""
|
79
|
-
Include a module in order to load its
|
79
|
+
Include a module in order to load its configuration.
|
80
80
|
|
81
|
-
|
81
|
+
It will load and include all the submodule as well.
|
82
82
|
|
83
|
-
|
83
|
+
Here is an example.
|
84
84
|
|
85
|
-
|
85
|
+
```python
|
86
|
+
from fastlife import Configurator, configure
|
86
87
|
|
87
|
-
|
88
|
-
|
89
|
-
config.include(".views")
|
88
|
+
def home() -> dict[str, str]:
|
89
|
+
return {"hello": "world"}
|
90
90
|
|
91
|
+
@configure
|
92
|
+
def includeme(config: Configurator) -> None:
|
93
|
+
config.add_route("home", "/", home)
|
94
|
+
```
|
91
95
|
|
92
96
|
:param module: a module to include.
|
93
97
|
"""
|
@@ -114,7 +118,7 @@ class Configurator:
|
|
114
118
|
self,
|
115
119
|
name: str,
|
116
120
|
path: str,
|
117
|
-
endpoint: Callable[...,
|
121
|
+
endpoint: Callable[..., Any],
|
118
122
|
*,
|
119
123
|
permission: str | None = None,
|
120
124
|
status_code: int | None = None,
|
@@ -142,7 +146,26 @@ class Configurator:
|
|
142
146
|
# generate_unique_id
|
143
147
|
# ),
|
144
148
|
) -> "Configurator":
|
145
|
-
"""
|
149
|
+
"""
|
150
|
+
Add a route to the app.
|
151
|
+
|
152
|
+
Fastlife does not use a decorator to attach routes, instead the decorator
|
153
|
+
:func:`fastlife.config.configurator.configure` has to be used to
|
154
|
+
inject routes inside a method and call the add_route method.
|
155
|
+
|
156
|
+
:param name: name of the route, used to build route from the helper
|
157
|
+
:meth:`fastlife.request.request.Request.url_for` in order to create links.
|
158
|
+
:param path: path of the route, use `{curly_brace}` to inject FastAPI Path
|
159
|
+
parameters.
|
160
|
+
:param endpoint: the function that will reveive the request.
|
161
|
+
:param permission: a permission to validate by the
|
162
|
+
:attr:`fastlife.config.settings.Settings.check_permission` function.
|
163
|
+
|
164
|
+
:param methods: restrict route to a list of http methods.
|
165
|
+
:param response_description: description for the response.
|
166
|
+
:param deprecated: mark the route as deprecated.
|
167
|
+
:return: the configurator.
|
168
|
+
"""
|
146
169
|
dependencies: List[DependsType] = []
|
147
170
|
if permission:
|
148
171
|
dependencies.append(Depends(self.registry.check_permission(permission)))
|
@@ -181,7 +204,7 @@ class Configurator:
|
|
181
204
|
"""
|
182
205
|
Mount a directory to an http endpoint.
|
183
206
|
|
184
|
-
:param route_path: the root path for the statics
|
207
|
+
:param route_path: the root path for the statics.
|
185
208
|
:param directory: the directory on the filesystem where the statics files are.
|
186
209
|
:param name: a name for the route in the starlette app.
|
187
210
|
:return: the configurator
|
@@ -195,8 +218,8 @@ class Configurator:
|
|
195
218
|
) -> "Configurator":
|
196
219
|
"""Add an exception handler the application."""
|
197
220
|
|
198
|
-
def exception_handler(request:
|
199
|
-
req =
|
221
|
+
def exception_handler(request: BaseRequest, exc: Exception) -> Any:
|
222
|
+
req = Request(self.registry, request)
|
200
223
|
return handler(req, exc)
|
201
224
|
|
202
225
|
self._app.add_exception_handler(status_code_or_exc, exception_handler)
|
@@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Annotated
|
|
2
2
|
|
3
3
|
from fastapi import Depends
|
4
4
|
|
5
|
-
from fastlife.
|
5
|
+
from fastlife.request.request import Request
|
6
6
|
from fastlife.security.policy import CheckPermission
|
7
7
|
from fastlife.shared_utils.resolver import resolve
|
8
8
|
|
@@ -33,15 +33,7 @@ class AppRegistry:
|
|
33
33
|
self.check_permission = resolve(settings.check_permission)
|
34
34
|
|
35
35
|
|
36
|
-
def
|
37
|
-
# global DEFAULT_REGISTRY
|
38
|
-
# if DEFAULT_REGISTRY is not None: # type: ignore
|
39
|
-
# raise ValueError("Registry is already set")
|
40
|
-
AppRegistryCls = resolve(settings.registry_class)
|
41
|
-
return AppRegistryCls(settings) # type: ignore
|
42
|
-
|
43
|
-
|
44
|
-
def get_registry(request: FastlifeRequest) -> AppRegistry:
|
36
|
+
def get_registry(request: Request) -> AppRegistry:
|
45
37
|
return request.registry
|
46
38
|
|
47
39
|
|
@@ -15,7 +15,9 @@ class Settings(BaseSettings):
|
|
15
15
|
"""
|
16
16
|
|
17
17
|
model_config = SettingsConfigDict(env_prefix="fastlife_")
|
18
|
-
"""
|
18
|
+
"""
|
19
|
+
Set the prefix ``fastlife_`` for configuration using operating system environment.
|
20
|
+
"""
|
19
21
|
|
20
22
|
fastlife_route_prefix: str = Field(default="/_fl")
|
21
23
|
"""Route prefix used for fastlife internal views."""
|
@@ -27,7 +29,7 @@ class Settings(BaseSettings):
|
|
27
29
|
a python module name. for instance `fastlife:templates` is the direcotry templates
|
28
30
|
found in the fastlife package.
|
29
31
|
"""
|
30
|
-
registry_class: str = Field(default="fastlife.
|
32
|
+
registry_class: str = Field(default="fastlife.config.registry:AppRegistry")
|
31
33
|
"""Implementation class for the application regitry."""
|
32
34
|
template_renderer_class: str = Field(
|
33
35
|
default="fastlife.templating.renderer:JinjaxTemplateRenderer"
|
@@ -40,7 +42,9 @@ class Settings(BaseSettings):
|
|
40
42
|
Pydantic form default model prefix for serialized field in www-urlencoded-form.
|
41
43
|
"""
|
42
44
|
csrf_token_name: str = Field(default="csrf_token")
|
43
|
-
"""
|
45
|
+
"""
|
46
|
+
Name of the html input field and for the http cookie for csrf token.
|
47
|
+
"""
|
44
48
|
|
45
49
|
jinjax_use_cache: bool = Field(default=True)
|
46
50
|
"""
|
fastlife/middlewares/__init__.py
CHANGED
@@ -0,0 +1,24 @@
|
|
1
|
+
"""Build you own middleware."""
|
2
|
+
|
3
|
+
import abc
|
4
|
+
|
5
|
+
from starlette.types import Receive, Scope, Send
|
6
|
+
|
7
|
+
|
8
|
+
class AbstractMiddleware(abc.ABC):
|
9
|
+
"""
|
10
|
+
Abstract Base Class that represent a fastlife middleware.
|
11
|
+
|
12
|
+
Starlette provide a middleware stack but does not have an abstract base class.
|
13
|
+
This is the only reason this class exists.
|
14
|
+
|
15
|
+
Fastlife middleware are starlette middlewares.
|
16
|
+
"""
|
17
|
+
|
18
|
+
@abc.abstractmethod
|
19
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
20
|
+
"""
|
21
|
+
Called every time an http request is reveived.
|
22
|
+
|
23
|
+
This method before the request object even exists.
|
24
|
+
"""
|
@@ -1,7 +1,23 @@
|
|
1
|
+
"""
|
2
|
+
A middleware that update the request scope for https behind a proxy.
|
3
|
+
|
4
|
+
The attempt of this middleware is to fix Starlette behavior that use client and scheme
|
5
|
+
header based on the header ``x-forwarded-*`` headers and the ``x-real-ip``.
|
6
|
+
|
7
|
+
the ``x-forwarded-for`` header is not parsed to find the appropriate value,
|
8
|
+
the ``x-real-ip`` is used.
|
9
|
+
notethat the ``x-forwarded-port`` header is not used.
|
10
|
+
|
11
|
+
Note that uvicorn or hypercorn offer the same kind middleware.
|
12
|
+
|
13
|
+
Norw, every website is in https, so, this middleware is active by default.
|
14
|
+
"""
|
1
15
|
from fastlife import Configurator, configure
|
2
16
|
|
3
17
|
from .x_forwarded import XForwardedStar
|
4
18
|
|
19
|
+
__all__ = ["XForwardedStar"]
|
20
|
+
|
5
21
|
|
6
22
|
@configure
|
7
23
|
def includeme(config: Configurator) -> None:
|
@@ -1,22 +1,9 @@
|
|
1
|
-
"""
|
2
|
-
A middleware that update the request scope for https behind a proxy.
|
3
|
-
|
4
|
-
The attempt of this middleware is to fix Starlette behavior that use client and scheme
|
5
|
-
header based on the header x-forwarded-* headers and the x-real-ip.
|
6
|
-
|
7
|
-
the x-forwarded-for header is not parsed to find the appropriate value,
|
8
|
-
the x-real-ip is used.
|
9
|
-
notethat the x-forwarded-port header is not used.
|
10
|
-
|
11
|
-
Note that uvicorn or hypercorn offer the same kind middleware.
|
12
|
-
"""
|
13
|
-
|
14
1
|
import logging
|
15
2
|
from typing import Optional, Sequence, Tuple
|
16
3
|
|
17
4
|
from starlette.types import ASGIApp, Receive, Scope, Send
|
18
5
|
|
19
|
-
from fastlife.
|
6
|
+
from fastlife.middlewares.base import AbstractMiddleware
|
20
7
|
|
21
8
|
log = logging.getLogger(__name__)
|
22
9
|
|
@@ -1,7 +1,23 @@
|
|
1
|
+
"""
|
2
|
+
Initialize a session.
|
3
|
+
|
4
|
+
|
5
|
+
The session :attr:`fastlife.config.settings.Settings.session_secret_key` must
|
6
|
+
be set in order to create a session.
|
7
|
+
|
8
|
+
This secret is used to sign session content in order to prevent malicious user
|
9
|
+
to write their own session content. Note that the provided session implementation
|
10
|
+
does not cipher session content, it just sign. No secret should be placed in the
|
11
|
+
session.
|
12
|
+
"""
|
13
|
+
|
1
14
|
from fastlife import Configurator, configure
|
2
15
|
from fastlife.shared_utils.resolver import resolve
|
3
16
|
|
4
17
|
from .middleware import SessionMiddleware
|
18
|
+
from .serializer import AbsractSessionSerializer, SignedSessionSerializer
|
19
|
+
|
20
|
+
__all__ = ["SessionMiddleware", "AbsractSessionSerializer", "SignedSessionSerializer"]
|
5
21
|
|
6
22
|
|
7
23
|
@configure
|
@@ -1,3 +1,5 @@
|
|
1
|
+
"""Deal with http session."""
|
2
|
+
|
1
3
|
from datetime import timedelta
|
2
4
|
from typing import Literal, Type
|
3
5
|
|
@@ -5,12 +7,14 @@ from starlette.datastructures import MutableHeaders
|
|
5
7
|
from starlette.requests import HTTPConnection
|
6
8
|
from starlette.types import ASGIApp, Message, Receive, Scope, Send
|
7
9
|
|
8
|
-
from fastlife.
|
10
|
+
from fastlife.middlewares.base import AbstractMiddleware
|
9
11
|
|
10
12
|
from .serializer import AbsractSessionSerializer, SignedSessionSerializer
|
11
13
|
|
12
14
|
|
13
15
|
class SessionMiddleware(AbstractMiddleware):
|
16
|
+
"""Http session based on cookie."""
|
17
|
+
|
14
18
|
def __init__(
|
15
19
|
self,
|
16
20
|
app: ASGIApp,
|
@@ -36,6 +40,7 @@ class SessionMiddleware(AbstractMiddleware):
|
|
36
40
|
self.security_flags += f"; Domain={cookie_domain}"
|
37
41
|
|
38
42
|
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
43
|
+
"""Initialize a session based on cookie."""
|
39
44
|
if scope["type"] not in ("http", "websocket"): # pragma: no cover
|
40
45
|
await self.app(scope, receive, send)
|
41
46
|
return
|
@@ -1,3 +1,4 @@
|
|
1
|
+
"""Serialize session."""
|
1
2
|
import abc
|
2
3
|
import json
|
3
4
|
from base64 import b64decode, b64encode
|
@@ -7,31 +8,51 @@ import itsdangerous
|
|
7
8
|
|
8
9
|
|
9
10
|
class AbsractSessionSerializer(abc.ABC):
|
11
|
+
"""Session serializer base class"""
|
12
|
+
|
10
13
|
@abc.abstractmethod
|
11
14
|
def __init__(self, secret_key: str, max_age: int) -> None:
|
12
15
|
...
|
13
16
|
|
14
17
|
@abc.abstractmethod
|
15
18
|
def serialize(self, data: Mapping[str, Any]) -> bytes:
|
19
|
+
"""Serialize the session content to bytes in order to be saved."""
|
16
20
|
...
|
17
21
|
|
18
22
|
@abc.abstractmethod
|
19
23
|
def deserialize(self, data: bytes) -> Tuple[Mapping[str, Any], bool]:
|
24
|
+
"""Derialize the session raw bytes content and return it as a mapping."""
|
20
25
|
...
|
21
26
|
|
22
27
|
|
23
28
|
class SignedSessionSerializer(AbsractSessionSerializer):
|
29
|
+
"""
|
30
|
+
The default fastlife session serializer.
|
31
|
+
|
32
|
+
It's based on the itsdangerous package to sign the session with a secret key.
|
33
|
+
|
34
|
+
:param secret_key: a secret used to sign the session payload.
|
35
|
+
|
36
|
+
:param max_age: session lifetime in seconds.
|
37
|
+
"""
|
38
|
+
|
24
39
|
def __init__(self, secret_key: str, max_age: int) -> None:
|
25
40
|
self.signer = itsdangerous.TimestampSigner(secret_key)
|
26
41
|
self.max_age = max_age
|
27
42
|
|
28
43
|
def serialize(self, data: Mapping[str, Any]) -> bytes:
|
44
|
+
"""Serialize and sign the session."""
|
29
45
|
dump = json.dumps(data).encode("utf-8")
|
30
46
|
encoded = b64encode(dump)
|
31
47
|
signed = self.signer.sign(encoded)
|
32
48
|
return signed
|
33
49
|
|
34
50
|
def deserialize(self, data: bytes) -> Tuple[Mapping[str, Any], bool]:
|
51
|
+
"""Deserialize the session.
|
52
|
+
|
53
|
+
If the signature is incorect, the session restart from the begining.
|
54
|
+
No exception raised.
|
55
|
+
"""
|
35
56
|
try:
|
36
57
|
data = self.signer.unsign(data, max_age=self.max_age)
|
37
58
|
# We can't deserialize something wrong since the serialize
|
fastlife/request/__init__.py
CHANGED
fastlife/request/form.py
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
+
"""HTTP Form serialization."""
|
2
|
+
|
1
3
|
from typing import Any, Callable, Generic, Mapping, Type, TypeVar, get_origin
|
2
4
|
|
3
5
|
from fastapi import Depends
|
4
6
|
from pydantic import BaseModel, ValidationError
|
5
7
|
|
6
|
-
from fastlife.
|
8
|
+
from fastlife.config.registry import Registry
|
7
9
|
from fastlife.request.form_data import MappingFormData
|
8
10
|
from fastlife.shared_utils.infer import is_union
|
9
11
|
|
fastlife/request/form_data.py
CHANGED
@@ -1,3 +1,7 @@
|
|
1
|
+
"""
|
2
|
+
Set of functions to unserialize www-form-urlencoded format to python simple types.
|
3
|
+
"""
|
4
|
+
|
1
5
|
from typing import (
|
2
6
|
Annotated,
|
3
7
|
Any,
|
@@ -10,7 +14,7 @@ from typing import (
|
|
10
14
|
|
11
15
|
from fastapi import Depends, Request
|
12
16
|
|
13
|
-
from fastlife.
|
17
|
+
from fastlife.config.registry import Registry
|
14
18
|
|
15
19
|
|
16
20
|
def unflatten_struct(
|
@@ -20,6 +24,12 @@ def unflatten_struct(
|
|
20
24
|
*,
|
21
25
|
csrf_token_name: Optional[str] = None,
|
22
26
|
) -> Mapping[str, Any] | Sequence[Any]:
|
27
|
+
"""
|
28
|
+
Take a flatten_input map, with key segmented by `.` and build a nested dict.
|
29
|
+
|
30
|
+
Fastlife use plain old web form to send data via HTTP POST, this function
|
31
|
+
prepare the data before get injected to pydantic for serialization.
|
32
|
+
"""
|
23
33
|
# we sort to ensure that list index are ordered
|
24
34
|
# formkeys = sorted(flatten_input.keys())
|
25
35
|
for key in flatten_input:
|
@@ -71,8 +81,12 @@ def unflatten_struct(
|
|
71
81
|
|
72
82
|
|
73
83
|
async def unflatten_mapping_form_data(
|
74
|
-
request: Request,
|
84
|
+
request: Request, registry: Registry
|
75
85
|
) -> Mapping[str, Any]:
|
86
|
+
"""
|
87
|
+
Parse the :meth:`fastlife.request.request.form` and build a nested structure.
|
88
|
+
"""
|
89
|
+
|
76
90
|
form_data = await request.form()
|
77
91
|
form_data_decode_list: MutableMapping[str, Any] = {}
|
78
92
|
for key, val in form_data.multi_items():
|
@@ -90,7 +104,7 @@ async def unflatten_mapping_form_data(
|
|
90
104
|
form_data_decode_list[key] = val
|
91
105
|
|
92
106
|
ret = unflatten_struct(
|
93
|
-
form_data_decode_list, {}, csrf_token_name=
|
107
|
+
form_data_decode_list, {}, csrf_token_name=registry.settings.csrf_token_name
|
94
108
|
) # type: ignore
|
95
109
|
return ret # type: ignore
|
96
110
|
|
@@ -98,6 +112,9 @@ async def unflatten_mapping_form_data(
|
|
98
112
|
async def unflatten_sequence_form_data(
|
99
113
|
request: Request, reg: Registry
|
100
114
|
) -> Sequence[str]:
|
115
|
+
"""
|
116
|
+
Parse the :meth:`fastlife.request.request.form` and build a list of structure.
|
117
|
+
"""
|
101
118
|
form_data = await request.form()
|
102
119
|
# Could raise a value error !
|
103
120
|
return unflatten_struct(
|
@@ -106,4 +123,12 @@ async def unflatten_sequence_form_data(
|
|
106
123
|
|
107
124
|
|
108
125
|
MappingFormData = Annotated[Mapping[str, Any], Depends(unflatten_mapping_form_data)]
|
126
|
+
"""
|
127
|
+
Fast API Dependency to deserialize a :meth:`fastlife.request.request.Request.form`
|
128
|
+
to a dict.
|
129
|
+
"""
|
109
130
|
SequenceFormData = Annotated[Sequence[str], Depends(unflatten_sequence_form_data)]
|
131
|
+
"""
|
132
|
+
Fast API Dependency to deserialize a :meth:`fastlife.request.request.Request.form`
|
133
|
+
to a list.
|
134
|
+
"""
|
@@ -0,0 +1,18 @@
|
|
1
|
+
"""HTTP Request representation in a python object."""
|
2
|
+
from typing import TYPE_CHECKING
|
3
|
+
|
4
|
+
from fastapi import Request as BaseRequest
|
5
|
+
|
6
|
+
if TYPE_CHECKING:
|
7
|
+
from fastlife.config.registry import AppRegistry # coverage: ignore
|
8
|
+
|
9
|
+
|
10
|
+
class Request(BaseRequest):
|
11
|
+
"""HTTP Request representation."""
|
12
|
+
|
13
|
+
registry: "AppRegistry"
|
14
|
+
"""Direct access to the application registry."""
|
15
|
+
|
16
|
+
def __init__(self, registry: "AppRegistry", request: BaseRequest) -> None:
|
17
|
+
super().__init__(request.scope, request.receive)
|
18
|
+
self.registry = registry
|
fastlife/routing/__init__.py
CHANGED
@@ -0,0 +1,45 @@
|
|
1
|
+
"""HTTP Route."""
|
2
|
+
from typing import TYPE_CHECKING, Any, Callable, Coroutine
|
3
|
+
|
4
|
+
from fastapi.routing import APIRoute
|
5
|
+
from starlette.requests import Request as StarletteRequest
|
6
|
+
from starlette.responses import Response
|
7
|
+
|
8
|
+
from fastlife.request.request import Request
|
9
|
+
|
10
|
+
if TYPE_CHECKING:
|
11
|
+
from fastlife.config.registry import AppRegistry # coverage: ignore
|
12
|
+
|
13
|
+
|
14
|
+
class Route(APIRoute):
|
15
|
+
"""
|
16
|
+
Routing for fastlife application.
|
17
|
+
|
18
|
+
The fastlife router construct fastlife request object in order to
|
19
|
+
have the registry property available in every received request.
|
20
|
+
"""
|
21
|
+
|
22
|
+
_registry: "AppRegistry"
|
23
|
+
"""
|
24
|
+
The application registry.
|
25
|
+
|
26
|
+
this static variable is initialized by the configurator during
|
27
|
+
the startup and keep the registry during the lifetime of the application.
|
28
|
+
|
29
|
+
this variable should be accessed via the request object or the
|
30
|
+
:class:`fastlife.config.Registry` depenency injection.
|
31
|
+
"""
|
32
|
+
|
33
|
+
def get_route_handler(
|
34
|
+
self,
|
35
|
+
) -> Callable[[StarletteRequest], Coroutine[Any, Any, Response]]:
|
36
|
+
"""
|
37
|
+
Replace the request object by the fastlife request associated with the registry.
|
38
|
+
"""
|
39
|
+
orig_route_handler = super().get_route_handler()
|
40
|
+
|
41
|
+
async def route_handler(request: StarletteRequest) -> Response:
|
42
|
+
req = Request(self._registry, request)
|
43
|
+
return await orig_route_handler(req)
|
44
|
+
|
45
|
+
return route_handler
|
fastlife/routing/router.py
CHANGED
@@ -1,13 +1,21 @@
|
|
1
|
+
"""
|
2
|
+
FastApi router for fastlife application.
|
3
|
+
|
4
|
+
The aim of this router is get :class:`fastlife.routing.route.Route`
|
5
|
+
available in the FastApi request depency injection.
|
6
|
+
"""
|
1
7
|
from typing import Any
|
2
8
|
|
3
9
|
from fastapi import APIRouter
|
4
10
|
|
5
|
-
from fastlife.
|
11
|
+
from fastlife.routing.route import Route
|
6
12
|
|
7
13
|
|
8
|
-
class
|
9
|
-
"""
|
14
|
+
class Router(APIRouter):
|
15
|
+
"""
|
16
|
+
The router used split your app in many routes.
|
17
|
+
"""
|
10
18
|
|
11
19
|
def __init__(self, **kwargs: Any) -> None:
|
12
|
-
kwargs["route_class"] =
|
20
|
+
kwargs["route_class"] = Route
|
13
21
|
super().__init__(**kwargs)
|
fastlife/security/__init__.py
CHANGED
@@ -0,0 +1 @@
|
|
1
|
+
"""Security features."""
|