fastlifeweb 0.9.7__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 +64 -41
  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/{model_result.py → form.py} +21 -9
  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 -7
  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 +18 -0
  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 +16 -3
  61. fastlife/templates/pydantic_form/Sequence.jinja +15 -6
  62. fastlife/templates/pydantic_form/Text.jinja +2 -2
  63. fastlife/templates/pydantic_form/Textarea.jinja +32 -0
  64. fastlife/templates/pydantic_form/Union.jinja +7 -1
  65. fastlife/templates/pydantic_form/Widget.jinja +6 -3
  66. fastlife/templating/binding.py +18 -4
  67. fastlife/templating/renderer/__init__.py +3 -1
  68. fastlife/templating/renderer/abstract.py +21 -8
  69. fastlife/templating/renderer/constants.py +82 -0
  70. fastlife/templating/renderer/jinjax.py +269 -6
  71. fastlife/templating/renderer/widgets/base.py +43 -7
  72. fastlife/templating/renderer/widgets/boolean.py +21 -0
  73. fastlife/templating/renderer/widgets/checklist.py +23 -0
  74. fastlife/templating/renderer/widgets/dropdown.py +22 -2
  75. fastlife/templating/renderer/widgets/factory.py +100 -29
  76. fastlife/templating/renderer/widgets/hidden.py +16 -0
  77. fastlife/templating/renderer/widgets/model.py +7 -1
  78. fastlife/templating/renderer/widgets/sequence.py +8 -6
  79. fastlife/templating/renderer/widgets/text.py +80 -4
  80. fastlife/templating/renderer/widgets/union.py +25 -2
  81. fastlife/testing/testclient.py +3 -3
  82. fastlife/views/pydantic_form.py +2 -2
  83. {fastlifeweb-0.9.7.dist-info → fastlifeweb-0.11.0.dist-info}/METADATA +4 -9
  84. {fastlifeweb-0.9.7.dist-info → fastlifeweb-0.11.0.dist-info}/RECORD +86 -84
  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.9.7.dist-info → fastlifeweb-0.11.0.dist-info}/LICENSE +0 -0
  90. {fastlifeweb-0.9.7.dist-info → fastlifeweb-0.11.0.dist-info}/WHEEL +0 -0
@@ -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."""
fastlife/security/csrf.py CHANGED
@@ -1,15 +1,27 @@
1
1
  """
2
- Prevents CSRF attack using cookie and html hidden field comparaison.
2
+ Prevents CSRF attack using cookie and html hidden field comparison.
3
+
4
+ Fast life did not reinvent the wheel on CSRF Protection.
5
+
6
+ It use the good old method. A CSRF token is saved in a cookie.
7
+ Forms post the CSRF token, and the token in the cookies and the form must match
8
+ to process the request, otherwise an exception
9
+ :class:`fastlife.security.csrf.CSRFAttack` is raised.
10
+
11
+ The cookie named is configurabllefia the settings
12
+ :attr:`fastlife.config.settings.Settings.csrf_token_name`
13
+
14
+ While using the `<Form/>` JinjaX tag, the csrf token is always sent.
15
+
16
+ The cookie is always set when you render any template. At the moment, there is
17
+ no way to prevent to set the cookie in the request.
3
18
 
4
- Fast life did not reinvent the wheel on CSRF Protection. It use the good old method.
5
19
  """
6
- import secrets
7
- from typing import TYPE_CHECKING, Any, Callable, Coroutine
8
20
 
9
- from fastapi import Request
21
+ import secrets
22
+ from typing import Any, Callable, Coroutine
10
23
 
11
- if TYPE_CHECKING:
12
- from fastlife.configurator.registry import Registry # coverage: ignore
24
+ from fastlife.request import Request
13
25
 
14
26
 
15
27
  class CSRFAttack(Exception):
@@ -23,12 +35,17 @@ def create_csrf_token() -> str:
23
35
  return secrets.token_urlsafe(5)
24
36
 
25
37
 
26
- def check_csrf(registry: "Registry") -> Callable[[Request], Coroutine[Any, Any, bool]]:
38
+ def check_csrf() -> Callable[[Request], Coroutine[Any, Any, bool]]:
27
39
  """
