pyview-web 0.3.0__py3-none-any.whl → 0.8.0a2__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.
Files changed (78) hide show
  1. pyview/__init__.py +16 -6
  2. pyview/assets/js/app.js +1 -0
  3. pyview/assets/js/uploaders.js +221 -0
  4. pyview/assets/package-lock.json +16 -14
  5. pyview/assets/package.json +2 -2
  6. pyview/async_stream_runner.py +2 -1
  7. pyview/auth/__init__.py +3 -1
  8. pyview/auth/provider.py +6 -6
  9. pyview/auth/required.py +7 -10
  10. pyview/binding/__init__.py +47 -0
  11. pyview/binding/binder.py +134 -0
  12. pyview/binding/context.py +33 -0
  13. pyview/binding/converters.py +191 -0
  14. pyview/binding/helpers.py +78 -0
  15. pyview/binding/injectables.py +119 -0
  16. pyview/binding/params.py +105 -0
  17. pyview/binding/result.py +32 -0
  18. pyview/changesets/__init__.py +2 -0
  19. pyview/changesets/changesets.py +8 -3
  20. pyview/cli/commands/create_view.py +4 -3
  21. pyview/cli/main.py +1 -1
  22. pyview/components/__init__.py +72 -0
  23. pyview/components/base.py +212 -0
  24. pyview/components/lifecycle.py +85 -0
  25. pyview/components/manager.py +366 -0
  26. pyview/components/renderer.py +14 -0
  27. pyview/components/slots.py +73 -0
  28. pyview/csrf.py +4 -2
  29. pyview/events/AutoEventDispatch.py +98 -0
  30. pyview/events/BaseEventHandler.py +51 -8
  31. pyview/events/__init__.py +2 -1
  32. pyview/instrumentation/__init__.py +3 -3
  33. pyview/instrumentation/interfaces.py +57 -33
  34. pyview/instrumentation/noop.py +21 -18
  35. pyview/js.py +20 -23
  36. pyview/live_routes.py +5 -3
  37. pyview/live_socket.py +167 -44
  38. pyview/live_view.py +24 -12
  39. pyview/meta.py +14 -2
  40. pyview/phx_message.py +7 -8
  41. pyview/playground/__init__.py +10 -0
  42. pyview/playground/builder.py +118 -0
  43. pyview/playground/favicon.py +39 -0
  44. pyview/pyview.py +54 -20
  45. pyview/session.py +2 -0
  46. pyview/static/assets/app.js +2088 -806
  47. pyview/static/assets/uploaders.js +221 -0
  48. pyview/stream.py +308 -0
  49. pyview/template/__init__.py +11 -1
  50. pyview/template/live_template.py +12 -8
  51. pyview/template/live_view_template.py +338 -0
  52. pyview/template/render_diff.py +33 -7
  53. pyview/template/root_template.py +21 -9
  54. pyview/template/serializer.py +2 -5
  55. pyview/template/template_view.py +170 -0
  56. pyview/template/utils.py +3 -2
  57. pyview/uploads.py +344 -55
  58. pyview/vendor/flet/pubsub/__init__.py +3 -1
  59. pyview/vendor/flet/pubsub/pub_sub.py +10 -18
  60. pyview/vendor/ibis/__init__.py +3 -7
  61. pyview/vendor/ibis/compiler.py +25 -32
  62. pyview/vendor/ibis/context.py +13 -15
  63. pyview/vendor/ibis/errors.py +0 -6
  64. pyview/vendor/ibis/filters.py +70 -76
  65. pyview/vendor/ibis/loaders.py +6 -7
  66. pyview/vendor/ibis/nodes.py +40 -42
  67. pyview/vendor/ibis/template.py +4 -5
  68. pyview/vendor/ibis/tree.py +62 -3
  69. pyview/vendor/ibis/utils.py +14 -15
  70. pyview/ws_handler.py +116 -86
  71. {pyview_web-0.3.0.dist-info → pyview_web-0.8.0a2.dist-info}/METADATA +39 -33
  72. pyview_web-0.8.0a2.dist-info/RECORD +80 -0
  73. pyview_web-0.8.0a2.dist-info/WHEEL +4 -0
  74. pyview_web-0.8.0a2.dist-info/entry_points.txt +3 -0
  75. pyview_web-0.3.0.dist-info/LICENSE +0 -21
  76. pyview_web-0.3.0.dist-info/RECORD +0 -58
  77. pyview_web-0.3.0.dist-info/WHEEL +0 -4
  78. pyview_web-0.3.0.dist-info/entry_points.txt +0 -3
