fastlifeweb 0.20.1__py3-none-any.whl → 0.22.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 (93) hide show
  1. CHANGELOG.md +18 -1
  2. fastlife/__init__.py +45 -13
  3. fastlife/adapters/__init__.py +1 -1
  4. fastlife/adapters/fastapi/__init__.py +9 -0
  5. fastlife/adapters/fastapi/form.py +26 -0
  6. fastlife/{request → adapters/fastapi}/form_data.py +1 -1
  7. fastlife/{request → adapters/fastapi}/localizer.py +4 -2
  8. fastlife/adapters/fastapi/request.py +33 -0
  9. fastlife/{routing → adapters/fastapi/routing}/route.py +3 -3
  10. fastlife/{routing → adapters/fastapi/routing}/router.py +1 -1
  11. fastlife/adapters/itsdangerous/__init__.py +3 -0
  12. fastlife/adapters/itsdangerous/session.py +50 -0
  13. fastlife/adapters/jinjax/jinjax_ext/inspectable_component.py +7 -7
  14. fastlife/adapters/jinjax/jinjax_ext/jinjax_doc.py +1 -1
  15. fastlife/adapters/jinjax/renderer.py +9 -57
  16. fastlife/adapters/jinjax/widget_factory/bool_builder.py +2 -2
  17. fastlife/adapters/jinjax/widget_factory/emailstr_builder.py +5 -4
  18. fastlife/adapters/jinjax/widget_factory/enum_builder.py +2 -2
  19. fastlife/adapters/jinjax/widget_factory/factory.py +32 -23
  20. fastlife/adapters/jinjax/widget_factory/literal_builder.py +7 -6
  21. fastlife/adapters/jinjax/widget_factory/model_builder.py +3 -3
  22. fastlife/adapters/jinjax/widget_factory/secretstr_builder.py +2 -2
  23. fastlife/adapters/jinjax/widget_factory/sequence_builder.py +3 -3
  24. fastlife/adapters/jinjax/widget_factory/set_builder.py +2 -2
  25. fastlife/adapters/jinjax/widget_factory/simpletype_builder.py +7 -8
  26. fastlife/adapters/jinjax/widget_factory/union_builder.py +3 -3
  27. fastlife/adapters/jinjax/widgets/base.py +36 -36
  28. fastlife/adapters/jinjax/widgets/boolean.py +13 -34
  29. fastlife/adapters/jinjax/widgets/checklist.py +36 -42
  30. fastlife/adapters/jinjax/widgets/dropdown.py +32 -38
  31. fastlife/adapters/jinjax/widgets/hidden.py +7 -15
  32. fastlife/adapters/jinjax/widgets/model.py +36 -43
  33. fastlife/adapters/jinjax/widgets/sequence.py +63 -42
  34. fastlife/adapters/jinjax/widgets/text.py +39 -78
  35. fastlife/adapters/jinjax/widgets/union.py +51 -58
  36. fastlife/components/CsrfToken.jinja +1 -1
  37. fastlife/components/Form.jinja +1 -1
  38. fastlife/components/pydantic_form/FatalError.jinja +8 -0
  39. fastlife/components/pydantic_form/Widget.jinja +4 -3
  40. fastlife/config/__init__.py +3 -6
  41. fastlife/config/configurator.py +80 -32
  42. fastlife/config/exceptions.py +0 -2
  43. fastlife/config/resources.py +1 -2
  44. fastlife/config/views.py +2 -4
  45. fastlife/domain/__init__.py +1 -0
  46. fastlife/domain/model/__init__.py +1 -0
  47. fastlife/domain/model/asgi.py +3 -0
  48. fastlife/domain/model/csrf.py +19 -0
  49. fastlife/{request → domain/model}/form.py +13 -22
  50. fastlife/{request → domain/model}/request.py +26 -30
  51. fastlife/domain/model/security_policy.py +105 -0
  52. fastlife/{templates/inline.py → domain/model/template.py} +8 -0
  53. fastlife/domain/model/types.py +17 -0
  54. fastlife/middlewares/base.py +1 -1
  55. fastlife/middlewares/reverse_proxy/x_forwarded.py +1 -2
  56. fastlife/middlewares/session/__init__.py +2 -2
  57. fastlife/middlewares/session/middleware.py +4 -3
  58. fastlife/middlewares/session/serializer.py +0 -44
  59. fastlife/{services/policy.py → service/check_permission.py} +1 -1
  60. fastlife/{security → service}/csrf.py +5 -15
  61. fastlife/{services → service}/locale_negociator.py +5 -8
  62. fastlife/{config → service}/registry.py +13 -7
  63. fastlife/service/security_policy.py +100 -0
  64. fastlife/{services → service}/templates.py +10 -48
  65. fastlife/{services → service}/translations.py +15 -0
  66. fastlife/{config/settings.py → settings.py} +6 -12
  67. fastlife/shared_utils/infer.py +24 -1
  68. fastlife/{templates/constants.py → template_globals.py} +2 -2
  69. fastlife/testing/testclient.py +2 -2
  70. fastlife/views/__init__.py +1 -0
  71. fastlife/views/pydantic_form.py +6 -0
  72. {fastlifeweb-0.20.1.dist-info → fastlifeweb-0.22.0.dist-info}/METADATA +1 -1
  73. {fastlifeweb-0.20.1.dist-info → fastlifeweb-0.22.0.dist-info}/RECORD +79 -80
  74. tailwind.config.js +1 -1
  75. fastlife/components/pydantic_form/Boolean.jinja +0 -13
  76. fastlife/components/pydantic_form/Checklist.jinja +0 -21
  77. fastlife/components/pydantic_form/Dropdown.jinja +0 -18
  78. fastlife/components/pydantic_form/Hidden.jinja +0 -3
  79. fastlife/components/pydantic_form/Model.jinja +0 -30
  80. fastlife/components/pydantic_form/Sequence.jinja +0 -47
  81. fastlife/components/pydantic_form/Text.jinja +0 -11
  82. fastlife/components/pydantic_form/Textarea.jinja +0 -38
  83. fastlife/components/pydantic_form/Union.jinja +0 -34
  84. fastlife/request/__init__.py +0 -5
  85. fastlife/security/__init__.py +0 -1
  86. fastlife/security/policy.py +0 -188
  87. fastlife/templates/__init__.py +0 -12
  88. fastlife/templates/binding.py +0 -52
  89. /fastlife/{routing → adapters/fastapi/routing}/__init__.py +0 -0
  90. /fastlife/{services → service}/__init__.py +0 -0
  91. {fastlifeweb-0.20.1.dist-info → fastlifeweb-0.22.0.dist-info}/WHEEL +0 -0
  92. {fastlifeweb-0.20.1.dist-info → fastlifeweb-0.22.0.dist-info}/entry_points.txt +0 -0
  93. {fastlifeweb-0.20.1.dist-info → fastlifeweb-0.22.0.dist-info}/licenses/LICENSE +0 -0
