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 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 LiveTemplate, template_file, RenderedContent, LiveRender
4
- import inspect
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
- cf = inspect.getfile(m.__class__)
47
- return _find_template(cf)
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 RootTemplate, RootTemplateContext, defaultRootTemplate
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))
@@ -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
@@ -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
  )
@@ -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 []
@@ -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 wrender(self, context):
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
- rendered = template.root_node.render(context)
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(self, myJoinId, socket: LiveViewSocket):
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": rendered | hook_events}, "status": "ok"},
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": rendered}, "status": "ok"},
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": rendered}, "status": "ok"},
230
+ {"response": {"diff": diff}, "status": "ok"},
218
231
  ]
219
232
 
220
233
  await self.manager.send_personal_message(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyview-web
3
- Version: 0.0.20
3
+ Version: 0.0.22
4
4
  Summary: LiveView in Python
5
5
  Home-page: https://pyview.rocks
6
6
  License: MIT
@@ -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=SeZ78aA_leqfubPUybRaaCUhqLJJirjPSkf9omvJyTs,1486
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=UfVaYBAzPU_RalijjmXup6oDpIG_w9acjUG1AXm11r4,2342
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=bDaxDV7QhUwBXfy672Ft0NBDNvhT4kEKDV5VUWhADe8,206
21
+ pyview/template/__init__.py,sha256=c5hLRfsF2fDOz8aOsoOgoCeBV6VBzdqN_Ktg3mYPw8A,509
22
22
  pyview/template/live_template.py,sha256=wSKyBw7ejpUY5qXUZdE36Jeeix8Of0CUq8eZdQwxXyg,1864
23
- pyview/template/root_template.py,sha256=OBTYtaw03yDJIsXDV9ZFqy__U579FGttRjlAXnLaNEo,1882
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=jNRmlTCHXC4xCF8nfDRlLWINlaYiFa8NGrzebJFJgEM,25128
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=Mkbw6UKEy4HYURSZUdmpF95oWiuEymzS_VGG8WV8mGw,7977
41
- pyview_web-0.0.20.dist-info/LICENSE,sha256=M_bADaBm9_MV9llX3lCicksLhwk3eZUjA2srE0uUWr0,1071
42
- pyview_web-0.0.20.dist-info/METADATA,sha256=6EY-yiCteR7fFY029hG2T5jCp8wCMRKpr2FaGhV8tJY,5276
43
- pyview_web-0.0.20.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
44
- pyview_web-0.0.20.dist-info/RECORD,,
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,,