fastlifeweb 0.14.0__py3-none-any.whl → 0.15.1__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.
fastlife/__init__.py CHANGED
@@ -12,14 +12,13 @@ from .config import (
12
12
  from .request import Request
13
13
 
14
14
  # from .request.form_data import model
15
- from .templates import Template, template
15
+ from .services.templates import TemplateParams
16
16
 
17
17
  __all__ = [
18
18
  # Config
19
19
  "configure",
20
20
  "Configurator",
21
- "template",
22
- "Template",
21
+ "TemplateParams",
23
22
  "Registry",
24
23
  "Settings",
25
24
  "view_config",
@@ -0,0 +1 @@
1
+ """HTML Form generation using widgets."""
@@ -1,3 +1,4 @@
1
+ """Widget base class."""
1
2
  import abc
2
3
  import secrets
3
4
  from typing import Any, Generic, Mapping, Type, TypeVar
@@ -8,6 +8,6 @@
8
8
  #}
9
9
  <tbody
10
10
  {%- if id %} id="{{id}}" {%- endif %}
11
- {%- if class %} class="{{attrs.class}}" {%- endif %}>
11
+ {%- if attrs.class %} class="{{attrs.class}}" {%- endif %}>
12
12
  {{- content -}}
13
13
  </tbody>
@@ -8,6 +8,6 @@
8
8
  #}
9
9
  <tfoot
10
10
  {%- if id %} id="{{id}}" {%- endif %}
11
- {%- if class %} class="{{attrs.class}}" {%- endif %}>
11
+ {%- if attrs.class %} class="{{attrs.class}}" {%- endif %}>
12
12
  {{- content -}}
13
13
  </tfoot>
@@ -8,6 +8,6 @@
8
8
  #}
9
9
  <thead
10
10
  {%- if id %} id="{{id}}" {%- endif %}
11
- {%- if class %} class="{{attrs.class}}" {%- endif %}>
11
+ {%- if attrs.class %} class="{{attrs.class}}" {%- endif %}>
12
12
  {{- content -}}
13
13
  </thead>
@@ -8,6 +8,6 @@
8
8
  #}
9
9
  <tr
10
10
  {%- if id %} id="{{id}}" {%- endif %}
11
- {%- if class %} class="{{attrs.class}}" {%- endif %}>
11
+ {%- if attrs.class %} class="{{attrs.class}}" {%- endif %}>
12
12
  {{- content -}}
13
13
  </tr>
@@ -28,20 +28,22 @@ from fastapi import Response
28
28
  from fastapi.params import Depends as DependsType
29
29
  from fastapi.staticfiles import StaticFiles
30
30
  from fastapi.types import IncEx
31
- from pydantic import BaseModel, Field
32
31
 
32
+ from fastlife.config.openapiextra import OpenApiTag
33
33
  from fastlife.middlewares.base import AbstractMiddleware
34
34
  from fastlife.request.request import Request
35
35
  from fastlife.routing.route import Route
36
36
  from fastlife.routing.router import Router
37
37
  from fastlife.security.csrf import check_csrf
38
+ from fastlife.services.policy import check_permission
38
39
  from fastlife.shared_utils.resolver import resolve
39
40
 
40
41
  from .settings import Settings
41
42
 
42
43
  if TYPE_CHECKING:
44
+ from fastlife.security.policy import AbstractSecurityPolicy # coverage: ignore
43
45
  from fastlife.services.templates import (
44
- AbstractTemplateRendererFactory, # coverage: ignore; coverage: ignore
46
+ AbstractTemplateRendererFactory, # coverage: ignore
45
47
  )
46
48
 
47
49
  from .registry import AppRegistry, LocaleNegociator # coverage: ignore
@@ -58,24 +60,53 @@ class ConfigurationError(Exception):
58
60
  """
59
61
 
60
62
 
61
- class ExternalDocs(BaseModel):
62
- """OpenAPI externalDocs object."""
63
+ def rebuild_router(router: Router) -> Router:
64
+ """
65
+ Fix the router.
63
66
 
64
- description: str
65
- """link's description."""
66
- url: str
67
- """link's URL."""
67
+ FastAPI routers has dependencies that are injected to routes where they are added.
68
68
 
69
+ It means that if you add a dependencies in the router after the route has
70
+ been added, then the dependencies is missing in the route added before.
69
71
 
70
- class OpenApiTag(BaseModel):
71
- """OpenAPI tag object."""
72
+ To prenvents issues, we rebuild the router route with the dependency.
72
73
 
73
- name: str
74
- """name of the tag."""
75
- description: str
76
- """explanation of the tag."""
77
- external_docs: ExternalDocs | None = Field(alias="externalDocs", default=None)
78
- """external link to the doc."""
74
+ :param router: the router to rebuild
75
+ :return: a new router with fixed routes.
76
+ """
77
+ if not router.dependencies:
78
+ return router
79
+ _router = Router(prefix=router.prefix)
80
+ _router.dependencies = router.dependencies
81
+ route: Route
82
+ for route in router.routes: # type: ignore
83
+ dependencies = [
84
+ dep for dep in route.dependencies if dep not in _router.dependencies
85
+ ]
86
+ _router.add_api_route(
87
+ path=route.path,
88
+ endpoint=route.endpoint,
89
+ response_model=route.response_model,
90
+ status_code=route.status_code,
91
+ tags=route.tags,
92
+ dependencies=dependencies,
93
+ summary=route.summary,
94
+ description=route.description,
95
+ response_description=route.response_description,
96
+ deprecated=route.deprecated,
97
+ methods=route.methods,
98
+ operation_id=route.operation_id,
99
+ response_model_include=route.response_model_include,
100
+ response_model_exclude=route.response_model_exclude,
101
+ response_model_by_alias=route.response_model_by_alias,
102
+ response_model_exclude_unset=route.response_model_exclude_unset,
103
+ response_model_exclude_defaults=route.response_model_exclude_defaults,
104
+ response_model_exclude_none=route.response_model_exclude_none,
105
+ include_in_schema=route.include_in_schema,
106
+ name=route.name,
107
+ openapi_extra=route.openapi_extra,
108
+ )
109
+ return _router
79
110
 
80
111
 
81
112
  class Configurator:
@@ -107,6 +138,7 @@ class Configurator:
107
138
 
108
139
  self._route_prefix: str = ""
109
140
  self._routers: dict[str, Router] = defaultdict(Router)
141
+ self._security_policies: dict[str, "type[AbstractSecurityPolicy[Any]]"] = {}
110
142
 
111
143
  self.scanner = venusian.Scanner(fastlife=self)
112
144
  self.include("fastlife.views")
@@ -145,8 +177,6 @@ class Configurator:
145
177
  else None,
146
178
  )
147
179
  app.router.route_class = Route
148
- for prefix, router in self._routers.items():
149
- app.include_router(router, prefix=prefix)
150
180
 
151
181
  for middleware_class, options in self.middlewares:
152
182
  app.add_middleware(middleware_class, **options) # type: ignore
@@ -154,6 +184,9 @@ class Configurator:
154
184
  for status_code_or_exc, exception_handler in self.exception_handlers:
155
185
  app.add_exception_handler(status_code_or_exc, exception_handler)
156
186
 
187
+ for prefix, router in self._routers.items():
188
+ app.include_router(rebuild_router(router), prefix=prefix)
189
+
157
190
  for route_path, directory, name in self.mounts:
158
191
  app.mount(route_path, StaticFiles(directory=directory), name=name)
159
192
  return app
@@ -262,6 +295,27 @@ class Configurator:
262
295
  self.middlewares.append((middleware_class, options))
263
296
  return self
264
297
 
298
+ def set_security_policy(
299
+ self, security_policy: "type[AbstractSecurityPolicy[Any]]"
300
+ ) -> Self:
301
+ """
302
+ Set a security policy for the application.
303
+
304
+ ```{important}
305
+ The security policy is **per route_prefix**.
306
+ It means that if the application is splitted via multiple
307
+ route_prefix using the {meth}`Configurator.include`, they
308
+ all have a distinct security policy. A secutity policy has
309
+ to be install by all of those include call.
310
+
311
+ :param security_policy: The security policy that will applied for the app
312
+ portion behind the route prefix.
313
+ ```
314
+ """
315
+ self._security_policies[self._route_prefix] = security_policy
316
+ self._current_router.dependencies.append(Depends(security_policy))
317
+ return self
318
+
265
319
  def add_api_route(
266
320
  self,
267
321
  name: str,
@@ -311,11 +365,9 @@ class Configurator:
311
365
  :param path: path of the route, use `{curly_brace}` to inject FastAPI Path
312
366
  parameters.
313
367
  :param endpoint: the function that will reveive the request.
314
- :param permission: a permission to validate by the
315
- :attr:`fastlife.config.settings.Settings.check_permission` function.
316
-
368
+ :param permission: a permission to validate by the security policy.
317
369
  :param methods: restrict route to a list of http methods.
318
- :param description:OpenAPI description for the route.
370
+ :param description:{term}`OpenAPI` description for the route.
319
371
  :param summary: OpenAPI summary for the route.
320
372
  :param response_description: OpenAPI description for the response.
321
373
  :param operation_id: OpenAPI optional unique string used to identify an
@@ -333,13 +385,13 @@ class Configurator:
333
385
  :param response_model_exclude_none: exclude fields instead of serialize to
334
386
  null value.
335
387
  :param include_in_schema: expose or not the route in the doc.
336
- :param openapi_extra: open api documentation extra fields.
388
+ :param openapi_extra: OpenAPI documentation extra fields.
337
389
 
338
390
  :return: the configurator.
339
391
  """
340
392
  dependencies: list[DependsType] = []
341
393
  if permission:
342
- dependencies.append(Depends(self.registry.check_permission(permission)))
394
+ dependencies.append(Depends(check_permission(permission)))
343
395
 
344
396
  self._current_router.add_api_route(
345
397
  path,
@@ -394,15 +446,13 @@ class Configurator:
394
446
  :param path: path of the route, use `{curly_brace}` to inject FastAPI Path
395
447
  parameters.
396
448
  :param endpoint: the function that will reveive the request.
397
- :param permission: a permission to validate by the
398
- :attr:`fastlife.config.settings.Settings.check_permission` function.
399
-
449
+ :param permission: a permission to validate by the security policy.
400
450
  :param methods: restrict route to a list of http methods.
401
451
  :return: the configurator.
402
452
  """
403
453
  dependencies: list[DependsType] = []
404
454
  if permission:
405
- dependencies.append(Depends(self.registry.check_permission(permission)))
455
+ dependencies.append(Depends(check_permission(permission)))
406
456
 
407
457
  if template:
408
458
 
@@ -459,6 +509,11 @@ class Configurator:
459
509
  """
460
510
 
461
511
  def exception_handler(request: BaseRequest, exc: Exception) -> Any:
512
+ # FastAPI exception handler does not provide our request object
513
+ # it seems like it is rebuild from the asgi scope. Even the router
514
+ # class is wrong.
515
+ # Until we store a security policy per rooter, we rebuild an
516
+ # incomplete request here.
462
517
  request = Request(self.registry, request)
463
518
  resp = handler(request, exc)
464
519
  if isinstance(resp, Response):
@@ -494,7 +549,7 @@ class Configurator:
494
549
  self.registry.renderers[f".{file_ext.lstrip('.')}"] = renderer # type: ignore
495
550
  return self
496
551
 
497
- def add_template_search_path(self, path: str) -> Self:
552
+ def add_template_search_path(self, path: str | Path) -> Self:
498
553
  """
499
554
  Add a template search path directly from the code.
500
555
 
@@ -0,0 +1,22 @@
1
+ """Types for OpenAPI documentation."""
2
+ from pydantic import BaseModel, Field
3
+
4
+
5
+ class ExternalDocs(BaseModel):
6
+ """OpenAPI externalDocs object."""
7
+
8
+ description: str
9
+ """link's description."""
10
+ url: str
11
+ """link's URL."""
12
+
13
+
14
+ class OpenApiTag(BaseModel):
15
+ """OpenAPI tag object."""
16
+
17
+ name: str
18
+ """name of the tag."""
19
+ description: str
20
+ """explanation of the tag."""
21
+ external_docs: ExternalDocs | None = Field(alias="externalDocs", default=None)
22
+ """external link to the doc."""
@@ -5,9 +5,7 @@ from fastapi import Depends
5
5
  from fastapi import Request as FastAPIRequest
6
6
 
7
7
  from fastlife.request.request import Request
8
- from fastlife.security.policy import CheckPermission
9
8
  from fastlife.services.translations import LocalizerFactory
10
- from fastlife.shared_utils.resolver import resolve
11
9
 
12
10
  if TYPE_CHECKING:
13
11
  from fastlife.services.templates import ( # coverage: ignore
@@ -34,13 +32,11 @@ class AppRegistry:
34
32
 
35
33
  settings: Settings
36
34
  renderers: Mapping[str, "AbstractTemplateRendererFactory"]
37
- check_permission: CheckPermission
38
35
  locale_negociator: LocaleNegociator
39
36
  localizer: LocalizerFactory
40
37
 
41
38
  def __init__(self, settings: Settings) -> None:
42
39
  self.settings = settings
43
- self.check_permission = resolve(settings.check_permission)
44
40
  self.locale_negociator = _default_negociator(self.settings)
45
41
  self.renderers = {}
46
42
  self.localizer = LocalizerFactory()
@@ -13,11 +13,12 @@ from typing import Any, Callable
13
13
  import venusian
14
14
  from fastapi.types import IncEx
15
15
 
16
+ from fastlife.config.openapiextra import ExternalDocs
17
+
16
18
  from .configurator import (
17
19
  VENUSIAN_CATEGORY,
18
20
  ConfigurationError,
19
21
  Configurator,
20
- ExternalDocs,
21
22
  OpenApiTag,
22
23
  )
23
24
 
@@ -143,6 +144,7 @@ def resource(
143
144
 
144
145
 
145
146
  def resource_view(
147
+ *,
146
148
  permission: str | None = None,
147
149
  status_code: int | None = None,
148
150
  summary: str | None = None,
@@ -106,9 +106,6 @@ class Settings(BaseSettings):
106
106
  domain_name: str = Field(default="", title="domain name where the app is served")
107
107
  """Domain name whre the app is served."""
108
108
 
109
- check_permission: str = Field(default="fastlife.security.policy:check_permission")
110
- """Handler for checking permission set on any views using the configurator."""
111
-
112
109
  decode_reverse_proxy_headers: bool = Field(default=True)
113
110
  """Ensure that the request object has information based on http proxy headers."""
114
111
 
@@ -1,11 +1,15 @@
1
1
  """HTTP Request representation in a python object."""
2
2
 
3
- from typing import TYPE_CHECKING
3
+ from typing import TYPE_CHECKING, Any
4
4
 
5
5
  from fastapi import Request as FastAPIRequest
6
6
 
7
7
  if TYPE_CHECKING:
8
8
  from fastlife.config.registry import AppRegistry # coverage: ignore
9
+ from fastlife.security.policy import ( # coverage: ignore
10
+ AbstractSecurityPolicy,
11
+ HasPermission,
12
+ )
9
13
 
10
14
 
11
15
  class Request(FastAPIRequest):
@@ -16,7 +20,30 @@ class Request(FastAPIRequest):
16
20
  locale_name: str
17
21
  """Request locale used for the i18n of the response."""
18
22
 
23
+ security_policy: "AbstractSecurityPolicy[Any] | None"
24
+ """Request locale used for the i18n of the response."""
25
+
19
26
  def __init__(self, registry: "AppRegistry", request: FastAPIRequest) -> None:
20
27
  super().__init__(request.scope, request.receive)
21
28
  self.registry = registry
22
29
  self.locale_name = registry.locale_negociator(self)
30
+ self.security_policy = None # build it from the ? registry
31
+
32
+ async def has_permission(
33
+ self, permission: str
34
+ ) -> "HasPermission | type[HasPermission]":
35
+ """
36
+ A helper to check that a user has the given permission.
37
+
38
+ Not that this method does not raised, it return a boolean like object.
39
+ It allows batch permission checks.
40
+ You might need to check multiple permissions in different contexts or
41
+ for different resources before raising an http error.
42
+ """
43
+ if self.security_policy is None:
44
+ raise RuntimeError(
45
+ f"Request {self.url.path} require a security policy, "
46
+ "explicit fastlife.security.policy.InsecurePolicy is required."
47
+ )
48
+
49
+ return await self.security_policy.has_permission(permission)
@@ -1,26 +1,171 @@
1
1
  """Security policy."""
2
- from typing import Any, Callable, Coroutine
2
+
3
+ import abc
4
+ import logging
5
+ from typing import Any, Callable, Coroutine, Literal, TypeVar
6
+ from uuid import UUID
7
+
8
+ from fastapi import HTTPException
9
+ from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN
10
+ from typing_extensions import Generic
11
+
12
+ from fastlife import Request
3
13
 
4
14
  CheckPermissionHook = Callable[..., Coroutine[Any, Any, None]] | Callable[..., None]
5
15
  CheckPermission = Callable[[str], CheckPermissionHook]
6
16
 
17
+ TUser = TypeVar("TUser")
18
+
19
+ log = logging.getLogger(__name__)
20
+
21
+
22
+ class Unauthorized(HTTPException):
23
+ """An exception raised to stop a request exectution and return an HTTP Error."""
24
+
25
+ def __init__(
26
+ self,
27
+ status_code: int = HTTP_401_UNAUTHORIZED,
28
+ detail: str = "Unauthorized",
29
+ headers: dict[str, str] | None = None,
30
+ ) -> None:
31
+ super().__init__(status_code, detail, headers)
32
+
33
+
34
+ class Forbidden(HTTPException):
35
+ """An exception raised to stop a request exectution and return an HTTP Error."""
36
+
37
+ def __init__(
38
+ self,
39
+ status_code: int = HTTP_403_FORBIDDEN,
40
+ detail: str = "Forbidden",
41
+ headers: dict[str, str] | None = None,
42
+ ) -> None:
43
+ super().__init__(status_code, detail, headers)
44
+
45
+
46
+ class BoolMeta(type):
47
+ def __bool__(cls) -> bool:
48
+ return cls.kind == "allowed" # type: ignore
49
+
50
+ def __repr__(cls) -> str:
51
+ return cls.reason # type: ignore
52
+
53
+
54
+ class HasPermission(int, metaclass=BoolMeta):
55
+ kind: Literal["allowed", "unauthenticated", "denied"]
56
+ reason: str
57
+
58
+ def __new__(cls, reason: str) -> "HasPermission":
59
+ instance = super().__new__(cls)
60
+ instance.reason = reason
61
+ return instance
62
+
63
+ def __repr__(self) -> str:
64
+ return self.reason
65
+
66
+ def __bool__(self) -> bool:
67
+ return self.kind == "allowed"
68
+
69
+
70
+ class Allowed(HasPermission):
71
+ """Represent a permission check result that is allowed."""
72
+
73
+ kind = "allowed"
74
+ reason = "Allowed"
75
+
7
76
 
8
- def check_permission(permission_name: str) -> CheckPermissionHook:
77
+ class Unauthenticated(HasPermission):
78
+ """
79
+ Represent a permission check result that is not allowed due to
80
+ missing authentication mechanism.
9
81
  """
10
- A closure that check that a user as the given permission_name.
11
82
 
12
- This method has to be overriden using the setting
13
- :attr:`fastlife.config.settings.Settings.check_permission` to implement it.
83
+ kind = "unauthenticated"
84
+ reason = "Authentication required"
14
85
 
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`
18
86
 
19
- :param permission_name: a permission name set in a view to check access.
20
- :return: a function that raise http exceptions or any configured exception here.
87
+ class Denied(HasPermission):
88
+ """
89
+ Represent a permission check result that is not allowed due to lack of permission.
21
90
  """
22
91
 
23
- def depencency_injection() -> None:
24
- ...
92
+ kind = "denied"
93
+ reason = "Access denied to this resource"
94
+
95
+
96
+ class AbstractSecurityPolicy(abc.ABC, Generic[TUser]):
97
+ """Security policy base classe."""
98
+
99
+ Forbidden = Forbidden
100
+ """The exception raised if the user identified is not granted."""
101
+ Unauthorized = Unauthorized
102
+ """The exception raised if no user has been identified."""
103
+
104
+ def __init__(self, request: Request):
105
+ """
106
+ Build the security policy.
107
+
108
+ When implementing a security policy, multiple parameters can be added
109
+ to the constructor as FastAPI dependencies, using the `Depends` FastAPI
110
+ annotation.
111
+ The security policy is installed has a depenency of the router that hold
112
+ a route prefix of the application.
113
+ """
114
+ self.request = request
115
+ self.request.security_policy = self # we do backref to implement has_permission
116
+
117
+ @abc.abstractmethod
118
+ async def identity(self) -> TUser | None:
119
+ """
120
+ Return app-specific user object or raise an HTTPException.
121
+ """
122
+
123
+ @abc.abstractmethod
124
+ async def authenticated_userid(self) -> str | UUID | None:
125
+ """
126
+ Return app-specific user object or raise an HTTPException.
127
+ """
128
+
129
+ @abc.abstractmethod
130
+ async def has_permission(
131
+ self, permission: str
132
+ ) -> HasPermission | type[HasPermission]:
133
+ """Allow access to everything if signed in."""
134
+
135
+ @abc.abstractmethod
136
+ async def remember(self, user: TUser) -> None:
137
+ """Save the user identity in the request session."""
138
+
139
+ @abc.abstractmethod
140
+ async def forget(self) -> None:
141
+ """Destroy the request session."""
142
+
143
+
144
+ class InsecurePolicy(AbstractSecurityPolicy[None]):
145
+ """
146
+ An implementation of the security policy made for explicit unsecured access.
147
+
148
+ Setting a permission on a view require a security policy, if not set, accessing
149
+ to a view will raise a RuntimeError. To bypass this error for testing purpose
150
+ or your own reason, the InsecurePolicy has to be set to the configurator.
151
+ """
152
+
153
+ async def identity(self) -> None:
154
+ """Nobodies is identified."""
155
+ return None
156
+
157
+ async def authenticated_userid(self) -> str | UUID:
158
+ """An uuid mades of 0."""
159
+ return UUID(int=0)
160
+
161
+ async def has_permission(
162
+ self, permission: str
163
+ ) -> HasPermission | type[HasPermission]:
164
+ """Access is allways granted."""
165
+ return Allowed
166
+
167
+ async def remember(self, user: None) -> None:
168
+ """Do nothing."""
25
169
 
26
- return depencency_injection
170
+ async def forget(self) -> None:
171
+ """Do nothing."""
@@ -0,0 +1,39 @@
1
+ """Security policy."""
2
+
3
+ from typing import Any, Callable, Coroutine
4
+
5
+ CheckPermissionHook = Callable[..., Coroutine[Any, Any, None]] | Callable[..., None]
6
+ CheckPermission = Callable[[str], CheckPermissionHook]
7
+
8
+
9
+ def check_permission(permission_name: str) -> CheckPermissionHook:
10
+ """
11
+ A closure that check that a user as the given permission_name.
12
+
13
+ Adding a permission on the route requires that a security policy has been
14
+ added using the method
15
+ {meth}`fastlife.config.configurator.Configurator.set_security_policy`
16
+
17
+ :param permission_name: a permission name set in a view to check access.
18
+ :return: a function that raise http exceptions or any configured exception here.
19
+ """
20
+ # the check_permission is called by the configurator
21
+ # and the request is exposed in the public module creating a circular dependency.
22
+ from fastlife import Request # a type must be resolved to inject a dependency.
23
+
24
+ async def depencency_injection(request: Request) -> None:
25
+ if request.security_policy is None:
26
+ raise RuntimeError(
27
+ f"Request {request.url.path} require a security policy, "
28
+ "explicit fastlife.security.policy.InsecurePolicy is required"
29
+ )
30
+ allowed = await request.security_policy.has_permission(permission_name)
31
+ match allowed.kind:
32
+ case "allowed":
33
+ return
34
+ case "denied":
35
+ raise request.security_policy.Forbidden(detail=allowed.reason)
36
+ case "unauthenticated":
37
+ raise request.security_policy.Unauthorized(detail=allowed.reason)
38
+
39
+ return depencency_injection
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fastlifeweb
3
- Version: 0.14.0
3
+ Version: 0.15.1
4
4
  Summary: High-level web framework
5
5
  Home-page: https://github.com/mardiros/fastlife
6
6
  License: BSD-derived
@@ -17,8 +17,9 @@ Classifier: Programming Language :: Python :: 3.11
17
17
  Classifier: Programming Language :: Python :: 3.12
18
18
  Classifier: Topic :: Internet :: WWW/HTTP
19
19
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Provides-Extra: testing
20
21
  Requires-Dist: babel (>=2.16.0,<3.0.0)
21
- Requires-Dist: beautifulsoup4[testing] (>=4.12.2,<5.0.0)
22
+ Requires-Dist: beautifulsoup4 (>=4.12.2,<5.0.0) ; extra == "testing"
22
23
  Requires-Dist: fastapi[standard] (>=0.115.0,<0.116.0)
23
24
  Requires-Dist: itsdangerous (>=2.1.2,<3.0.0)
24
25
  Requires-Dist: jinjax (>=0.44,<0.45)
@@ -42,12 +43,12 @@ Description-Content-Type: text/markdown
42
43
  > Please note that this project is still in active development. Features and APIs may change frequently.
43
44
  > Even the name is not definitive.
44
45
 
45
- An opinionated framework Python web framework (based on FastAPI).
46
+ An opinionated Python web framework (based on FastAPI).
46
47
 
47
48
  ## Purpose
48
49
 
49
- Fastlife helps at building Web Application with html form generated from pydantic schema
50
- using customizable widget.
50
+ Fastlife helps at building Web Application with session, security, html test client,
51
+ and html form generated from pydantic schema using customizable widget.
51
52
 
52
53
  Templates are made using [JinjaX](https://jinjax.scaletti.dev/) and an extensible [set of
53
54
  component](https://mardiros.github.io/fastlife/components/index.html) is available
@@ -1,9 +1,9 @@
1
- fastlife/__init__.py,sha256=qjWjYd07qVhyG-WVMZqnBTJy_AV2jC7CHHFzAuTdv98,543
1
+ fastlife/__init__.py,sha256=sOuA910DThakr7Pn4VY7rGVMbbjU0JP8QQ3VqEpVzkM,538
2
2
  fastlife/adapters/__init__.py,sha256=WYjEN8gp4r7LCHqmIO5VzzvsT8QGRE3w4G47UwYDtAo,94
3
3
  fastlife/adapters/jinjax/__init__.py,sha256=jy88zyqk7nFlaY-0lmgAoe0HyO5r_NKckQb3faQiUv4,137
4
4
  fastlife/adapters/jinjax/renderer.py,sha256=tKX_C0fGnooMcMXBz4_8Ym2VLCKEkRk9_AXq6HrN11I,13040
5
- fastlife/adapters/jinjax/widgets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- fastlife/adapters/jinjax/widgets/base.py,sha256=idw9rVYfY-J9ASkxUFyH2EHtDraYORLd586Noo0-5I4,3990
5
+ fastlife/adapters/jinjax/widgets/__init__.py,sha256=HERnX9xiXUbTDz3XtlnHWABTBjhIq_kkBgWs5E6ZIMY,42
6
+ fastlife/adapters/jinjax/widgets/base.py,sha256=JoB1bLI97ZW2ycfBcHgrPxiOszE9t_SFFK5L1bR-cSo,4015
7
7
  fastlife/adapters/jinjax/widgets/boolean.py,sha256=w4hZMo_8xDoThStlIUR4eVfLm8JwUp0-TaGCjGSyCbA,1145
8
8
  fastlife/adapters/jinjax/widgets/checklist.py,sha256=VIIfJ8JB7ZAISwFFiGZ7jfGuNJ9sjkhRSVFqDTr-RR4,1655
9
9
  fastlife/adapters/jinjax/widgets/dropdown.py,sha256=x2Y9BOfHfSuzWD_HNrvCkiJtKDxl8Vs05Uk8QKvRxyY,1622
@@ -34,13 +34,13 @@ fastlife/components/Radio.jinja,sha256=ohucEGNofDsC4-Hp6Ovexi5XW812-kfAzsFCcOyVF
34
34
  fastlife/components/Select.jinja,sha256=aPqdPrCh6ooxj3XkeptefwvzFqC5XC4FLtW8XLj3CpM,556
35
35
  fastlife/components/Summary.jinja,sha256=EFVG2d7JJu7j5RRLNqKHxXcWAEJmv3Wif1aI5HXq1Zc,853
36
36
  fastlife/components/Table.jinja,sha256=X87pJkqp-fl4Uno52Ijj1Lw4MFYsqr4Jc6Oi4KGQVhM,390
37
- fastlife/components/Tbody.jinja,sha256=LG6B41wnKc3My3LoCQJPYiLnd7pz3JmRAJ2o7a_m1hs,325
37
+ fastlife/components/Tbody.jinja,sha256=FqldzLFhV6dfG0UsB55kDtALixpnECdIISXGWXiXxEM,331
38
38
  fastlife/components/Td.jinja,sha256=NTV86bdtm8t_2_2Pj1X8vsCqm4NSE4H5NPUoSt1i8cA,374
39
39
  fastlife/components/Textarea.jinja,sha256=0WsTi-yxVwYXndmJJN93WblQVqM0h0NCSMUDwoC3kdc,636
40
- fastlife/components/Tfoot.jinja,sha256=hxdYkc-3P27Qm9ZobSxhpjugOFJifVia20uHzTXHvIk,325
40
+ fastlife/components/Tfoot.jinja,sha256=Hw_eFhmbHcOx0dCHrNkyi4q1lrKye5RpihHBwjaT3zY,331
41
41
  fastlife/components/Th.jinja,sha256=tuIlLlwtvZiBHhQD_J7p8V-An-EeA4mzm_nRxUoGDzQ,375
42
- fastlife/components/Thead.jinja,sha256=FB0ZaYwQR1KEJun5XpuO4sXsUuyPabXxf8M5qkeD-xI,324
43
- fastlife/components/Tr.jinja,sha256=tWMonYy0XPCaGCeheo6aqqSXo7wxqxz6hPUBGAWrHNI,316
42
+ fastlife/components/Thead.jinja,sha256=TpnnbtbgYe97e0QDjeJulbh0_-Cbu3z_Qh9dsraQNb4,330
43
+ fastlife/components/Tr.jinja,sha256=kWy-jx4ShT2dv9vuHqJtMnc3O2pZ3y8KCWEAifMpjrU,322
44
44
  fastlife/components/icons/AcademicCap.jinja,sha256=0yJiwNF09SXGmssEHFkEE_2GPb2LFN1BIR4DChDRBls,528
45
45
  fastlife/components/icons/AdjustmentsHorizontal.jinja,sha256=eaAOfrTqfmkJmNaFeCiBSzSCohu6_kqNSuFIB1Vz1RE,568
46
46
  fastlife/components/icons/AdjustmentsVertical.jinja,sha256=n7jBi400_q6KnhMABtUwKBBM_7n1IXFtdtaNwamrfxs,560
@@ -1666,11 +1666,12 @@ fastlife/components/pydantic_form/Textarea.jinja,sha256=NzfCi5agRUSVcb5RXw0QamM8
1666
1666
  fastlife/components/pydantic_form/Union.jinja,sha256=czTska54z9KCZKu-FaycLmOvtH6y6CGUFQ8DHnkjrJk,1461
1667
1667
  fastlife/components/pydantic_form/Widget.jinja,sha256=EXskDqt22D5grpGVwlZA3ndve2Wr_6yQH4qVE9c31Og,397
1668
1668
  fastlife/config/__init__.py,sha256=O_Mw2XOxo55SArHdGKRhlTrroRN8ymwfzYKlHG0eV_s,418
1669
- fastlife/config/configurator.py,sha256=LNLFLyuqhoz6y6ORX6qeU0ZycPBSKu0KVZYcARAge7k,18540
1669
+ fastlife/config/configurator.py,sha256=jOZvlUVZH7jE1ay9iwVVTudApN1uYNXGQcWT3xaJ5lg,21236
1670
1670
  fastlife/config/exceptions.py,sha256=2MS2MFgb3mDbtdHEwnC-QYubob3Rl0v8O8y615LY0ds,1180
1671
- fastlife/config/registry.py,sha256=rwCS_vyu1Hli8-4Cs6Z-Jxn7N7ZXWSyZqnXIIV0EJ5I,1958
1672
- fastlife/config/resources.py,sha256=Db183g_CC0Voa6IblaNSzcv7XBH1S3s2nTAFNXtz9Cg,8732
1673
- fastlife/config/settings.py,sha256=ZMjfx_XQhu6mPE5q3eI9dG5disnbGUcALUqNYDCknhk,4199
1671
+ fastlife/config/openapiextra.py,sha256=_9rBYeTqB7nVuzvUHMwZU387bTfYFHYLlP05NP0vEDs,513
1672
+ fastlife/config/registry.py,sha256=Zm3i9ZfQZOP3vNKNKQ8nar7XevI91bqy2tUXX492Fuk,1749
1673
+ fastlife/config/resources.py,sha256=pM0j5VKVbVak4Z5mFRHBjAjUqORP4TAtCnZM3res5o8,8776
1674
+ fastlife/config/settings.py,sha256=FepHFZVHPLy3yA3dQux79GiH65MkipqWyT-zgXUBOKE,4028
1674
1675
  fastlife/config/views.py,sha256=Dxi6lO4gFs6GriAW7Rh5GDvebwbrpS2HzYhf30pXJiE,2058
1675
1676
  fastlife/middlewares/__init__.py,sha256=C3DUOzR5EhlAv5Zq7h-Abyvkd7bUsJohTRSB2wpRYQE,220
1676
1677
  fastlife/middlewares/base.py,sha256=9OYqByRuVoIrLt353NOedPQTLdr7LSmxhb2BZcp20qk,638
@@ -1684,14 +1685,15 @@ fastlife/request/__init__.py,sha256=C5lZAsUZJDw0TfTegRyN5BEhzgfiqvfc8YXu2vp0siU,
1684
1685
  fastlife/request/form.py,sha256=FucGua79LCKqNBP6Ycle7-5JU6EMI6SrHCgoJBcvGY4,3532
1685
1686
  fastlife/request/form_data.py,sha256=yoP-AYF-dSClpCQuZNRTY-c1OnDga5MoTjBKIzgpTs8,4459
1686
1687
  fastlife/request/localizer.py,sha256=9MXAcsod-Po5qeg4lttD3dyumiI0y5vGHCwSSmt9or8,349
1687
- fastlife/request/request.py,sha256=ACteAZUEaQHhE7l7LDEzY1wDhTegCKr-rsQmzTwtsGY,695
1688
+ fastlife/request/request.py,sha256=NiRtEV_iw8BuESQPf7gqXxiHsQwLiyOu1yzQy7Ck9eM,1779
1688
1689
  fastlife/routing/__init__.py,sha256=8EMnQE5n8oA4J9_c3nxzwKDVt3tefZ6fGH0d2owE8mo,195
1689
1690
  fastlife/routing/route.py,sha256=8RMnAqun8GTfWp6PFIWmWu-q-K89M3SiVSi2A-DYqyM,1405
1690
1691
  fastlife/routing/router.py,sha256=bLZ4k2aDs4L423znwGnw-X2LnM36O8fjhDWc8q1WewI,481
1691
1692
  fastlife/security/__init__.py,sha256=QYDcJ3oXQzqXQxoDD_6biGAtercFrtePttoifiL1j34,25
1692
1693
  fastlife/security/csrf.py,sha256=PvC9Fqdb6c0IzzsnaMx2quQdjjKrb-nOPoAHfcwoAe8,2141
1693
- fastlife/security/policy.py,sha256=MYNPQlvnTBe1XsYNJoIwiGW2DFb8vciU3XyUi9ZlLt0,945
1694
+ fastlife/security/policy.py,sha256=3aENmA_plxHbnUKzsIWtCAboT3oyVew0dcJKDrrILqE,4880
1694
1695
  fastlife/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1696
+ fastlife/services/policy.py,sha256=ZK4K3fVGT1fUeBdo5sSWnEcJg49CZuaI3z2gyg3KguQ,1653
1695
1697
  fastlife/services/templates.py,sha256=7gPJxGWD-XqputbZpy_Icsz3WHKJaWg2JgkVOeKrjfA,3840
1696
1698
  fastlife/services/translations.py,sha256=Bo5CIjdbQ3g_ihbv4Bz60hzd8VOtqEEPOyhJEbGcvP4,4363
1697
1699
  fastlife/shared_utils/__init__.py,sha256=i66ytuf-Ezo7jSiNQHIsBMVIcB-tDX0tg28-pUOlhzE,26
@@ -1704,7 +1706,7 @@ fastlife/testing/__init__.py,sha256=vuqwoNUd3BuIp3fm7nkvmYkIGjIimf5zUGhDkeWrg2s,
1704
1706
  fastlife/testing/testclient.py,sha256=BC7lLQ_jc59UmknAKzgRtW9a3cpX_V_QLp9Mg2ScLA8,20546
1705
1707
  fastlife/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1706
1708
  fastlife/views/pydantic_form.py,sha256=fvjk_5-JAugpBPxwD5GkxxsQiz9eAxzWeCSU9kiyc6s,1438
1707
- fastlifeweb-0.14.0.dist-info/LICENSE,sha256=F75xSseSKMwqzFj8rswYU6NWS3VoWOc_gY3fJYf9_LI,1504
1708
- fastlifeweb-0.14.0.dist-info/METADATA,sha256=fvjfxD3v-keA5bZ1agcr4wjeJ3VxhBwwtR0Gs7yHamk,3278
1709
- fastlifeweb-0.14.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
1710
- fastlifeweb-0.14.0.dist-info/RECORD,,
1709
+ fastlifeweb-0.15.1.dist-info/LICENSE,sha256=F75xSseSKMwqzFj8rswYU6NWS3VoWOc_gY3fJYf9_LI,1504
1710
+ fastlifeweb-0.15.1.dist-info/METADATA,sha256=3OB6Kxf1HPxQD0Qi1GFRt2_LvH8VTEWzZn1WDjbyI4o,3345
1711
+ fastlifeweb-0.15.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
1712
+ fastlifeweb-0.15.1.dist-info/RECORD,,