@@ -2,52 +2,73 @@ from collections.abc import Sequence
2
2
  from typing import Any
3
3
 
4
4
  from markupsafe import Markup
5
+ from pydantic import Field
5
6
 
6
- from fastlife.services.templates import AbstractTemplateRenderer
7
-
8
- from .base import TypeWrapper, Widget
9
-
10
-
11
- class SequenceWidget(Widget[Sequence[Widget[Any]]]):
12
- def __init__(
13
- self,
14
- name: str,
15
- *,
16
- title: str | None,
17
- hint: str | None = None,
18
- aria_label: str | None = None,
19
- value: Sequence[Widget[Any]] | None,
20
- error: str | None = None,
21
- item_type: type[Any],
22
- token: str,
23
- removable: bool,
24
- ):
25
- super().__init__(
26
- name,
27
- value=value,
28
- error=error,
29
- title=title,
30
- hint=hint,
31
- aria_label=aria_label,
32
- token=token,
33
- removable=removable,
34
- )
35
- self.item_type = item_type
36
-
37
- def get_template(self) -> str:
38
- return "pydantic_form.Sequence.jinja"
7
+ from fastlife.service.templates import AbstractTemplateRenderer
8
+
9
+ from .base import TWidget, TypeWrapper, Widget
10
+
11
+
12
+ class SequenceWidget(Widget[Sequence[TWidget]]):
13
+ template = """
14
+ <pydantic_form.Widget :widget_id="id" :removable="removable">
15
+ <Details :id="id">
16
+ <Summary :id="id + '-summary'">
17
+ <H3 :class="H3_SUMMARY_CLASS">{{title}}</H3>
18
+ <pydantic_form.Error :text="error" />
19
+ </Summary>
20
+ <div>
21
+ {% set fnGetName = "get" + id.replace("-", "_") %}
22
+ <script>
23
+ function {{ fnGetName }} () {
24
+ const el = document.getElementById("{{id}}-content");
25
+ const len = el.dataset.length;
26
+ el.dataset.length = parseInt(len) + 1;
27
+ return "{{wrapped_type.name}}." + len;
28
+ }
29
+ </script>
30
+
31
+ <div id="{{id}}-content" class="m-4"
32
+ data-length="{{children_widgets|length|string}}">
33
+ {% set container_id = id + "-children-container" %}
34
+ <div id="{{container_id}}">
35
+ {% for child in children_widgets %}
36
+ {{ child }}
37
+ {% endfor%}
38
+ </div>
39
+ </div>
40
+
41
+ <div>
42
+ {% set container_id = "#" + id + "-children-container" %}
43
+ {% set add_id = id + "-add" %}
44
+ {% set vals = 'js:{"name": '
45
+ + fnGetName
46
+ + '(), "token": "'
47
+ + wrapped_type.token + '", "removable": true}' %}
48
+ <Button type="button"
49
+ :hx-target="container_id" hx-swap="beforeend"
50
+ :id="add_id"
51
+ :hx-vals="vals"
52
+ :hx-get="wrapped_type.url">
53
+ Add
54
+ </Button>
55
+ </div>
56
+ </div>
57
+ </Details>
58
+ </pydantic_form.Widget>
59
+ """
60
+
61
+ item_type: type[Any]
62
+ wrapped_type: TypeWrapper | None = Field(default=None)
63
+ children_widgets: list[str] = Field(default=None)
39
64
 