@@ -1,17 +1,17 @@
1
1
  import ast
2
+ import collections
3
+ import itertools
2
4
  import operator
3
5
  import re
4
- import itertools
5
- import collections
6
- from pyview.vendor import ibis
7
6
  from typing import Any, Callable
8
7
 
9
- from . import utils
10
- from . import filters
11
- from . import errors
12
- from .tree import PartsTree
13
8
  from markupsafe import Markup, escape
14
9
 
10
+ from pyview.vendor import ibis
11
+
12
+ from . import errors, filters, utils
13
+ from .tree import PartsTree, StreamComprehension
14
+
15
15
  # Dictionary of registered keywords for instruction tags.
16
16
  instruction_keywords = {}
17
17
 
@@ -53,7 +53,6 @@ def register(keyword, endword=None):
53
53
  # foo.bar.baz('bam')|filter(25, 'text')
54
54
  #
55
55
  class Expression:
56
-
57
56
  re_func_call = re.compile(r"^([\w.]+)\((.*)\)$")
58
57
  re_varstring = re.compile(r"^[\w.]+$")
59
58
 
@@ -99,9 +98,7 @@ class Expression:
99
98
  for filter_expr in filter_list:
100
99
  _, filter_name, filter_args = self._try_parse_as_func_call(filter_expr)
101
100
  if filter_name in filters.filtermap:
102
- self.filters.append(
103
- (filter_name, filters.filtermap[filter_name], filter_args)
104
- )
101
+ self.filters.append((filter_name, filters.filtermap[filter_name], filter_args))
105
102
  else:
106
103
  msg = f"Unrecognised filter name '{filter_name}'."
107
104
  raise errors.TemplateSyntaxError(msg, self.token)
@@ -154,9 +151,7 @@ class Node:
154
151
  raise
155
152
  except Exception as err:
156
153
  if token:
157
- tagname = (
158
- f"'{token.keyword}'" if token.type == "INSTRUCTION" else token.type
159
- )
154
+ tagname = f"'{token.keyword}'" if token.type == "INSTRUCTION" else token.type
160
155
  msg = f"An unexpected error occurred while parsing the {tagname} tag: "
161
156
  msg += f"{err.__class__.__name__}: {err}"
162
157
  else:
@@ -184,9 +179,7 @@ class Node:
184
179
  if self.token.type == "INSTRUCTION"
185
180
  else self.token.type
186
181
  )
187
- msg = (
188
- f"An unexpected error occurred while rendering the {tagname} tag: "
189
- )
182
+ msg = f"An unexpected error occurred while rendering the {tagname} tag: "
190
183
  msg += f"{err.__class__.__name__}: {err}"
191
184
  else:
192
185
  msg = f"Unexpected rendering error: {err.__class__.__name__}: {err}"
@@ -285,13 +278,12 @@ class PrintNode(Node):
285
278
  if content:
286
279
  break
287
280
 
288
- return (
289
- str(content) if isinstance(content, Markup) else str(escape(str(content)))
290
- )
281
+ return str(content) if isinstance(content, Markup) else str(escape(str(content)))
291
282
 
292
283
 
293
284
  NodeVisitor = Callable[[Node, Any], Any]
294
285
 
286
+
295
287
  # ForNodes implement `for ... in ...` looping over iterables.
296
288
  #
297
289
  # {% for <var> in <expr> %} ... [ {% empty %} ... ] {% endfor %}
@@ -302,13 +294,12 @@ NodeVisitor = Callable[[Node, Any], Any]
302
294
  #
303
295
  @register("for", "endfor")
304
296
  class ForNode(Node):
305
-
306
297
  regex = re.compile(r"for\s+(\w+(?:,\s*\w+)*)\s+in\s+(.+)")
307
298
 
308
299
  def process_token(self, token):
309
300
  match = self.regex.match(token.text)
310
301
  if match is None:
311
- msg = f"Malformed 'for' tag."
302
+ msg = "Malformed 'for' tag."
312
303
  raise errors.TemplateSyntaxError(msg, token)
313
304
  self.loopvars = [var.strip() for var in match.group(1).split(",")]
314
305
  self.expr = Expression(match.group(2), token)
@@ -327,9 +318,9 @@ class ForNode(Node):
327
318
  context.push()
328
319
  if unpack:
329
320
  try:
330
- unpacked = dict(zip(self.loopvars, item))
321
+ unpacked = dict(zip(self.loopvars, item, strict=False))
331
322
  except Exception as err:
332
- msg = f"Unpacking error."
323
+ msg = "Unpacking error."
333
324
  raise errors.TemplateRenderingError(msg, self.token) from err
