pyview-web 0.1.0__tar.gz → 0.2.1__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.

Files changed (52) hide show
  1. {pyview_web-0.1.0 → pyview_web-0.2.1}/PKG-INFO +4 -4
  2. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyproject.toml +4 -3
  3. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyview/__init__.py +2 -2
  4. pyview_web-0.2.1/pyview/events/BaseEventHandler.py +38 -0
  5. pyview_web-0.2.1/pyview/events/__init__.py +4 -0
  6. pyview_web-0.2.1/pyview/js.py +120 -0
  7. pyview_web-0.2.1/pyview/live_routes.py +47 -0
  8. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyview/live_socket.py +6 -1
  9. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyview/live_view.py +3 -2
  10. pyview_web-0.2.1/pyview/meta.py +6 -0
  11. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyview/pyview.py +16 -9
  12. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyview/template/__init__.py +2 -0
  13. pyview_web-0.2.1/pyview/template/context_processor.py +17 -0
  14. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyview/template/live_template.py +15 -10
  15. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyview/vendor/ibis/template.py +1 -0
  16. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyview/ws_handler.py +23 -4
  17. pyview_web-0.1.0/pyview/js.py +0 -34
  18. pyview_web-0.1.0/pyview/live_routes.py +0 -20
  19. {pyview_web-0.1.0 → pyview_web-0.2.1}/LICENSE +0 -0
  20. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyview/assets/js/app.js +0 -0
  21. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyview/assets/package-lock.json +0 -0
  22. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyview/assets/package.json +0 -0
  23. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyview/auth/__init__.py +0 -0
  24. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyview/auth/provider.py +0 -0
  25. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyview/auth/required.py +0 -0
  26. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyview/changesets/__init__.py +0 -0
  27. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyview/changesets/changesets.py +0 -0
  28. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyview/csrf.py +0 -0
  29. /pyview_web-0.1.0/pyview/events.py → /pyview_web-0.2.1/pyview/events/info_event.py +0 -0
  30. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyview/phx_message.py +0 -0
  31. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyview/secret.py +0 -0
  32. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyview/session.py +0 -0
  33. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyview/static/assets/app.js +0 -0
  34. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyview/template/render_diff.py +0 -0
  35. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyview/template/root_template.py +0 -0
  36. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyview/template/serializer.py +0 -0
  37. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyview/template/utils.py +0 -0
  38. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyview/test_csrf.py +0 -0
  39. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyview/uploads.py +0 -0
  40. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyview/vendor/__init__.py +0 -0
  41. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyview/vendor/flet/pubsub/__init__.py +0 -0
  42. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyview/vendor/flet/pubsub/pub_sub.py +0 -0
  43. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyview/vendor/ibis/__init__.py +0 -0
  44. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyview/vendor/ibis/compiler.py +0 -0
  45. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyview/vendor/ibis/context.py +0 -0
  46. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyview/vendor/ibis/errors.py +0 -0
  47. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyview/vendor/ibis/filters.py +0 -0
  48. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyview/vendor/ibis/loaders.py +0 -0
  49. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyview/vendor/ibis/nodes.py +0 -0
  50. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyview/vendor/ibis/tree.py +0 -0
  51. {pyview_web-0.1.0 → pyview_web-0.2.1}/pyview/vendor/ibis/utils.py +0 -0
  52. {pyview_web-0.1.0 → pyview_web-0.2.1}/readme.md +0 -0
@@ -1,8 +1,7 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: pyview-web
3
- Version: 0.1.0
3
+ Version: 0.2.1
4
4
  Summary: LiveView in Python
5
- Home-page: https://pyview.rocks
6
5
  License: MIT
7
6
  Keywords: web,api,LiveView
8
7
  Author: Larry Ogrodnek
@@ -36,9 +35,10 @@ Requires-Dist: itsdangerous (>=2.1.2,<3.0.0)
36
35
  Requires-Dist: markupsafe (>=2.1.2,<3.0.0)