40
65
  def build_item_type(self, route_prefix: str) -> TypeWrapper:
41
66
  return TypeWrapper(self.item_type, route_prefix, self.name, self.token)
42
67
 
43
68
  def to_html(self, renderer: "AbstractTemplateRenderer") -> Markup:
44
69
  """Return the html version."""
45
- children = [Markup(item.to_html(renderer)) for item in self.value or []]
46
- return Markup(
47
- renderer.render_template(
48
- self.get_template(),
49
- widget=self,
50
- type=self.build_item_type(renderer.route_prefix),
51
- children_widgets=children,
52
- )
53
- )
70
+ self.wrapped_type = self.build_item_type(renderer.route_prefix)
71
+ self.children_widgets = [
72
+ Markup(item.to_html(renderer)) for item in self.value or []
73
+ ]
74
+ return Markup(renderer.render_template(self))
@@ -1,52 +1,31 @@
1
1
  from collections.abc import Sequence
2
2
 
3
+ from pydantic import Field
4
+
5
+ from fastlife.domain.model.types import Builtins
6
+
3
7
  from .base import Widget
4
8
 
5
9
 
6
- class TextWidget(Widget[str]):
10
+ class TextWidget(Widget[Builtins]):
7
11
  """
8
12
  Widget for text like field (email, ...).
9
-
10
- :param name: input name.
11
- :param title: title for the widget.
12
- :param hint: hint for human.
13
- :param aria_label: html input aria-label value.
14
- :param placeholder: html input placeholder value.
15
- :param error: error of the value if any.
16
- :param value: current value.
17
- :param removable: display a button to remove the widget for optional fields.
18
- :param token: token used to get unique id on the form.
19
13
  """
20
14
 