334
325
  else:
335
326
  context.update(unpacked)
@@ -384,11 +375,23 @@ class ForNode(Node):
384
375
  }
385
376
 
386
377
  def tree_parts(self, context):
378
+ # Import here to avoid circular imports
379
+ from pyview.stream import Stream
380
+
387
381
  output = []
388
382
 
389
383
  def visitor(node, ctx):
390
384
  output.append(node.tree_parts(ctx))
391
385
 
386
+ # Get the collection to check if it's a Stream
387
+ collection = self.expr.eval(context)
388
+
389
+ # Check if this is a Stream
390
+ if isinstance(collection, Stream):
391
+ self.visit_nodes(context, visitor)
392
+ return StreamComprehension(parts=output, stream=collection)
393
+
394
+ # Regular collection - use standard visit
392
395
  self.visit_nodes(context, visitor)
393
396
  return output
394
397
 
@@ -421,7 +424,6 @@ class EmptyNode(Node):
421
424
  # Note that explicit brackets are not supported.
422
425
  @register("if", "endif")
423
426
  class IfNode(Node):
424
-
425
427
  condition = collections.namedtuple("Condition", "negated lhs op rhs")
426
428
 
427
429
  re_condition = re.compile(
@@ -484,7 +486,7 @@ class IfNode(Node):
484
486
  else:
485
487
  result = operator.truth(cond.lhs.eval(context))
486
488
  except Exception as err:
487
- msg = f"An exception was raised while evaluating the condition in the "
489
+ msg = "An exception was raised while evaluating the condition in the "
488
490
  msg += f"'{self.tag}' tag."
489
491
  raise errors.TemplateRenderingError(msg, self.token) from err
490
492
  if cond.negated:
@@ -577,14 +579,14 @@ class CycleNode(Node):
577
579
  try:
578
580
  tag, arg = token.text.split(None, 1)
579
581
  except:
580
- msg = f"Malformed 'cycle' tag."
582
+ msg = "Malformed 'cycle' tag."
581
583
  raise errors.TemplateSyntaxError(msg, token) from None
582
584
  self.expr = Expression(arg, token)
583
585
 
584
586
  def wrender(self, context):
585
587
  # We store our state info on the context object to avoid a threading mess if
586
588
  # the template is being simultaneously rendered by multiple threads.
587
- if not self in context.stash:
589
+ if self not in context.stash:
588
590
  items = self.expr.eval(context)
589
591
  if not hasattr(items, "__iter__"):
590
592
  items = ""
@@ -620,9 +622,7 @@ class IncludeNode(Node):
620
622
  name, expr = chunk.split("=", 1)
621
623
  self.variables[name.strip()] = Expression(expr.strip(), token)
622
624
  except:
623
- raise errors.TemplateSyntaxError(
624
- "Malformed 'include' tag.", token
625
- ) from None
625
+ raise errors.TemplateSyntaxError("Malformed 'include' tag.", token) from None
626
626
  else:
627
627
  raise errors.TemplateSyntaxError("Malformed 'include' tag.", token)
628
628
 
@@ -637,25 +637,27 @@ class IncludeNode(Node):
637
637
  visitor(context, template.root_node)
638
638
  context.pop()
639
639
  else:
640
- msg = f"No template loader has been specified. "
641
- msg += f"A template loader is required by the 'include' tag in "
640
+ msg = "No template loader has been specified. "
641
+ msg += "A template loader is required by the 'include' tag in "
642
642
  msg += f"template '{self.token.template_id}', line {self.token.line_number}."
643
643
  raise errors.TemplateLoadError(msg)
644
644
  else:
645
- msg = f"Invalid argument for the 'include' tag. "
645
+ msg = "Invalid argument for the 'include' tag. "
646
646
  msg += f"The variable '{self.template_arg}' should evaluate to a string. "
647
647
  msg += f"This variable has the value: {repr(template_name)}."
648
648
  raise errors.TemplateRenderingError(msg, self.token)
649
-
649
+
650
650
  def wrender(self, context):
651
651
  output = []
652
652
  self.visit_node(context, lambda ctx, node: output.append(node.render(ctx)))
653
653
  return "".join(output)
654
-
654
+
655
655
  def tree_parts(self, context) -> PartsTree:
656
656
  output = []
657
+
657
658
  def visitor(ctx, node):
658
659
  output.append(node.tree_parts(ctx))
660
+
659
661
  self.visit_node(context, visitor)
660
662
  return output[0]
661
663
 
@@ -672,9 +674,7 @@ class ExtendsNode(Node):
672
674
  try:
673
675
  tag, arg = token.text.split(None, 1)
674
676
  except:
675
- raise errors.TemplateSyntaxError(
676
- "Malformed 'extends' tag.", token
677
- ) from None
677
+ raise errors.TemplateSyntaxError("Malformed 'extends' tag.", token) from None
678
678
 
679
679
  expr = Expression(arg, token)
680
680
  if expr.is_literal and isinstance(expr.literal, str):
@@ -744,9 +744,7 @@ class WithNode(Node):
744
744
  name, expr = chunk.split("=", 1)
745
745
  self.variables[name.strip()] = Expression(expr.strip(), token)
746
746
  except:
747
- raise errors.TemplateSyntaxError(
748
- "Malformed 'with' tag.", token
749
- ) from None
747
+ raise errors.TemplateSyntaxError("Malformed 'with' tag.", token) from None
750
748
 
751
749
  def wrender(self, context):
752
750
  context.push()
@@ -1,6 +1,7 @@
1
1
  from pyview.vendor import ibis
2
+
2
3
  from .context import Context
3
- from .nodes import ExtendsNode, BlockNode
4
+ from .nodes import BlockNode, ExtendsNode
4
5
 
5
6
 
6
7
  # A Template object is initialized with a template string containing template markup and a
@@ -35,14 +36,12 @@ class Template:
35
36
 
36
37
  def _render(self, context):
37
38
  context.templates.append(self)
38
- if self.root_node.children and isinstance(
39
- self.root_node.children[0], ExtendsNode
40
- ):
39
+ if self.root_node.children and isinstance(self.root_node.children[0], ExtendsNode):
41
40
  if ibis.loader:
42
41
  parent_template = ibis.loader(self.root_node.children[0].parent_name)
43
42
  return parent_template._render(context)
44
43
  else:
45
- msg = f"No template loader has been specified. A template loader is required "
44
+ msg = "No template loader has been specified. A template loader is required "
46
45
  msg += f"by the 'extends' tag in template '{self.template_id}'."
47
46
  raise ibis.errors.TemplateLoadError(msg)
48
47
  else:
@@ -1,7 +1,10 @@
1
1
  from dataclasses import dataclass, field
2
- from typing import Any, Union
2
+ from typing import TYPE_CHECKING, Any, Union
3
3
 
4
- Part = Union[str, "PartsTree", "PartsComprehension"]
4
+ if TYPE_CHECKING:
5
+ from pyview.stream import Stream
6
+
7
+ Part = Union[str, "PartsTree", "PartsComprehension", "StreamComprehension"]
5
8
 
6
9
 
7
10
  @dataclass
@@ -33,6 +36,59 @@ class PartsComprehension:
33
36
  }
