pyview-web 0.0.6a0__tar.gz → 0.0.7a0__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.

Potentially problematic release.


This version of pyview-web might be problematic. Click here for more details.

Files changed (38) hide show
  1. {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/PKG-INFO +2 -2
  2. {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyproject.toml +1 -1
  3. {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/csrf.py +3 -21
  4. {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/live_view.py +3 -2
  5. {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/pyview.py +16 -14
  6. pyview_web-0.0.7a0/pyview/secret.py +18 -0
  7. pyview_web-0.0.7a0/pyview/session.py +13 -0
  8. {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/ws_handler.py +9 -10
  9. {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/readme.md +1 -1
  10. {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/LICENSE +0 -0
  11. {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/__init__.py +0 -0
  12. {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/assets/js/app.js +0 -0
  13. {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/assets/package-lock.json +0 -0
  14. {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/assets/package.json +0 -0
  15. {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/changesets/__init__.py +0 -0
  16. {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/changesets/changesets.py +0 -0
  17. {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/events.py +0 -0
  18. {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/js.py +0 -0
  19. {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/live_routes.py +0 -0
  20. {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/live_socket.py +0 -0
  21. {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/static/assets/app.js +0 -0
  22. {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/template/__init__.py +0 -0
  23. {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/template/live_template.py +0 -0
  24. {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/template/serializer.py +0 -0
  25. {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/test_csrf.py +0 -0
  26. {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/vendor/__init__.py +0 -0
  27. {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/vendor/flet/pubsub/__init__.py +0 -0
  28. {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/vendor/flet/pubsub/pub_sub.py +0 -0
  29. {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/vendor/ibis/__init__.py +0 -0
  30. {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/vendor/ibis/compiler.py +0 -0
  31. {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/vendor/ibis/context.py +0 -0
  32. {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/vendor/ibis/errors.py +0 -0
  33. {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/vendor/ibis/filters.py +0 -0
  34. {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/vendor/ibis/loaders.py +0 -0
  35. {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/vendor/ibis/nodes.py +0 -0
  36. {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/vendor/ibis/template.py +0 -0
  37. {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/vendor/ibis/tree.py +0 -0
  38. {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/vendor/ibis/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyview-web
3
- Version: 0.0.6a0
3
+ Version: 0.0.7a0
4
4
  Summary: LiveView in Python
5
5
  Home-page: https://pyview.rocks
6
6
  License: MIT
@@ -79,7 +79,7 @@ class CountContext(TypedDict):
79
79
 
80
80
 
81
81
  class CountLiveView(LiveView[CountContext]):
82
- async def mount(self, socket: LiveViewSocket[CountContext]):
82
+ async def mount(self, socket: LiveViewSocket[CountContext], _session):
83
83
  socket.context = {"count": 0}
84
84
 
85
85
  async def handle_event(self, event, payload, socket: LiveViewSocket[CountContext]):
@@ -5,7 +5,7 @@ packages = [
5
5
  { include = "pyview" },
6
6
  ]
7
7
 
8
- version = "0.0.6a"
8
+ version = "0.0.7a"
9
9
  description = "LiveView in Python"
10
10
  authors = ["Larry Ogrodnek <ogrodnek@gmail.com>"]
11
11
  license = "MIT"
@@ -1,15 +1,14 @@
1
1
  from itsdangerous import BadData, SignatureExpired, URLSafeTimedSerializer
2
- import secrets
3
2
  from typing import Optional
4
3
  import hmac
5
- import os
4
+ from pyview.secret import get_secret
6
5
 
7
6
 
8
7
  def generate_csrf_token(value: str, salt: Optional[str] = None) -> str:
9
8
  """
10
9
  Generate a CSRF token.
11
10
  """
12
- s = URLSafeTimedSerializer(_get_secret(), salt=salt or "pyview-csrf-token")
11
+ s = URLSafeTimedSerializer(get_secret(), salt=salt or "pyview-csrf-token")
13
12
  return s.dumps(value) # type: ignore
14
13
 
15
14
 
@@ -17,27 +16,10 @@ def validate_csrf_token(data: str, expected: str, salt: Optional[str] = None) ->
17
16
  """
18
17
  Validate a CSRF token.
19
18
  """
20
- s = URLSafeTimedSerializer(_get_secret(), salt=salt or "pyview-csrf-token")
19
+ s = URLSafeTimedSerializer(get_secret(), salt=salt or "pyview-csrf-token")
21
20
  try:
22
21
  token = s.loads(data, max_age=3600)
23
22
  return hmac.compare_digest(token, expected)
24
23
  except (BadData, SignatureExpired) as e:
25
24
  print(e)
26
25
  return False
27
-
28
-
29
- _SECRET = None
30
-
31
-
32
- def _get_secret() -> str:
33
- """
34
- Get the secret key from the environment, or generate a new one.
35
- """
36
- global _SECRET
37
- if _SECRET is None:
38
- secret = os.environ.get("PYVIEW_SECRET")
39
- if secret is None:
40
- secret = secrets.token_urlsafe(16)
41
- _SECRET = secret
42
-
43
- return _SECRET
@@ -1,4 +1,4 @@
1
- from typing import TypeVar, Generic, Optional, Union
1
+ from typing import TypeVar, Generic, Optional, Union, Any
2
2
  from .live_socket import LiveViewSocket, UnconnectedSocket
3
3
  from pyview.template import LiveTemplate, template_file, RenderedContent, LiveRender
4
4
  import inspect
@@ -7,13 +7,14 @@ from pyview.events import InfoEvent
7
7
  T = TypeVar("T")
8
8
 
9
9
  AnySocket = Union[LiveViewSocket[T], UnconnectedSocket[T]]
10
+ Session = dict[str, Any]
10
11
 
11
12
 
12
13
  class LiveView(Generic[T]):
13
14
  def __init__(self):
14
15
  pass
15
16
 
16
- async def mount(self, socket: AnySocket):
17
+ async def mount(self, socket: AnySocket, session: Session):
17
18
  pass
18
19
 
19
20
  async def handle_event(self, event, payload, socket: LiveViewSocket[T]):
@@ -2,12 +2,16 @@ from starlette.applications import Starlette
2
2
  from fastapi import WebSocket
3
3
  from fastapi.responses import HTMLResponse
4
4
  from starlette.middleware.gzip import GZipMiddleware
5
+ from starlette.middleware.sessions import SessionMiddleware
5
6
  from starlette.routing import Route
7
+ from starlette.requests import Request
6
8
  import uuid
7
9
  from urllib.parse import parse_qs
8
10
 
9
11
  from pyview.live_socket import UnconnectedSocket
10
12
  from pyview.csrf import generate_csrf_token
13
+ from pyview.session import serialize_session
14
+ from pyview.secret import get_secret
11
15
  from .ws_handler import LiveSocketHandler
12
16
  from .live_view import LiveView
13
17
  from .live_routes import LiveViewLookup
@@ -20,6 +24,7 @@ class RootTemplateContext(TypedDict):
20
24
  title: Optional[str]
21
25
  css: Optional[str]
22
26
  csrf_token: str
27
+ session: Optional[str]
23
28
 
24
29
 
25
30
  RootTemplate = Callable[[RootTemplateContext], str]
@@ -39,25 +44,25 @@ class PyView(Starlette):
39
44
 
40
45
  self.add_websocket_route("/live/websocket", live_websocket_endpoint)
41
46
  self.add_middleware(GZipMiddleware)
47
+ # self.add_middleware(SessionMiddleware, secret_key=get_secret())
42
48
 
43
49
  def add_live_view(self, path: str, view: Callable[[], LiveView]):
44
- async def lv(request):
45
- return await liveview_container(
46
- self.rootTemplate, self.view_lookup, request
47
- )
50
+ async def lv(request: Request):
51
+ return await liveview_container(self.rootTemplate, self.view_lookup, request)
48
52
 
49
53
  self.view_lookup.add(path, view)
50
54
  self.routes.append(Route(path, lv, methods=["GET"]))
51
55
 
52
56
 
53
- async def liveview_container(
54
- template: RootTemplate, view_lookup: LiveViewLookup, request
55
- ):
57
+ async def liveview_container(template: RootTemplate, view_lookup: LiveViewLookup, request: Request):
56
58
  url = request.url
57
59
  path = url.path
58
60
  lv: LiveView = view_lookup.get(path)
59
61
  s = UnconnectedSocket()
60
- await lv.mount(s)
62
+
63
+ session = request.session if "session" in request.scope else {}
64
+
65
+ await lv.mount(s, session)
61
66
  await lv.handle_params(url, parse_qs(url.query), s)
62
67
  r = await lv.render(s.context)
63
68
 
@@ -69,6 +74,7 @@ async def liveview_container(
69
74
  "title": s.live_title,
70
75
  "csrf_token": generate_csrf_token("lv:phx-" + id),
71
76
  "css": None,
77
+ "session": serialize_session(session),
72
78
  }
73
79
 
74
80
  return HTMLResponse(template(context))
@@ -84,11 +90,7 @@ def defaultRootTemplate(css: str) -> RootTemplate:
84
90
 
85
91
  def _defaultRootTemplate(context: RootTemplateContext) -> str:
86
92
  suffix = " | LiveView"
87
- render_title = (
88
- (context["title"] + suffix) # type: ignore
89
- if context.get("title", None) is not None
90
- else "LiveView"
91
- )
93
+ render_title = (context["title"] + suffix) if context.get("title", None) is not None else "LiveView" # type: ignore
92
94
  css = context["css"] if context.get("css", None) is not None else ""
93
95
  return f"""
94
96
  <!DOCTYPE html>
@@ -107,7 +109,7 @@ def _defaultRootTemplate(context: RootTemplateContext) -> str:
107
109
  <a href="/">Home</a>
108
110
  <div
109
111
  data-phx-main="true"
110
- data-phx-session=""
112
+ data-phx-session="{context['session']}"
111
113
  data-phx-static=""
112
114
  id="phx-{context['id']}"
113
115
  >
@@ -0,0 +1,18 @@
1
+ import os
2
+ import secrets
3
+
4
+ _SECRET = None
5
+
6
+
7
+ def get_secret() -> str:
8
+ """
9
+ Get the secret key from the environment, or generate a new one.
10
+ """
11
+ global _SECRET
12
+ if _SECRET is None:
13
+ secret = os.environ.get("PYVIEW_SECRET")
14
+ if secret is None:
15
+ secret = secrets.token_urlsafe(16)
16
+ _SECRET = secret
17
+
18
+ return _SECRET
@@ -0,0 +1,13 @@
1
+ from typing import Any, cast
2
+ from itsdangerous import URLSafeSerializer
3
+ from pyview.secret import get_secret
4
+
5
+
6
+ def serialize_session(session: dict[str, Any]) -> str:
7
+ s = URLSafeSerializer(get_secret(), salt="pyview-session")
8
+ return cast(str, s.dumps(session))
9
+
10
+
11
+ def deserialize_session(ser: str) -> dict[str, Any]:
12
+ s = URLSafeSerializer(get_secret(), salt="pyview-session")
13
+ return cast(dict[str, Any], s.loads(ser))
@@ -5,6 +5,7 @@ from urllib.parse import urlparse, parse_qs
5
5
  from pyview.live_socket import LiveViewSocket
6
6
  from pyview.live_routes import LiveViewLookup
7
7
  from pyview.csrf import validate_csrf_token
8
+ from pyview.session import deserialize_session
8
9
 
9
10
 
10
11
  class LiveSocketHandler:
@@ -31,7 +32,11 @@ class LiveSocketHandler:
31
32
  if not validate_csrf_token(payload["params"]["_csrf_token"], topic):
32
33
  raise Exception("Invalid CSRF token")
33
34
 
34
- await lv.mount(socket)
35
+ session = {}
36
+ if "session" in payload:
37
+ session = deserialize_session(payload["session"])
38
+
39
+ await lv.mount(socket, session)
35
40
  await lv.handle_params(url, parse_qs(url.query), socket)
36
41
 
37
42
  rendered = await _render(socket)
@@ -66,9 +71,7 @@ class LiveSocketHandler:
66
71
  "phx_reply",
67
72
  {"response": {}, "status": "ok"},
68
73
  ]
69
- await self.manager.send_personal_message(
70
- json.dumps(resp), socket.websocket
71
- )
74
+ await self.manager.send_personal_message(json.dumps(resp), socket.websocket)
72
75
  continue
73
76
 
74
77
  if event == "event":
@@ -86,9 +89,7 @@ class LiveSocketHandler:
86
89
  "phx_reply",
87
90
  {"response": {"diff": rendered}, "status": "ok"},
88
91
  ]
89
- await self.manager.send_personal_message(
90
- json.dumps(resp), socket.websocket
91
- )
92
+ await self.manager.send_personal_message(json.dumps(resp), socket.websocket)
92
93
  continue
93
94
 
94
95
  if event == "live_patch":
@@ -105,9 +106,7 @@ class LiveSocketHandler:
105
106
  "phx_reply",
106
107
  {"response": {"diff": rendered}, "status": "ok"},
107
108
  ]
108
- await self.manager.send_personal_message(
109
- json.dumps(resp), socket.websocket
110
- )
109
+ await self.manager.send_personal_message(json.dumps(resp), socket.websocket)
111
110
  continue
112
111
 
113
112
 
@@ -32,7 +32,7 @@ class CountContext(TypedDict):
32
32
 
33
33
 
34
34
  class CountLiveView(LiveView[CountContext]):
35
- async def mount(self, socket: LiveViewSocket[CountContext]):
35
+ async def mount(self, socket: LiveViewSocket[CountContext], _session):
36
36
  socket.context = {"count": 0}
37
37
 
38
38
  async def handle_event(self, event, payload, socket: LiveViewSocket[CountContext]):
File without changes
File without changes