21
- def __init__(
22
- self,
23
- name: str,
24
- *,
25
- title: str | None,
26
- hint: str | None = None,
27
- aria_label: str | None = None,
28
- placeholder: str | None = None,
29
- error: str | None = None,
30
- value: str = "",
31
- input_type: str = "text",
32
- removable: bool = False,
33
- token: str,
34
- ) -> None:
35
- super().__init__(
36
- name,
37
- value=value,
38
- title=title,
39
- hint=hint,
40
- aria_label=aria_label,
41
- token=token,
42
- error=error,
43
- removable=removable,
44
- )
45
- self.placeholder = placeholder or ""
46
- self.input_type = input_type
15
+ template = """
16
+ <pydantic_form.Widget :widget_id="id" :removable="removable">
17
+ <div class="pt-4">
18
+ <Label :for="id">{{title}}</Label>
19
+ <pydantic_form.Error :text="error" />
20
+ <Input :name="name" :value="value" :type="input_type" :id="id"
21
+ :aria-label="aria_label" :placeholder="placeholder" />
22
+ <pydantic_form.Hint :text="hint" />
23
+ </div>
24
+ </pydantic_form.Widget>
25
+ """
47
26
 
48
- def get_template(self) -> str:
49
- return "pydantic_form.Text.jinja"
27
+ input_type: str = Field(default="text")
28
+ placeholder: str | None = Field(default=None)
50
29
 
51
30
 
52
31
  class TextareaWidget(Widget[Sequence[str]]):
@@ -54,12 +33,13 @@ class TextareaWidget(Widget[Sequence[str]]):
54
33
  Render a Textearea for a string or event a sequence of string.
55
34
 
56
35
  ```
36
+ from fastlife.adapters.jinjax.widgets.base import CustomWidget
57
37
  from fastlife.adapters.jinjax.widgets.text import TextareaWidget
58
38
  from pydantic import BaseModel, Field, field_validator
59
39
 
60
40
  class TaggedParagraphForm(BaseModel):
61
- paragraph: Annotated[str, TextareaWidget] = Field(...)
62
- tags: Annotated[Sequence[str], TextareaWidget] = Field(
41
+ paragraph: Annotated[str, CustomWidget(TextareaWidget)] = Field(...)
42
+ tags: Annotated[Sequence[str], CustomWidget(TextareaWidget)] = Field(
63
43
  default_factory=list,
64
44
  title="Tags",
65
45
  description="One tag per line",
@@ -69,43 +49,24 @@ class TextareaWidget(Widget[Sequence[str]]):
69
49
  def split(cls, s: Any) -> Sequence[str]:
70
50
  return s.split() if s else []
71
51
  ```
72
-
73
- :param name: input name.
74
- :param title: title for the widget.
75
- :param hint: hint for human.
76
- :param aria_label: html input aria-label value.
77
- :param placeholder: html input placeholder value.
78
- :param error: error of the value if any.
79
- :param value: current value.
80
- :param removable: display a button to remove the widget for optional fields.
81
- :param token: token used to get unique id on the form.
82
-
83
52
  """
84
53
 
85
- def __init__(
86
- self,
87
- name: str,
88
- *,
89
- title: str | None,
90
- hint: str | None = None,
91
- aria_label: str | None = None,
92
- placeholder: str | None = None,
93
- error: str | None = None,
94
- value: Sequence[str] | None = None,
95
- removable: bool = False,
96
- token: str,
97
- ) -> None:
98
- super().__init__(
99
- name,
100
- value=value or [],
101
- title=title,
102
- hint=hint,
103
- aria_label=aria_label,
104
- token=token,
105
- error=error,
106
- removable=removable,
107
- )
108
- self.placeholder = placeholder or ""
54
+ template = """
55
+ <pydantic_form.Widget :widget_id="id" :removable="removable">
56
+ <div class="pt-4">
57
+ <Label :for="id">{{title}}</Label>
58
+ <pydantic_form.Error :text="error" />
59
+ <Textarea :name="name" :id="id" :aria-label="aria_label">
60
+ {%- if v is string -%}
61
+ {{- v -}}}
62
+ {%- else -%}
63
+ {%- for v in value %}{{v}}
64
+ {% endfor -%}
65
+ {% endif %}
66
+ </Textarea>
67
+ <pydantic_form.Hint :text="hint" />
68
+ </div>
69
+ </pydantic_form.Widget>
70
+ """
109
71
 
110
- def get_template(self) -> str:
111
- return "pydantic_form.Textarea.jinja"
72
+ placeholder: str = Field(default="")
@@ -3,57 +3,59 @@ Widget for field of type Union.
3
3
  """