34
37
 
35
38
 
39
+ @dataclass
40
+ class StreamComprehension:
41
+ """
42
+ A comprehension that includes stream metadata for the wire format.
43
+
44
+ This is returned by ForNode.tree_parts() when iterating over a Stream.
45
+ It produces the same {"s": [...], "d": [...]} format as PartsComprehension,
46
+ but also includes a "stream" key with the stream operations.
47
+ """
48
+
49
+ parts: list["PartsTree"]
50
+ stream: "Stream"
51
+
52
+ def render_parts(self) -> Union[dict[str, Any], str]:
53
+ # Import here to avoid circular imports
54
+
55
+ # Handle empty stream
56
+ if len(self.parts) == 0:
57
+ # Still need to check for delete/reset operations
58
+ ops = self.stream._get_pending_ops()
59
+ if ops is None:
60
+ return ""
61
+ return {"stream": self.stream._to_wire_format(ops)}
62
+
63
+ if len(self.parts) == 1:
64
+ if isinstance(self.parts[0], PartsTree) and self.parts[0].is_empty():
65
+ # Check for delete/reset operations even with empty parts
66
+ ops = self.stream._get_pending_ops()
67
+ if ops is None:
68
+ return ""
69
+ return {"stream": self.stream._to_wire_format(ops)}
70
+
71
+ def render(p: Part) -> Any:
72
+ if isinstance(p, str):
73
+ return p
74
+ return p.render_parts()
75
+
76
+ statics = self.parts[0].statics
77
+ dynamics = [[render(d) for d in p.dynamics] for p in self.parts]
78
+
79
+ result: dict[str, Any] = {
80
+ "s": statics,
81
+ "d": dynamics,
82
+ }
83
+
84
+ # Add stream metadata
85
+ ops = self.stream._get_pending_ops()
86
+ if ops is not None:
87
+ result["stream"] = self.stream._to_wire_format(ops)
88
+
89
+ return result
90
+
91
+
36
92
  @dataclass
