fastlifeweb 0.2.1__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.
@@ -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.domain_name,
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
- reset_session = False
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"], reset_session = self.serializer.deserialize(data)
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 reset_session:
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 json.loads(b64decode(data)), False
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: bs4.Tag):
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.find_all("input")
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.find("button", string=re.compile(f".*{text}.*")) is not None
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) -> bs4.Tag:
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) -> bs4.Tag:
85
- body = self.html.find("body")
86
- assert body is not None
87
- assert isinstance(body, bs4.Tag)
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[bs4.Tag]:
123
- return self.html.find_all(node_name, attrs or {})
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
- if settings.session_cookie_name in self.client.cookies:
145
- data, exists = self.srlz.deserialize(
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, exists = {}, False
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
- if self.new_session:
158
- self.client.cookies.set(
159
- settings.session_cookie_name,
160
- data,
161
- settings.domain_name,
162
- settings.session_cookie_path,
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
- else:
165
- self.client.cookies[settings.session_cookie_name] = data
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
- self.testclient = TestClient(app, cookies=cookies or {})
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, # don't follow for cookie processing
277
+ follow_redirects=False,
216
278
  )
217
279
  resp = WebResponse(
218
280
  self,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fastlifeweb
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: High-level web framework
5
5
  License: BSD-derived
6
6
  Author: Guillaume Gauvrit
@@ -3,16 +3,16 @@ fastlife/configurator/__init__.py,sha256=2EPjM1o5iHJIViPwgJjaPQS3pMhE-9dik_mm53e
3
3
  fastlife/configurator/base.py,sha256=2ahvTudLmD99YQjnIeGN5JDPCSl3k-mauu7bsSEB5RE,216
4
4
  fastlife/configurator/configurator.py,sha256=SfzHlS6ou2JrUhzm0Q--G8R_XGo4nARQO-Mwzy-Mfn4,5474
5
5
  fastlife/configurator/registry.py,sha256=fSwlfeqrVzLJmaR2-bAu41rJP8uRMZd-8vZnfsfN2xg,1332
6
- fastlife/configurator/settings.py,sha256=UuZlVOicrm1txpvC0TRvaoKrPzOqEO71Lq1Tz1tAFSE,1343
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
12
  fastlife/security/policy.py,sha256=5jV5nypy5O8XPFFRJ_bzG8Ltk5xcPWclkz23qiG1_I8,509
13
- fastlife/session/__init__.py,sha256=0BIArO93ylDlHhVUNyAv7UfVuh9nO9YRUh0YvNDc-IM,828
14
- fastlife/session/middleware.py,sha256=kCPQY8nODX1kXPDJzhgyZF4IIT4soG8WS7EqSYn2vEk,2930
15
- fastlife/session/serializer.py,sha256=3VwDafQ_dXttlf3Dj2WULhV0PVDBy0EvetVYpdcThM8,1169
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
16
16
  fastlife/shared_utils/infer.py,sha256=_hmGzu84VlZAkdw_owkW8eHknULqH3MLDBlXj7LkEsc,466
17
17
  fastlife/shared_utils/resolver.py,sha256=cZYcaV27sIC5vLc_xo-yj0S3nTimeY5KRZTanHY6e_Y,1295
18
18
  fastlife/templates/base.jinja2,sha256=JOHL2bexmNfaRwStdOnd_oLW1YhKnhQbNQfVItMVBkM,112
@@ -41,10 +41,10 @@ fastlife/templating/renderer/widgets/sequence.py,sha256=b2e7YOU4BCASJ46peYtSaaoq
41
41
  fastlife/templating/renderer/widgets/text.py,sha256=6sQ9tlmWVn8-bogSbb8m2gAL-1Lrkb026W5ekez4Jlc,789
42
42
  fastlife/templating/renderer/widgets/union.py,sha256=pT_Mcrb-_ZTZV3ZPkyQYdEW2AE3PglojXdYaMfrgZ0k,1645
43
43
  fastlife/testing/__init__.py,sha256=KgTlRI0g8z7HRpL7mD5QgI__LT9Y4QDSzKMlxJG3wNk,67
44
- fastlife/testing/testclient.py,sha256=p1VwviwQT4xstJSWejtuBvHDwmeqNCjwLvWLtkkCl-I,7979
44
+ fastlife/testing/testclient.py,sha256=GBSL9xECrLf4jkFiZCT2h0NajFNBSVuFY4zm5MyolBw,10095
45
45
  fastlife/views/__init__.py,sha256=nn4B_8YTbTmhGPvSd20yyKK_9Dh1Pfh_Iq7z6iK8-CE,154
46
46
  fastlife/views/pydantic_form.py,sha256=5fBmw94wYLKuEN-YwqTnCLDDCq-TEzeJNmzWdsB2I3M,887
47
- fastlifeweb-0.2.1.dist-info/LICENSE,sha256=F75xSseSKMwqzFj8rswYU6NWS3VoWOc_gY3fJYf9_LI,1504
48
- fastlifeweb-0.2.1.dist-info/METADATA,sha256=_Nl5kt21IDRZrhtiJNIIOu9V9hyZL_kkjNr2LLxJ4S0,1750
49
- fastlifeweb-0.2.1.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
50
- fastlifeweb-0.2.1.dist-info/RECORD,,
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,,