fastlifeweb 0.6.1__tar.gz → 0.7.0__tar.gz

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 (78) hide show
  1. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/PKG-INFO +3 -3
  2. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/pyproject.toml +3 -3
  3. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/configurator/__init__.py +1 -0
  4. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/configurator/configurator.py +51 -3
  5. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/configurator/registry.py +14 -13
  6. fastlifeweb-0.7.0/src/fastlife/configurator/route_handler.py +30 -0
  7. fastlifeweb-0.7.0/src/fastlife/configurator/settings.py +88 -0
  8. fastlifeweb-0.7.0/src/fastlife/routing/router.py +13 -0
  9. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/security/csrf.py +16 -1
  10. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/security/policy.py +5 -1
  11. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templates/Button.jinja +6 -3
  12. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templating/__init__.py +4 -0
  13. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templating/binding.py +7 -0
  14. fastlifeweb-0.7.0/src/fastlife/templating/renderer/abstract.py +107 -0
  15. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templating/renderer/jinjax.py +13 -16
  16. fastlifeweb-0.7.0/src/fastlife/templating/renderer/widgets/__init__.py +0 -0
  17. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templating/renderer/widgets/factory.py +1 -1
  18. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/testing/__init__.py +1 -0
  19. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/testing/testclient.py +12 -1
  20. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/views/pydantic_form.py +3 -0
  21. fastlifeweb-0.6.1/src/fastlife/configurator/settings.py +0 -36
  22. fastlifeweb-0.6.1/src/fastlife/templating/renderer/abstract.py +0 -60
  23. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/LICENSE +0 -0
  24. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/README.md +0 -0
  25. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/__init__.py +0 -0
  26. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/configurator/base.py +0 -0
  27. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/py.typed +0 -0
  28. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/request/__init__.py +0 -0
  29. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/request/form_data.py +0 -0
  30. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/request/model_result.py +0 -0
  31. {fastlifeweb-0.6.1/src/fastlife/security → fastlifeweb-0.7.0/src/fastlife/routing}/__init__.py +0 -0
  32. {fastlifeweb-0.6.1/src/fastlife/shared_utils → fastlifeweb-0.7.0/src/fastlife/security}/__init__.py +0 -0
  33. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/session/__init__.py +0 -0
  34. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/session/middleware.py +0 -0
  35. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/session/serializer.py +0 -0
  36. {fastlifeweb-0.6.1/src/fastlife/templates → fastlifeweb-0.7.0/src/fastlife/shared_utils}/__init__.py +0 -0
  37. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/shared_utils/infer.py +0 -0
  38. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/shared_utils/resolver.py +0 -0
  39. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templates/A.jinja +0 -0
  40. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templates/Checkbox.jinja +0 -0
  41. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templates/CsrfToken.jinja +0 -0
  42. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templates/Form.jinja +0 -0
  43. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templates/H1.jinja +0 -0
  44. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templates/H2.jinja +0 -0
  45. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templates/H3.jinja +0 -0
  46. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templates/H4.jinja +0 -0
  47. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templates/H5.jinja +0 -0
  48. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templates/H6.jinja +0 -0
  49. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templates/Hidden.jinja +0 -0
  50. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templates/Input.jinja +0 -0
  51. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templates/Label.jinja +0 -0
  52. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templates/Option.jinja +0 -0
  53. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templates/P.jinja +0 -0
  54. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templates/Radio.jinja +0 -0
  55. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templates/Select.jinja +0 -0
  56. {fastlifeweb-0.6.1/src/fastlife/templating/renderer/widgets → fastlifeweb-0.7.0/src/fastlife/templates}/__init__.py +0 -0
  57. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templates/pydantic_form/Boolean.jinja +0 -0
  58. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templates/pydantic_form/Checklist.jinja +0 -0
  59. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templates/pydantic_form/Dropdown.jinja +0 -0
  60. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templates/pydantic_form/Error.jinja +0 -0
  61. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templates/pydantic_form/Hidden.jinja +0 -0
  62. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templates/pydantic_form/Hint.jinja +0 -0
  63. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templates/pydantic_form/Model.jinja +0 -0
  64. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templates/pydantic_form/Sequence.jinja +0 -0
  65. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templates/pydantic_form/Text.jinja +0 -0
  66. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templates/pydantic_form/Union.jinja +0 -0
  67. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templates/pydantic_form/Widget.jinja +0 -0
  68. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templating/renderer/__init__.py +0 -0
  69. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templating/renderer/widgets/base.py +0 -0
  70. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templating/renderer/widgets/boolean.py +0 -0
  71. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templating/renderer/widgets/checklist.py +0 -0
  72. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templating/renderer/widgets/dropdown.py +0 -0
  73. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templating/renderer/widgets/hidden.py +0 -0
  74. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templating/renderer/widgets/model.py +0 -0
  75. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templating/renderer/widgets/sequence.py +0 -0
  76. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templating/renderer/widgets/text.py +0 -0
  77. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/templating/renderer/widgets/union.py +0 -0
  78. {fastlifeweb-0.6.1 → fastlifeweb-0.7.0}/src/fastlife/views/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fastlifeweb