37
93
  class PartsTree:
38
94
  statics: list[str] = field(default_factory=list)
@@ -41,12 +97,15 @@ class PartsTree:
41
97
  def add_static(self, s: str):
42
98
  self.statics.append(s)
43
99
 
44
- def add_dynamic(self, d: Union[Part, list[Part]]):
100
+ def add_dynamic(self, d: Union[Part, list[Part], StreamComprehension]):
45
101
  if len(self.statics) < len(self.dynamics) + 1:
46
102
  self.statics.append("")
47
103
 
48
104
  if isinstance(d, str):
49
105
  self.dynamics.append(d)
106
+ elif isinstance(d, StreamComprehension):
107
+ # StreamComprehension is added directly (not wrapped)
108
+ self.dynamics.append(d)
50
109
  elif isinstance(d, list):
51
110
  self.dynamics.append(PartsComprehension(d))
52
111
  else:
@@ -3,7 +3,6 @@ import re
3
3
 
4
4
  # Splits a string on instances of a delimiter character. Ignores quoted delimiters.
5
5
  def splitc(s, delimiter, strip=False, discard_empty=False, maxsplit=-1):
6
-
7
6
  tokens, buf, expecting, escaped = [], [], None, False
8
7
 
9
8
  for index, char in enumerate(s):
@@ -13,18 +12,18 @@ def splitc(s, delimiter, strip=False, discard_empty=False, maxsplit=-1):
13
12
  expecting = None
14
13
  else:
15
14
  if char == delimiter:
16
- tokens.append(''.join(buf))
15
+ tokens.append("".join(buf))
17
16
  buf = []
18
17
  if len(tokens) == maxsplit:
19
- buf.append(s[index+1:])
18
+ buf.append(s[index + 1 :])
20
19
  break
21
20
  else:
22
21
  buf.append(char)
23
22
  if char in ('"', "'"):
24
23
  expecting = char
25
- escaped = not escaped if char == '\\' else False
24
+ escaped = not escaped if char == "\\" else False
26
25
 
27
- tokens.append(''.join(buf))
26
+ tokens.append("".join(buf))
28
27
 
29
28
  if strip:
30
29
  tokens = [t.strip() for t in tokens]
@@ -49,20 +48,20 @@ def splitws(s, maxsplit=-1):
49
48
  if char.isspace():
50
49
  if wsrun:
51
50
  continue
52
- tokens.append(''.join(buf))
51
+ tokens.append("".join(buf))
53
52
  buf = []
54
53
  wsrun = True
55
54
  if len(tokens) == maxsplit:
56
- buf.append(s[index+1:].lstrip())
55
+ buf.append(s[index + 1 :].lstrip())
57
56
  break
58
57
  else:
59
58
  buf.append(char)
60
59
  wsrun = False
61
60
  if char in ('"', "'"):
62
61
  expecting = char
63
- escaped = not escaped if char == '\\' else False
62
+ escaped = not escaped if char == "\\" else False
64
63
 
65
- tokens.append(''.join(buf))
64
+ tokens.append("".join(buf))
66
65
  return tokens
67
66
 
68
67
 
@@ -71,22 +70,22 @@ def splitre(s, delimiters, keep_delimiters=False):
71
70
  tokens, buf = [], []
72
71
  end_last_match = 0
73
72
 
74
- pattern = r'''"(?:[^\\"]|\\.)*"|'(?:[^\\']|\\.)*'|%s'''
75
- pattern %= '|'.join(delimiters)
73
+ pattern = r""""(?:[^\\"]|\\.)*"|'(?:[^\\']|\\.)*'|%s"""
74
+ pattern %= "|".join(delimiters)
76
75
 
77
76
  for match in re.finditer(pattern, s):
78
77
  if match.group()[0] in ["'", '"']:
79
- buf.append(s[end_last_match:match.end()])
78
+ buf.append(s[end_last_match : match.end()])
80
79
  end_last_match = match.end()
81
80
  continue
82
- buf.append(s[end_last_match:match.start()])
83
- tokens.append(''.join(buf))
81
+ buf.append(s[end_last_match : match.start()])
82
+ tokens.append("".join(buf))
84
83
  buf = []
85
84
  end_last_match = match.end()
86
85
  if keep_delimiters:
87
86
  tokens.append(match.group())
88
87
 
89
88
  buf.append(s[end_last_match:])
90
- tokens.append(''.join(buf))
89
+ tokens.append("".join(buf))
91
90
 
92
91
  return tokens