pyview-web 0.0.6a0__py3-none-any.whl → 0.0.8a0__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.
Potentially problematic release.
This version of pyview-web might be problematic. Click here for more details.
- pyview/auth/__init__.py +2 -0
- pyview/auth/provider.py +32 -0
- pyview/auth/required.py +33 -0
- pyview/csrf.py +3 -21
- pyview/live_view.py +3 -2
- pyview/pyview.py +15 -64
- pyview/secret.py +18 -0
- pyview/session.py +13 -0
- pyview/template/__init__.py +1 -0
- pyview/template/root_template.py +71 -0
- pyview/ws_handler.py +25 -22
- {pyview_web-0.0.6a0.dist-info → pyview_web-0.0.8a0.dist-info}/METADATA +3 -5
- {pyview_web-0.0.6a0.dist-info → pyview_web-0.0.8a0.dist-info}/RECORD +15 -9
- {pyview_web-0.0.6a0.dist-info → pyview_web-0.0.8a0.dist-info}/WHEEL +1 -1
- {pyview_web-0.0.6a0.dist-info → pyview_web-0.0.8a0.dist-info}/LICENSE +0 -0
pyview/auth/__init__.py
ADDED
pyview/auth/provider.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from typing import Protocol, TypeVar, Callable
|
|
2
|
+
from starlette.websockets import WebSocket
|
|
3
|
+
from pyview import LiveView
|
|
4
|
+
|
|
5
|
+
_CallableType = TypeVar("_CallableType", bound=Callable)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AuthProvider(Protocol):
|
|
9
|
+
async def has_required_auth(self, websocket: WebSocket) -> bool:
|
|
10
|
+
...
|
|
11
|
+
|
|
12
|
+
def wrap(self, func: _CallableType) -> _CallableType:
|
|
13
|
+
...
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AllowAllAuthProvider(AuthProvider):
|
|
17
|
+
async def has_required_auth(self, websocket: WebSocket) -> bool:
|
|
18
|
+
return True
|
|
19
|
+
|
|
20
|
+
def wrap(self, func: _CallableType) -> _CallableType:
|
|
21
|
+
return func
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AuthProviderFactory:
|
|
25
|
+
@classmethod
|
|
26
|
+
def get(cls, lv: type[LiveView]) -> AuthProvider:
|
|
27
|
+
return getattr(lv, "__pyview_auth_provider__", AllowAllAuthProvider())
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def set(cls, lv: type[LiveView], auth_provider: AuthProvider) -> type[LiveView]:
|
|
31
|
+
setattr(lv, "__pyview_auth_provider__", auth_provider)
|
|
32
|
+
return lv
|
pyview/auth/required.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from starlette.websockets import WebSocket
|
|
4
|
+
from starlette.authentication import requires as starlette_requires, has_required_scope
|
|
5
|
+
from .provider import AuthProvider, AuthProviderFactory
|
|
6
|
+
from pyview import LiveView
|
|
7
|
+
|
|
8
|
+
_CallableType = typing.TypeVar("_CallableType", bound=typing.Callable)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class RequiredScopeAuthProvider(AuthProvider):
|
|
13
|
+
scopes: typing.Sequence[str]
|
|
14
|
+
status_code: int = 403
|
|
15
|
+
redirect: typing.Optional[str] = None
|
|
16
|
+
|
|
17
|
+
def wrap(self, func: _CallableType) -> _CallableType:
|
|
18
|
+
return starlette_requires(self.scopes, self.status_code, self.redirect)(func)
|
|
19
|
+
|
|
20
|
+
def has_required_auth(self, websocket: WebSocket) -> bool:
|
|
21
|
+
return has_required_scope(websocket, self.scopes)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def requires(
|
|
25
|
+
scopes: typing.Union[str, typing.Sequence[str]],
|
|
26
|
+
status_code: int = 403,
|
|
27
|
+
redirect: typing.Optional[str] = None,
|
|
28
|
+
) -> typing.Callable[[type[LiveView]], type[LiveView]]:
|
|
29
|
+
def decorator(cls: type[LiveView]) -> type[LiveView]:
|
|
30
|
+
scopes_list = [scopes] if isinstance(scopes, str) else list(scopes)
|
|
31
|
+
return AuthProviderFactory.set(cls, RequiredScopeAuthProvider(scopes_list, status_code, redirect))
|
|
32
|
+
|
|
33
|
+
return decorator
|
pyview/csrf.py
CHANGED
|
@@ -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
|
pyview/live_view.py
CHANGED
|
@@ -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]):
|
pyview/pyview.py
CHANGED
|
@@ -3,26 +3,18 @@ from fastapi import WebSocket
|
|
|
3
3
|
from fastapi.responses import HTMLResponse
|
|
4
4
|
from starlette.middleware.gzip import GZipMiddleware
|
|
5
5
|
from starlette.routing import Route
|
|
6
|
+
from starlette.requests import Request
|
|
6
7
|
import uuid
|
|
7
8
|
from urllib.parse import parse_qs
|
|
8
9
|
|
|
9
10
|
from pyview.live_socket import UnconnectedSocket
|
|
10
11
|
from pyview.csrf import generate_csrf_token
|
|
12
|
+
from pyview.session import serialize_session
|
|
13
|
+
from pyview.auth import AuthProviderFactory
|
|
11
14
|
from .ws_handler import LiveSocketHandler
|
|
12
15
|
from .live_view import LiveView
|
|
13
16
|
from .live_routes import LiveViewLookup
|
|
14
|
-
from
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class RootTemplateContext(TypedDict):
|
|
18
|
-
id: str
|
|
19
|
-
content: str
|
|
20
|
-
title: Optional[str]
|
|
21
|
-
css: Optional[str]
|
|
22
|
-
csrf_token: str
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
RootTemplate = Callable[[RootTemplateContext], str]
|
|
17
|
+
from .template import RootTemplate, RootTemplateContext, defaultRootTemplate
|
|
26
18
|
|
|
27
19
|
|
|
28
20
|
class PyView(Starlette):
|
|
@@ -30,7 +22,7 @@ class PyView(Starlette):
|
|
|
30
22
|
|
|
31
23
|
def __init__(self, *args, **kwargs):
|
|
32
24
|
super().__init__(*args, **kwargs)
|
|
33
|
-
self.rootTemplate = defaultRootTemplate(
|
|
25
|
+
self.rootTemplate = defaultRootTemplate()
|
|
34
26
|
self.view_lookup = LiveViewLookup()
|
|
35
27
|
self.live_handler = LiveSocketHandler(self.view_lookup)
|
|
36
28
|
|
|
@@ -40,24 +32,28 @@ class PyView(Starlette):
|
|
|
40
32
|
self.add_websocket_route("/live/websocket", live_websocket_endpoint)
|
|
41
33
|
self.add_middleware(GZipMiddleware)
|
|
42
34
|
|
|
43
|
-
def add_live_view(self, path: str, view:
|
|
44
|
-
async def lv(request):
|
|
35
|
+
def add_live_view(self, path: str, view: type[LiveView]):
|
|
36
|
+
async def lv(request: Request):
|
|
45
37
|
return await liveview_container(
|
|
46
38
|
self.rootTemplate, self.view_lookup, request
|
|
47
39
|
)
|
|
48
40
|
|
|
49
41
|
self.view_lookup.add(path, view)
|
|
50
|
-
|
|
42
|
+
auth = AuthProviderFactory.get(view)
|
|
43
|
+
self.routes.append(Route(path, auth.wrap(lv), methods=["GET"]))
|
|
51
44
|
|
|
52
45
|
|
|
53
46
|
async def liveview_container(
|
|
54
|
-
template: RootTemplate, view_lookup: LiveViewLookup, request
|
|
47
|
+
template: RootTemplate, view_lookup: LiveViewLookup, request: Request
|
|
55
48
|
):
|
|
56
49
|
url = request.url
|
|
57
50
|
path = url.path
|
|
58
51
|
lv: LiveView = view_lookup.get(path)
|
|
59
52
|
s = UnconnectedSocket()
|
|
60
|
-
|
|
53
|
+
|
|
54
|
+
session = request.session if "session" in request.scope else {}
|
|
55
|
+
|
|
56
|
+
await lv.mount(s, session)
|
|
61
57
|
await lv.handle_params(url, parse_qs(url.query), s)
|
|
62
58
|
r = await lv.render(s.context)
|
|
63
59
|
|
|
@@ -68,52 +64,7 @@ async def liveview_container(
|
|
|
68
64
|
"content": r.text(),
|
|
69
65
|
"title": s.live_title,
|
|
70
66
|
"csrf_token": generate_csrf_token("lv:phx-" + id),
|
|
71
|
-
"
|
|
67
|
+
"session": serialize_session(session),
|
|
72
68
|
}
|
|
73
69
|
|
|
74
70
|
return HTMLResponse(template(context))
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def defaultRootTemplate(css: str) -> RootTemplate:
|
|
78
|
-
def template(context: RootTemplateContext) -> str:
|
|
79
|
-
context["css"] = css
|
|
80
|
-
return _defaultRootTemplate(context)
|
|
81
|
-
|
|
82
|
-
return template
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def _defaultRootTemplate(context: RootTemplateContext) -> str:
|
|
86
|
-
suffix = " | LiveView"
|
|
87
|
-
render_title = (
|
|
88
|
-
(context["title"] + suffix) # type: ignore
|
|
89
|
-
if context.get("title", None) is not None
|
|
90
|
-
else "LiveView"
|
|
91
|
-
)
|
|
92
|
-
css = context["css"] if context.get("css", None) is not None else ""
|
|
93
|
-
return f"""
|
|
94
|
-
<!DOCTYPE html>
|
|
95
|
-
<html lang="en">
|
|
96
|
-
<head>
|
|
97
|
-
<title data-suffix="{suffix}">{render_title}</title>
|
|
98
|
-
<meta name="csrf-token" content="{context['csrf_token']}" />
|
|
99
|
-
<meta charset="utf-8">
|
|
100
|
-
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
101
|
-
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
|
102
|
-
<script defer type="text/javascript" src="/static/assets/app.js"></script>
|
|
103
|
-
{css}
|
|
104
|
-
</head>
|
|
105
|
-
<body>
|
|
106
|
-
<div>
|
|
107
|
-
<a href="/">Home</a>
|
|
108
|
-
<div
|
|
109
|
-
data-phx-main="true"
|
|
110
|
-
data-phx-session=""
|
|
111
|
-
data-phx-static=""
|
|
112
|
-
id="phx-{context['id']}"
|
|
113
|
-
>
|
|
114
|
-
{context['content']}
|
|
115
|
-
</div>
|
|
116
|
-
</div>
|
|
117
|
-
</body>
|
|
118
|
-
</html>
|
|
119
|
-
"""
|
pyview/secret.py
ADDED
|
@@ -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
|
pyview/session.py
ADDED
|
@@ -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))
|
pyview/template/__init__.py
CHANGED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from typing import Callable, Optional, TypedDict
|
|
2
|
+
from markupsafe import Markup
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class RootTemplateContext(TypedDict):
|
|
6
|
+
id: str
|
|
7
|
+
content: str
|
|
8
|
+
title: Optional[str]
|
|
9
|
+
csrf_token: str
|
|
10
|
+
session: Optional[str]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
RootTemplate = Callable[[RootTemplateContext], str]
|
|
14
|
+
ContentWrapper = Callable[[RootTemplateContext, Markup], Markup]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def defaultRootTemplate(
|
|
18
|
+
css: Optional[Markup] = None, content_wrapper: Optional[ContentWrapper] = None
|
|
19
|
+
) -> RootTemplate:
|
|
20
|
+
content_wrapper = content_wrapper or (lambda c, m: m)
|
|
21
|
+
|
|
22
|
+
def template(context: RootTemplateContext) -> str:
|
|
23
|
+
return _defaultRootTemplate(context, css or Markup(""), content_wrapper)
|
|
24
|
+
|
|
25
|
+
return template
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _defaultRootTemplate(
|
|
29
|
+
context: RootTemplateContext, css: Markup, contentWrapper: ContentWrapper
|
|
30
|
+
) -> str:
|
|
31
|
+
suffix = " | LiveView"
|
|
32
|
+
render_title = (context["title"] + suffix) if context.get("title", None) is not None else "LiveView" # type: ignore
|
|
33
|
+
main_content = contentWrapper(
|
|
34
|
+
context,
|
|
35
|
+
Markup(
|
|
36
|
+
f"""
|
|
37
|
+
<div
|
|
38
|
+
data-phx-main="true"
|
|
39
|
+
data-phx-session="{context['session']}"
|
|
40
|
+
data-phx-static=""
|
|
41
|
+
id="phx-{context['id']}"
|
|
42
|
+
>
|
|
43
|
+
{context['content']}
|
|
44
|
+
</div>"""
|
|
45
|
+
),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
Markup(
|
|
50
|
+
f"""
|
|
51
|
+
<!DOCTYPE html>
|
|
52
|
+
<html lang="en">
|
|
53
|
+
<head>
|
|
54
|
+
<title data-suffix="{suffix}">{render_title}</title>
|
|
55
|
+
<meta name="csrf-token" content="{context['csrf_token']}" />
|
|
56
|
+
<meta charset="utf-8">
|
|
57
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
58
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
|
59
|
+
<script defer type="text/javascript" src="/static/assets/app.js"></script>
|
|
60
|
+
{css}
|
|
61
|
+
</head>
|
|
62
|
+
<body>"""
|
|
63
|
+
)
|
|
64
|
+
+ main_content
|
|
65
|
+
+ Markup(
|
|
66
|
+
"""
|
|
67
|
+
</body>
|
|
68
|
+
</html>
|
|
69
|
+
"""
|
|
70
|
+
)
|
|
71
|
+
)
|
pyview/ws_handler.py
CHANGED
|
@@ -5,6 +5,12 @@ 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
|
|
9
|
+
from pyview.auth import AuthProviderFactory
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AuthException(Exception):
|
|
13
|
+
pass
|
|
8
14
|
|
|
9
15
|
|
|
10
16
|
class LiveSocketHandler:
|
|
@@ -13,6 +19,10 @@ class LiveSocketHandler:
|
|
|
13
19
|
self.manager = ConnectionManager()
|
|
14
20
|
self.sessions = 0
|
|
15
21
|
|
|
22
|
+
async def check_auth(self, websocket: WebSocket, lv):
|
|
23
|
+
if not await AuthProviderFactory.get(lv).has_required_auth(websocket):
|
|
24
|
+
raise AuthException()
|
|
25
|
+
|
|
16
26
|
async def handle(self, websocket: WebSocket):
|
|
17
27
|
await self.manager.connect(websocket)
|
|
18
28
|
|
|
@@ -24,14 +34,19 @@ class LiveSocketHandler:
|
|
|
24
34
|
data = await websocket.receive_text()
|
|
25
35
|
[joinRef, mesageRef, topic, event, payload] = json.loads(data)
|
|
26
36
|
if event == "phx_join":
|
|
37
|
+
if not validate_csrf_token(payload["params"]["_csrf_token"], topic):
|
|
38
|
+
raise AuthException("Invalid CSRF token")
|
|
39
|
+
|
|
27
40
|
url = urlparse(payload["url"])
|
|
28
41
|
lv = self.routes.get(url.path)
|
|
42
|
+
await self.check_auth(websocket, lv)
|
|
29
43
|
socket = LiveViewSocket(websocket, topic, lv)
|
|
30
44
|
|
|
31
|
-
|
|
32
|
-
|
|
45
|
+
session = {}
|
|
46
|
+
if "session" in payload:
|
|
47
|
+
session = deserialize_session(payload["session"])
|
|
33
48
|
|
|
34
|
-
await lv.mount(socket)
|
|
49
|
+
await lv.mount(socket, session)
|
|
35
50
|
await lv.handle_params(url, parse_qs(url.query), socket)
|
|
36
51
|
|
|
37
52
|
rendered = await _render(socket)
|
|
@@ -48,10 +63,12 @@ class LiveSocketHandler:
|
|
|
48
63
|
await self.handle_connected(socket)
|
|
49
64
|
|
|
50
65
|
except WebSocketDisconnect:
|
|
51
|
-
self.manager.disconnect(websocket)
|
|
52
66
|
if socket:
|
|
53
67
|
await socket.close()
|
|
54
68
|
self.sessions -= 1
|
|
69
|
+
except AuthException:
|
|
70
|
+
await websocket.close()
|
|
71
|
+
self.sessions -= 1
|
|
55
72
|
|
|
56
73
|
async def handle_connected(self, socket: LiveViewSocket):
|
|
57
74
|
while True:
|
|
@@ -66,9 +83,7 @@ class LiveSocketHandler:
|
|
|
66
83
|
"phx_reply",
|
|
67
84
|
{"response": {}, "status": "ok"},
|
|
68
85
|
]
|
|
69
|
-
await self.manager.send_personal_message(
|
|
70
|
-
json.dumps(resp), socket.websocket
|
|
71
|
-
)
|
|
86
|
+
await self.manager.send_personal_message(json.dumps(resp), socket.websocket)
|
|
72
87
|
continue
|
|
73
88
|
|
|
74
89
|
if event == "event":
|
|
@@ -86,9 +101,7 @@ class LiveSocketHandler:
|
|
|
86
101
|
"phx_reply",
|
|
87
102
|
{"response": {"diff": rendered}, "status": "ok"},
|
|
88
103
|
]
|
|
89
|
-
await self.manager.send_personal_message(
|
|
90
|
-
json.dumps(resp), socket.websocket
|
|
91
|
-
)
|
|
104
|
+
await self.manager.send_personal_message(json.dumps(resp), socket.websocket)
|
|
92
105
|
continue
|
|
93
106
|
|
|
94
107
|
if event == "live_patch":
|
|
@@ -105,9 +118,7 @@ class LiveSocketHandler:
|
|
|
105
118
|
"phx_reply",
|
|
106
119
|
{"response": {"diff": rendered}, "status": "ok"},
|
|
107
120
|
]
|
|
108
|
-
await self.manager.send_personal_message(
|
|
109
|
-
json.dumps(resp), socket.websocket
|
|
110
|
-
)
|
|
121
|
+
await self.manager.send_personal_message(json.dumps(resp), socket.websocket)
|
|
111
122
|
continue
|
|
112
123
|
|
|
113
124
|
|
|
@@ -123,18 +134,10 @@ async def _render(socket: LiveViewSocket):
|
|
|
123
134
|
|
|
124
135
|
class ConnectionManager:
|
|
125
136
|
def __init__(self):
|
|
126
|
-
|
|
137
|
+
pass
|
|
127
138
|
|
|
128
139
|
async def connect(self, websocket: WebSocket):
|
|
129
140
|
await websocket.accept()
|
|
130
|
-
self.active_connections.append(websocket)
|
|
131
|
-
|
|
132
|
-
def disconnect(self, websocket: WebSocket):
|
|
133
|
-
self.active_connections.remove(websocket)
|
|
134
141
|
|
|
135
142
|
async def send_personal_message(self, message: str, websocket: WebSocket):
|
|
136
143
|
await websocket.send_text(message)
|
|
137
|
-
|
|
138
|
-
async def broadcast(self, message: str):
|
|
139
|
-
for connection in self.active_connections:
|
|
140
|
-
await connection.send_text(message)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pyview-web
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.8a0
|
|
4
4
|
Summary: LiveView in Python
|
|
5
5
|
Home-page: https://pyview.rocks
|
|
6
6
|
License: MIT
|
|
@@ -22,10 +22,8 @@ Classifier: Programming Language :: Python
|
|
|
22
22
|
Classifier: Programming Language :: Python :: 3
|
|
23
23
|
Classifier: Programming Language :: Python :: 3.10
|
|
24
24
|
Classifier: Programming Language :: Python :: 3.11
|
|
25
|
-
Classifier: Programming Language :: Python :: 3
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
26
26
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
27
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
28
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
29
27
|
Classifier: Programming Language :: Python :: 3.9
|
|
30
28
|
Classifier: Topic :: Internet
|
|
31
29
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
@@ -79,7 +77,7 @@ class CountContext(TypedDict):
|
|
|
79
77
|
|
|
80
78
|
|
|
81
79
|
class CountLiveView(LiveView[CountContext]):
|
|
82
|
-
async def mount(self, socket: LiveViewSocket[CountContext]):
|
|
80
|
+
async def mount(self, socket: LiveViewSocket[CountContext], _session):
|
|
83
81
|
socket.context = {"count": 0}
|
|
84
82
|
|
|
85
83
|
async def handle_event(self, event, payload, socket: LiveViewSocket[CountContext]):
|
|
@@ -2,18 +2,24 @@ pyview/__init__.py,sha256=MN3RrkiFdM6bOZV6qJgvwifXV81Qx9k_PN2p8zJvq2I,223
|
|
|
2
2
|
pyview/assets/js/app.js,sha256=n7FiM_VoxZ3ZUkLjtyxbOnSZU88zZkN1A5b8jjhiFoo,2508
|
|
3
3
|
pyview/assets/package-lock.json,sha256=kFCrEUJc3G7VD7EsBQf6__EKQhaKAok-I5rrwiAoX0w,2425
|
|
4
4
|
pyview/assets/package.json,sha256=E6xaX8KMUAektIIedLmI55jGnmlNMSeD2tgKYXWk1vg,151
|
|
5
|
+
pyview/auth/__init__.py,sha256=vMlirETRhD4va61NOzwg8VY8ep9wVOF96GznJGBmzD0,109
|
|
6
|
+
pyview/auth/provider.py,sha256=fwriy2JZcOStutVXD-8VlMPAFXjILCM0l08lhTgmuyE,935
|
|
7
|
+
pyview/auth/required.py,sha256=4Xat6-LePwm82NhBABnp4KFznxj9rRYU1yevW9u7DRs,1223
|
|
5
8
|
pyview/changesets/__init__.py,sha256=55CLari2JHZtwy4hapHe7CqUyKjcP4dkM_t5d3CY2gU,46
|
|
6
9
|
pyview/changesets/changesets.py,sha256=B1q1nXwI2iuZZQpE3P2T0PpwI21PHjqcsuIQmkKPCvI,1747
|
|
7
|
-
pyview/csrf.py,sha256=
|
|
10
|
+
pyview/csrf.py,sha256=VIURva9EJqXXYGC7engweh3SwDQCnHlhV2zWdcdnFqc,789
|
|
8
11
|
pyview/events.py,sha256=Zv8G2F1XeXUk1wrnfomeFfxB0OPYmHdjSvxRjQew3No,125
|
|
9
12
|
pyview/js.py,sha256=4OnPEfBfuvmekeQlm9444As4PLR22zLMIyyzQIIkmls,751
|
|
10
13
|
pyview/live_routes.py,sha256=tsKFh2gmH2BWsjsZQZErzRp_-KiAZcn4lFKNLRIN5Nc,498
|
|
11
14
|
pyview/live_socket.py,sha256=p3eTynzGtvLiooOZyTemmXTrusAJKaoueuZ6Q5gfeYk,3314
|
|
12
|
-
pyview/live_view.py,sha256=
|
|
13
|
-
pyview/pyview.py,sha256=
|
|
15
|
+
pyview/live_view.py,sha256=RRhj89NMianv6dkYd9onOQEJgpRF-pqUb7nmbQr6R6E,1255
|
|
16
|
+
pyview/pyview.py,sha256=LdW2irgsd4KRls5TLU22DZJIy5Pmo_2L-ajOwXi9cEs,2302
|
|
17
|
+
pyview/secret.py,sha256=HbaNpGAkFs4uxMVAmk9HwE3FIehg7dmwEOlED7C9moM,363
|
|
18
|
+
pyview/session.py,sha256=nC8ExyVwfCgQfx9T-aJGyFhr2C7jsrEY_QFkaXtP28U,432
|
|
14
19
|
pyview/static/assets/app.js,sha256=vyD-RACuZxOBWGy7VD-BY5-qkgbFyJM43-M-WGgaAHo,199984
|
|
15
|
-
pyview/template/__init__.py,sha256=
|
|
20
|
+
pyview/template/__init__.py,sha256=bDaxDV7QhUwBXfy672Ft0NBDNvhT4kEKDV5VUWhADe8,206
|
|
16
21
|
pyview/template/live_template.py,sha256=wSKyBw7ejpUY5qXUZdE36Jeeix8Of0CUq8eZdQwxXyg,1864
|
|
22
|
+
pyview/template/root_template.py,sha256=0U50QIBhLVshLEog0SCbTDWgRjMpkfNDVcqowKFmGSQ,1876
|
|
17
23
|
pyview/template/serializer.py,sha256=WDZfqJr2LMlf36fUW2CmWc2aREc63553_y_GRP2-qYc,826
|
|
18
24
|
pyview/test_csrf.py,sha256=QWTOtfagDMkoYDK_ehYxua34F7-ltPsSeTwQGEOlqHU,684
|
|
19
25
|
pyview/vendor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -29,8 +35,8 @@ pyview/vendor/ibis/nodes.py,sha256=jNRmlTCHXC4xCF8nfDRlLWINlaYiFa8NGrzebJFJgEM,2
|
|
|
29
35
|
pyview/vendor/ibis/template.py,sha256=IX9z-Ig13yJyRnMqtB52eiRLe002qdIxnfa7fYEXLqM,2314
|
|
30
36
|
pyview/vendor/ibis/tree.py,sha256=5LAjl3q9iPMZBb6QbKurWj9-QGKLVf11K2_bQotWlUc,2293
|
|
31
37
|
pyview/vendor/ibis/utils.py,sha256=nLSaxPR9vMphzV9qinlz_Iurv9c49Ps6Knv8vyNlewU,2768
|
|
32
|
-
pyview/ws_handler.py,sha256=
|
|
33
|
-
pyview_web-0.0.
|
|
34
|
-
pyview_web-0.0.
|
|
35
|
-
pyview_web-0.0.
|
|
36
|
-
pyview_web-0.0.
|
|
38
|
+
pyview/ws_handler.py,sha256=tokN9gtC_Gn1tlPTxfhYGSFfquiSUvD3sHigzjh1gYM,4738
|
|
39
|
+
pyview_web-0.0.8a0.dist-info/LICENSE,sha256=M_bADaBm9_MV9llX3lCicksLhwk3eZUjA2srE0uUWr0,1071
|
|
40
|
+
pyview_web-0.0.8a0.dist-info/METADATA,sha256=xURzdj61BS-VXiF-eDfi1wO330RonBEhPOb9rThSbR4,4970
|
|
41
|
+
pyview_web-0.0.8a0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
42
|
+
pyview_web-0.0.8a0.dist-info/RECORD,,
|
|
File without changes
|