3
- Version: 0.6.1
3
+ Version: 0.7.0
4
4
  Summary: High-level web framework
5
5
  Home-page: https://github.com/mardiros/fastlife
6
6
  License: BSD-derived
@@ -19,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)
@@ -15,15 +15,15 @@ classifiers = [
15
15
  "Topic :: Software Development :: Libraries :: Python Modules",
16
16
  "Topic :: Internet :: WWW/HTTP",
17
17
  ]
18
- version = "0.6.1"
18
+ version = "0.7.0"
19
19
 
20
20
  [tool.poetry.dependencies]
21
21
  python = "^3.11"
22
22
  beautifulsoup4 = {version = "^4.12.2", extras = ["testing"]}
23
23
  behave = "^1.2.6"
24
- fastapi = "^0.110.0"
24
+ fastapi = "^0.112.0"
25
25
  itsdangerous = "^2.1.2"
26
- jinjax = "^0.34"
26
+ jinjax = "^0.37"
27
27
  markupsafe = "^2.1.3"
28
28
  multidict = "^6.0.5"
29
29
  pydantic = "^2.5.3"
@@ -1,3 +1,4 @@
1
+ """Configure fastlife app for dependency injection."""
1
2
  from .configurator import Configurator, configure
2
3
 
3
4
  __all__ = ["Configurator", "configure"]
@@ -2,6 +2,7 @@
2
2
  The configurator is here to register routes in a fastapi app,
3
3
  with dependency injection.