37
36
  Requires-Dist: psutil (>=5.9.4,<6.0.0)
38
37
  Requires-Dist: pydantic (>=2.9.2,<3.0.0)
39
- Requires-Dist: starlette (==0.38.5)
38
+ Requires-Dist: starlette (==0.40.0)
40
39
  Requires-Dist: uvicorn (==0.30.6)
41
40
  Requires-Dist: wsproto (==1.2.0)
41
+ Project-URL: Homepage, https://pyview.rocks
42
42
  Project-URL: Repository, https://github.com/ogrodnek/pyview
43
43
  Description-Content-Type: text/markdown
44
44
 
@@ -5,7 +5,7 @@ packages = [
5
5
  { include = "pyview" },
6
6
  ]
7
7
 
8
- version = "0.1.0"
8
+ version = "0.2.1"
9
9
  description = "LiveView in Python"
10
10
  authors = ["Larry Ogrodnek <ogrodnek@gmail.com>"]
11
11
  license = "MIT"
@@ -42,7 +42,7 @@ classifiers = [
42
42
 
43
43
  [tool.poetry.dependencies]
44
44
  python = ">=3.10,<3.13"
45
- starlette = "0.38.5"
45
+ starlette = "0.40.0"
46
46
  uvicorn = "0.30.6"
47
47
  wsproto = "1.2.0"
48
48
  APScheduler = "3.9.1.post1"
@@ -54,8 +54,9 @@ pydantic = "^2.9.2"
54
54
  [tool.poetry.group.dev.dependencies]
55
55
  pytest = "^7.2.0"
56
56
  black = "24.3.0"
57
- pyright = "1.1.380"
57
+ pyright = "1.1.400"
58
58
  aiohttp = "^3.8.4"
59
+ pytest-cov = "^6.1.1"
59
60
 
60
61
  [tool.poetry.group.profiling.dependencies]
61
62
  scalene = "^1.5.19"
@@ -6,7 +6,7 @@ from pyview.live_socket import (
6
6
  UnconnectedSocket,
7
7
  )
8
8
  from pyview.pyview import PyView, defaultRootTemplate
9
- from pyview.js import js
9
+ from pyview.js import JsCommand
10
10
  from pyview.pyview import RootTemplateContext, RootTemplate
11
11
 
12
12
  __all__ = [
@@ -14,7 +14,7 @@ __all__ = [
14
14
  "LiveViewSocket",
15
15
  "PyView",
16
16
  "defaultRootTemplate",
17
- "js",
17
+ "JsCommand",
18
18
  "RootTemplateContext",
19
19
  "RootTemplate",
20
20
  "is_connected",
@@ -0,0 +1,38 @@
1
+ from typing import Callable
2
+ import logging
3
+
4
+
5
+ def event(*event_names):
6
+ """Decorator that marks methods as event handlers."""
7
+
8
+ def decorator(func):
9
+ func._event_names = event_names
10
+ return func
11
+
12
+ return decorator
13
+
14
+
15
+ class BaseEventHandler:
16
+ """Base class for event handlers to handle dispatching events."""
17
+
18
+ _event_handlers: dict[str, Callable] = {}
19
+
20
+ def __init_subclass__(cls, **kwargs):
21
+ super().__init_subclass__(**kwargs)
22
+
23
+ # Find all decorated methods and register them
24
+ cls._event_handlers = {}
25
+ for attr_name in dir(cls):
26
+ if not attr_name.startswith("_"):
27
+ attr = getattr(cls, attr_name)
28
+ if hasattr(attr, "_event_names"):
29
+ for event_name in attr._event_names:
30
+ cls._event_handlers[event_name] = attr
31
+
32
+ async def handle_event(self, event: str, payload: dict, socket):
33
+ handler = self._event_handlers.get(event)
34
+
35
+ if handler:
36
+ return await handler(self, event, payload, socket)
37
+ else:
38
+ logging.warning(f"Unhandled event: {event} {payload}")
@@ -0,0 +1,4 @@
1
+ from .BaseEventHandler import BaseEventHandler, event
2
+ from .info_event import InfoEvent
3
+
4
+ __all__ = ["BaseEventHandler", "event", "InfoEvent"]
@@ -0,0 +1,120 @@
1
+ import json
2
+ from typing import Any, Optional
3
+ from pyview.vendor.ibis import filters
4
+ from pyview.template.context_processor import context_processor
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @context_processor
9
+ def add_js(meta):
10
+ return {"js": JsCommands([])}
11
+
12
+
13
+ @filters.register("js.add_class")
14
+ def js_add_class(js: "JsCommands", selector: str, *classes):
15
+ return js.add_class(selector, *classes)
16
+
17
+
18
+ @filters.register("js.remove_class")
19
+ def js_remove_class(js: "JsCommands", selector: str, *classes):
20
+ return js.remove_class(selector, *classes)
21
+
22
+
23
+ @filters.register("js.show")
24
+ def js_show(js: "JsCommands", selector: str):
25
+ return js.show(selector)
26
+
27
+
28
+ @filters.register("js.hide")
29
+ def js_hide(js: "JsCommands", selector: str):
30
+ return js.hide(selector)
31
+
32
+
33
+ @filters.register("js.toggle")
34
+ def js_toggle(js: "JsCommands", selector: str):
35
+ return js.toggle(selector)
36
+
37
+
38
+ @filters.register("js.dispatch")
39
+ def js_dispatch(js: "JsCommands", event: str, selector: str):
40
+ return js.dispatch(event, selector)
41
+
42
+
43
+ @filters.register("js.push")
44
+ def js_push(js: "JsCommands", event: str, payload: Optional[dict[str, Any]] = None):
45
+ return js.push(event, payload)
46
+
47
+
48
+ @filters.register("js.focus")
49
+ def js_focus(js: "JsCommands", selector: str):
50
+ return js.focus(selector)
51
+
52
+
53
+ @filters.register("js.focus_first")
54
+ def js_focus_first(js: "JsCommands", selector: str):
55
+ return js.focus_first(selector)
56
+
57
+
58
+ @filters.register("js.transition")
59
+ def js_transition(js: "JsCommands", selector: str, transition: str, time: int = 200):
60
+ return js.transition(selector, transition, time)
61
+
62
+
63
+ @dataclass
64
+ class JsCommand:
65
+ cmd: str
66
+ opts: dict[str, Any]
67
+
68
+
69
+ @dataclass
70
+ class JsCommands:
71
+ commands: list[JsCommand]
72
+
73
+ def add(self, cmd: JsCommand) -> "JsCommands":
74
+ return JsCommands(self.commands + [cmd])
75
+
76
+ def show(self, selector: str) -> "JsCommands":
77
+ return self.add(JsCommand("show", {"to": selector}))
78
+
79
+ def hide(self, selector: str) -> "JsCommands":
80
+ return self.add(JsCommand("hide", {"to": selector}))
81
+
82
+ def toggle(self, selector: str) -> "JsCommands":
83
+ return self.add(JsCommand("toggle", {"to": selector}))
84
+
85
+ def add_class(self, selector: str, *classes: str) -> "JsCommands":
86
+ return self.add(JsCommand("add_class", {"to": selector, "names": classes}))
87
+
88
+ def remove_class(self, selector: str, *classes: str) -> "JsCommands":
89
+ return self.add(JsCommand("remove_class", {"to": selector, "names": classes}))
90
+
91
+ def dispatch(self, event: str, selector: str) -> "JsCommands":
92
+ return self.add(JsCommand("dispatch", {"to": selector, "event": event}))
93
+
94
+ def push(
95
+ self, event: str, payload: Optional[dict[str, Any]] = None
96
+ ) -> "JsCommands":
97
+ return self.add(
98
+ JsCommand(
99
+ "push", {"event": event} | ({"value": payload} if payload else {})
100
+ )
101
+ )
102
+
103
+ def focus(self, selector: str) -> "JsCommands":
104
+ return self.add(JsCommand("focus", {"to": selector}))
105
+
106
+ def focus_first(self, selector: str) -> "JsCommands":
107
+ return self.add(JsCommand("focus_first", {"to": selector}))
108
+
109
+ def transition(
110
+ self, selector: str, transition: str, time: int = 200
111
+ ) -> "JsCommands":
112
+ return self.add(
113
+ JsCommand(
114
+ "transition",
115
+ {"to": selector, "time": time, "transition": [[transition], [], []]},
116
+ )
117
+ )
118
+
119
+ def __str__(self):
120
+ return json.dumps([(c.cmd, c.opts) for c in self.commands])
@@ -0,0 +1,47 @@
1
+ from pyview.live_view import LiveView
2
+ from typing import Callable, Any
3
+ from starlette.routing import compile_path
4
+
5
+
6
+ class LiveViewLookup:
7
+ def __init__(self):
8
+ self.routes = [] # [(path_format, path_regex, param_convertors, lv)]
9
+
10
+ def add(self, path: str, lv: Callable[[], LiveView]):
11
+ path_regex, path_format, param_convertors = compile_path(path)
12
+ self.routes.append((path_format, path_regex, param_convertors, lv))
13
+
14
+ def get(self, path: str) -> tuple[LiveView, dict[str, Any]]:
15
+ # Find all matching routes
16
+ matches = []
17
+
18
+ for path_format, path_regex, param_convertors, lv in self.routes:
19
+ match_obj = path_regex.match(path)
20
+ if match_obj is not None:
21
+ params = match_obj.groupdict()
22
+
23
+ # Convert path params using Starlette's convertors
24
+ for param_name, convertor in param_convertors.items():
25
+ if param_name in params:
26
+ params[param_name] = convertor.convert(params[param_name])
27
+
28
+ # Store the match with its priority information
29
+ has_params = bool(param_convertors)
30
+ matches.append((lv, params, has_params))
31
+
32
+ # Sort matches by priority: static routes (has_params=False) come first
33
+ matches.sort(key=lambda x: x[2]) # Sort by has_params (False comes before True)
34
+
35
+ if matches:
36
+ lv, params, _ = matches[0]
37
+ return lv(), params
38
+
39
+ # Check for trailing slash
40
+ if path.endswith("/"):
41
+ try:
42
+ return self.get(path[:-1])
43
+ except ValueError:
44
+ pass
45
+
46
+ # No matches found
47
+ raise ValueError(f"No LiveView found for path: {path}")
@@ -16,6 +16,7 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler
16
16
  from pyview.vendor.flet.pubsub import PubSubHub, PubSub
17
17
  from pyview.events import InfoEvent
18
18
  from pyview.uploads import UploadConstraints, UploadConfig, UploadManager
19
+ from pyview.meta import PyViewMeta
19
20
  import datetime
20
21
 
21
22
 
@@ -62,6 +63,10 @@ class ConnectedLiveViewSocket(Generic[T]):
62
63
  self.pending_events = []
63
64
  self.upload_manager = UploadManager()
64
65
 
66
+ @property
67
+ def meta(self) -> PyViewMeta:
68
+ return PyViewMeta()
69
+
65
70
  async def subscribe(self, topic: str):
66
71
  await self.pub_sub.subscribe_topic_async(topic, self._topic_callback_internal)
67
72
 
@@ -94,7 +99,7 @@ class ConnectedLiveViewSocket(Generic[T]):
94
99
 
95
100
  async def send_info(self, event: InfoEvent):
96
101
  await self.liveview.handle_info(event, self)
97
- r = await self.liveview.render(self.context)
102
+ r = await self.liveview.render(self.context, self.meta)
98
103
  resp = [None, None, self.topic, "diff", self.diff(r.tree())]
99
104
 
100
105
  try:
@@ -8,6 +8,7 @@ from pyview.template import (
8
8
  find_associated_file,
9
9
  )
10
10
  from pyview.events import InfoEvent
11
+ from pyview.meta import PyViewMeta
11
12
  from urllib.parse import ParseResult
12
13
 
13
14
  T = TypeVar("T")
@@ -37,11 +38,11 @@ class LiveView(Generic[T]):
37
38
  async def disconnect(self, socket: ConnectedLiveViewSocket[T]):
38
39
  pass
39
40
 
40
- async def render(self, assigns: T) -> RenderedContent:
41
+ async def render(self, assigns: T, meta: PyViewMeta) -> RenderedContent:
41
42
  html_render = _find_render(self)
42
43
 
43
44
  if html_render:
44
- return LiveRender(html_render, assigns)
45
+ return LiveRender(html_render, assigns, meta)
45
46
 
46
47
  raise NotImplementedError()
47
48
 
@@ -0,0 +1,6 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class PyViewMeta:
6
+ pass
@@ -1,8 +1,7 @@
1
1
  from starlette.applications import Starlette
2
- from starlette.websockets import WebSocket
3
2
  from starlette.responses import HTMLResponse
4
3
  from starlette.middleware.gzip import GZipMiddleware
5
- from starlette.routing import Route
4
+ from starlette.routing import Route, WebSocketRoute
6
5
  from starlette.requests import Request
7
6
  import uuid
8
7
  from urllib.parse import parse_qs, urlparse
@@ -11,6 +10,7 @@ from pyview.live_socket import UnconnectedSocket
11
10
  from pyview.csrf import generate_csrf_token
12
11
  from pyview.session import serialize_session
13
12
  from pyview.auth import AuthProviderFactory
13
+ from pyview.meta import PyViewMeta
14
14
  from .ws_handler import LiveSocketHandler
15
15
  from .live_view import LiveView
16
16
  from .live_routes import LiveViewLookup
@@ -31,10 +31,7 @@ class PyView(Starlette):
31
31
  self.view_lookup = LiveViewLookup()
32
32
  self.live_handler = LiveSocketHandler(self.view_lookup)
33
33
 
34
- async def live_websocket_endpoint(websocket: WebSocket):
35
- await self.live_handler.handle(websocket)
36
-
37
- self.add_websocket_route("/live/websocket", live_websocket_endpoint)
34
+ self.routes.append(WebSocketRoute("/live/websocket", self.live_handler.handle))
38
35
  self.add_middleware(GZipMiddleware)
39
36
 
40
37
  def add_live_view(self, path: str, view: type[LiveView]):
@@ -53,14 +50,24 @@ async def liveview_container(
53
50
  ):
54
51
  url = request.url
55
52
  path = url.path
56
- lv: LiveView = view_lookup.get(path)
53
+ lv, path_params = view_lookup.get(path)
57
54
  s = UnconnectedSocket()
58
55
 
59
56
  session = request.session if "session" in request.scope else {}
60
57
 
61
58
  await lv.mount(s, session)
62
- await lv.handle_params(urlparse(url._url), parse_qs(url.query), s)
63
- r = await lv.render(s.context)
59
+
60
+ # Parse query parameters
61
+ query_params = parse_qs(url.query)
62
+
63
+ # Merge path parameters with query parameters
64
+ # Path parameters take precedence in case of conflict
65
+ merged_params = {**query_params, **path_params}
66
+
67
+ # Pass merged parameters to handle_params
68
+ await lv.handle_params(urlparse(url._url), merged_params, s)
69
+
70
+ r = await lv.render(s.context, PyViewMeta())
64
71
 
65
72
  liveview_css = find_associated_css(lv)
66
73
 
@@ -2,6 +2,7 @@ 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
4
  from .utils import find_associated_css, find_associated_file
5
+ from .context_processor import context_processor
5
6
 
6
7
  __all__ = [
7
8
  "Template",
@@ -14,4 +15,5 @@ __all__ = [
14
15
  "defaultRootTemplate",
15
16
  "find_associated_css",
16
17
  "find_associated_file",
18
+ "context_processor",
17
19
  ]
@@ -0,0 +1,17 @@
1
+ from pyview.meta import PyViewMeta
2
+
3
+ context_processors = []
4
+
5
+
6
+ def context_processor(func):
7
+ context_processors.append(func)
8
+ return func
9
+
10
+
11
+ def apply_context_processors(meta: PyViewMeta):
12
+ context = {}
13
+
14
+ for processor in context_processors:
15
+ context.update(processor(meta))
16
+
17
+ return context
@@ -3,6 +3,8 @@ from typing import Any, Union, Protocol, Optional, ClassVar
3
3
  from dataclasses import asdict, Field
4
4
  from .serializer import serialize
5
5
  import os.path
6
+ from pyview.template.context_processor import apply_context_processors
7
+ from pyview.meta import PyViewMeta
6
8
 
7
9
 
8
10
  class DataclassInstance(Protocol):
@@ -23,18 +25,20 @@ class LiveTemplate:
23
25
  def __init__(self, template: Template):
24
26
  self.t = template
25
27
 
26
- def tree(self, assigns: Assigns) -> dict[str, Any]:
28
+ def tree(self, assigns: Assigns, meta: PyViewMeta) -> dict[str, Any]:
27
29
  if not isinstance(assigns, dict):
28
30
  assigns = serialize(assigns)
29
- return self.t.tree(assigns)
31
+ additional_context = apply_context_processors(meta)
32
+ return self.t.tree(additional_context | assigns)
30
33
 
31
- def render(self, assigns: Assigns) -> str:
34
+ def render(self, assigns: Assigns, meta: PyViewMeta) -> str:
32
35
  if not isinstance(assigns, dict):
33
36
  assigns = asdict(assigns)
34
- return self.t.render(assigns)
37
+ additional_context = apply_context_processors(meta)
38
+ return self.t.render(additional_context | assigns)
35
39
 
36
- def text(self, assigns: Assigns) -> str:
37
- return self.render(assigns)
40
+ def text(self, assigns: Assigns, meta: PyViewMeta) -> str:
41
+ return self.render(assigns, meta)
38
42
 
39
43
  def debug(self) -> str:
40
44
  return self.t.root_node.to_str()
@@ -47,15 +51,16 @@ class RenderedContent(Protocol):
47
51
 
48
52
 
49
53
  class LiveRender:
50
- def __init__(self, template: LiveTemplate, assigns: Any):
54
+ def __init__(self, template: LiveTemplate, assigns: Any, meta: PyViewMeta):
51
55
  self.template = template
52
56
  self.assigns = assigns
57
+ self.meta = meta
53
58
 
54
59
  def tree(self) -> dict[str, Any]:
55
- return self.template.tree(self.assigns)
60
+ return self.template.tree(self.assigns, self.meta)
56
61
 
57
62
  def text(self) -> str:
58
- return self.template.text(self.assigns)
63
+ return self.template.text(self.assigns, self.meta)
59
64
 
60
65
 
61
66
  _cache = {}
@@ -73,6 +78,6 @@ def template_file(filename: str) -> Optional[LiveTemplate]:
73
78
  return cached_template
74
79
 
75
80
  with open(filename, "r") as f:
76
- t = LiveTemplate(Template(f.read()))
81
+ t = LiveTemplate(Template(f.read(), template_id=filename))
77
82
  _cache[filename] = (mtime, t)
78
83
  return t
@@ -10,6 +10,7 @@ class Template:
10
10
  def __init__(self, template_string, template_id="UNIDENTIFIED"):
11
11
  self.root_node = ibis.compiler.compile(template_string, template_id)
12
12
  self.blocks = self._register_blocks(self.root_node, {})
13
+ self.template_id = template_id
13
14
 
14
15
  def __str__(self):
15
16
  return str(self.root_node)
@@ -42,7 +42,7 @@ class LiveSocketHandler:
42
42
  self.myJoinId = topic
43
43
 
44
44
  url = urlparse(payload["url"])
45
- lv = self.routes.get(url.path)
45
+ lv, path_params = self.routes.get(url.path)
46
46
  await self.check_auth(websocket, lv)
47
47
  socket = ConnectedLiveViewSocket(websocket, topic, lv)
48
48
 
@@ -51,7 +51,13 @@ class LiveSocketHandler:
51
51
  session = deserialize_session(payload["session"])
52
52
 
53
53
  await lv.mount(socket, session)
54
- await lv.handle_params(url, parse_qs(url.query), socket)
54
+
55
+ # Parse query parameters and merge with path parameters
56
+ query_params = parse_qs(url.query)
57
+ merged_params = {**query_params, **path_params}
58
+
59
+ # Pass merged parameters to handle_params
60
+ await lv.handle_params(url, merged_params, socket)
55
61
 
56
62
  rendered = await _render(socket)
57
63
 
@@ -129,7 +135,20 @@ class LiveSocketHandler:
129
135
  lv = socket.liveview
130
136
  url = urlparse(payload["url"])
131
137
 
132
- await lv.handle_params(url, parse_qs(url.query), socket)
138
+ # Extract and merge parameters
139
+ query_params = parse_qs(url.query)
140
+ path_params = {}
141
+
142
+ # We need to get path params for the new URL
143
+ try:
144
+ # TODO: I don't think this is actually going to work...
145
+ _, path_params = self.routes.get(url.path)
146
+ except ValueError:
147
+ pass # Handle case where the path doesn't match any route
148
+
149
+ merged_params = {**query_params, **path_params}
150
+
151
+ await lv.handle_params(url, merged_params, socket)
133
152
  rendered = await _render(socket)
134
153
  diff = calc_diff(prev_rendered, rendered)
135
154
  prev_rendered = rendered
@@ -236,7 +255,7 @@ class LiveSocketHandler:
236
255
 
237
256
 
238
257
  async def _render(socket: ConnectedLiveViewSocket):
239
- rendered = (await socket.liveview.render(socket.context)).tree()
258
+ rendered = (await socket.liveview.render(socket.context, socket.meta)).tree()
240
259
 
241
260
  if socket.live_title:
242
261
  rendered["t"] = socket.live_title
@@ -1,34 +0,0 @@
1
- import json
2
- from typing import Union
3
- from pyview.vendor.ibis import filters
4
-
5
-
6
- JsArgs = Union[tuple[str, str], tuple[str, str, list[str]]]
7
-
8
-
9
- @filters.register
10
- def js(args: JsArgs):
11
- if len(args) > 2:
12
- cmd, id, names = args # type: ignore
13
- return Js(cmd, id, names)
14
- cmd, id = args # type: ignore
15
- return Js(cmd, id)
16
-
17
-
18
- class Js:
19
- def __init__(self, cmd: str, id: str, names: list[str] = []):
20
- self.cmd = cmd
21
- self.id = id
22
- self.names = names
23
-
24
- def __str__(self):
25
- opts = {
26
- "to": self.id,
27
- "time": 200,
28
- "transition": [[], [], []],
29
- }
30
-
31
- if len(self.names) > 0:
32
- opts["names"] = self.names
33
-
34
- return json.dumps([[self.cmd, opts]])
@@ -1,20 +0,0 @@
1
- from pyview.live_view import LiveView
2
- from typing import Callable
3
-
4
-
5
- class LiveViewLookup:
6
- def __init__(self):
7
- self.routes = {}
8
-
9
- def add(self, path: str, lv: Callable[[], LiveView]):
10
- self.routes[path] = lv
11
-
12
- def get(self, path: str) -> LiveView:
13
- lv = self.routes.get(path)
14
- if not lv and path.endswith("/"):
15
- lv = self.routes[path[:-1]]
16
-
17
- if not lv:
18
- raise ValueError("No LiveView found for path: " + path)
19
-
20
- return lv()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes