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.
Files changed (90) hide show
  1. fastlife/__init__.py +2 -2
  2. fastlife/config/__init__.py +13 -0
  3. fastlife/{configurator → config}/configurator.py +62 -39
  4. fastlife/{configurator → config}/registry.py +2 -10
  5. fastlife/{configurator → config}/settings.py +7 -3
  6. fastlife/middlewares/__init__.py +7 -0
  7. fastlife/middlewares/base.py +24 -0
  8. fastlife/middlewares/reverse_proxy/__init__.py +16 -0
  9. fastlife/middlewares/reverse_proxy/x_forwarded.py +1 -14
  10. fastlife/middlewares/session/__init__.py +16 -0
  11. fastlife/middlewares/session/middleware.py +6 -1
  12. fastlife/middlewares/session/serializer.py +21 -0
  13. fastlife/request/__init__.py +5 -0
  14. fastlife/request/form.py +3 -1
  15. fastlife/request/form_data.py +28 -3
  16. fastlife/request/request.py +18 -0
  17. fastlife/routing/__init__.py +7 -0
  18. fastlife/routing/route.py +45 -0
  19. fastlife/routing/router.py +12 -4
  20. fastlife/security/__init__.py +1 -0
  21. fastlife/security/csrf.py +29 -11
  22. fastlife/security/policy.py +6 -2
  23. fastlife/shared_utils/__init__.py +1 -0
  24. fastlife/shared_utils/infer.py +7 -0
  25. fastlife/shared_utils/resolver.py +10 -2
  26. fastlife/templates/A.jinja +33 -9
  27. fastlife/templates/Button.jinja +55 -32
  28. fastlife/templates/Checkbox.jinja +20 -6
  29. fastlife/templates/CsrfToken.jinja +4 -0
  30. fastlife/templates/Details.jinja +31 -3
  31. fastlife/templates/Form.jinja +45 -8
  32. fastlife/templates/H1.jinja +14 -1
  33. fastlife/templates/H2.jinja +14 -1
  34. fastlife/templates/H3.jinja +14 -1
  35. fastlife/templates/H4.jinja +14 -1
  36. fastlife/templates/H5.jinja +14 -1
  37. fastlife/templates/H6.jinja +14 -1
  38. fastlife/templates/Hidden.jinja +3 -3
  39. fastlife/templates/Input.jinja +21 -8
  40. fastlife/templates/Label.jinja +18 -2
  41. fastlife/templates/Option.jinja +14 -2
  42. fastlife/templates/P.jinja +14 -2
  43. fastlife/templates/Radio.jinja +34 -12
  44. fastlife/templates/Select.jinja +15 -4
  45. fastlife/templates/Summary.jinja +13 -2
  46. fastlife/templates/Table.jinja +12 -1
  47. fastlife/templates/Tbody.jinja +11 -1
  48. fastlife/templates/Td.jinja +12 -1
  49. fastlife/templates/Textarea.jinja +15 -8
  50. fastlife/templates/Tfoot.jinja +11 -1
  51. fastlife/templates/Th.jinja +12 -1
  52. fastlife/templates/Thead.jinja +11 -1
  53. fastlife/templates/Tr.jinja +11 -1
  54. fastlife/templates/pydantic_form/Boolean.jinja +3 -2
  55. fastlife/templates/pydantic_form/Checklist.jinja +3 -5
  56. fastlife/templates/pydantic_form/Dropdown.jinja +3 -2
  57. fastlife/templates/pydantic_form/Error.jinja +4 -3
  58. fastlife/templates/pydantic_form/Hidden.jinja +2 -1
  59. fastlife/templates/pydantic_form/Hint.jinja +2 -1
  60. fastlife/templates/pydantic_form/Model.jinja +9 -2
  61. fastlife/templates/pydantic_form/Sequence.jinja +10 -2
  62. fastlife/templates/pydantic_form/Text.jinja +2 -2
  63. fastlife/templates/pydantic_form/Textarea.jinja +24 -2
  64. fastlife/templates/pydantic_form/Union.jinja +7 -1
  65. fastlife/templates/pydantic_form/Widget.jinja +5 -2
  66. fastlife/templating/binding.py +18 -4
  67. fastlife/templating/renderer/__init__.py +3 -1
  68. fastlife/templating/renderer/abstract.py +19 -6
  69. fastlife/templating/renderer/constants.py +82 -0
  70. fastlife/templating/renderer/jinjax.py +267 -4
  71. fastlife/templating/renderer/widgets/base.py +40 -8
  72. fastlife/templating/renderer/widgets/boolean.py +17 -0
  73. fastlife/templating/renderer/widgets/checklist.py +19 -0
  74. fastlife/templating/renderer/widgets/dropdown.py +18 -0
  75. fastlife/templating/renderer/widgets/factory.py +23 -13
  76. fastlife/templating/renderer/widgets/hidden.py +14 -0
  77. fastlife/templating/renderer/widgets/model.py +1 -1
  78. fastlife/templating/renderer/widgets/sequence.py +1 -1
  79. fastlife/templating/renderer/widgets/text.py +50 -4
  80. fastlife/templating/renderer/widgets/union.py +21 -2
  81. fastlife/testing/testclient.py +3 -3
  82. fastlife/views/pydantic_form.py +2 -2
  83. {fastlifeweb-0.10.0.dist-info → fastlifeweb-0.11.0.dist-info}/METADATA +4 -9
  84. {fastlifeweb-0.10.0.dist-info → fastlifeweb-0.11.0.dist-info}/RECORD +86 -86
  85. fastlife/configurator/__init__.py +0 -4
  86. fastlife/configurator/base.py +0 -9
  87. fastlife/configurator/route_handler.py +0 -29
  88. fastlife/templates/__init__.py +0 -0
  89. {fastlifeweb-0.10.0.dist-info → fastlifeweb-0.11.0.dist-info}/LICENSE +0 -0
  90. {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 .configurator import Configurator, configure
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 is here to register routes in a fastapi app,
3
- with dependency injection.
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, Request, Response
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.configurator.base import AbstractMiddleware
30
- from fastlife.configurator.route_handler import FastlifeRoute
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
- from .registry import initialize_registry # XXX circular import
56
-
57
- self.registry = initialize_registry(settings)
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(self.registry))],
59
+ dependencies=[Depends(check_csrf())],
60
60
  docs_url=None,
61
61
  redoc_url=None,
62
62
  )
63
- FastlifeRoute.registry = self.registry
64
- self._app.router.route_class = FastlifeRoute
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 get_app(self) -> FastAPI:
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 views.
79
+ Include a module in order to load its configuration.
80
80
 
81
- Here is an example.
81
+ It will load and include all the submodule as well.
82
82
 
83
- ::
83
+ Here is an example.
84
84
 
85
- from fastlife import Configurator, configure
85
+ ```python
86
+ from fastlife import Configurator, configure
86
87
 
87
- @configure
88
- def includeme(config: Configurator) -> None:
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[..., Coroutine[Any, Any, Response]],
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
- """Add a route to the app."""
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: Request, exc: Exception) -> Any:
199
- req = FastlifeRequest(self.registry, request)
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.configurator.route_handler import FastlifeRequest
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 initialize_registry(settings: Settings) -> AppRegistry:
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
- """Set the prefix fastlife_ for configuration using operating system environment."""
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.configurator.registry:AppRegistry")
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
- """Name of the html input field for csrf token."""
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
  """
@@ -0,0 +1,7 @@
1
+ """
2
+ Fastlife HTTP middlewares are Starlette HTTP Middleware.
3
+
4
+ HTTP middleware is a function that processes requests before they reach
5
+ the view function or modifies responses after the view has processed
6
+ the request.
7
+ """
@@ -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.configurator.base import AbstractMiddleware
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.configurator.base import AbstractMiddleware
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
@@ -0,0 +1,5 @@
1
+ """HTTP Request."""
2
+
3
+ from .request import Request
4
+
5
+ __all__ = ["Request"]
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.configurator.registry import Registry
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
 
@@ -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.configurator.registry import Registry
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, reg: Registry
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=reg.settings.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
@@ -0,0 +1,7 @@
1
+ """
2
+ HTTP Routing.
3
+
4
+ The http routing is the process that choose the proper view functions based
5
+ on the http request received. Usually it is based on the request path_info
6
+ and the http method.
7
+ """
@@ -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
@@ -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.configurator.route_handler import FastlifeRoute
11
+ from fastlife.routing.route import Route
6
12
 
7
13
 
8
- class FastLifeRouter(APIRouter):
9
- """The router used split your app in many routes."""
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"] = FastlifeRoute
20
+ kwargs["route_class"] = Route
13
21
  super().__init__(**kwargs)
@@ -0,0 +1 @@
1
+ """Security features."""