4
4
  """
5
+
5
6
  import importlib
6
7
  import inspect
7
8
  import logging
@@ -26,6 +27,7 @@ from fastapi.params import Depends as DependsType
26
27
  from fastapi.staticfiles import StaticFiles
27
28
 
28
29
  from fastlife.configurator.base import AbstractMiddleware
30
+ from fastlife.configurator.route_handler import FastlifeRoute
29
31
  from fastlife.security.csrf import check_csrf
30
32
 
31
33
  from .settings import Settings
@@ -38,6 +40,14 @@ VENUSIAN_CATEGORY = "fastlife"
38
40
 
39
41
 
40
42
  class Configurator:
43
+ """
44
+ Configure and build an application.
45
+
46
+ Initialize the app from the settings.
47
+
48
+ :param settings: Application settings.
49
+ """
50
+
41
51
  registry: "AppRegistry"
42
52
 
43
53
  def __init__(self, settings: Settings) -> None:
@@ -49,14 +59,37 @@ class Configurator:
49
59
  docs_url=None,
50
60
  redoc_url=None,
51
61
  )
62
+ FastlifeRoute.registry = self.registry
63
+ self._app.router.route_class = FastlifeRoute
52
64
  self.scanner = venusian.Scanner(fastlife=self)
53
65
  self.include("fastlife.views")
54
66
  self.include("fastlife.session")
55
67
 
56
68
  def get_app(self) -> FastAPI:
69
+ """
70
+ Get the app after configuration in order to start after beeing configured.
71
+
72
+ :return: FastAPI application
73
+ """
57
74
  return self._app
58
75
 
59
76
  def include(self, module: str | ModuleType) -> "Configurator":
77
+ """
78
+ Include a module in order to load its views.
79
+
80
+ Here is an example.
81
+
82
+ ::
83
+
84
+ from fastlife import Configurator, configure
85
+
86
+ @configure
87
+ def includeme(config: Configurator) -> None:
88
+ config.include(".views")
89
+
90
+
91
+ :param module: a module to include.
92
+ """
60
93
  if isinstance(module, str):
61
94
  package = None
62
95
  if module.startswith("."):
@@ -70,7 +103,10 @@ class Configurator:
70
103
  def add_middleware(
71
104
  self, middleware_class: Type[AbstractMiddleware], **options: Any
72
105
  ) -> Self:
73
- self._app.add_middleware(middleware_class, **options)
106
+ """
107
+ Add a starlette middleware to the FastAPI app.
108
+ """
109
+ self._app.add_middleware(middleware_class, **options) # type: ignore
74
110
  return self
75
111
 
76
112
  def add_route(
@@ -105,6 +141,7 @@ class Configurator:
105
141
  # generate_unique_id
106
142
  # ),
107
143
  ) -> "Configurator":
144
+ """Add a route to the app."""
108
145
  dependencies: List[DependsType] = []
109
146
  if permission:
110
147
  dependencies.append(Depends(self.registry.check_permission(permission)))
@@ -140,13 +177,22 @@ class Configurator:
140
177
  def add_static_route(
141
178
  self, route_path: str, directory: Path, name: str = "static"
142
179
  ) -> "Configurator":
143
- """Mount a directory to an http endpoint."""
180
+ """
181
+ Mount a directory to an http endpoint.
182
+
183
+ :param route_path: the root path for the statics
184
+ :param directory: the directory on the filesystem where the statics files are.
185
+ :param name: a name for the route in the starlette app.
186
+ :return: the configurator
187
+
188
+ """
144
189
  self._app.mount(route_path, StaticFiles(directory=directory), name=name)
145
190
  return self
146
191
 
147
192
  def add_exception_handler(
148
193
  self, status_code_or_exc: int | Type[Exception], handler: Any
149
194
  ) -> "Configurator":
195
+ """Add an exception handler the application."""
150
196
  self._app.add_exception_handler(status_code_or_exc, handler)
151
197
  return self
152
198
 
@@ -154,7 +200,9 @@ class Configurator:
154
200
  def configure(
155
201
  wrapped: Callable[[Configurator], None]
156
202
  ) -> Callable[[Configurator], None]:
157
- """Decorator used to attach route in a submodule while using the confirator.scan"""
203
+ """
204
+ Decorator used to attach route in a submodule while using the configurator.include.
205
+ """
158
206
 
159
207
  def callback(
160
208
  scanner: venusian.Scanner, name: str, ob: Callable[[venusian.Scanner], None]
@@ -2,6 +2,7 @@ from typing import TYPE_CHECKING, Annotated
2
2
 
3
3
  from fastapi import Depends
4
4
 
5
+ from fastlife.configurator.route_handler import FastlifeRequest
5
6
  from fastlife.security.policy import CheckPermission
6
7
  from fastlife.shared_utils.resolver import resolve
7
8
 
@@ -14,6 +15,11 @@ from .settings import Settings
14
15
 
15
16
 
16
17
  class AppRegistry:
18
+ """
19
+ The application registry got fastlife dependency injection.
20
+ It is initialized by the configurator and accessed by the `fastlife.Registry`.
21
+ """
22
+
17
23
  settings: Settings
18
24
  renderer: "AbstractTemplateRendererFactory"
19
25
  check_permission: CheckPermission
@@ -27,22 +33,17 @@ class AppRegistry:
27
33
  self.check_permission = resolve(settings.check_permission)
28
34
 
29
35
 
30
- DEFAULT_REGISTRY: AppRegistry = None # type: ignore
31
-
32
-
33
36
  def initialize_registry(settings: Settings) -> AppRegistry:
34
- global DEFAULT_REGISTRY
35
- if DEFAULT_REGISTRY is not None:
36
- raise ValueError("Registry is already set")
37
+ # global DEFAULT_REGISTRY
38
+ # if DEFAULT_REGISTRY is not None: # type: ignore
39
+ # raise ValueError("Registry is already set")
37
40
  AppRegistryCls = resolve(settings.registry_class)
38
- DEFAULT_REGISTRY = AppRegistryCls(settings) # type: ignore
39
- return DEFAULT_REGISTRY
41
+ return AppRegistryCls(settings) # type: ignore
40
42
 
41
43
 
42
- def cleanup_registry() -> None:
43
- """ "Method to cleanup the registry, used for testing"""
44
- global DEFAULT_REGISTRY
45
- DEFAULT_REGISTRY = None # type: ignore
44
+ def get_registry(request: FastlifeRequest) -> AppRegistry:
45
+ return request.registry
46
46
 
47
47
 