4
4
 
5
5
  from collections.abc import Sequence
6
- from typing import Any, Union
6
+ from typing import Union
7
7
 
8
8
  from markupsafe import Markup
9
- from pydantic import BaseModel
9
+ from pydantic import BaseModel, Field
10
10
 
11
- from fastlife.services.templates import AbstractTemplateRenderer
11
+ from fastlife.service.templates import AbstractTemplateRenderer
12
12
 
13
- from .base import TypeWrapper, Widget
13
+ from .base import TWidget, TypeWrapper, Widget
14
14
 
15
15
 
16
- class UnionWidget(Widget[Widget[Any]]):
16
+ class UnionWidget(Widget[TWidget]):
17
17
  """
18
18
  Widget for union types.
19
+ """
19
20
 
20
- :param name: input name.
21
- :param title: title for the widget.
22
- :param hint: hint for human.
23
- :param aria_label: html input aria-label value.
24
- :param value: current value.
25
- :param error: error of the value if any.
26
- :param children_types: childrens types list.
27
- :param removable: display a button to remove the widget for optional fields.
28
- :param token: token used to get unique id on the form.
29
-
21
+ template = """
22
+ <pydantic_form.Widget :widget_id="id" :removable="removable">
23
+ <div id="{{id}}">
24
+ <Details>
25
+ <Summary :id="id + '-union-summary'">
26
+ <H3 :class="H3_SUMMARY_CLASS">{{title}}</H3>
27
+ <pydantic_form.Error :text="error" />
28
+ </Summary>
29
+ <div hx-sync="this" id="{{id}}-child">
30
+ {% if child %}
31
+ {{ child }}
32
+ {% else %}
33
+ {% for typ in types %}
34
+ <Button type="button"
35
+ hx-target="closest div"
36
+ :hx-get="typ.url"
37
+ :hx-vals="typ.params|tojson"
38
+ :id="typ.id"
39
+ onclick={{ "document.getElementById('" + id + "-remove-btn').hidden=false" }}
40
+ :class="SECONDARY_BUTTON_CLASS">{{typ.title}}</Button>
41
+ {% endfor %}
42
+ {% endif %}
43
+ </div>
44
+ <Button type="button" :id="id + '-remove-btn'" :hx-target="'#' + id"
45
+ :hx-vals="parent_type.params|tojson" :hx-get="parent_type.url" :hidden="not child"
46
+ :class="SECONDARY_BUTTON_CLASS">
47
+ Remove
48
+ </Button>
49
+ </Details>
50
+ </div>
51
+ </pydantic_form.Widget>
30
52
  """
31
53
 
32
- def __init__(
33
- self,
34
- name: str,
35
- *,
36
- title: str | None,
37
- hint: str | None = None,
38
- aria_label: str | None = None,
39
- value: Widget[Any] | None,
40
- error: str | None = None,
41
- children_types: Sequence[type[BaseModel]],
42
- removable: bool = False,
43
- token: str,
44
- ):
45
- super().__init__(
46
- name,
47
- value=value,
48
- error=error,
49
- title=title,
50
- hint=hint,
51
- aria_label=aria_label,
52
- token=token,
53
- removable=removable,
54
- )
55
- self.children_types = children_types
56
- self.parent_name = name
54
+ children_types: Sequence[type[BaseModel]]
55
+ parent_type: TypeWrapper | None = Field(default=None)
56
+
57
+ types: Sequence[TypeWrapper] | None = Field(default=None)
58
+ child: str = Field(default="")
57
59
 
58
60
  def build_types(self, route_prefix: str) -> Sequence[TypeWrapper]:
59
61
  """Wrap types in the union in order to get the in their own widgets."""
@@ -62,24 +64,15 @@ class UnionWidget(Widget[Widget[Any]]):
62
64
  for typ in self.children_types
63
65
  ]
64
66
 
65
- def get_template(self) -> str:
66
- return "pydantic_form.Union.jinja"
67
-
68
67
  def to_html(self, renderer: "AbstractTemplateRenderer") -> Markup:
69
68
  """Return the html version."""