28
40
  A global application dependency, that is always active.
29
41
 
30
- If you don't want csrf token, its simple don't use the
42
+ If you don't want csrf token, its simple: don't use the
31
43
  application/x-www-form-urlencoded on a POST method.
44
+
45
+ For security reason, there it no other options to disable this policy.
46
+
47
+ :raises: {class}`.CSRFAttack` if the cookie and the csrf
48
+ posted in the form does not match.
32
49
  """
33
50
 
34
51
  async def check_csrf(request: Request) -> bool:
@@ -38,13 +55,14 @@ def check_csrf(registry: "Registry") -> Callable[[Request], Coroutine[Any, Any,
38
55
  != "application/x-www-form-urlencoded"
39
56
  ):
40
57
  return True
58
+ csrf_token_name = request.registry.settings.csrf_token_name
41
59
 
42
- cookie = request.cookies.get(registry.settings.csrf_token_name)
60
+ cookie = request.cookies.get(csrf_token_name)
43
61
  if not cookie:
44
62
  raise CSRFAttack("CSRF token did not match")
45
63
 
46
64
  form_data = await request.form()
47
- value = form_data.get(registry.settings.csrf_token_name)
65
+ value = form_data.get(csrf_token_name)
48
66
  if value != cookie:
49
67
  raise CSRFAttack("CSRF token did not match")
50
68
 
@@ -9,8 +9,12 @@ def check_permission(permission_name: str) -> CheckPermissionHook:
9
9
  """
10
10
  A closure that check that a user as the given permission_name.
11
11
 
12
- This method has to be overriden using the setting check_permission
13
- to implement it.
12
+ This method has to be overriden using the setting
13
+ :attr:`fastlife.config.settings.Settings.check_permission` to implement it.
14
+
15
+ When the check permission is properly set in the settings., the hook is called
16
+ for every route added with a permission keyword.
17
+ :meth:`fastlife.config.configurator.Configurator.add_route`
14
18
 
15
19
  :param permission_name: a permission name set in a view to check access.
16
20
  :return: a function that raise http exceptions or any configured exception here.
@@ -0,0 +1 @@
1
+ """Utilities function."""
@@ -1,3 +1,4 @@
1
+ """Type inference."""
1
2
  from types import UnionType
2
3
  from typing import Any, Type, Union, get_origin
3
4
 
@@ -5,10 +6,16 @@ from pydantic import BaseModel
5
6
 
6
7
 
7
8
  def is_complex_type(typ: Type[Any]) -> bool:
9
+ """
10
+ Used to detect complex type such as Mapping, Sequence and pydantic BaseModel.
11
+
12
+ This method cannot be used outside pydantic serialization.
13
+ """
8
14
  return bool(get_origin(typ) or issubclass(typ, BaseModel))
9
15
 
10
16
 
11
17
  def is_union(typ: Type[Any]) -> bool:
18
+ """Used to detect unions like Optional[T], Union[T, U] or T | U."""
12
19
  type_origin = get_origin(typ)
13
20
  if type_origin:
14
21
  if type_origin is Union: # Optional[T]
@@ -1,6 +1,8 @@
1
+ """Resolution of python objects for dependency injection and more."""
1
2
  import importlib.util
2
3
  from pathlib import Path
3
- from typing import Any, Type, Union
4
+ from types import UnionType
5
+ from typing import Any, Union
4
6
 
5
7
 
6
8
  def resolve(value: str) -> Any:
@@ -24,7 +26,8 @@ def resolve(value: str) -> Any:
24
26
  return attr
25
27
 
26
28
 
27
- def resolve_extended(value: str) -> Type[Any]:
29
+ def resolve_extended(value: str) -> UnionType:
30
+ """Resolve many types separed by a pipe (``|``), the union separator."""
28
31
  values = value.split("|")
29
32
  if len(values) == 1:
30
33
  return resolve(value)
@@ -33,6 +36,11 @@ def resolve_extended(value: str) -> Type[Any]:
33
36
 
34
37
 
