fastlifeweb 0.20.1__py3-none-any.whl → 0.21.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 (54) hide show
  1. CHANGELOG.md +10 -0
  2. fastlife/__init__.py +5 -0
  3. fastlife/adapters/jinjax/renderer.py +4 -52
  4. fastlife/adapters/jinjax/widget_factory/bool_builder.py +2 -2
  5. fastlife/adapters/jinjax/widget_factory/emailstr_builder.py +5 -4
  6. fastlife/adapters/jinjax/widget_factory/enum_builder.py +2 -2
  7. fastlife/adapters/jinjax/widget_factory/factory.py +20 -21
  8. fastlife/adapters/jinjax/widget_factory/literal_builder.py +7 -6
  9. fastlife/adapters/jinjax/widget_factory/model_builder.py +3 -3
  10. fastlife/adapters/jinjax/widget_factory/secretstr_builder.py +2 -2
  11. fastlife/adapters/jinjax/widget_factory/sequence_builder.py +3 -3
  12. fastlife/adapters/jinjax/widget_factory/set_builder.py +2 -2
  13. fastlife/adapters/jinjax/widget_factory/simpletype_builder.py +7 -8
  14. fastlife/adapters/jinjax/widget_factory/union_builder.py +3 -3
  15. fastlife/adapters/jinjax/widgets/base.py +35 -35
  16. fastlife/adapters/jinjax/widgets/boolean.py +13 -34
  17. fastlife/adapters/jinjax/widgets/checklist.py +36 -42
  18. fastlife/adapters/jinjax/widgets/dropdown.py +32 -38
  19. fastlife/adapters/jinjax/widgets/hidden.py +7 -15
  20. fastlife/adapters/jinjax/widgets/model.py +33 -40
  21. fastlife/adapters/jinjax/widgets/sequence.py +61 -40
  22. fastlife/adapters/jinjax/widgets/text.py +39 -78
  23. fastlife/adapters/jinjax/widgets/union.py +50 -57
  24. fastlife/components/CsrfToken.jinja +1 -1
  25. fastlife/components/pydantic_form/Widget.jinja +4 -3
  26. fastlife/config/configurator.py +65 -19
  27. fastlife/config/exceptions.py +0 -2
  28. fastlife/config/views.py +0 -2
  29. fastlife/domain/__init__.py +1 -0
  30. fastlife/domain/model/__init__.py +1 -0
  31. fastlife/domain/model/security.py +19 -0
  32. fastlife/domain/model/template.py +30 -0
  33. fastlife/domain/model/types.py +17 -0
  34. fastlife/request/request.py +19 -0
  35. fastlife/security/csrf.py +3 -13
  36. fastlife/services/templates.py +9 -42
  37. fastlife/services/translations.py +12 -0
  38. fastlife/templates/__init__.py +1 -6
  39. fastlife/templates/inline.py +18 -14
  40. {fastlifeweb-0.20.1.dist-info → fastlifeweb-0.21.0.dist-info}/METADATA +1 -1
  41. {fastlifeweb-0.20.1.dist-info → fastlifeweb-0.21.0.dist-info}/RECORD +44 -49
  42. fastlife/components/pydantic_form/Boolean.jinja +0 -13
  43. fastlife/components/pydantic_form/Checklist.jinja +0 -21
  44. fastlife/components/pydantic_form/Dropdown.jinja +0 -18
  45. fastlife/components/pydantic_form/Hidden.jinja +0 -3
  46. fastlife/components/pydantic_form/Model.jinja +0 -30
  47. fastlife/components/pydantic_form/Sequence.jinja +0 -47
  48. fastlife/components/pydantic_form/Text.jinja +0 -11
  49. fastlife/components/pydantic_form/Textarea.jinja +0 -38
  50. fastlife/components/pydantic_form/Union.jinja +0 -34
  51. fastlife/templates/binding.py +0 -52
  52. {fastlifeweb-0.20.1.dist-info → fastlifeweb-0.21.0.dist-info}/WHEEL +0 -0
  53. {fastlifeweb-0.20.1.dist-info → fastlifeweb-0.21.0.dist-info}/entry_points.txt +0 -0
  54. {fastlifeweb-0.20.1.dist-info → fastlifeweb-0.21.0.dist-info}/licenses/LICENSE +0 -0
