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.
- {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/PKG-INFO +2 -2
- {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyproject.toml +1 -1
- {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/csrf.py +3 -21
- {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/live_view.py +3 -2
- {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/pyview.py +16 -14
- pyview_web-0.0.7a0/pyview/secret.py +18 -0
- pyview_web-0.0.7a0/pyview/session.py +13 -0
- {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/ws_handler.py +9 -10
- {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/readme.md +1 -1
- {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/LICENSE +0 -0
- {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/__init__.py +0 -0
- {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/assets/js/app.js +0 -0
- {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/assets/package-lock.json +0 -0
- {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/assets/package.json +0 -0
- {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/changesets/__init__.py +0 -0
- {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/changesets/changesets.py +0 -0
- {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/events.py +0 -0
- {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/js.py +0 -0
- {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/live_routes.py +0 -0
- {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/live_socket.py +0 -0
- {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/static/assets/app.js +0 -0
- {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/template/__init__.py +0 -0
- {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/template/live_template.py +0 -0
- {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/template/serializer.py +0 -0
- {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/test_csrf.py +0 -0
- {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/vendor/__init__.py +0 -0
- {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/vendor/flet/pubsub/__init__.py +0 -0
- {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/vendor/flet/pubsub/pub_sub.py +0 -0
- {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/vendor/ibis/__init__.py +0 -0
- {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/vendor/ibis/compiler.py +0 -0
- {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/vendor/ibis/context.py +0 -0
- {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/vendor/ibis/errors.py +0 -0
- {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/vendor/ibis/filters.py +0 -0
- {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/vendor/ibis/loaders.py +0 -0
- {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/vendor/ibis/nodes.py +0 -0
- {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/vendor/ibis/template.py +0 -0
- {pyview_web-0.0.6a0 → pyview_web-0.0.7a0}/pyview/vendor/ibis/tree.py +0 -0
- {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.
|
|
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]):
|
|
@@ -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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|