fastlifeweb 0.2.0__py3-none-any.whl → 0.2.2__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/configurator/configurator.py +22 -12
- fastlife/configurator/registry.py +5 -0
- fastlife/configurator/settings.py +3 -0
- fastlife/security/policy.py +18 -0
- fastlife/session/__init__.py +1 -1
- fastlife/session/middleware.py +7 -5
- fastlife/session/serializer.py +6 -1
- fastlife/testing/testclient.py +110 -48
- fastlife/views/pydantic_form.py +4 -1
- {fastlifeweb-0.2.0.dist-info → fastlifeweb-0.2.2.dist-info}/METADATA +6 -1
- {fastlifeweb-0.2.0.dist-info → fastlifeweb-0.2.2.dist-info}/RECORD +13 -12
- {fastlifeweb-0.2.0.dist-info → fastlifeweb-0.2.2.dist-info}/LICENSE +0 -0
- {fastlifeweb-0.2.0.dist-info → fastlifeweb-0.2.2.dist-info}/WHEEL +0 -0
@@ -21,7 +21,7 @@ from typing import (
|
|
21
21
|
|
22
22
|
import venusian # type: ignore
|
23
23
|
from fastapi import Depends, FastAPI, Response
|
24
|
-
from fastapi.
|
24
|
+
from fastapi.params import Depends as DependsType
|
25
25
|
from fastapi.staticfiles import StaticFiles
|
26
26
|
|
27
27
|
from fastlife.configurator.base import AbstractMiddleware
|
@@ -69,20 +69,21 @@ class Configurator:
|
|
69
69
|
|
70
70
|
def add_route(
|
71
71
|
self,
|
72
|
+
name: str,
|
72
73
|
path: str,
|
73
74
|
endpoint: Callable[..., Coroutine[Any, Any, Response]],
|
74
75
|
*,
|
75
|
-
|
76
|
-
status_code:
|
77
|
-
tags:
|
78
|
-
# dependencies: Optional[Sequence[Depends]] = None, # type: ignore
|
76
|
+
permission: str | None = None,
|
77
|
+
status_code: int | None = None,
|
78
|
+
tags: List[Union[str, Enum]] | None = None,
|
79
79
|
summary: Optional[str] = None,
|
80
80
|
description: Optional[str] = None,
|
81
|
-
|
81
|
+
response_description: str = "Successful Response",
|
82
82
|
# responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
83
83
|
deprecated: Optional[bool] = None,
|
84
84
|
methods: Optional[List[str]] = None,
|
85
|
-
operation_id: Optional[str] = None,
|
85
|
+
# operation_id: Optional[str] = None,
|
86
|
+
# response_model: Any = Default(None),
|
86
87
|
# response_model_include: Optional[IncEx] = None,
|
87
88
|
# response_model_exclude: Optional[IncEx] = None,
|
88
89
|
# response_model_by_alias: bool = True,
|
@@ -93,26 +94,29 @@ class Configurator:
|
|
93
94
|
# response_class: Union[Type[Response], DefaultPlaceholder] = Default(
|
94
95
|
# HTMLResponse
|
95
96
|
# ),
|
96
|
-
name: Optional[str] = None,
|
97
97
|
# openapi_extra: Optional[Dict[str, Any]] = None,
|
98
98
|
# generate_unique_id_function: Callable[[APIRoute], str] = Default(
|
99
99
|
# generate_unique_id
|
100
100
|
# ),
|
101
101
|
) -> "Configurator":
|
102
|
+
dependencies: List[DependsType] = []
|
103
|
+
if permission:
|
104
|
+
dependencies.append(Depends(self.registry.check_permission(permission)))
|
105
|
+
|
102
106
|
self._app.add_api_route(
|
103
107
|
path,
|
104
108
|
endpoint,
|
105
|
-
response_model=response_model,
|
109
|
+
# response_model=response_model,
|
106
110
|
status_code=status_code,
|
107
111
|
tags=tags,
|
108
|
-
|
112
|
+
dependencies=dependencies,
|
109
113
|
summary=summary,
|
110
114
|
description=description,
|
111
|
-
|
115
|
+
response_description=response_description,
|
112
116
|
# responses=responses,
|
113
117
|
deprecated=deprecated,
|
114
118
|
methods=methods,
|
115
|
-
operation_id=operation_id,
|
119
|
+
# operation_id=operation_id,
|
116
120
|
# response_model_include=response_model_include,
|
117
121
|
# response_model_exclude=response_model_exclude,
|
118
122
|
# response_model_by_alias=response_model_by_alias,
|
@@ -134,6 +138,12 @@ class Configurator:
|
|
134
138
|
self._app.mount(route_path, StaticFiles(directory=directory), name=name)
|
135
139
|
return self
|
136
140
|
|
141
|
+
def add_exception_handler(
|
142
|
+
self, status_code_or_exc: int | Type[Exception], handler: Any
|
143
|
+
) -> "Configurator":
|
144
|
+
self._app.add_exception_handler(status_code_or_exc, handler)
|
145
|
+
return self
|
146
|
+
|
137
147
|
|
138
148
|
def configure(
|
139
149
|
wrapped: Callable[[Configurator], None]
|
@@ -2,6 +2,7 @@ from typing import Annotated
|
|
2
2
|
|
3
3
|
from fastapi import Depends
|
4
4
|
|
5
|
+
from fastlife.security.policy import CheckPermission
|
5
6
|
from fastlife.shared_utils.resolver import resolve
|
6
7
|
from fastlife.templating.renderer import AbstractTemplateRenderer
|
7
8
|
|
@@ -11,11 +12,15 @@ from .settings import Settings
|
|
11
12
|
class AppRegistry:
|
12
13
|
settings: Settings
|
13
14
|
renderer: AbstractTemplateRenderer
|
15
|
+
check_permission: CheckPermission
|
14
16
|
|
15
17
|
def __init__(self, settings: Settings) -> None:
|
18
|
+
# Abtract class resolved for dependency injection
|
16
19
|
TemplateRenderer = resolve(settings.template_renderer_class)
|
20
|
+
|
17
21
|
self.settings = settings
|
18
22
|
self.renderer = TemplateRenderer(settings)
|
23
|
+
self.check_permission = resolve(settings.check_permission)
|
19
24
|
|
20
25
|
|
21
26
|
DEFAULT_REGISTRY: AppRegistry = None # type: ignore
|
@@ -19,6 +19,7 @@ class Settings(BaseSettings):
|
|
19
19
|
|
20
20
|
session_secret_key: str = Field(default="")
|
21
21
|
session_cookie_name: str = Field(default="flsess")
|
22
|
+
session_cookie_domain: str = Field(default="")
|
22
23
|
session_cookie_path: str = Field(default="/")
|
23
24
|
session_duration: timedelta = Field(default=timedelta(days=14))
|
24
25
|
session_cookie_same_site: Literal["lax", "strict", "none"] = Field(default="lax")
|
@@ -28,3 +29,5 @@ class Settings(BaseSettings):
|
|
28
29
|
)
|
29
30
|
|
30
31
|
domain_name: str = Field(default="", title="domain name where the app is served")
|
32
|
+
|
33
|
+
check_permission: str = Field(default="fastlife.security.policy:check_permission")
|
@@ -0,0 +1,18 @@
|
|
1
|
+
from typing import Any, Callable, Coroutine
|
2
|
+
|
3
|
+
CheckPermissionHook = Callable[..., Coroutine[Any, Any, None]] | Callable[..., None]
|
4
|
+
CheckPermission = Callable[[str], CheckPermissionHook]
|
5
|
+
|
6
|
+
|
7
|
+
def check_permission(permission_name: str) -> CheckPermissionHook:
|
8
|
+
"""
|
9
|
+
A closure that check that a user as the given username.
|
10
|
+
|
11
|
+
This method has to be overriden using the setting check_permission
|
12
|
+
to implement it.
|
13
|
+
"""
|
14
|
+
|
15
|
+
def depencency_injection() -> None:
|
16
|
+
...
|
17
|
+
|
18
|
+
return depencency_injection
|
fastlife/session/__init__.py
CHANGED
@@ -17,6 +17,6 @@ def includeme(config: Configurator) -> None:
|
|
17
17
|
cookie_path=settings.session_cookie_path,
|
18
18
|
cookie_same_site=settings.session_cookie_same_site,
|
19
19
|
cookie_secure=settings.session_cookie_secure,
|
20
|
-
cookie_domain=settings.
|
20
|
+
cookie_domain=settings.session_cookie_domain,
|
21
21
|
serializer=session_serializer,
|
22
22
|
)
|
fastlife/session/middleware.py
CHANGED
@@ -41,16 +41,18 @@ class SessionMiddleware(AbstractMiddleware):
|
|
41
41
|
return
|
42
42
|
|
43
43
|
connection = HTTPConnection(scope)
|
44
|
-
|
45
|
-
|
46
|
-
if self.cookie_name in connection.cookies:
|
44
|
+
existing_session = self.cookie_name in connection.cookies
|
45
|
+
if existing_session:
|
47
46
|
data = connection.cookies[self.cookie_name].encode("utf-8")
|
48
|
-
scope["session"],
|
47
|
+
scope["session"], broken_session = self.serializer.deserialize(data)
|
48
|
+
if broken_session:
|
49
|
+
existing_session = False
|
49
50
|
else:
|
50
51
|
scope["session"] = {}
|
51
52
|
|
52
53
|
async def send_wrapper(message: Message) -> None:
|
53
54
|
if message["type"] == "http.response.start":
|
55
|
+
headers = None
|
54
56
|
if scope["session"]:
|
55
57
|
# We have session data to persist.
|
56
58
|
data = self.serializer.serialize(scope["session"]).decode("utf-8")
|
@@ -60,7 +62,7 @@ class SessionMiddleware(AbstractMiddleware):
|
|
60
62
|
f"Max-Age={self.max_age}; {self.security_flags}"
|
61
63
|
)
|
62
64
|
headers.append("set-cookie", header_value)
|
63
|
-
elif
|
65
|
+
elif existing_session:
|
64
66
|
# The session has been cleared.
|
65
67
|
headers = MutableHeaders(scope=message)
|
66
68
|
expires = "expires=Thu, 01 Jan 1970 00:00:00 GMT; "
|
fastlife/session/serializer.py
CHANGED
@@ -34,6 +34,11 @@ class SignedSessionSerializer(AbsractSessionSerializer):
|
|
34
34
|
def deserialize(self, data: bytes) -> Tuple[Mapping[str, Any], bool]:
|
35
35
|
try:
|
36
36
|
data = self.signer.unsign(data, max_age=self.max_age)
|
37
|
+
# We can't deserialize something wrong since the serialize
|
38
|
+
# is signing the content.
|
39
|
+
# If the signature key is compromise and we have invalid payload,
|
40
|
+
# raising exceptions here is fine, it's dangerous afterall.
|
41
|
+
session = json.loads(b64decode(data))
|
37
42
|
except itsdangerous.BadSignature:
|
38
43
|
return {}, True
|
39
|
-
return
|
44
|
+
return session, False
|
fastlife/testing/testclient.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
import re
|
2
|
+
import time
|
2
3
|
from collections.abc import MutableMapping
|
3
4
|
from typing import Any, Literal, Mapping
|
4
5
|
from urllib.parse import urlencode
|
@@ -13,13 +14,60 @@ from fastlife.session.serializer import AbsractSessionSerializer
|
|
13
14
|
from fastlife.shared_utils.resolver import resolve
|
14
15
|
|
15
16
|
|
17
|
+
class Element:
|
18
|
+
def __init__(self, client: "WebTestClient", tag: bs4.Tag):
|
19
|
+
self._client = client
|
20
|
+
self._tag = tag
|
21
|
+
|
22
|
+
def click(self) -> "WebResponse":
|
23
|
+
return self._client.get(self._tag.attrs["href"])
|
24
|
+
|
25
|
+
@property
|
26
|
+
def attrs(self) -> dict[str, str]:
|
27
|
+
return self._tag.attrs
|
28
|
+
|
29
|
+
@property
|
30
|
+
def form(self) -> "Element | None":
|
31
|
+
return Element(self._client, self._tag.form) if self._tag.form else None
|
32
|
+
|
33
|
+
def by_text(self, text: str, *, node_name: str | None = None) -> "Element | None":
|
34
|
+
nodes = self._tag.find_all(string=re.compile(rf"\s*{text}\s*"))
|
35
|
+
for node in nodes:
|
36
|
+
if isinstance(node, bs4.NavigableString):
|
37
|
+
node = node.parent
|
38
|
+
|
39
|
+
if node_name:
|
40
|
+
while node is not None:
|
41
|
+
if node.name == node_name:
|
42
|
+
return Element(self._client, node)
|
43
|
+
node = node.parent
|
44
|
+
elif node:
|
45
|
+
return Element(self._client, node)
|
46
|
+
return None
|
47
|
+
|
48
|
+
def by_label_text(self, text: str) -> "Element | None":
|
49
|
+
label = self.by_text(text, node_name="label")
|
50
|
+
assert label is not None
|
51
|
+
assert label.attrs.get("for") is not None
|
52
|
+
resp = self._tag.find(id=label.attrs["for"])
|
53
|
+
assert not isinstance(resp, bs4.NavigableString)
|
54
|
+
return Element(self._client, resp) if resp else None
|
55
|
+
|
56
|
+
def by_node_name(
|
57
|
+
self, node_name: str, *, attrs: dict[str, str] | None = None
|
58
|
+
) -> list["Element"]:
|
59
|
+
return [
|
60
|
+
Element(self._client, e) for e in self._tag.find_all(node_name, attrs or {})
|
61
|
+
]
|
62
|
+
|
63
|
+
|
16
64
|
class WebForm:
|
17
|
-
def __init__(self, client: "WebTestClient", origin: str, form:
|
65
|
+
def __init__(self, client: "WebTestClient", origin: str, form: Element):
|
18
66
|
self._client = client
|
19
67
|
self._form = form
|
20
68
|
self._origin = origin
|
21
69
|
self._formdata: dict[str, str] = {}
|
22
|
-
inputs = self._form.
|
70
|
+
inputs = self._form.by_node_name("input")
|
23
71
|
for input in inputs:
|
24
72
|
if input.attrs.get("type") == "checkbox" and "checked" not in input.attrs:
|
25
73
|
continue
|
@@ -32,7 +80,7 @@ class WebForm:
|
|
32
80
|
self._formdata[fieldname] = value
|
33
81
|
|
34
82
|
def button(self, text: str) -> "WebForm":
|
35
|
-
assert self._form.
|
83
|
+
assert self._form.by_text(text, node_name="button") is not None
|
36
84
|
return self
|
37
85
|
|
38
86
|
def submit(self, follow_redirects: bool = True) -> "WebResponse":
|
@@ -45,6 +93,9 @@ class WebForm:
|
|
45
93
|
target, data=self._formdata, follow_redirects=follow_redirects
|
46
94
|
)
|
47
95
|
|
96
|
+
def __contains__(self, key: str) -> bool:
|
97
|
+
return key in self._formdata
|
98
|
+
|
48
99
|
|
49
100
|
class WebResponse:
|
50
101
|
def __init__(self, client: "WebTestClient", origin: str, response: httpx.Response):
|
@@ -75,39 +126,16 @@ class WebResponse:
|
|
75
126
|
return self._response.text
|
76
127
|
|
77
128
|
@property
|
78
|
-
def html(self) ->
|
129
|
+
def html(self) -> Element:
|
79
130
|
if self._html is None:
|
80
131
|
self._html = bs4.BeautifulSoup(self._response.text, "html.parser")
|
81
|
-
return self._html
|
132
|
+
return Element(self._client, self._html)
|
82
133
|
|
83
134
|
@property
|
84
|
-
def html_body(self) ->
|
85
|
-
body = self.html.
|
86
|
-
assert body
|
87
|
-
|
88
|
-
return body
|
89
|
-
|
90
|
-
def by_text(self, text: str, *, node_name: str | None = None) -> bs4.Tag | None:
|
91
|
-
nodes = self.html.find_all(string=re.compile(rf"\s*{text}\s*"))
|
92
|
-
for node in nodes:
|
93
|
-
if isinstance(node, bs4.NavigableString):
|
94
|
-
node = node.parent
|
95
|
-
|
96
|
-
if node_name:
|
97
|
-
while node is not None:
|
98
|
-
if node.name == node_name:
|
99
|
-
return node
|
100
|
-
node = node.parent
|
101
|
-
|
102
|
-
return None
|
103
|
-
|
104
|
-
def by_label_text(self, text: str) -> bs4.Tag | None:
|
105
|
-
label = self.by_text(text, node_name="label")
|
106
|
-
assert label is not None
|
107
|
-
assert label.attrs.get("for") is not None
|
108
|
-
resp = self.html.find(id=label.attrs["for"])
|
109
|
-
assert not isinstance(resp, bs4.NavigableString)
|
110
|
-
return resp
|
135
|
+
def html_body(self) -> Element:
|
136
|
+
body = self.html.by_node_name("body")
|
137
|
+
assert len(body) == 1
|
138
|
+
return body[0]
|
111
139
|
|
112
140
|
@property
|
113
141
|
def form(self) -> WebForm:
|
@@ -117,10 +145,16 @@ class WebResponse:
|
|
117
145
|
self._form = WebForm(self._client, self._origin, form)
|
118
146
|
return self._form
|
119
147
|
|
148
|
+
def by_text(self, text: str, *, node_name: str | None = None) -> Element | None:
|
149
|
+
return self.html.by_text(text, node_name=node_name)
|
150
|
+
|
151
|
+
def by_label_text(self, text: str) -> Element | None:
|
152
|
+
return self.html.by_label_text(text)
|
153
|
+
|
120
154
|
def by_node_name(
|
121
155
|
self, node_name: str, *, attrs: dict[str, str] | None = None
|
122
|
-
) -> list[
|
123
|
-
return self.html.
|
156
|
+
) -> list[Element]:
|
157
|
+
return self.html.by_node_name(node_name, attrs=attrs)
|
124
158
|
|
125
159
|
|
126
160
|
CookieTypes = httpx._types.CookieTypes # type: ignore
|
@@ -141,28 +175,51 @@ class Session(dict[str, Any]):
|
|
141
175
|
assert settings is not None
|
142
176
|
self.settings = settings
|
143
177
|
data: Mapping[str, Any]
|
144
|
-
|
145
|
-
|
178
|
+
self.has_session = settings.session_cookie_name in self.client.cookies
|
179
|
+
if self.has_session:
|
180
|
+
data, error = self.srlz.deserialize(
|
146
181
|
self.client.cookies[settings.session_cookie_name].encode("utf-8")
|
147
182
|
)
|
183
|
+
if error:
|
184
|
+
self.has_session = False
|
148
185
|
else:
|
149
|
-
data
|
150
|
-
self.new_session = not exists
|
186
|
+
data = {}
|
151
187
|
super().__init__(data)
|
152
188
|
|
153
189
|
def __setitem__(self, __key: Any, __value: Any) -> None:
|
154
190
|
super().__setitem__(__key, __value)
|
155
191
|
settings = self.settings
|
156
192
|
data = self.serialize()
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
settings.
|
193
|
+
from http.cookiejar import Cookie
|
194
|
+
|
195
|
+
self.client.cookies.jar.set_cookie(
|
196
|
+
Cookie(
|
197
|
+
version=0,
|
198
|
+
name=settings.session_cookie_name,
|
199
|
+
value=data,
|
200
|
+
port=None,
|
201
|
+
port_specified=False,
|
202
|
+
domain=f".{settings.session_cookie_domain}",
|
203
|
+
domain_specified=True,
|
204
|
+
domain_initial_dot=True,
|
205
|
+
path="/",
|
206
|
+
path_specified=True,
|
207
|
+
secure=False,
|
208
|
+
expires=int(time.time() + settings.session_duration.total_seconds()),
|
209
|
+
discard=False,
|
210
|
+
comment=None,
|
211
|
+
comment_url=None,
|
212
|
+
rest={"HttpOnly": None, "SameSite": "lax"}, # type: ignore
|
213
|
+
rfc2109=False,
|
163
214
|
)
|
164
|
-
|
165
|
-
|
215
|
+
)
|
216
|
+
# this does not work
|
217
|
+
# self.client.cookies.set(
|
218
|
+
# settings.session_cookie_name,
|
219
|
+
# data,
|
220
|
+
# settings.session_cookie_domain,
|
221
|
+
# settings.session_cookie_path,
|
222
|
+
# )
|
166
223
|
|
167
224
|
def serialize(self) -> str:
|
168
225
|
return self.srlz.serialize(self).decode("utf-8")
|
@@ -177,7 +234,12 @@ class WebTestClient:
|
|
177
234
|
cookies: CookieTypes | None = None,
|
178
235
|
) -> None:
|
179
236
|
self.app = app
|
180
|
-
|
237
|
+
if settings is None:
|
238
|
+
settings = Settings()
|
239
|
+
settings.domain_name = settings.domain_name or "testserver.local"
|
240
|
+
self.testclient = TestClient(
|
241
|
+
app, base_url=f"http://{settings.domain_name}", cookies=cookies or {}
|
242
|
+
)
|
181
243
|
self.settings = settings
|
182
244
|
self.session_serializer: AbsractSessionSerializer | None = None
|
183
245
|
if settings:
|
@@ -212,7 +274,7 @@ class WebTestClient:
|
|
212
274
|
url=url,
|
213
275
|
headers=headers,
|
214
276
|
content=content,
|
215
|
-
follow_redirects=False,
|
277
|
+
follow_redirects=False,
|
216
278
|
)
|
217
279
|
resp = WebResponse(
|
218
280
|
self,
|
fastlife/views/pydantic_form.py
CHANGED
@@ -23,5 +23,8 @@ async def show_widget(
|
|
23
23
|
def includeme(config: Configurator) -> None:
|
24
24
|
route_prefix = config.registry.settings.fastlife_route_prefix
|
25
25
|
config.add_route(
|
26
|
-
|
26
|
+
"fl-pydantic-form-widget",
|
27
|
+
f"{route_prefix}/pydantic-form/widgets/{{typ}}",
|
28
|
+
show_widget,
|
29
|
+
methods=["GET"],
|
27
30
|
)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: fastlifeweb
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.2
|
4
4
|
Summary: High-level web framework
|
5
5
|
License: BSD-derived
|
6
6
|
Author: Guillaume Gauvrit
|
@@ -30,6 +30,11 @@ Description-Content-Type: text/markdown
|
|
30
30
|
|
31
31
|
# Fastlife
|
32
32
|
|
33
|
+
[](https://github.com/mardiros/fastlife/actions/workflows/main.yml)
|
34
|
+
|
35
|
+
[](https://codecov.io/gh/mardiros/fastlife)
|
36
|
+
|
37
|
+
|
33
38
|
A high-level Python web framework based on FastAPI, Jinja2 and Pydandic and htmx.
|
34
39
|
|
35
40
|
The intention is to prototype fast application.
|
@@ -1,17 +1,18 @@
|
|
1
1
|
fastlife/__init__.py,sha256=__RsTYXTkhcxwHRvT1xWQ5XfygdwBtotbrff71lu-kk,190
|
2
2
|
fastlife/configurator/__init__.py,sha256=2EPjM1o5iHJIViPwgJjaPQS3pMhE-9dik_mm53eX2DY,91
|
3
3
|
fastlife/configurator/base.py,sha256=2ahvTudLmD99YQjnIeGN5JDPCSl3k-mauu7bsSEB5RE,216
|
4
|
-
fastlife/configurator/configurator.py,sha256=
|
5
|
-
fastlife/configurator/registry.py,sha256=
|
6
|
-
fastlife/configurator/settings.py,sha256=
|
4
|
+
fastlife/configurator/configurator.py,sha256=SfzHlS6ou2JrUhzm0Q--G8R_XGo4nARQO-Mwzy-Mfn4,5474
|
5
|
+
fastlife/configurator/registry.py,sha256=fSwlfeqrVzLJmaR2-bAu41rJP8uRMZd-8vZnfsfN2xg,1332
|
6
|
+
fastlife/configurator/settings.py,sha256=ui40ldu3f6v9ARDVijhNcD2AxF1fWFbfOkkZ7z_2gH0,1394
|
7
7
|
fastlife/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
8
|
fastlife/request/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
9
9
|
fastlife/request/form_data.py,sha256=7gV2hLeFyhPRrNYL_UlRAqM-oqxSu2-VvxKHMv5F900,3849
|
10
10
|
fastlife/security/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
11
11
|
fastlife/security/csrf.py,sha256=-2XVfkSpmU1HJB2-_tZiOmqs9KLtib0tb_l1QbIFj34,1005
|
12
|
-
fastlife/
|
13
|
-
fastlife/session/
|
14
|
-
fastlife/session/
|
12
|
+
fastlife/security/policy.py,sha256=5jV5nypy5O8XPFFRJ_bzG8Ltk5xcPWclkz23qiG1_I8,509
|
13
|
+
fastlife/session/__init__.py,sha256=OnzRCYRzc1lw9JB0UdKi-aRLPNT2n8mM8kwY1P4w7uU,838
|
14
|
+
fastlife/session/middleware.py,sha256=tz5ANgTKXaIqXGaqNM9czfSRdkfwRChODmKAqWGzP60,3050
|
15
|
+
fastlife/session/serializer.py,sha256=qpVnHQjYTxw3aOnoEOKIjOFJg2z45KjiX5sipWk2gws,1458
|
15
16
|
fastlife/shared_utils/infer.py,sha256=_hmGzu84VlZAkdw_owkW8eHknULqH3MLDBlXj7LkEsc,466
|
16
17
|
fastlife/shared_utils/resolver.py,sha256=cZYcaV27sIC5vLc_xo-yj0S3nTimeY5KRZTanHY6e_Y,1295
|
17
18
|
fastlife/templates/base.jinja2,sha256=JOHL2bexmNfaRwStdOnd_oLW1YhKnhQbNQfVItMVBkM,112
|
@@ -40,10 +41,10 @@ fastlife/templating/renderer/widgets/sequence.py,sha256=b2e7YOU4BCASJ46peYtSaaoq
|
|
40
41
|
fastlife/templating/renderer/widgets/text.py,sha256=6sQ9tlmWVn8-bogSbb8m2gAL-1Lrkb026W5ekez4Jlc,789
|
41
42
|
fastlife/templating/renderer/widgets/union.py,sha256=pT_Mcrb-_ZTZV3ZPkyQYdEW2AE3PglojXdYaMfrgZ0k,1645
|
42
43
|
fastlife/testing/__init__.py,sha256=KgTlRI0g8z7HRpL7mD5QgI__LT9Y4QDSzKMlxJG3wNk,67
|
43
|
-
fastlife/testing/testclient.py,sha256=
|
44
|
+
fastlife/testing/testclient.py,sha256=GBSL9xECrLf4jkFiZCT2h0NajFNBSVuFY4zm5MyolBw,10095
|
44
45
|
fastlife/views/__init__.py,sha256=nn4B_8YTbTmhGPvSd20yyKK_9Dh1Pfh_Iq7z6iK8-CE,154
|
45
|
-
fastlife/views/pydantic_form.py,sha256=
|
46
|
-
fastlifeweb-0.2.
|
47
|
-
fastlifeweb-0.2.
|
48
|
-
fastlifeweb-0.2.
|
49
|
-
fastlifeweb-0.2.
|
46
|
+
fastlife/views/pydantic_form.py,sha256=5fBmw94wYLKuEN-YwqTnCLDDCq-TEzeJNmzWdsB2I3M,887
|
47
|
+
fastlifeweb-0.2.2.dist-info/LICENSE,sha256=F75xSseSKMwqzFj8rswYU6NWS3VoWOc_gY3fJYf9_LI,1504
|
48
|
+
fastlifeweb-0.2.2.dist-info/METADATA,sha256=AU7ZtKIfvQIFIaqSJhU5D4Fk5jzo4M-L2Kf5VGpTJBc,1750
|
49
|
+
fastlifeweb-0.2.2.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
50
|
+
fastlifeweb-0.2.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|