@@ -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
11
  from fastlife.services.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" />
@@ -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>
@@ -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
@@ -34,6 +35,7 @@ from fastlife.routing.router import Router
34
35
  from fastlife.security.csrf import check_csrf
35
36
  from fastlife.services.policy import check_permission
36
37
  from fastlife.shared_utils.resolver import resolve, resolve_maybe_relative
38
+ from fastlife.templates.inline import is_inline_template_returned
37
39
 
38
40
  from .registry import DefaultRegistry, TRegistry
39
41
  from .settings import Settings
@@ -43,6 +45,7 @@ if TYPE_CHECKING:
43
45
  from fastlife.services.templates import (
44
46
  AbstractTemplateRendererFactory, # coverage: ignore
45
47
  )
48
+ from fastlife.templates.inline import InlineTemplate
46
49
 
47
50
  from fastlife.services.locale_negociator import LocaleNegociator
48
51
 
@@ -143,6 +146,8 @@ class GenericConfigurator(Generic[TRegistry]):
143
146
  ] = {}
144
147
 
145
148
  self._registered_permissions: set[str] = set()
149
+
150
+ self._renderer_globals: dict[str, Any] = {}
146
151
  self.scanner = venusian.Scanner(fastlife=self)
147
152
  self.include("fastlife.views")
148
153
  self.include("fastlife.middlewares")
@@ -441,6 +446,53 @@ class GenericConfigurator(Generic[TRegistry]):
441
446
  )
442
447
  return self
443
448
 