35
38
  def resolve_path(value: str) -> str:
39
+ """
40
+ Resole a path on the disk from a python package name.
41
+
42
+ This helper is used to find static assets inside a python package.
43
+ """
36
44
  package_name, resource_name = value.split(":", 1)
37
45
  spec = importlib.util.find_spec(package_name)
38
46
  if not spec or not spec.origin:
@@ -1,14 +1,38 @@
1
+ {# doc
2
+ Create html ``<a>`` node with htmx support by default.
3
+ The `hx-get` parameter is set with the href directly unless the
4
+ `disabled-htmx` attribute has been set.
5
+ #}
1
6
  {# def
2
- href,
3
- disable_htmx=false,
4
- hx_target="#maincontent",
5
- hx_select=None,
6
- hx_swap="innerHTML show:body:top",
7
- hx_push_url=true
7
+ href: Annotated[str, "target link."],
8
+ id: Annotated[str | None, "unique identifier of the element."] = None,
9
+ class_: Annotated[
10
+ str | None,
11
+ "css class for the node, defaults to :attr:`fastlife.templating.renderer.constants.Constants.A_CLASS`."
12
+ ] = None,
13
+ hx_target: Annotated[
14
+ str,
15
+ "target the element for swapping than the one issuing the AJAX request."
16
+ ] = "#maincontent",
17
+ hx_select: Annotated[str | None, "select the content swapped from response of the AJAX request."] = None,
18
+ hx_swap: Annotated[
19
+ str,
20
+ "specify how the response will be swapped in relative to the target of an AJAX request."
21
+ ] = "innerHTML show:body:top",
22
+ hx_push_url: Annotated[bool, "replace the browser url with the link."] = True,
23
+ disable_htmx: Annotated[bool, "do not add any `hx-*` attibute to the link."] = False
8
24
  #}
9
25
 
10
- <a href="{{href}}" {% if not disable_htmx %} hx-get="{{href}}" hx-target="{{hx_target}}" hx-swap="{{hx_swap}}" {% if
11
- hx_push_url %} hx-push-url="true" {%- endif %}{% if hx_select %} hx-select="{{hx_select}}" {%- endif %}{%- endif
12
- %}class="{{attrs.class or A_CLASS}}">
26
+ <a href="{{href}}"
27
+ {%- if id %} id="{{ id }}" {%- endif %}
28
+ {%- if not disable_htmx %}
29
+ hx-get="{{ href }}"
30
+ hx-target="{{ hx_target }}"
31
+ hx-swap="{{ hx_swap }}"
32
+ {%- if hx_push_url %} hx-push-url="true" {%- endif %}
33
+ {%- if hx_select %} hx-select="{{ hx_select }}" {%- endif %}
34
+ {%- endif %}
35
+ class="{{ attrs.class or A_CLASS }}"
36
+ >
13
37
  {{- content -}}
14
38
  </a>
@@ -1,35 +1,58 @@
1
+ {# doc
2
+ Create html ``<button>`` node.
3
+ #}
1
4
  {# def
2
- type="submit",
3
- id="",
4
- name="action",
5
- value="submit",
6
- hidden=false,
7
- aria_label="",
8
- onclick="",
9
- hx_target="",
10
- hx_swap="",
11
- hx_select="",
12
- hx_after_request="",
13
- hx_vals="",
14
- hx_confirm="",
15
- hx_get="",
16
- hx_post="",
17
- hx_put="",
18
- hx_patch="",
19
- hx_delete="",
20
- hx_params=None,
21
- hx_push_url=false,
22
- full_width=false,
5
+ type: Annotated[
6
+ Literal["submit", "button", "reset"],
7
+ "Define button behavior."
8
+ ] = "submit",
9
+ id: Annotated[str | None, "unique identifier of the element."] = None,
10
+ class_: Annotated[
11
+ str | None,
12
+ "css class for the node, defaults to "
13
+ ":attr:`fastlife.templating.renderer.constants.Constants.BUTTON_CLASS`."
14
+ ] = None,
15
+ name: str = "action",
16
+ value: str = "submit",
17
+ hidden: bool = False,
18
+ aria_label: str | None = None,
19
+ onclick: str | None = None,
20
+ hx_target: str | None = None,
21
+ hx_swap: str | None = None,
22
+ hx_select: str | None = None,
23
+ hx_after_request: Annotated[str, "Produce the hx-on::after-request"] = "",
24
+ hx_vals: str | None = None,
25
+ hx_confirm: str | None = None,
26
+ hx_get: str | None = None,
27
+ hx_post: str | None = None,
28
+ hx_put: str | None = None,
29
+ hx_patch: str | None = None,
30
+ hx_delete: str | None = None,
31
+ hx_params: str | None = None,
32
+ hx_push_url: Annotated[bool, "Replace the browser url by the ajax request"] = false,
33
+ full_width: Annotated[str, "Append tailwind class w-full to get full width"] = false,
23
34
  #}
24
- <button type="{{type}}" {%if id %}id="{{id}}" {%endif%} name="{{name}}" value="{{value}}" {% if
25
- hx_target%}hx-target="{{hx_target}}" {% endif %} {% if hx_swap %}hx-swap="{{hx_swap}}" {% endif %} {% if hx_select
26
- %}hx-select="{{hx_select}}" {% endif %} {% if onclick %}onclick="{{onclick}}" {% endif %}{% if hx_after_request
27
- %}hx-on::after-request="{{hx_after_request}}" {% endif %} {% if hx_vals %}hx-vals='{{hx_vals|safe}}' {% endif %} {% if
28
- hx_confirm %}hx-confirm="{{hx_confirm}}" {% endif %} {% if hx_get %}hx-get="{{hx_get}}" {% endif %} {% if hx_post
29
- %}hx-post="{{hx_post}}" {% endif %} {% if hx_put %}hx-put="{{hx_put}}" {% endif %}{% if hx_patch
30
- %}hx-patch="{{hx_patch}}" {% endif %}{% if hx_delete %}hx-delete="{{hx_delete}}" {% endif %} {% if hx_push_url %}
31
- hx-push-url="true" {%- endif %} {% if hx_params %} hx-params="{{hx_params}}" {%- endif %}{% if aria_label
32
- %}aria-label="{{aria_label}}" {% endif %} class="{% if full_width %}w-full {% endif %}{{attrs.class or BUTTON_CLASS}}"
33
- {% if hidden %}hidden{% endif %}>
34
- {{- content -}}
35
+ <button type="{{ type }}"
36
+ name="{{ name }}"
37
+ value="{{ value }}"
38
+ {%- if id %} id="{{ id }}" {%- endif %}
39
+ {%- if hx_target %} hx-target="{{ hx_target }}" {%- endif %}
40
+ {%- if hx_swap %} hx-swap="{{ hx_swap }}" {%- endif %}
41
+ {%- if hx_select %} hx-select="{{ hx_select }}" {%- endif %}
42
+ {%- if onclick %} onclick="{{ onclick }}" {%- endif %}
43
+ {%- if hx_after_request %} hx-on::after-request="{{ hx_after_request }}" {%- endif %}
44
+ {%- if hx_vals %} hx-vals='{{ hx_vals|safe }}' {%- endif %}
45
+ {%- if hx_confirm %} hx-confirm="{{ hx_confirm }}" {%- endif %}
46
+ {%- if hx_get %} hx-get="{{ hx_get }}" {%- endif %}
47
+ {%- if hx_post %} hx-post="{{ hx_post }}" {%- endif %}
48
+ {%- if hx_put %} hx-put="{{ hx_put }}" {%- endif %}
49
+ {%- if hx_patch %} hx-patch="{{ hx_patch }}" {%- endif %}
50
+ {%- if hx_delete %} hx-delete="{{ hx_delete }}" {%- endif %}
51
+ {%- if hx_push_url %} hx-push-url="true" {%- endif %}
52
+ {%- if hx_params %} hx-params="{{ hx_params }}" {%- endif %}
53
+ {%- if aria_label %} aria-label="{{ aria_label }}" {%- endif %}
54
+ class="{% if full_width %}w-full {% endif %}{{ attrs.class or BUTTON_CLASS }}"
55
+ {%- if hidden %} hidden{% endif -%}
56
+ >
57
+ {{- content -}}
35
58
  </button>
@@ -1,8 +1,22 @@
1
+ {# doc
2
+ Create html ``<input type="checkbox" />`` node.
3
+ #}
1
4
  {# def
2
- name,
3
- id,
4
- value=None,
5
- checked=False,
5
+ name: Annotated[str, "Name of the checkbox"],
6
+ id: Annotated[str | None, "unique identifier of the element."] = None,
7
+ class_: Annotated[
8
+ str | None,
9
+ "css class for the node, defaults to "
10
+ ":attr:`fastlife.templating.renderer.constants.Constants.CHECKBOX_CLASS`."
11
+ ] = None,
12
+ value: Annotated[str | None, "http submitted value if the checkbox is checked"] = None,
13
+ checked: Annotated[bool, "Initialized the checkbox as ticked"] = False,
6
14
  #}
7
- <input name="{{name}}" {% if value is not none %}value="{{value}}" {% endif %} {% if checked %}checked{% endif %}
8
- type="checkbox" id="{{id}}" class="{{attrs.class or CHECKBOX_CLASS}}" />
15
+ <input
16
+ name="{{ name }}"
17
+ type="checkbox"
18
+ {%- if id %} id="{{id}}" {%- endif %}
19
+ {%- if value is not none %} value="{{ value }}" {%- endif %}
20
+ class="{{ attrs.class or CHECKBOX_CLASS }}"
21
+ {%- if checked %} checked{% endif -%}
22
+ />
@@ -1 +1,5 @@
1
+ {# doc
2
+ a :jinjax:component:`Hidden` field automaticaly injected in every
3
+ :jinjax:component:`Form` to protect against CSRF Attacks.
4
+ #}
1
5
  <Hidden :name="csrf_token.name" :value="csrf_token.value" />
@@ -1,4 +1,32 @@
1
- {# def id=None, open=True #}
2
- <details{% if id%} id="{{id}}"{% endif%} class="{{attrs.class or DETAILS_CLASS}}" {% if open %}open{% endif %}>
3
- {{content}}
1
+ {# doc
2
+
3
+ Produce a ``<details>`` html node in order to create a collapsible box.
4
+
5
+ .. code-block:: html
6
+
7
+ <Details>
8
+ <Summary :id="my-summary">
9
+ <H3 :class="H3_SUMMARY_CLASS">A title</H3>
10
+ </Summary>
11
+ <div>
12
+ Some content
13
+ </div>
14
+ </Details>
15
+
16
+ #}
17
+ {# def
18
+ id: Annotated[str | None, "unique identifier of the element."] = None,
19
+ class_: Annotated[
20
+ str | None,
21
+ "css class for the node, defaults to "
22
+ ":attr:`fastlife.templating.renderer.constants.Constants.DETAILS_CLASS`."
23
+ ] = None,
24
+ open: Annotated[bool, "open/close state."] = True,
25
+ #}
26
+ <details
27
+ {%- if id %} id="{{id}}" {% endif %}
28
+ class="{{attrs.class or DETAILS_CLASS}}"
29
+ {%- if open %} open {%- endif -%}
30
+ >
31
+ {{- content -}}
4
32
  </details>
@@ -1,11 +1,49 @@
1
+ {# doc
2
+ Create html ``<form>`` node with htmx support by default.
3
+ A :jinjax:component:`CsrfToken` will always be included in the form
4
+ and will be checked by the
5
+ :func:`csrf policy method <fastlife.security.csrf.check_csrf>`.
6
+
7
+ ::
8
+
9
+ <Form :hx-post="true">
10
+ <Input name="name" placeholder="Bob" />
11
+ <Button>Submit</Button>
12
+ </Form>
13
+
14
+ #}
15
+
1
16
  {# def
2
- method="",
3
- action=None,
4
- hx_post=None,
17
+ id: Annotated[str | None, "unique identifier of the element."] = None,
18
+ class_: Annotated[
19
+ str | None,
20
+ "css class for the node, defaults to "
21
+ ":attr:`fastlife.templating.renderer.constants.Constants.FORM_CLASS`"
22
+ ] = None,
23
+ method: Annotated[Literal["get", "post"] | None, "Http method used"] = None,
24
+ action: Annotated[str | None, "url where the form will be submitted"] = None,
25
+ hx_post: Annotated[
26
+ str | Literal[True] | None,
27
+ "url where the form will be submitted using htmx. if ``True``, the current url is used."\
28
+ ] = None,
29
+ hx_disable: Annotated[
30
+ Literal[True] | None,
31
+ "if true, then htmx will be disabled for the form and for all its children nodes."
32
+ ] = None,
5
33
  #}
6
- <form class="{{attrs.class or FORM_CLASS}}" {% if hx_post is not none
7
- %}hx-post="{% if hx_post is not true %}{{hx_post}}{% endif%}" {% endif %}{% if action is not none
8
- %}action="{{action}}" {% endif %}{% if method %}method="{{method}}" {% endif %}>
34
+ <form
35
+ {%- if id %} id="{{id}}" {% endif %}
36
+ class="{{attrs.class or FORM_CLASS}}"
37
+ {%- if hx_disable %}
38
+ hx-disable
39
+ {%- else %}
40
+ {%- if hx_post is not none %}
41
+ hx-post="{% if hx_post is not true %}{{hx_post}}{% endif %}"
42
+ {%- endif %}
43
+ {%- endif %}
44
+ {%- if action is not none %} action="{{action}}" {%- endif %}
45
+ {%- if method %} method="{{method}}" {%- endif -%}
46
+ >
9
47
  <CsrfToken />
10
- {{ content }}
48
+ {{- content -}}
11
49
  </form>
@@ -1 +1,14 @@
1
- <h1 class="{{attrs.class or H1_CLASS}}">{{ content }}</h1>
1
+ {# doc html ``<h1>`` node. #}
2
+ {# def
3
+ id: Annotated[str | None, "unique identifier of the element."] = None,
4
+ class_: Annotated[
5
+ str | None,
6
+ "css class for the node, defaults to "
7
+ ":attr:`fastlife.templating.renderer.constants.Constants.H1_CLASS`."
8
+ ] = None,
9
+ #}
10
+ <h1
11
+ {%- if id %} id="{{id}}" {%- endif %}
12
+ class="{{attrs.class or H1_CLASS}}">
13
+ {{- content -}}
14
+ </h1>
@@ -1 +1,14 @@
1
- <h2 class="{{attrs.class or H2_CLASS}}">{{ content }}</h2>
1
+ {# doc html ``<h2>`` node. #}
2
+ {# def
3
+ id: Annotated[str | None, "unique identifier of the element."] = None,
4
+ class_: Annotated[
5
+ str | None,
6
+ "css class for the node, defaults to "
7
+ ":attr:`fastlife.templating.renderer.constants.Constants.H2_CLASS`."
8
+ ] = None,
9
+ #}
10
+ <h2
11
+ {%- if id %} id="{{id}}" {%- endif %}
12
+ class="{{attrs.class or H2_CLASS}}">
13
+ {{- content -}}
14
+ </h2>
@@ -1 +1,14 @@
1
- <h3 class="{{attrs.class or H3_CLASS}}">{{ content }}</h3>
1
+ {# doc html ``<h3>`` node. #}
2
+ {# def
3
+ id: Annotated[str | None, "unique identifier of the element."] = None,
4
+ class_: Annotated[
5
+ str | None,
6
+ "css class for the node, defaults to "
7
+ ":attr:`fastlife.templating.renderer.constants.Constants.H3_CLASS`."
8
+ ] = None,
9
+ #}
10
+ <h3
11
+ {%- if id %} id="{{id}}" {%- endif %}
12
+ class="{{attrs.class or H3_CLASS}}">
13
+ {{- content -}}
14
+ </h3>
@@ -1 +1,14 @@
1
- <h4 class="{{attrs.class or H4_CLASS}}">{{ content }}</h4>
1
+ {# doc html ``<h4>`` node. #}
2
+ {# def
3
+ id: Annotated[str | None, "unique identifier of the element."] = None,
4
+ class_: Annotated[
5
+ str | None,
6
+ "css class for the node, defaults to "
7
+ ":attr:`fastlife.templating.renderer.constants.Constants.H4_CLASS`."
8
+ ] = None,
9
+ #}
10
+ <h4
11
+ {%- if id %} id="{{id}}" {%- endif %}
12
+ class="{{attrs.class or H4_CLASS}}">
13
+ {{- content -}}
14
+ </h4>
@@ -1 +1,14 @@
1
- <h5 class="{{attrs.class or H5_CLASS}}">{{ content }}</h5>
1
+ {# doc html ``<h5>`` node. #}
2
+ {# def
3
+ id: Annotated[str | None, "unique identifier of the element."] = None,
4
+ class_: Annotated[
5
+ str | None,
6
+ "css class for the node, defaults to "
7
+ ":attr:`fastlife.templating.renderer.constants.Constants.H5_CLASS`."
8
+ ] = None,
9
+ #}
10
+ <h5
11
+ {%- if id %} id="{{id}}" {%- endif %}
12
+ class="{{attrs.class or H5_CLASS}}">
13
+ {{- content -}}
14
+ </h5>
@@ -1 +1,14 @@
1
- <h6 class="{{attrs.class or H6_CLASS}}">{{ content }}</h6>
1
+ {# doc html ``<h6>`` node. #}
2
+ {# def
3
+ id: Annotated[str | None, "unique identifier of the element."] = None,
4
+ class_: Annotated[
5
+ str | None,
6
+ "css class for the node, defaults to "
7
+ ":attr:`fastlife.templating.renderer.constants.Constants.H6_CLASS`."
8
+ ] = None,
9
+ #}
10
+ <h6
11
+ {%- if id %} id="{{id}}" {%- endif %}
12
+ class="{{attrs.class or H6_CLASS}}">
13
+ {{- content -}}
14
+ </h6>
@@ -1,6 +1,6 @@
1
1
  {# def
2
- name,
3
- value,
4
- id="",
2
+ name: Annotated[str, "submitted name in the form"],
3
+ value: Annotated[str, "submitted value in the form"],
4
+ id: Annotated[str | None, "unique identifier of the element."] = None,
5
5
  #}
6
6
  <input name="{{name}}" value="{{value}}" type="hidden" {%if id %}id="{{id}}" {%endif%} />
@@ -1,12 +1,25 @@
1
+ {# doc
2
+ Produce ``<input>`` node.
3
+ #}
1
4
  {# def
2
- name,
3
- value="",
4
- type="text",
5
- id="",
6
- aria_label="",
7
- placeholder="",
5
+ name: Annotated[str, "submitted name in the form"],
6
+ value: Annotated[str, "submitted value in the form"] = "",
7
+ type: Annotated[str, "type of the control"] = "text",
8
+ id: Annotated[str | None, "unique identifier of the element."] = None,
9
+ class_: Annotated[
10
+ str | None,
11
+ "css class for the node, defaults to "
12
+ ":attr:`fastlife.templating.renderer.constants.Constants.INPUT_CLASS`."
13
+ ] = None,
14
+ aria_label: Annotated[str | None, "aria-label"] = None,
15
+ placeholder: Annotated[
16
+ str | None,
17
+ "brief hint to the user as to what kind of information is expected in the field."
18
+ ] = None,
8
19
  #}
9
20
 
10
- <input name="{{name}}" value="{{value}}" type="{{type}}" {% if id %}id="{{id}}" {% endif %}{% if aria_label
11
- %}aria-label="{{aria_label}}" {% endif %} {% if placeholder %}placeholder="{{placeholder}}" {% endif %}
21
+ <input name="{{name}}" value="{{value}}" type="{{type}}"
22
+ {%- if id %} id="{{id}}" {%- endif %}
23
+ {%- if aria_label %} aria-label="{{aria_label}}" {%- endif %}
24
+ {%- if placeholder %} placeholder="{{placeholder}}" {%- endif %}
12
25
  class="{{attrs.class or INPUT_CLASS}}" />