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
@@ -1,3 +0,0 @@
1
- {# doc Render a hidden input field for a field. #}
2
- {# def widget: Annotated[fastlife.adapters.jinjax.widgets.hidden.HiddenWidget, "widget to display."] #}
3
- <Hidden :name="widget.name" :value="widget.value" :id="widget.id" />
@@ -1,30 +0,0 @@
1
- {# doc Widget for pydantic BaseModel subclasses. #}
2
- {# def
3
- widget: Annotated[fastlife.adapters.jinjax.widgets.model.ModelWidget, "widget to display."],
4
- children_widget : Annotated[
5
- Sequence[fastlife.adapters.jinjax.widgets.base.Widget],
6
- "child widgets for every fields of the model."
7
- ],
8
- #}
9
-
10
- <pydantic_form.Widget :widget="widget">
11
- <div id="{{widget.id}}"{% if widget.nested %} class="m-4"{%endif%}>
12
- {% if widget.nested %}
13
- <Details>
14
- <Summary :id="widget.id + '-summary'">
15
- <H3 :class="H3_SUMMARY_CLASS">{{widget.title}}</H3>
16
- <pydantic_form.Error :text="widget.error" />
17
- </Summary>
18
- <div>
19
- {% for child in children_widget %}
20
- {{ child }}
21
- {% endfor %}
22
- </div>
23
- </Details>
24
- {% else %}
25
- {% for child in children_widget %}
26
- {{ child }}
27
- {% endfor %}
28
- {% endif %}
29
- </div>
30
- </pydantic_form.Widget>
@@ -1,47 +0,0 @@
1
- {# doc Widget for pydantic BaseModel subclasses. #}
2
- {# def
3
- widget: Annotated[fastlife.adapters.jinjax.widgets.sequence.SequenceWidget, "widget to display."],
4
- children_widgets : Annotated[
5
- Sequence[fastlife.adapters.jinjax.widgets.base.Widget],
6
- "child widgets for every fields of the model."
7
- ],
8
- type: Annotated[fastlife.adapters.jinjax.widgets.base.TypeWrapper, "child type wrapped."]
9
- #}
10
-
11
- <pydantic_form.Widget :widget="widget">
12
- <Details :id="widget.id">
13
- <Summary :id="widget.id + '-summary'">
14
- <H3 :class="H3_SUMMARY_CLASS">{{widget.title}}</H3>
15
- <pydantic_form.Error :text="widget.error" />
16
- </Summary>
17
- <div>
18
- {% set fnGetName = "get" + widget.id.replace("-", "_") %}
19
- <script>
20
- function {{ fnGetName }} () {
21
- const el = document.getElementById("{{widget.id}}-content");
22
- const len = el.dataset.length;
23
- el.dataset.length = parseInt(len) + 1;
24
- return "{{type.name}}." + len;
25
- }
26
- </script>
27
-
28
- <div id="{{widget.id}}-content" class="m-4" data-length="{{children_widgets|length|string}}">
29
- {% set container_id = widget.id + "-children-container" %}
30
- <div id="{{container_id}}">
31
- {% for child in children_widgets %}
32
- {{ child }}
33
- {% endfor%}
34
- </div>
35
- </div>
36
-
37
- <div>
38
- {% set container_id = "#" + widget.id + "-children-container" %}
39
- {% set add_id = widget.id + "-add" %}
40
- {% set vals = 'js:{"name": '+ fnGetName + '(), "token": "' + type.token + '", "removable": true}' %}
41
- <Button type="button" :hx-target="container_id" hx-swap="beforeend" :id="add_id" :hx-vals="vals" :hx-get="type.url">
42
- Add
43
- </Button>
44
- </div>
45
- </div>
46
- </Details>
47
- </pydantic_form.Widget>
@@ -1,11 +0,0 @@
1
- {# def widget: Annotated[fastlife.adapters.jinjax.widgets.text.TextWidget, "widget to display."] #}
2
-
3
- <pydantic_form.Widget :widget="widget">
4
- <div class="pt-4">
5
- <Label :for="widget.id">{{widget.title}}</Label>
6
- <pydantic_form.Error :text="widget.error" />
7
- <Input :name="widget.name" :value="widget.value" :type="widget.input_type" :id="widget.id"
8
- :aria-label="widget.aria_label" :placeholder="widget.placeholder" />
9
- <pydantic_form.Hint :text="widget.hint" />
10
- </div>
11
- </pydantic_form.Widget>
@@ -1,38 +0,0 @@
1
- {# doc
2
- Render textarea widget for field of type text of event sequence.
3
-
4
- ::
5
-
6
- from fastlife.adapters.jinjax.widgets.text import TextareaWidget
7
- from pydantic import BaseModel, Field, field_validator
8
-
9
- class TagsForm(BaseModel):
10
-
11
- tags: Annotated[Sequence[str], TextareaWidget] = Field(
12
- default_factory=list,
13
- title="Tags",
14
- description="One tag per line",
15
- )
16
-
17
- @field_validator("tags", mode="before")
18
- def split(cls, s: Any) -> Sequence[str]:
19
- return s.split() if s else []
20
-
21
- #}
22
- {# def widget: Annotated[fastlife.adapters.jinjax.widgets.text.TextareaWidget, "widget to display."] #}
23
-
24
- <pydantic_form.Widget :widget="widget">
25
- <div class="pt-4">
26
- <Label :for="widget.id">{{widget.title}}</Label>
27
- <pydantic_form.Error :text="widget.error" />
28
- <Textarea :name="widget.name" :id="widget.id" :aria-label="widget.aria_label">
29
- {%- if v is string -%}
30
- {{- v -}}}
31
- {%- else -%}
32
- {%- for v in widget.value %}{{v}}
33
- {% endfor -%}
34
- {% endif %}
35
- </Textarea>
36
- <pydantic_form.Hint :text="widget.hint" />
37
- </div>
38
- </pydantic_form.Widget>
@@ -1,34 +0,0 @@
1
- {# doc display widget for union type field #}
2
- {# def
3
- widget: Annotated[fastlife.adapters.jinjax.widgets.base.Widget, "widget to display."],
4
- child: Annotated[fastlife.adapters.jinjax.widgets.base.Widget, "current widget if any"],
5
- types: Annotated[Sequence[fastlife.adapters.jinjax.widgets.base.TypeWrapper], "Child types to choose"],
6
- parent_type: Annotated[fastlife.adapters.jinjax.widgets.base.TypeWrapper, "parent type"]
7
- #}
8
-
9
- <pydantic_form.Widget :widget="widget">
10
- <div id="{{widget.id}}">
11
- <Details>
12
- <Summary :id="widget.id + '-union-summary'">
13
- <H3 :class="H3_SUMMARY_CLASS">{{widget.title}}</H3>
14
- <pydantic_form.Error :text="widget.error" />
15
- </Summary>
16
- <div hx-sync="this" id="{{widget.id}}-child">
17
- {% if child %}
18
- {{ child }}
19
- {% else %}
20
- {% for typ in types %}
21
- <Button type="button" hx-target="closest div" :hx-get="typ.url" :hx-vals="typ.params|tojson" :id="typ.id"
22
- onclick={{ "document.getElementById('" + widget.id + "-remove-btn').hidden=false" }}
23
- :class="SECONDARY_BUTTON_CLASS">{{typ.title}}</Button>
24
- {% endfor %}
25
- {% endif %}
26
- </div>
27
- <Button type="button" :id="widget.id + '-remove-btn'" :hx-target="'#' + widget.id"
28
- :hx-vals="parent_type.params|tojson" :hx-get="parent_type.url" :hidden="not child"
29
- :class="SECONDARY_BUTTON_CLASS">
30
- Remove
31
- </Button>
32
- </Details>
33
- </div>
34
- </pydantic_form.Widget>
@@ -1,5 +0,0 @@
1
- """HTTP Request."""
2
-
3
- from .request import GenericRequest, Registry, Request, get_request
4
-
5
- __all__ = ["Request", "Registry", "GenericRequest", "get_request"]
@@ -1 +0,0 @@
1
- """Security features."""
@@ -1,188 +0,0 @@
1
- """Security policy."""
2
-
3
- import abc
4
- import logging
5
- from collections.abc import Callable, Coroutine
6
- from typing import Annotated, Any, Generic, Literal, TypeVar
7
- from uuid import UUID
8
-
9
- from fastapi import Depends, HTTPException
10
- from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN
11
-
12
- from fastlife import GenericRequest, get_request
13
- from fastlife.config.registry import TRegistry
14
-
15
- CheckPermissionHook = Callable[..., Coroutine[Any, Any, None]] | Callable[..., None]
16
- CheckPermission = Callable[[str], CheckPermissionHook]
17
-
18
- TUser = TypeVar("TUser")
19
-
20
- log = logging.getLogger(__name__)
21
-
22
-
23
- class Unauthorized(HTTPException):
24
- """An exception raised to stop a request exectution and return an HTTP Error."""
25
-
26
- def __init__(
27
- self,
28
- status_code: int = HTTP_401_UNAUTHORIZED,
29
- detail: str = "Unauthorized",
30
- headers: dict[str, str] | None = None,
31
- ) -> None:
32
- super().__init__(status_code, detail, headers)
33
-
34
-
35
- class Forbidden(HTTPException):
36
- """An exception raised to stop a request exectution and return an HTTP Error."""
37
-
38
- def __init__(
39
- self,
40
- status_code: int = HTTP_403_FORBIDDEN,
41
- detail: str = "Forbidden",
42
- headers: dict[str, str] | None = None,
43
- ) -> None:
44
- super().__init__(status_code, detail, headers)
45
-
46
-
47
- class BoolMeta(type):
48
- def __bool__(cls) -> bool:
49
- return cls.kind == "allowed" # type: ignore
50
-
51
- def __repr__(cls) -> str:
52
- return cls.reason # type: ignore
53
-
54
-
55
- class HasPermission(int, metaclass=BoolMeta):
56
- """
57
- A type used to know if a permission is allowed or not.
58
-
59
- It behave has a boolean, but 3 possibilities exists defind has 3 sub-types
60
- {class}`Allowed` {class}`Unauthenticated` or {class}`Denied`.
61
-
62
- In many cases Unauthenticated call may redirect to a login page,
63
- where authenticated user are not redirected. they have an error message,
64
- or the frontend may use the information to adapt its interface.
65
- """
66
-
67
- kind: Literal["allowed", "unauthenticated", "denied"]
68
- reason: str
69
-
70
- def __new__(cls, reason: str) -> "HasPermission":
71
- instance = super().__new__(cls)
72
- instance.reason = reason
73
- return instance
74
-
75
- def __repr__(self) -> str:
76
- return self.reason
77
-
78
- def __bool__(self) -> bool:
79
- return self.kind == "allowed"
80
-
81
-
82
- class Allowed(HasPermission):
83
- """Represent a permission check result that is allowed."""
84
-
85
- kind = "allowed"
86
- reason = "Allowed"
87
-
88
-
89
- class Unauthenticated(HasPermission):
90
- """
91
- Represent a permission check result that is not allowed due to
92
- missing authentication mechanism.
93
- """
94
-
95
- kind = "unauthenticated"
96
- reason = "Authentication required"
97
-
98
-
99
- class Denied(HasPermission):
100
- """
101
- Represent a permission check result that is not allowed due to lack of permission.
102
- """
103
-
104
- kind = "denied"
105
- reason = "Access denied to this resource"
106
-
107
-
108
- class AbstractSecurityPolicy(abc.ABC, Generic[TUser, TRegistry]):
109
- """Security policy base classe."""
110
-
111
- Forbidden = Forbidden
112
- """The exception raised if the user identified is not granted."""
113
- Unauthorized = Unauthorized
114
- """The exception raised if no user has been identified."""
115
-
116
- request: GenericRequest[TRegistry]
117
- """Request where the security policy is applied."""
118
-
119
- def __init__(
120
- self, request: Annotated[GenericRequest[TRegistry], Depends(get_request)]
121
- ):
122
- """
123
- Build the security policy.
124
-
125
- When implementing a security policy, multiple parameters can be added
126
- to the constructor as FastAPI dependencies, using the `Depends` FastAPI
127
- annotation.
128
- The security policy is installed has a depenency of the router that hold
129
- a route prefix of the application.
130
- """
131
- self.request = request
132
- self.request.security_policy = self # we do backref to implement has_permission
133
-
134
- @abc.abstractmethod
135
- async def identity(self) -> TUser | None:
136
- """
137
- Return app-specific user object or raise an HTTPException.
138
- """
139
-
140
- @abc.abstractmethod
141
- async def authenticated_userid(self) -> str | UUID | None:
142
- """
143
- Return app-specific user object or raise an HTTPException.
144
- """
145
-
146
- @abc.abstractmethod
147
- async def has_permission(
148
- self, permission: str
149
- ) -> HasPermission | type[HasPermission]:
150
- """Allow access to everything if signed in."""
151
-
152
- @abc.abstractmethod
153
- async def remember(self, user: TUser) -> None:
154
- """Save the user identity in the request session."""
155
-
156
- @abc.abstractmethod
157
- async def forget(self) -> None:
158
- """Destroy the request session."""
159
-
160
-
161
- class InsecurePolicy(AbstractSecurityPolicy[None, Any]):
162
- """
163
- An implementation of the security policy made for explicit unsecured access.
164
-
165
- Setting a permission on a view require a security policy, if not set, accessing
166
- to a view will raise a RuntimeError. To bypass this error for testing purpose
167
- or your own reason, the InsecurePolicy has to be set to the configurator.
168
- """
169
-
170
- async def identity(self) -> None:
171
- """Nobodies is identified."""
172
- return None
173
-
174
- async def authenticated_userid(self) -> str | UUID:
175
- """An uuid mades of 0."""
176
- return UUID(int=0)
177
-
178
- async def has_permission(
179
- self, permission: str
180
- ) -> HasPermission | type[HasPermission]:
181
- """Access is allways granted."""
182
- return Allowed
183
-
184
- async def remember(self, user: None) -> None:
185
- """Do nothing."""
186
-
187
- async def forget(self) -> None:
188
- """Do nothing."""
@@ -1,12 +0,0 @@
1
- """
2
- Utilities for rendering HTML templates for page and components as FastAPI dependencies.
3
- """
4
-
5
- from .binding import Template, template
6
- from .inline import InlineTemplate
7
-
8
- __all__ = [
9
- "Template",
10
- "template",
11
- "InlineTemplate",
12
- ]
@@ -1,52 +0,0 @@
1
- """
2
- Bind template to the view in order to build an html response.
3
- """
4
-
5
- from collections.abc import Callable
6
- from typing import Any
7
-
8
- from fastapi import Depends, Response
9
-
10
- from fastlife.request import Request
11
- from fastlife.security.csrf import create_csrf_token
12
-
13
- Template = Callable[..., Response]
14
- """Type to annotate a FastAPI depency injection."""
15
-
16
- TemplateEngine = Callable[[Request], Template]
17
-
18
-
19
- def get_template(template: str, *, content_type: str = "text/html") -> TemplateEngine:
20
- """
21
- Return a closure to render the given template.
22
-
23
- :param template: path to template to render.
24
- :param content_type: response ``Content-Type`` header.
25
- """
26
-
27
- def render_template(
28
- request: Request,
29
- *,
30
- _create_csrf_token: Callable[..., str] = create_csrf_token,
31
- ) -> Template:
32
- reg = request.registry
33
-
34
- def parametrizer(**kwargs: Any) -> Response:
35
- return reg.get_renderer(template)(request).render(
36
- template, content_type=content_type, params=kwargs
37
- )
38
-
39
- return parametrizer
40
-
41
- return render_template
42
-
43
-
44
- def template(template_path: str) -> Template:
45
- """
46
- Return a FastAPI dependency template engine ready to render the template.
47
-
48
-
49
- :param template_path: path to template to render by the engine setup in the regitry.
50
- :return: A callable accepting kwargs to pass as the context, returning a string.
51
- """
52
- return Depends(get_template(template_path))
File without changes