449
+ def add_renderer_global(
450
+ self, name: str, value: Any, *, evaluate: bool = True
451
+ ) -> None:
452
+ """
453
+ Add a rendering global value.
454
+
455
+ :param name: the name or key of the global value available in the template.
456
+ :param value: a value, or a callable or
457
+ an async function with a request in parameter that will evaluate the value.
458
+ :param evaluate: set to false if you want to inject helper methods in the
459
+ template.
460
+ """
461
+ self._renderer_globals[name] = value, evaluate
462
+
463
+ async def _build_renderer_globals(self, request: Request) -> dict[str, Any]:
464
+ """
465
+ Build globals variables accessible in any templates.
466
+
467
+ * `request` is the {class}`current request <fastlife.request.request.Request>`
468
+ * `authenticated_user` is used to access to the authenticated user if the
469
+ security policy has been installed.
470
+ * `csrf_token` is used to build for {jinjax:component}`CsrfToken`.
471
+ * `gettext`, `ngettext`, `dgettext`, `dngettext`, `pgettext`, `dpgettext`,
472
+ `npgettext`, `dnpgettext` methods are installed for i18n purpose.
473
+ """
474
+ lczr = request.registry.localizer(request)
475
+ custom_globals = {}
476
+ for key, (val, evaluate) in self._renderer_globals.items():
477
+ if evaluate and callable(val):
478
+ val = val(request)
479
+ if iscoroutine(val):
480
+ val = await val
481
+ custom_globals[key] = val
482
+ return {
483
+ "request": request,
484
+ "gettext": lczr.gettext,
485
+ "ngettext": lczr.ngettext,
486
+ "dgettext": lczr.dgettext,
487
+ "dngettext": lczr.dngettext,
488
+ "pgettext": lczr.pgettext,
489
+ "dpgettext": lczr.dpgettext,
490
+ "npgettext": lczr.npgettext,
491
+ "dnpgettext": lczr.dnpgettext,
492
+ **custom_globals,
493
+ **request.renderer_globals,
494
+ }
495
+
444
496
  def add_route(
445
497
  self,
446
498
  name: str,
@@ -466,8 +518,6 @@ class GenericConfigurator(Generic[TRegistry]):
466
518
  :param path: path of the route, use `{curly_brace}` to inject FastAPI Path
467
519
  parameters.
468
520
  :param endpoint: the function that will reveive the request.
469
- :param template: the template rendered by the
470
- {class}`fastlife.service.templates.AbstractTemplateRenderer`.
471
521
  :param permission: a permission to validate by the
472
522
  {class}`Security Policy <fastlife.security.policy.AbstractSecurityPolicy>`.
473
523
  :param status_code: customize response status code.
@@ -479,17 +529,21 @@ class GenericConfigurator(Generic[TRegistry]):
479
529
  self._registered_permissions.add(permission)
480
530
  dependencies.append(Depends(check_permission(permission)))
481
531
 
482
- if template:
532
+ if is_inline_template_returned(endpoint):
483
533
 
484
- def render(
534
+ async def render(
485
535
  request: Request,
486
- resp: Annotated[Response | Mapping[str, Any], Depends(endpoint)],
536
+ resp: Annotated["Response | InlineTemplate", Depends(endpoint)],
487
537
  ) -> Response:
488
538
  if isinstance(resp, Response):
489
539
  return resp
540
+
541
+ template = resp.renderer
542
+ globs = await self._build_renderer_globals(request)
490
543
  return request.registry.get_renderer(template)(request).render(
491
544
  template,
492
545
  params=resp,
546
+ globals=globs,
493
547
  )
494
548
 
495
549
  endpoint = render
@@ -523,9 +577,8 @@ class GenericConfigurator(Generic[TRegistry]):
523
577
  def add_exception_handler(
524
578
  self,
525
579
  status_code_or_exc: int | type[Exception],
526
- handler: Any,
580
+ handler: Callable[..., "Response | InlineTemplate"],
527
581
  *,
528
- template: str | None = None,
529
582
  status_code: int = 500,
530
583
  ) -> Self:
531
584
  """
@@ -533,7 +586,7 @@ class GenericConfigurator(Generic[TRegistry]):
533
586
 
534
587
  """
535
588
 
536
- def exception_handler(request: BaseRequest, exc: Exception) -> Any:
589
+ async def exception_handler(request: BaseRequest, exc: Exception) -> Any:
537
590
  # FastAPI exception handler does not provide our request object
538
591
  # it seems like it is rebuild from the asgi scope. Even the router
539
592
  # class is wrong.
@@ -544,18 +597,11 @@ class GenericConfigurator(Generic[TRegistry]):
544
597
  if isinstance(resp, Response):
545
598
  return resp
546
599
 
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,
600
+ return req.registry.get_renderer(resp.renderer)(req).render(
601
+ resp.template,
557
602
  params=resp,
558
603
  status_code=status_code,
604
+ globals=(await self._build_renderer_globals(req)),
559
605
  )
560
606
 
561
607
  self.exception_handlers.append((status_code_or_exc, exception_handler))
@@ -11,7 +11,6 @@ from .configurator import VENUSIAN_CATEGORY, Configurator
11
11
  def exception_handler(
12
12
  exception: type[Exception],
13
13
  *,
14
- template: str | None = None,
15
14
  status_code: int | None = None,
16
15
  ) -> Callable[..., Any]:
17
16
  """
@@ -34,7 +33,6 @@ def exception_handler(
34
33
  config.add_exception_handler(
35
34
  exception,
36
35
  wrapped,
37
- template=template,
38
36
  **({} if status_code is None else {"status_code": status_code}),
39
37
  )
40
38
 
fastlife/config/views.py CHANGED
@@ -31,7 +31,6 @@ def view_config(
31
31
  path: str,
32
32
  *,
33
33
  permission: str | None = None,
34
- template: str | None = None,
35
34
  status_code: int | None = None,
36
35
  methods: list[str] | None = None,
37
36
  ) -> Callable[..., Any]:
@@ -72,7 +71,6 @@ def view_config(
72
71
  permission=permission,
73
72
  status_code=status_code,
74
73
  methods=methods,
75
- template=template,
76
74
  )
77
75
 
78
76
  venusian.attach(wrapped, callback, category=VENUSIAN_CATEGORY) # type: ignore
@@ -0,0 +1 @@
1
+ """Framework core domain."""
@@ -0,0 +1 @@
1
+ """Core Domain Classes."""
@@ -0,0 +1,19 @@
1
+ """Models relative to the security."""
2
+
3
+ import secrets
4
+
5
+ from pydantic import BaseModel
6
+
7
+
8
+ def create_csrf_token() -> str:
9
+ """A helper that create a csrf token."""
10
+ return secrets.token_urlsafe(5)
11
+
12
+
13
+ class CSRFToken(BaseModel):
14
+ """Represent the CSRF Token"""
15
+
16
+ name: str
17
+ """Name of the token while serialized."""
18
+ value: str
19
+ """Value that must match between parts, cookie and posted form."""
@@ -0,0 +1,30 @@
1
+ """Inline templates."""
2
+
3
+ from typing import ClassVar
4
+
5
+ from pydantic import BaseModel
6
+ from pydantic.config import ConfigDict
7
+
8
+
9
+ class InlineTemplate(BaseModel):
10
+ """
11
+ Inline templates are used to encourage the location of behavior and the view typing.
12
+
13
+ Pages produce templates that are not reusable and don't need to be reusable
14
+ in there essence, they don't need to be in a component library.
15
+ They use a component lirary to stay small but contains a view logic
16
+ tighly coupled with the view and its code can stay in the same module of that view.
17
+ """
18
+
19
+ model_config = ConfigDict(arbitrary_types_allowed=True)
20
+
21
+ template: ClassVar[str]
22
+ """The template string to render."""
23
+ renderer: ClassVar[str]
24
+ """Template render engine to use."""
25
+
26
+
27
+ class JinjaXTemplate(InlineTemplate):
28
+ """Template that render JinjaX"""
29
+
30
+ renderer = ".jinja"
@@ -0,0 +1,17 @@
1
+ """Types that are serialized over HTTP and forms."""
2
+
3
+ from decimal import Decimal
4
+ from typing import Any
5
+ from uuid import UUID
6
+
7
+ from pydantic.networks import EmailStr
8
+
9
+ Builtins = str | int | str | float | Decimal | UUID | EmailStr
10
+ """Builtins types."""
11
+
12
+
13
+ AnyLiteral = Any
14
+ """
15
+ Something like Literal[...] or Literal[...] which does not exists
16
+ or Idon't know where it is hidden.
17
+ """
@@ -6,6 +6,7 @@ from fastapi import Request as FastAPIRequest
6
6
  from fastapi.params import Depends
7
7
 
8
8
  from fastlife.config.registry import DefaultRegistry, TRegistry
9
+ from fastlife.domain.model.security import CSRFToken, create_csrf_token
9
10
 
10
11
  if TYPE_CHECKING:
11
12
  from fastlife.security.policy import ( # coverage: ignore
@@ -25,11 +26,29 @@ class GenericRequest(FastAPIRequest, Generic[TRegistry]):
25
26
  security_policy: "AbstractSecurityPolicy[Any, TRegistry] | None"
26
27
  """Request locale used for the i18n of the response."""
27
28
 
29
+ renderer_globals: dict[str, Any]
30
+
28
31
  def __init__(self, registry: TRegistry, request: FastAPIRequest) -> None:
29
32
  super().__init__(request.scope, request.receive)
30
33
  self.registry = registry
31
34
  self.locale_name = registry.locale_negociator(self)
32
35
  self.security_policy = None # build it from the ? registry
36
+ self.renderer_globals = {}
37
+ self._csrf_token: CSRFToken | None = None
38
+
39
+ @property
40
+ def csrf_token(self) -> CSRFToken:
41
+ if self._csrf_token is None:
42
+ name = self.registry.settings.csrf_token_name
43
+ value = self.cookies.get(name) or create_csrf_token()
44
+ self._csrf_token = CSRFToken(name=name, value=value)
45
+ return self._csrf_token
46
+
47
+ def add_renderer_globals(self, **kwargs: Any) -> None:
48
+ """
49
+ Add global variables to the template renderer context for the current request.
50
+ """
51
+ self.renderer_globals.update(kwargs)
33
52
 
34
53
  async def has_permission(
35
54
  self, permission: str
fastlife/security/csrf.py CHANGED
@@ -18,7 +18,6 @@ no way to prevent to set the cookie in the request.
18
18
 
19
19
  """
20
20
 
21
- import secrets
22
21
  from collections.abc import Callable, Coroutine
23
22
  from typing import Any
24
23
 
@@ -31,11 +30,6 @@ class CSRFAttack(Exception):
31
30
  """
32
31
 
33
32
 
34
- def create_csrf_token() -> str:
35
- """A helper that create a csrf token."""
36
- return secrets.token_urlsafe(5)
37
-
38
-
39
33
  def check_csrf() -> Callable[[Request], Coroutine[Any, Any, bool]]:
40
34
  """
41
35
  A global application dependency, that is always active.
@@ -56,15 +50,11 @@ def check_csrf() -> Callable[[Request], Coroutine[Any, Any, bool]]:
56
50
  != "application/x-www-form-urlencoded"
57
51
  ):
58
52
  return True
59
- csrf_token_name = request.registry.settings.csrf_token_name
60
-
61
- cookie = request.cookies.get(csrf_token_name)
62
- if not cookie:
63
- raise CSRFAttack("CSRF token did not match")
64
53
 
54
+ token = request.csrf_token
65
55
  form_data = await request.form()
66
- value = form_data.get(csrf_token_name)
67
- if value != cookie:
56
+ value = form_data.get(token.name)
57
+ if value != token.value:
68
58
  raise CSRFAttack("CSRF token did not match")
69
59
 
70
60
  return True
@@ -9,11 +9,10 @@ More template engine can be registered using the configurator method
9
9
  """
10
10
 
11
11
  import abc
12
- from collections.abc import Callable, Mapping
12
+ from collections.abc import Mapping
13
13
  from typing import Any
14
14
 
15
15
  from fastlife import Request, Response
16
- from fastlife.security.csrf import create_csrf_token
17
16
  from fastlife.templates.inline import InlineTemplate
18
17
 
19
18
  TemplateParams = Mapping[str, Any]
@@ -30,6 +29,7 @@ class AbstractTemplateRenderer(abc.ABC):
30
29
 
31
30
  def __init__(self, request: Request) -> None:
32
31
  self.request = request
32
+ self.globals: dict[str, Any] = {}
33
33
 
34
34
  @property
35
35
  def route_prefix(self) -> str:
@@ -43,27 +43,21 @@ class AbstractTemplateRenderer(abc.ABC):
43
43
  status_code: int = 200,
44
44
  content_type: str = "text/html",
45
45
  globals: Mapping[str, Any] | None = None,
46
- params: TemplateParams | InlineTemplate,
47
- _create_csrf_token: Callable[..., str] = create_csrf_token,
46
+ params: InlineTemplate,
48
47
  ) -> Response:
49
48
  """
50
49
  Render the template and build the HTTP Response.
51
50
  """
52
51
  request = self.request
53
- reg = request.registry
54
- request.scope[reg.settings.csrf_token_name] = (
55
- request.cookies.get(reg.settings.csrf_token_name) or _create_csrf_token()
56
- )
57
- if isinstance(params, InlineTemplate):
58
- data = self.render_inline(params)
59
- else:
60
- data = self.render_template(template, **params)
52
+ if globals:
53
+ self.globals.update(globals)
54
+ data = self.render_template(params)
61
55
  resp = Response(
62
56
  data, status_code=status_code, headers={"Content-Type": content_type}
63
57
  )
64
58
  resp.set_cookie(
65
- reg.settings.csrf_token_name,
66
- request.scope[reg.settings.csrf_token_name],
59
+ request.csrf_token.name,
60
+ request.csrf_token.value,
67
61
  secure=request.url.scheme == "https",
68
62
  samesite="strict",
69
63
  max_age=60 * 15,
@@ -71,34 +65,7 @@ class AbstractTemplateRenderer(abc.ABC):
71
65
  return resp
72
66
 
73
67
  @abc.abstractmethod
74
- def render_template(
75
- self,
76
- template: str,
77
- *,
78
- globals: Mapping[str, Any] | None = None,
79
- **params: Any,
80
- ) -> str:
81
- """
82
- Render the given template with the given params.
83
-
84
- While rendering templates, the globals parameter is keps by the instantiated
85
- renderer and sent to every rendering made by the request.
86
- This is used by the pydantic form method that will render other templates
87
- for the request.
88
- In traditional frameworks, only one template is rendered containing the whole
89
- pages. But, while rendering a pydantic form, every field is rendered in its
90
- distinct template. The template renderer keep the globals and git it back
91
- to every templates. This can be used to fillout options in a select without
92
- performing an ajax request for example.
93
-
94
- :param template: name of the template to render.
95
- :param globals: some variable that will be passed to all rendered templates.
96
- :param params: paramaters that are limited to the main rendered templates.
97
- :return: The template rendering result.
98
- """
99
-
100
- @abc.abstractmethod
101
- def render_inline(self, template: InlineTemplate) -> str:
68
+ def render_template(self, template: InlineTemplate) -> str:
102
69
  """
103
70
  Render an inline template.
104
71
 
@@ -117,6 +117,18 @@ class Localizer:
117
117
  ret = ret.format(**mapping)
118
118
  return ret
119
119
 
120
+ def npgettext(
121
+ self,
122
+ context: str,
123
+ singular: str,
124
+ plural: str,
125
+ n: int,
126
+ mapping: dict[str, str] | None = None,
127
+ ) -> str:
128
+ ret = self.global_translations.npgettext(context, singular, plural, n)
129
+ mapping_num = {"num": n, **(mapping or {})}
130
+ return ret.format(**mapping_num)
131
+
120
132
  def dnpgettext(
121
133
  self,
122
134
  domain: str,
@@ -2,11 +2,6 @@
2
2
  Utilities for rendering HTML templates for page and components as FastAPI dependencies.
3
3
  """
4
4
 
5
- from .binding import Template, template
6
5
  from .inline import InlineTemplate
7
6
 
8
- __all__ = [
9
- "Template",
10
- "template",
11
- "InlineTemplate",
12
- ]
7
+ __all__ = ["InlineTemplate"]
@@ -1,22 +1,26 @@
1
1
  """Inline templates."""
2
2
 
3
- from typing import ClassVar
3
+ import inspect
4
+ from collections.abc import Callable
5
+ from typing import Any, get_args
4
6
 
5
- from pydantic import BaseModel
6
- from pydantic.config import ConfigDict
7
+ from fastlife.domain.model.template import InlineTemplate
8
+ from fastlife.shared_utils.infer import is_union
7
9
 
8
10
 
9
- class InlineTemplate(BaseModel):
10
- """
11
- Inline templates are used to encourage the location of behavior and the view typing.
11
+ def is_inline_template_returned(endpoint: Callable[..., Any]) -> bool:
12
+ signature = inspect.signature(endpoint)
13
+ return_annotation = signature.return_annotation
12
14
 
13
- Pages produce templates that are not reusable and don't need to be reusable
14
- in there essence, they don't need to be in a component library.
15
- They use a component lirary to stay small but contains a view logic
16
- tighly coupled with the view and its code can stay in the same module of that view.
17
- """
15
+ if isinstance(return_annotation, type) and issubclass(
16
+ return_annotation, InlineTemplate
17
+ ):
18
+ return True
18
19
 
19
- model_config = ConfigDict(arbitrary_types_allowed=True)
20
+ if is_union(return_annotation):
21
+ return any(
22
+ isinstance(arg, type) and issubclass(arg, InlineTemplate)
23
+ for arg in get_args(return_annotation)
24
+ )
20
25
 
21
- template: ClassVar[str]
22
- """The template string to render."""
26
+ return False