fastlifeweb 0.2.1__tar.gz → 0.2.2__tar.gz
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.
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/PKG-INFO +1 -1
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/pyproject.toml +1 -1
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/configurator/settings.py +1 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/session/__init__.py +1 -1
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/session/middleware.py +7 -5
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/session/serializer.py +6 -1
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/testing/testclient.py +110 -48
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/LICENSE +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/README.md +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/__init__.py +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/configurator/__init__.py +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/configurator/base.py +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/configurator/configurator.py +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/configurator/registry.py +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/py.typed +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/request/__init__.py +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/request/form_data.py +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/security/__init__.py +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/security/csrf.py +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/security/policy.py +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/shared_utils/infer.py +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/shared_utils/resolver.py +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/templates/base.jinja2 +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/templates/globals.jinja2 +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/templates/pydantic_form/boolean.jinja2 +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/templates/pydantic_form/dropdown.jinja2 +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/templates/pydantic_form/hidden.jinja2 +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/templates/pydantic_form/model.jinja2 +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/templates/pydantic_form/sequence.jinja2 +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/templates/pydantic_form/text.jinja2 +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/templates/pydantic_form/union.jinja2 +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/templates/pydantic_form/widget.jinja2 +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/templating/__init__.py +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/templating/binding.py +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/templating/renderer/__init__.py +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/templating/renderer/abstract.py +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/templating/renderer/jinja2.py +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/templating/renderer/widgets/__init__.py +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/templating/renderer/widgets/base.py +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/templating/renderer/widgets/boolean.py +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/templating/renderer/widgets/dropdown.py +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/templating/renderer/widgets/factory.py +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/templating/renderer/widgets/hidden.py +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/templating/renderer/widgets/model.py +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/templating/renderer/widgets/sequence.py +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/templating/renderer/widgets/text.py +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/templating/renderer/widgets/union.py +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/testing/__init__.py +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/views/__init__.py +0 -0
- {fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/views/pydantic_form.py +0 -0
@@ -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")
|
@@ -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
|
)
|
@@ -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; "
|
@@ -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
|
@@ -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,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/templates/pydantic_form/dropdown.jinja2
RENAMED
File without changes
|
File without changes
|
File without changes
|
{fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/templates/pydantic_form/sequence.jinja2
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/templating/renderer/widgets/__init__.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
{fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/templating/renderer/widgets/dropdown.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{fastlifeweb-0.2.1 → fastlifeweb-0.2.2}/src/fastlife/templating/renderer/widgets/sequence.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|