70
- child = Markup(self.value.to_html(renderer)) if self.value else ""
71
- return Markup(
72
- renderer.render_template(
73
- self.get_template(),
74
- widget=self,
75
- types=self.build_types(renderer.route_prefix),
76
- parent_type=TypeWrapper(
77
- Union[tuple(self.children_types)], # type: ignore # noqa: UP007
78
- renderer.route_prefix,
79
- self.parent_name,
80
- self.token,
81
- title=self.title,
82
- ),
83
- child=child,
84
- )
69
+ self.child = Markup(self.value.to_html(renderer)) if self.value else ""
70
+ self.types = self.build_types(renderer.route_prefix)
71
+ self.parent_type = TypeWrapper(
72
+ Union[tuple(self.children_types)], # type: ignore # noqa: UP007
73
+ renderer.route_prefix,
74
+ self.name,
75
+ self.token,
76
+ title=self.title,
85
77
  )
78
+ return Markup(renderer.render_template(self))
@@ -2,4 +2,4 @@
2
2
  a :jinjax:component:`Hidden` field automaticaly injected in every
3
3
  :jinjax:component:`Form` to protect against CSRF Attacks.
4
4
  #}
5
- <Hidden :name="csrf_token.name" :value="csrf_token.value" />
5
+ <Hidden :name="request.csrf_token.name" :value="request.csrf_token.value" />
@@ -2,7 +2,7 @@
2
2
  Create html ``<form>`` node with htmx support by default.
3
3
  A :jinjax:component:`CsrfToken` will always be included in the form
4
4
  and will be checked by the
5
- :func:`csrf policy method <fastlife.security.csrf.check_csrf>`.
5
+ :func:`csrf policy method <fastlife.service.csrf.check_csrf>`.
6
6
 
7
7
  ::
8
8
 
@@ -0,0 +1,8 @@
1
+ {# doc display an error for a field. #}
2
+ {# def message: Annotated[str | None, "error message"] #}
3
+ {%- if message -%}
4
+ <div class="flex items-center bg-red-50 border border-red-400 text-red-700" role="alert">
5
+ <icons.Fire class="m-3 w-16 h-16 fill-orange-500" />
6
+ <span class="sm:inline text-xl">{{ message }}</span>
7
+ </div>
8
+ {%- endif -%}
@@ -1,11 +1,12 @@
1
1
  {# doc Base component for widget #}
2
2
  {# def
3
- widget: Annotated[fastlife.adapters.jinjax.widgets.base.Widget, "widget to display."],
3
+ widget_id: Annotated[str, "widget to display."],
4
+ removable: Annotated[bool, "Set to true to add a remove button"],
4
5
  #}
5
- {% set container_id = widget.id + "-container" %}
6
+ {% set container_id = widget_id + "-container" %}
6
7
  <div id="{{container_id}}">
7
8
  {{ content }}
8
- {% if widget.removable %}
9
+ {% if removable %}
9
10
  <Button type="button" :onclick={{"document.getElementById('" + container_id + "').remove()" }}>
10
11
  Remove
11
12
  </Button>
@@ -1,19 +1,16 @@
1
1
  """Configure fastlife app for dependency injection."""
2
2
 
3
3
  from .configurator import Configurator, GenericConfigurator, configure
4
- from .registry import DefaultRegistry, GenericRegistry
4
+ from .exceptions import exception_handler
5
5
  from .resources import resource, resource_view
6
- from .settings import Settings
7
6
  from .views import view_config
8
7
 
9
8
  __all__ = [
10
9
  "Configurator",
11
10
  "GenericConfigurator",
12
11
  "configure",
13
- "view_config",
12
+ "exception_handler",
14
13
  "resource",
15
14
  "resource_view",
16
- "GenericRegistry",
17
- "DefaultRegistry",
18
- "Settings",
15
+ "view_config",
19
16
  ]
@@ -12,8 +12,9 @@ phase.
12
12
  """
13
13
 
14
14
  import logging
15
+ from asyncio import iscoroutine
15
16
  from collections import defaultdict
16
- from collections.abc import Callable, Mapping, Sequence
17
+ from collections.abc import Callable, Sequence
17
18
  from enum import Enum
18
19
  from pathlib import Path
19
20
  from types import ModuleType
@@ -26,25 +27,29 @@ from fastapi.params import Depends as DependsType
26
27
  from fastapi.staticfiles import StaticFiles
27
28
  from fastapi.types import IncEx
28
29
 
30
+ from fastlife.adapters.fastapi.request import GenericRequest, Request
31
+ from fastlife.adapters.fastapi.routing.route import Route
32
+ from fastlife.adapters.fastapi.routing.router import Router
29
33
  from fastlife.config.openapiextra import OpenApiTag
34
+ from fastlife.domain.model.template import InlineTemplate
30
35
  from fastlife.middlewares.base import AbstractMiddleware
31
- from fastlife.request.request import GenericRequest, Request
32
- from fastlife.routing.route import Route
33
- from fastlife.routing.router import Router
34
- from fastlife.security.csrf import check_csrf
35
- from fastlife.services.policy import check_permission
36
- from fastlife.shared_utils.resolver import resolve, resolve_maybe_relative
37
-
38
- from .registry import DefaultRegistry, TRegistry
39
- from .settings import Settings
36
+ from fastlife.service.check_permission import check_permission
37
+ from fastlife.service.csrf import check_csrf
38
+ from fastlife.service.registry import DefaultRegistry, TRegistry
39
+ from fastlife.settings import Settings
40
+ from fastlife.shared_utils.infer import is_inline_template_returned
41
+ from fastlife.shared_utils.resolver import (
42
+ resolve,
43
+ resolve_maybe_relative,
44
+ )
40
45
 
41
46
  if TYPE_CHECKING:
42
- from fastlife.security.policy import AbstractSecurityPolicy # coverage: ignore
43
- from fastlife.services.templates import (
47
+ from fastlife.service.security_policy import AbstractSecurityPolicy
48
+ from fastlife.service.templates import (
44
49
  AbstractTemplateRendererFactory, # coverage: ignore
45
50
  )
46
51
 
47
- from fastlife.services.locale_negociator import LocaleNegociator
52
+ from fastlife.service.locale_negociator import LocaleNegociator
48
53
 
49
54
  log = logging.getLogger(__name__)
50
55
  VENUSIAN_CATEGORY = "fastlife"
@@ -143,6 +148,8 @@ class GenericConfigurator(Generic[TRegistry]):
143
148
  ] = {}
144
149
 
145
150
  self._registered_permissions: set[str] = set()
151
+
152
+ self._renderer_globals: dict[str, Any] = {}
146
153
  self.scanner = venusian.Scanner(fastlife=self)
147
154
  self.include("fastlife.views")
148
155
  self.include("fastlife.middlewares")
@@ -441,6 +448,53 @@ class GenericConfigurator(Generic[TRegistry]):
441
448
  )
442
449
  return self
443
450
 
451
+ def add_renderer_global(
452
+ self, name: str, value: Any, *, evaluate: bool = True
453
+ ) -> None:
454
+ """
455
+ Add a rendering global value.
456
+
457
+ :param name: the name or key of the global value available in the template.
458
+ :param value: a value, or a callable or
459
+ an async function with a request in parameter that will evaluate the value.
460
+ :param evaluate: set to false if you want to inject helper methods in the
461
+ template.
462
+ """
463
+ self._renderer_globals[name] = value, evaluate
464
+
465
+ async def _build_renderer_globals(self, request: Request) -> dict[str, Any]:
466
+ """
467
+ Build globals variables accessible in any templates.
468
+
469
+ * `request` is the {class}`current request <fastlife.request.request.Request>`
470
+ * `authenticated_user` is used to access to the authenticated user if the
471
+ security policy has been installed.
472
+ * `csrf_token` is used to build for {jinjax:component}`CsrfToken`.
473
+ * `gettext`, `ngettext`, `dgettext`, `dngettext`, `pgettext`, `dpgettext`,
474
+ `npgettext`, `dnpgettext` methods are installed for i18n purpose.
475
+ """
476
+ lczr = request.registry.localizer(request)
477
+ custom_globals = {}
478
+ for key, (val, evaluate) in self._renderer_globals.items():
479
+ if evaluate and callable(val):
480
+ val = val(request)
481
+ if iscoroutine(val):
482
+ val = await val
483
+ custom_globals[key] = val
484
+ return {
485
+ "request": request,
486
+ "gettext": lczr.gettext,
487
+ "ngettext": lczr.ngettext,
488
+ "dgettext": lczr.dgettext,
489
+ "dngettext": lczr.dngettext,
490
+ "pgettext": lczr.pgettext,
491
+ "dpgettext": lczr.dpgettext,
492
+ "npgettext": lczr.npgettext,
493
+ "dnpgettext": lczr.dnpgettext,
494
+ **custom_globals,
495
+ **request.renderer_globals,
496
+ }
497
+
444
498
  def add_route(
445
499
  self,
446
500
  name: str,
@@ -466,10 +520,8 @@ class GenericConfigurator(Generic[TRegistry]):
466
520
  :param path: path of the route, use `{curly_brace}` to inject FastAPI Path
467
521
  parameters.
468
522
  :param endpoint: the function that will reveive the request.
469
- :param template: the template rendered by the
470
- {class}`fastlife.service.templates.AbstractTemplateRenderer`.
471
523
  :param permission: a permission to validate by the
472
- {class}`Security Policy <fastlife.security.policy.AbstractSecurityPolicy>`.
524
+ {class}`Security Policy <fastlife.service.security_policy.AbstractSecurityPolicy>`.
473
525
  :param status_code: customize response status code.
474
526
  :param methods: restrict route to a list of http methods.
475
527
  :return: the configurator.
@@ -479,17 +531,21 @@ class GenericConfigurator(Generic[TRegistry]):
479
531
  self._registered_permissions.add(permission)
480
532
  dependencies.append(Depends(check_permission(permission)))
481
533
 
482
- if template:
534
+ if is_inline_template_returned(endpoint):
483
535
 
484
- def render(
536
+ async def render(
485
537
  request: Request,
486
- resp: Annotated[Response | Mapping[str, Any], Depends(endpoint)],
538
+ resp: Annotated["Response | InlineTemplate", Depends(endpoint)],
487
539
  ) -> Response:
488
540
  if isinstance(resp, Response):
489
541
  return resp
542
+
543
+ template = resp.renderer
544
+ globs = await self._build_renderer_globals(request)
490
545
  return request.registry.get_renderer(template)(request).render(
491
546
  template,
492
547
  params=resp,
548
+ globals=globs,
493
549
  )
494
550
 
495
551
  endpoint = render
@@ -523,9 +579,8 @@ class GenericConfigurator(Generic[TRegistry]):
523
579
  def add_exception_handler(
524
580
  self,
525
581
  status_code_or_exc: int | type[Exception],
526
- handler: Any,
582
+ handler: Callable[..., "Response | InlineTemplate"],
527
583
  *,
528
- template: str | None = None,
529
584
  status_code: int = 500,
530
585
  ) -> Self:
531
586
  """
@@ -533,7 +588,7 @@ class GenericConfigurator(Generic[TRegistry]):
533
588
 
534
589
  """
535
590
 
536
- def exception_handler(request: BaseRequest, exc: Exception) -> Any:
591
+ async def exception_handler(request: BaseRequest, exc: Exception) -> Any:
537
592
  # FastAPI exception handler does not provide our request object
538
593
  # it seems like it is rebuild from the asgi scope. Even the router
539
594
  # class is wrong.
@@ -544,18 +599,11 @@ class GenericConfigurator(Generic[TRegistry]):
544
599
  if isinstance(resp, Response):
545
600
  return resp
546
601
 
547
- if not template:
548
- raise RuntimeError(
549
- "No template set for "
550
- f"{exc.__module__}:{exc.__class__.__qualname__} but "
551
- f"{handler.__module__}:{handler.__qualname__} "
552
- "did not return a Response"
553
- )
554
-
555
- return req.registry.get_renderer(template)(req).render(
556
- template,
602
+ return req.registry.get_renderer(resp.renderer)(req).render(
603
+ resp.template,
557
604
  params=resp,
558
605
  status_code=status_code,
606
+ globals=(await self._build_renderer_globals(req)),
559
607
  )
560
608
 
561
609
  self.exception_handlers.append((status_code_or_exc, exception_handler))