48
- Registry = Annotated[AppRegistry, Depends(lambda: DEFAULT_REGISTRY)]
48
+ Registry = Annotated[AppRegistry, Depends(get_registry)]
49
+ """FastAPI dependency to access to the registry."""
@@ -0,0 +1,30 @@
1
+ from typing import TYPE_CHECKING, Any, Callable, Coroutine
2
+
3
+ from fastapi import Request as BaseRequest
4
+ from fastapi.routing import APIRoute
5
+ from starlette.responses import Response
6
+ from starlette.types import Receive, Scope
7
+
8
+ if TYPE_CHECKING:
9
+ from .registry import AppRegistry # coverage: ignore
10
+
11
+
12
+ class FastlifeRequest(BaseRequest):
13
+ def __init__(self, registry: "AppRegistry", scope: Scope, receive: Receive) -> None:
14
+ super().__init__(scope, receive)
15
+ self.registry = registry
16
+
17
+
18
+ class FastlifeRoute(APIRoute):
19
+ registry: "AppRegistry" = None # type: ignore
20
+
21
+ def get_route_handler( # type: ignore
22
+ self,
23
+ ) -> Callable[[FastlifeRequest], Coroutine[Any, Any, Response]]:
24
+ orig_route_handler = super().get_route_handler()
25
+
26
+ async def route_handler(request: BaseRequest) -> FastlifeRequest:
27
+ req = FastlifeRequest(self.registry, request.scope, request.receive)
28
+ return await orig_route_handler(req) # type: ignore
29
+
30
+ return route_handler # type: ignore
@@ -0,0 +1,88 @@
1
+ """Settings for the fastlife."""
2
+ from datetime import timedelta
3
+ from typing import Literal
4
+
5
+ from pydantic import Field
6
+ from pydantic_settings import BaseSettings, SettingsConfigDict
7
+
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
+
16
+ model_config = SettingsConfigDict(env_prefix="fastlife_")
17
+ """Set the prefix fastlife_ for configuration using operating system environment."""
18
+
19
+ fastlife_route_prefix: str = Field(default="/_fl")
20
+ """Route prefix used for fastlife internal views."""
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
+ """
29
+ registry_class: str = Field(default="fastlife.configurator.registry:AppRegistry")
30
+ """Implementation class for the application regitry."""
31
+ template_renderer_class: str = Field(
32
+ default="fastlife.templating.renderer:JinjaxTemplateRenderer"
33
+ )
34
+ """
35
+ Implementation class for the :class:`fastlife.templatingAbstractTemplateRenderer`.
36
+ """
37
+ form_data_model_prefix: str = Field(default="payload")
38
+ """
39
+ Pydantic form default model prefix for serialized field in www-urlencoded-form.
40
+ """
41
+ csrf_token_name: str = Field(default="csrf_token")
42
+ """Name of the html input field for csrf token."""
43
+
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
+ """
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
+ """
56
+
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
+ """
63
+ session_cookie_name: str = Field(default="flsess")
64
+ """Cookie name for the session cookie."""
65
+ session_cookie_domain: str = Field(default="")
66
+ """Cookie domain for the session cookie."""
67
+ session_cookie_path: str = Field(default="/")
68
+ """Cookie path for the session cookie."""
69
+ session_duration: timedelta = Field(default=timedelta(days=14))
70
+ """Cookie duration for the session cookie."""
71
+ session_cookie_same_site: Literal["lax", "strict", "none"] = Field(default="lax")
72
+ """Cookie same-site for the session cookie."""
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
+ """
79
+ session_serializer: str = Field(
80
+ default="fastlife.session.serializer:SignedSessionSerializer"
81
+ )
82
+ """Cookie serializer for the session cookie."""
83
+
84
+ domain_name: str = Field(default="", title="domain name where the app is served")
85
+ """Domain name whre the app is served."""
86
+
87
+ check_permission: str = Field(default="fastlife.security.policy:check_permission")
88
+ """Handler for checking permission set on any views using the configurator."""
@@ -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)
@@ -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))
@@ -0,0 +1,107 @@
1
+ import abc
2
+ from typing import Any, Mapping, Optional, Type
3
+
4
+ from fastapi import Request
5
+ from markupsafe import Markup
6
+ from pydantic.fields import FieldInfo
7
+
8
+ from fastlife.request.model_result import ModelResult
9
+
10
+
11
+ class AbstractTemplateRenderer(abc.ABC):
12
+ """
13
+ An object that will be initialized by an AbstractTemplateRendererFactory,
14
+ passing the request to process.
15
+ """
16
+
17
+ route_prefix: str
18
+ """Used to buid pydantic form"""
19
+
20
+ @abc.abstractmethod
21
+ def render_template(
22
+ self,
23
+ template: str,
24
+ *,
25
+ globals: Optional[Mapping[str, Any]] = None,
26
+ **params: Any,
27
+ ) -> str:
28
+ """
29
+ Render the given template with the given params.
30
+
31
+ While rendering templates, the globals parameter is keps by the instantiated
32
+ renderer and sent to every rendering made by the request.
33
+ This is used by the pydantic form method that will render other templates
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.
45
+ """
46
+
47
+ @abc.abstractmethod
48
+ def pydantic_form(
49
+ self, model: ModelResult[Any], *, token: Optional[str] = None
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
+ """
72
+ ...
73
+
74
+ @abc.abstractmethod
75
+ def pydantic_form_field(
76
+ self,
77
+ model: Type[Any],
78
+ *,
79
+ name: str | None,
80
+ token: str | None,
81
+ removable: bool,
82
+ field: FieldInfo | None,
83
+ ) -> Markup:
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
+ """
92
+
93
+
94
+ class AbstractTemplateRendererFactory(abc.ABC):
95
+ """
96
+ The template render factory.
97
+ """
98
+
99
+ @abc.abstractmethod
100
+ def __call__(self, request: Request) -> AbstractTemplateRenderer:
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,36 +0,0 @@
1
- from datetime import timedelta
2
- from typing import Literal
3
-
4
- from pydantic import Field
5
- from pydantic_settings import BaseSettings, SettingsConfigDict
6
-
7
-
8
- class Settings(BaseSettings):
9
- model_config = SettingsConfigDict(env_prefix="fastlife_")
10
-
11
- fastlife_route_prefix: str = Field(default="/_fl")
12
- template_search_path: str = Field(default="fastlife:templates")
13
- registry_class: str = Field(default="fastlife.configurator.registry:AppRegistry")
14
- template_renderer_class: str = Field(
15
- default="fastlife.templating.renderer:JinjaxTemplateRenderer"
16
- )
17
- form_data_model_prefix: str = Field(default="payload")
18
- csrf_token_name: str = Field(default="csrf_token")
19
-
20
- jinjax_use_cache: bool = Field(default=True)
21
- jinjax_auto_reload: bool = Field(default=False)
22
-
23
- session_secret_key: str = Field(default="")
24
- session_cookie_name: str = Field(default="flsess")
25
- session_cookie_domain: str = Field(default="")
26
- session_cookie_path: str = Field(default="/")
27
- session_duration: timedelta = Field(default=timedelta(days=14))
28
- session_cookie_same_site: Literal["lax", "strict", "none"] = Field(default="lax")
29
- session_cookie_secure: bool = Field(default=False)
30
- session_serializer: str = Field(
31
- default="fastlife.session.serializer:SignedSessionSerializer"
32
- )
33
-
34
- domain_name: str = Field(default="", title="domain name where the app is served")
35
-
36
- check_permission: str = Field(default="fastlife.security.policy:check_permission")
@@ -1,60 +0,0 @@
1
- import abc
2
- from typing import Any, Mapping, Optional, Type
3
-
4
- from fastapi import Request
5
- from markupsafe import Markup
6
- from pydantic.fields import FieldInfo
7
-
8
- from fastlife.request.model_result import ModelResult
9
-
10
-
11
- class AbstractTemplateRenderer(abc.ABC):
12
- route_prefix: str
13
- """Used to buid pydantic form"""
14
-
15
- @abc.abstractmethod
16
- def render_template(
17
- self,
18
- template: str,
19
- *,
20
- globals: Optional[Mapping[str, Any]] = None,
21
- **params: Any,
22
- ) -> str:
23
- """
24
- Render the given template with the given params.
25
-
26
- While rendering templates, the globals parameter is keps by the instantiated
27
- renderer and sent to every rendering made by the request.
28
- This is used by the pydantic form method that will render other templates
29
- for the request.
30
- """
31
-
32
- @abc.abstractmethod
33
- 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,
41
- ) -> Markup:
42
- ...
43
-
44
- @abc.abstractmethod
45
- def pydantic_form_field(
46
- self,
47
- model: Type[Any],
48
- *,
49
- name: Optional[str] = None,
50
- token: Optional[str] = None,
51
- removable: bool = False,
52
- field: FieldInfo | None = None,
53
- ) -> Markup:
54
- ...
55
-
56
-
57
- class AbstractTemplateRendererFactory(abc.ABC):
58
- @abc.abstractmethod
59
- def __call__(self, request: Request) -> AbstractTemplateRenderer:
60
- ...
File without changes
File without changes