pyview-web 0.0.20__py3-none-any.whl → 0.0.22__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/live_view.py +10 -10
- pyview/pyview.py +9 -1
- pyview/template/__init__.py +14 -0
- pyview/template/render_diff.py +34 -0
- pyview/template/root_template.py +5 -1
- pyview/template/utils.py +24 -0
- pyview/vendor/ibis/nodes.py +16 -3
- pyview/ws_handler.py +19 -6
- {pyview_web-0.0.20.dist-info → pyview_web-0.0.22.dist-info}/METADATA +1 -1
- {pyview_web-0.0.20.dist-info → pyview_web-0.0.22.dist-info}/RECORD +12 -10
- {pyview_web-0.0.20.dist-info → pyview_web-0.0.22.dist-info}/LICENSE +0 -0
- {pyview_web-0.0.20.dist-info → pyview_web-0.0.22.dist-info}/WHEEL +0 -0
pyview/live_view.py
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
from typing import TypeVar, Generic, Optional, Union, Any
|
|
2
2
|
from .live_socket import LiveViewSocket, UnconnectedSocket
|
|
3
|
-
from pyview.template import
|
|
4
|
-
|
|
3
|
+
from pyview.template import (
|
|
4
|
+
LiveTemplate,
|
|
5
|
+
template_file,
|
|
6
|
+
RenderedContent,
|
|
7
|
+
LiveRender,
|
|
8
|
+
find_associated_file,
|
|
9
|
+
)
|
|
5
10
|
from pyview.events import InfoEvent
|
|
6
11
|
from urllib.parse import ParseResult
|
|
7
12
|
|
|
@@ -43,11 +48,6 @@ class LiveView(Generic[T]):
|
|
|
43
48
|
|
|
44
49
|
|
|
45
50
|
def _find_render(m: LiveView) -> Optional[LiveTemplate]:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def _find_template(cf: str) -> Optional[LiveTemplate]:
|
|
51
|
-
if cf.endswith(".py"):
|
|
52
|
-
cf = cf[:-3]
|
|
53
|
-
return template_file(cf + ".html")
|
|
51
|
+
html = find_associated_file(m, ".html")
|
|
52
|
+
if html is not None:
|
|
53
|
+
return template_file(html)
|
pyview/pyview.py
CHANGED
|
@@ -14,7 +14,12 @@ from pyview.auth import AuthProviderFactory
|
|
|
14
14
|
from .ws_handler import LiveSocketHandler
|
|
15
15
|
from .live_view import LiveView
|
|
16
16
|
from .live_routes import LiveViewLookup
|
|
17
|
-
from .template import
|
|
17
|
+
from .template import (
|
|
18
|
+
RootTemplate,
|
|
19
|
+
RootTemplateContext,
|
|
20
|
+
defaultRootTemplate,
|
|
21
|
+
find_associated_css,
|
|
22
|
+
)
|
|
18
23
|
|
|
19
24
|
|
|
20
25
|
class PyView(Starlette):
|
|
@@ -57,6 +62,8 @@ async def liveview_container(
|
|
|
57
62
|
await lv.handle_params(urlparse(url._url), parse_qs(url.query), s)
|
|
58
63
|
r = await lv.render(s.context)
|
|
59
64
|
|
|
65
|
+
liveview_css = find_associated_css(lv)
|
|
66
|
+
|
|
60
67
|
id = str(uuid.uuid4())
|
|
61
68
|
|
|
62
69
|
context: RootTemplateContext = {
|
|
@@ -65,6 +72,7 @@ async def liveview_container(
|
|
|
65
72
|
"title": s.live_title,
|
|
66
73
|
"csrf_token": generate_csrf_token("lv:phx-" + id),
|
|
67
74
|
"session": serialize_session(session),
|
|
75
|
+
"additional_head_elements": liveview_css,
|
|
68
76
|
}
|
|
69
77
|
|
|
70
78
|
return HTMLResponse(template(context))
|
pyview/template/__init__.py
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
1
|
from pyview.vendor.ibis import Template
|
|
2
2
|
from .live_template import LiveTemplate, template_file, RenderedContent, LiveRender
|
|
3
3
|
from .root_template import RootTemplate, RootTemplateContext, defaultRootTemplate
|
|
4
|
+
from .utils import find_associated_css, find_associated_file
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"Template",
|
|
8
|
+
"LiveTemplate",
|
|
9
|
+
"template_file",
|
|
10
|
+
"RenderedContent",
|
|
11
|
+
"LiveRender",
|
|
12
|
+
"RootTemplate",
|
|
13
|
+
"RootTemplateContext",
|
|
14
|
+
"defaultRootTemplate",
|
|
15
|
+
"find_associated_css",
|
|
16
|
+
"find_associated_file",
|
|
17
|
+
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def calc_diff(old_tree: dict[str, Any], new_tree: dict[str, Any]) -> dict[str, Any]:
|
|
5
|
+
diff = {}
|
|
6
|
+
for key in new_tree:
|
|
7
|
+
if key not in old_tree:
|
|
8
|
+
diff[key] = new_tree[key]
|
|
9
|
+
elif (
|
|
10
|
+
isinstance(new_tree[key], dict)
|
|
11
|
+
and "s" in new_tree[key]
|
|
12
|
+
and "d" in new_tree[key]
|
|
13
|
+
):
|
|
14
|
+
# Handle special case of for loop
|
|
15
|
+
old_dynamic = old_tree[key]["d"]
|
|
16
|
+
new_dynamic = new_tree[key]["d"]
|
|
17
|
+
|
|
18
|
+
old_static = old_tree[key]["s"]
|
|
19
|
+
new_static = new_tree[key]["s"]
|
|
20
|
+
|
|
21
|
+
if old_static != new_static:
|
|
22
|
+
diff[key] = {"s": new_static, "d": new_dynamic}
|
|
23
|
+
continue
|
|
24
|
+
|
|
25
|
+
if old_dynamic != new_dynamic:
|
|
26
|
+
diff[key] = {"d": new_dynamic}
|
|
27
|
+
elif isinstance(new_tree[key], dict):
|
|
28
|
+
nested_diff = calc_diff(old_tree[key], new_tree[key])
|
|
29
|
+
if nested_diff:
|
|
30
|
+
diff[key] = nested_diff
|
|
31
|
+
elif old_tree[key] != new_tree[key]:
|
|
32
|
+
diff[key] = new_tree[key]
|
|
33
|
+
|
|
34
|
+
return diff
|
pyview/template/root_template.py
CHANGED
|
@@ -8,6 +8,7 @@ class RootTemplateContext(TypedDict):
|
|
|
8
8
|
title: Optional[str]
|
|
9
9
|
csrf_token: str
|
|
10
10
|
session: Optional[str]
|
|
11
|
+
additional_head_elements: list[Markup]
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
RootTemplate = Callable[[RootTemplateContext], str]
|
|
@@ -45,6 +46,8 @@ def _defaultRootTemplate(
|
|
|
45
46
|
),
|
|
46
47
|
)
|
|
47
48
|
|
|
49
|
+
additional_head_elements = "\n".join(context["additional_head_elements"])
|
|
50
|
+
|
|
48
51
|
return (
|
|
49
52
|
Markup(
|
|
50
53
|
f"""
|
|
@@ -56,8 +59,9 @@ def _defaultRootTemplate(
|
|
|
56
59
|
<meta charset="utf-8">
|
|
57
60
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
58
61
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
|
59
|
-
{css}
|
|
62
|
+
{css}
|
|
60
63
|
<script defer type="text/javascript" src="/static/assets/app.js"></script>
|
|
64
|
+
{additional_head_elements}
|
|
61
65
|
</head>
|
|
62
66
|
<body>"""
|
|
63
67
|
)
|
pyview/template/utils.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
import inspect
|
|
3
|
+
import os
|
|
4
|
+
from markupsafe import Markup
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def find_associated_file(o: object, extension: str) -> Optional[str]:
|
|
8
|
+
object_file = inspect.getfile(o.__class__)
|
|
9
|
+
|
|
10
|
+
if object_file.endswith(".py"):
|
|
11
|
+
object_file = object_file[:-3]
|
|
12
|
+
|
|
13
|
+
associated_file = object_file + extension
|
|
14
|
+
if os.path.isfile(associated_file):
|
|
15
|
+
return associated_file
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def find_associated_css(o: object) -> list[Markup]:
|
|
19
|
+
css_file = find_associated_file(o, ".css")
|
|
20
|
+
if css_file:
|
|
21
|
+
with open(css_file, "r") as css:
|
|
22
|
+
return [Markup(f"<style>{css.read()}</style>")]
|
|
23
|
+
|
|
24
|
+
return []
|
pyview/vendor/ibis/nodes.py
CHANGED
|
@@ -203,6 +203,8 @@ class Node:
|
|
|
203
203
|
resp.add_static(child.token.text if child.token else "")
|
|
204
204
|
elif isinstance(child, PrintNode):
|
|
205
205
|
resp.add_dynamic(child.wrender(context))
|
|
206
|
+
elif isinstance(child, IncludeNode):
|
|
207
|
+
resp.add_dynamic(child.tree_parts(context))
|
|
206
208
|
else:
|
|
207
209
|
resp.add_dynamic(child.tree_parts(context))
|
|
208
210
|
|
|
@@ -624,7 +626,7 @@ class IncludeNode(Node):
|
|
|
624
626
|
else:
|
|
625
627
|
raise errors.TemplateSyntaxError("Malformed 'include' tag.", token)
|
|
626
628
|
|
|
627
|
-
def
|
|
629
|
+
def visit_node(self, context, visitor: NodeVisitor):
|
|
628
630
|
template_name = self.template_expr.eval(context)
|
|
629
631
|
if isinstance(template_name, str):
|
|
630
632
|
if ibis.loader:
|
|
@@ -632,9 +634,8 @@ class IncludeNode(Node):
|
|
|
632
634
|
context.push()
|
|
633
635
|
for name, expr in self.variables.items():
|
|
634
636
|
context[name] = expr.eval(context)
|
|
635
|
-
|
|
637
|
+
visitor(context, template.root_node)
|
|
636
638
|
context.pop()
|
|
637
|
-
return rendered
|
|
638
639
|
else:
|
|
639
640
|
msg = f"No template loader has been specified. "
|
|
640
641
|
msg += f"A template loader is required by the 'include' tag in "
|
|
@@ -645,6 +646,18 @@ class IncludeNode(Node):
|
|
|
645
646
|
msg += f"The variable '{self.template_arg}' should evaluate to a string. "
|
|
646
647
|
msg += f"This variable has the value: {repr(template_name)}."
|
|
647
648
|
raise errors.TemplateRenderingError(msg, self.token)
|
|
649
|
+
|
|
650
|
+
def wrender(self, context):
|
|
651
|
+
output = []
|
|
652
|
+
self.visit_node(context, lambda ctx, node: output.append(node.render(ctx)))
|
|
653
|
+
return "".join(output)
|
|
654
|
+
|
|
655
|
+
def tree_parts(self, context) -> PartsTree:
|
|
656
|
+
output = []
|
|
657
|
+
def visitor(ctx, node):
|
|
658
|
+
output.append(node.tree_parts(ctx))
|
|
659
|
+
self.visit_node(context, visitor)
|
|
660
|
+
return output[0]
|
|
648
661
|
|
|
649
662
|
|
|
650
663
|
# ExtendsNodes implement template inheritance. They indicate that the current template inherits
|
pyview/ws_handler.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Optional
|
|
1
|
+
from typing import Optional, Any
|
|
2
2
|
import json
|
|
3
3
|
from starlette.websockets import WebSocket, WebSocketDisconnect
|
|
4
4
|
from urllib.parse import urlparse, parse_qs
|
|
@@ -8,6 +8,7 @@ from pyview.csrf import validate_csrf_token
|
|
|
8
8
|
from pyview.session import deserialize_session
|
|
9
9
|
from pyview.auth import AuthProviderFactory
|
|
10
10
|
from pyview.phx_message import parse_message
|
|
11
|
+
from pyview.template.render_diff import calc_diff
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
class AuthException(Exception):
|
|
@@ -63,7 +64,7 @@ class LiveSocketHandler:
|
|
|
63
64
|
]
|
|
64
65
|
|
|
65
66
|
await self.manager.send_personal_message(json.dumps(resp), websocket)
|
|
66
|
-
await self.handle_connected(topic, socket)
|
|
67
|
+
await self.handle_connected(topic, socket, rendered)
|
|
67
68
|
|
|
68
69
|
except WebSocketDisconnect:
|
|
69
70
|
if socket:
|
|
@@ -73,7 +74,9 @@ class LiveSocketHandler:
|
|
|
73
74
|
await websocket.close()
|
|
74
75
|
self.sessions -= 1
|
|
75
76
|
|
|
76
|
-
async def handle_connected(
|
|
77
|
+
async def handle_connected(
|
|
78
|
+
self, myJoinId, socket: LiveViewSocket, prev_rendered: dict[str, Any]
|
|
79
|
+
):
|
|
77
80
|
while True:
|
|
78
81
|
message = await socket.websocket.receive()
|
|
79
82
|
[joinRef, mesageRef, topic, event, payload] = parse_message(message)
|
|
@@ -105,6 +108,9 @@ class LiveSocketHandler:
|
|
|
105
108
|
{} if not socket.pending_events else {"e": socket.pending_events}
|
|
106
109
|
)
|
|
107
110
|
|
|
111
|
+
diff = calc_diff(prev_rendered, rendered)
|
|
112
|
+
prev_rendered = rendered
|
|
113
|
+
|
|
108
114
|
socket.pending_events = []
|
|
109
115
|
|
|
110
116
|
resp = [
|
|
@@ -112,7 +118,7 @@ class LiveSocketHandler:
|
|
|
112
118
|
mesageRef,
|
|
113
119
|
topic,
|
|
114
120
|
"phx_reply",
|
|
115
|
-
{"response": {"diff":
|
|
121
|
+
{"response": {"diff": diff | hook_events}, "status": "ok"},
|
|
116
122
|
]
|
|
117
123
|
await self.manager.send_personal_message(
|
|
118
124
|
json.dumps(resp), socket.websocket
|
|
@@ -125,13 +131,15 @@ class LiveSocketHandler:
|
|
|
125
131
|
|
|
126
132
|
await lv.handle_params(url, parse_qs(url.query), socket)
|
|
127
133
|
rendered = await _render(socket)
|
|
134
|
+
diff = calc_diff(prev_rendered, rendered)
|
|
135
|
+
prev_rendered = rendered
|
|
128
136
|
|
|
129
137
|
resp = [
|
|
130
138
|
joinRef,
|
|
131
139
|
mesageRef,
|
|
132
140
|
topic,
|
|
133
141
|
"phx_reply",
|
|
134
|
-
{"response": {"diff":
|
|
142
|
+
{"response": {"diff": diff}, "status": "ok"},
|
|
135
143
|
]
|
|
136
144
|
await self.manager.send_personal_message(
|
|
137
145
|
json.dumps(resp), socket.websocket
|
|
@@ -142,7 +150,10 @@ class LiveSocketHandler:
|
|
|
142
150
|
allow_upload_response = socket.upload_manager.process_allow_upload(
|
|
143
151
|
payload
|
|
144
152
|
)
|
|
153
|
+
|
|
145
154
|
rendered = await _render(socket)
|
|
155
|
+
diff = calc_diff(prev_rendered, rendered)
|
|
156
|
+
prev_rendered = rendered
|
|
146
157
|
|
|
147
158
|
resp = [
|
|
148
159
|
joinRef,
|
|
@@ -208,13 +219,15 @@ class LiveSocketHandler:
|
|
|
208
219
|
if event == "progress":
|
|
209
220
|
socket.upload_manager.update_progress(joinRef, payload)
|
|
210
221
|
rendered = await _render(socket)
|
|
222
|
+
diff = calc_diff(prev_rendered, rendered)
|
|
223
|
+
prev_rendered = rendered
|
|
211
224
|
|
|
212
225
|
resp = [
|
|
213
226
|
joinRef,
|
|
214
227
|
mesageRef,
|
|
215
228
|
topic,
|
|
216
229
|
"phx_reply",
|
|
217
|
-
{"response": {"diff":
|
|
230
|
+
{"response": {"diff": diff}, "status": "ok"},
|
|
218
231
|
]
|
|
219
232
|
|
|
220
233
|
await self.manager.send_personal_message(
|
|
@@ -12,16 +12,18 @@ pyview/events.py,sha256=Zv8G2F1XeXUk1wrnfomeFfxB0OPYmHdjSvxRjQew3No,125
|
|
|
12
12
|
pyview/js.py,sha256=4OnPEfBfuvmekeQlm9444As4PLR22zLMIyyzQIIkmls,751
|
|
13
13
|
pyview/live_routes.py,sha256=tsKFh2gmH2BWsjsZQZErzRp_-KiAZcn4lFKNLRIN5Nc,498
|
|
14
14
|
pyview/live_socket.py,sha256=6SLEkEBzK-zIUNh_5j_OG5t6IHGTDNCpGXk7D7SMNJ4,4370
|
|
15
|
-
pyview/live_view.py,sha256=
|
|
15
|
+
pyview/live_view.py,sha256=A0vCCvHUy39_eEhRzDbYMEDzgpRqseZPjCnBAMjogxw,1406
|
|
16
16
|
pyview/phx_message.py,sha256=DUdPfl6tlw9K0FNXJ35ehq03JGgynvwA_JItHQ_dxMQ,2007
|
|
17
|
-
pyview/pyview.py,sha256=
|
|
17
|
+
pyview/pyview.py,sha256=xy8on2f-chU4JWVy6zGTDqjP8BW-kOoi16voNIgWRg4,2478
|
|
18
18
|
pyview/secret.py,sha256=HbaNpGAkFs4uxMVAmk9HwE3FIehg7dmwEOlED7C9moM,363
|
|
19
19
|
pyview/session.py,sha256=nC8ExyVwfCgQfx9T-aJGyFhr2C7jsrEY_QFkaXtP28U,432
|
|
20
20
|
pyview/static/assets/app.js,sha256=QoXfdcOCYwVYJftvjsIIVwFye7onaOJMxRpalyYqoMU,200029
|
|
21
|
-
pyview/template/__init__.py,sha256=
|
|
21
|
+
pyview/template/__init__.py,sha256=c5hLRfsF2fDOz8aOsoOgoCeBV6VBzdqN_Ktg3mYPw8A,509
|
|
22
22
|
pyview/template/live_template.py,sha256=wSKyBw7ejpUY5qXUZdE36Jeeix8Of0CUq8eZdQwxXyg,1864
|
|
23
|
-
pyview/template/
|
|
23
|
+
pyview/template/render_diff.py,sha256=UpmxQZ-ImDqEUIehcoLfpdClRVYV5StTOP9iKsU8AZ0,1090
|
|
24
|
+
pyview/template/root_template.py,sha256=zCUs1bt8R7qynhBE0tTSEYfdkGtbeKNmPhwzRiFNdsI,2031
|
|
24
25
|
pyview/template/serializer.py,sha256=WDZfqJr2LMlf36fUW2CmWc2aREc63553_y_GRP2-qYc,826
|
|
26
|
+
pyview/template/utils.py,sha256=S8593UjUJztUrtC3h1EL9MxQp5uH7rFDTNkv9C6A_xU,642
|
|
25
27
|
pyview/test_csrf.py,sha256=QWTOtfagDMkoYDK_ehYxua34F7-ltPsSeTwQGEOlqHU,684
|
|
26
28
|
pyview/uploads.py,sha256=cFNOlJD5dkA2VccZT_W1bJn_5vYAaphhRJX-RCfEXm8,9598
|
|
27
29
|
pyview/vendor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -33,12 +35,12 @@ pyview/vendor/ibis/context.py,sha256=tTZSKJDPI2r__nsC3hl9OTOZjEIlXUEpnih0NKbCc3k
|
|
|
33
35
|
pyview/vendor/ibis/errors.py,sha256=gtRX3LjkdWEP4NaX8HXL_6OU2fCX16IBBSiGMG5wiY4,1531
|
|
34
36
|
pyview/vendor/ibis/filters.py,sha256=M36KS6dlzfsb2NmHkbVuo8gJbiQ6aQjcHzXxHwZ3Afw,7042
|
|
35
37
|
pyview/vendor/ibis/loaders.py,sha256=NYW7_hlC7TRPDau37bgiOCsvsBgIPpUEpb1NbroUVUA,3457
|
|
36
|
-
pyview/vendor/ibis/nodes.py,sha256=
|
|
38
|
+
pyview/vendor/ibis/nodes.py,sha256=TgFt4q5MrVW3gC3PVitrs2LyXKllRveooM7XKydNATk,25617
|
|
37
39
|
pyview/vendor/ibis/template.py,sha256=IX9z-Ig13yJyRnMqtB52eiRLe002qdIxnfa7fYEXLqM,2314
|
|
38
40
|
pyview/vendor/ibis/tree.py,sha256=5LAjl3q9iPMZBb6QbKurWj9-QGKLVf11K2_bQotWlUc,2293
|
|
39
41
|
pyview/vendor/ibis/utils.py,sha256=nLSaxPR9vMphzV9qinlz_Iurv9c49Ps6Knv8vyNlewU,2768
|
|
40
|
-
pyview/ws_handler.py,sha256=
|
|
41
|
-
pyview_web-0.0.
|
|
42
|
-
pyview_web-0.0.
|
|
43
|
-
pyview_web-0.0.
|
|
44
|
-
pyview_web-0.0.
|
|
42
|
+
pyview/ws_handler.py,sha256=Vi5aIoxz_Z9qOEcA5fgxsSoJtLz7n_ytbJ8vZiWDvGc,8473
|
|
43
|
+
pyview_web-0.0.22.dist-info/LICENSE,sha256=M_bADaBm9_MV9llX3lCicksLhwk3eZUjA2srE0uUWr0,1071
|
|
44
|
+
pyview_web-0.0.22.dist-info/METADATA,sha256=Du3wF_Qe8pG3FAk8F7tRD80eahCG3pTuqpzzVlP0Kqo,5276
|
|
45
|
+
pyview_web-0.0.22.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
46
|
+
pyview_web-0.0.22.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|