pyview-web 0.1.0__py3-none-any.whl → 0.2.1__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/__init__.py +2 -2
- pyview/events/BaseEventHandler.py +38 -0
- pyview/events/__init__.py +4 -0
- pyview/js.py +109 -23
- pyview/live_routes.py +37 -10
- pyview/live_socket.py +6 -1
- pyview/live_view.py +3 -2
- pyview/meta.py +6 -0
- pyview/pyview.py +16 -9
- pyview/template/__init__.py +2 -0
- pyview/template/context_processor.py +17 -0
- pyview/template/live_template.py +15 -10
- pyview/vendor/ibis/template.py +1 -0
- pyview/ws_handler.py +23 -4
- {pyview_web-0.1.0.dist-info → pyview_web-0.2.1.dist-info}/METADATA +4 -4
- {pyview_web-0.1.0.dist-info → pyview_web-0.2.1.dist-info}/RECORD +19 -15
- {pyview_web-0.1.0.dist-info → pyview_web-0.2.1.dist-info}/WHEEL +1 -1
- /pyview/{events.py → events/info_event.py} +0 -0
- {pyview_web-0.1.0.dist-info → pyview_web-0.2.1.dist-info}/LICENSE +0 -0
pyview/__init__.py
CHANGED
|
@@ -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
|
|
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
|
-
"
|
|
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}")
|
pyview/js.py
CHANGED
|
@@ -1,34 +1,120 @@
|
|
|
1
1
|
import json
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import Any, Optional
|
|
3
3
|
from pyview.vendor.ibis import filters
|
|
4
|
+
from pyview.template.context_processor import context_processor
|
|
5
|
+
from dataclasses import dataclass
|
|
4
6
|
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
@context_processor
|
|
9
|
+
def add_js(meta):
|
|
10
|
+
return {"js": JsCommands([])}
|
|
7
11
|
|
|
8
12
|
|
|
9
|
-
@filters.register
|
|
10
|
-
def js
|
|
11
|
-
|
|
12
|
-
cmd, id, names = args # type: ignore
|
|
13
|
-
return Js(cmd, id, names)
|
|
14
|
-
cmd, id = args # type: ignore
|
|
15
|
-
return Js(cmd, id)
|
|
13
|
+
@filters.register("js.add_class")
|
|
14
|
+
def js_add_class(js: "JsCommands", selector: str, *classes):
|
|
15
|
+
return js.add_class(selector, *classes)
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
self.id = id
|
|
22
|
-
self.names = names
|
|
18
|
+
@filters.register("js.remove_class")
|
|
19
|
+
def js_remove_class(js: "JsCommands", selector: str, *classes):
|
|
20
|
+
return js.remove_class(selector, *classes)
|
|
23
21
|
|
|
24
|
-
def __str__(self):
|
|
25
|
-
opts = {
|
|
26
|
-
"to": self.id,
|
|
27
|
-
"time": 200,
|
|
28
|
-
"transition": [[], [], []],
|
|
29
|
-
}
|
|
30
22
|
|
|
31
|
-
|
|
32
|
-
|
|
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
|
+
|
|
33
62
|
|
|
34
|
-
|
|
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])
|
pyview/live_routes.py
CHANGED
|
@@ -1,20 +1,47 @@
|
|
|
1
1
|
from pyview.live_view import LiveView
|
|
2
|
-
from typing import Callable
|
|
2
|
+
from typing import Callable, Any
|
|
3
|
+
from starlette.routing import compile_path
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
class LiveViewLookup:
|
|
6
7
|
def __init__(self):
|
|
7
|
-
self.routes =
|
|
8
|
+
self.routes = [] # [(path_format, path_regex, param_convertors, lv)]
|
|
8
9
|
|
|
9
10
|
def add(self, path: str, lv: Callable[[], LiveView]):
|
|
10
|
-
|
|
11
|
+
path_regex, path_format, param_convertors = compile_path(path)
|
|
12
|
+
self.routes.append((path_format, path_regex, param_convertors, lv))
|
|
11
13
|
|
|
12
|
-
def get(self, path: str) -> LiveView:
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
lv = self.routes[path[:-1]]
|
|
14
|
+
def get(self, path: str) -> tuple[LiveView, dict[str, Any]]:
|
|
15
|
+
# Find all matching routes
|
|
16
|
+
matches = []
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
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()
|
|
19
22
|
|
|
20
|
-
|
|
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}")
|
pyview/live_socket.py
CHANGED
|
@@ -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:
|
pyview/live_view.py
CHANGED
|
@@ -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
|
|
pyview/meta.py
ADDED
pyview/pyview.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
63
|
-
|
|
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
|
|
pyview/template/__init__.py
CHANGED
|
@@ -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
|
pyview/template/live_template.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
pyview/vendor/ibis/template.py
CHANGED
|
@@ -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)
|
pyview/ws_handler.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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,8 +1,7 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: pyview-web
|
|
3
|
-
Version: 0.1
|
|
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
|
+
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
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
pyview/__init__.py,sha256=
|
|
1
|
+
pyview/__init__.py,sha256=5RJ_KtwJvI_-_Vhb3-py5Qf78YdH1HHvAzZO1ddzzrU,518
|
|
2
2
|
pyview/assets/js/app.js,sha256=XuuSgEMY4hx8v0OuEPwaa7trktu_vppL0tc3Bs9Fw7s,2524
|
|
3
3
|
pyview/assets/package-lock.json,sha256=kFCrEUJc3G7VD7EsBQf6__EKQhaKAok-I5rrwiAoX0w,2425
|
|
4
4
|
pyview/assets/package.json,sha256=E6xaX8KMUAektIIedLmI55jGnmlNMSeD2tgKYXWk1vg,151
|
|
@@ -8,18 +8,22 @@ pyview/auth/required.py,sha256=ZtNmLFth9nK39RxDiJkSzArXwS5Cvr55MUAzfJ1F2e0,1418
|
|
|
8
8
|
pyview/changesets/__init__.py,sha256=55CLari2JHZtwy4hapHe7CqUyKjcP4dkM_t5d3CY2gU,46
|
|
9
9
|
pyview/changesets/changesets.py,sha256=hImmvB_jS6RyLr5Mas5L7DO_0d805jR3c41LKJlnNL4,1720
|
|
10
10
|
pyview/csrf.py,sha256=VIURva9EJqXXYGC7engweh3SwDQCnHlhV2zWdcdnFqc,789
|
|
11
|
-
pyview/events.py,sha256=
|
|
12
|
-
pyview/
|
|
13
|
-
pyview/
|
|
14
|
-
pyview/
|
|
15
|
-
pyview/
|
|
11
|
+
pyview/events/BaseEventHandler.py,sha256=RIv1vYn-sNIAfPNAaRg5iJvM--0ZlQHt3X7av_GR0sw,1138
|
|
12
|
+
pyview/events/__init__.py,sha256=5qQhZUjBwbnL9SrQ9FWHNgrEgDETCI3ysMREx7Tab4E,142
|
|
13
|
+
pyview/events/info_event.py,sha256=Zv8G2F1XeXUk1wrnfomeFfxB0OPYmHdjSvxRjQew3No,125
|
|
14
|
+
pyview/js.py,sha256=E6HMsUfXQjrcLqYq26ieeYuzTjBeZqfJwwOm3uSR4ME,3498
|
|
15
|
+
pyview/live_routes.py,sha256=IN2Jmy8b1umcfx1R7ZgFXHZNbYDJp_kLIbADtDJknPM,1749
|
|
16
|
+
pyview/live_socket.py,sha256=D8fg7UQSPCVEboxohbDAZuK7J1xBEEoDycVFUuDfZxI,4767
|
|
17
|
+
pyview/live_view.py,sha256=mwAp7jiABSZCBgYF-GLQCB7zcJ7Wpz9cuC84zjzsp2U,1455
|
|
18
|
+
pyview/meta.py,sha256=01Z-qldB9jrewmIJHQpUqyIhuHodQGgCvpuY9YM5R6c,74
|
|
16
19
|
pyview/phx_message.py,sha256=DUdPfl6tlw9K0FNXJ35ehq03JGgynvwA_JItHQ_dxMQ,2007
|
|
17
|
-
pyview/pyview.py,sha256=
|
|
20
|
+
pyview/pyview.py,sha256=UuAeHdmrcmu3q681NR8IVQ1-LcMBnWyT1vIuQdrLlhY,2666
|
|
18
21
|
pyview/secret.py,sha256=HbaNpGAkFs4uxMVAmk9HwE3FIehg7dmwEOlED7C9moM,363
|
|
19
22
|
pyview/session.py,sha256=nC8ExyVwfCgQfx9T-aJGyFhr2C7jsrEY_QFkaXtP28U,432
|
|
20
23
|
pyview/static/assets/app.js,sha256=QoXfdcOCYwVYJftvjsIIVwFye7onaOJMxRpalyYqoMU,200029
|
|
21
|
-
pyview/template/__init__.py,sha256=
|
|
22
|
-
pyview/template/
|
|
24
|
+
pyview/template/__init__.py,sha256=0goMpA8-TCKcwHbhjvAgbPYnY929vBrwjc701t9RIQw,583
|
|
25
|
+
pyview/template/context_processor.py,sha256=y07t7mhL7XjZNbwHnTTyXJvYhXabtuTukDScycAFjVc,312
|
|
26
|
+
pyview/template/live_template.py,sha256=m8_1TCFGfpVkXyZOIWN6a3ksvsewPlo8vTzzPGDyEU0,2408
|
|
23
27
|
pyview/template/render_diff.py,sha256=v7EVmn8oJdh809N0vnSLK8OiDs1BOpErF36y4VUo9ew,1214
|
|
24
28
|
pyview/template/root_template.py,sha256=zCUs1bt8R7qynhBE0tTSEYfdkGtbeKNmPhwzRiFNdsI,2031
|
|
25
29
|
pyview/template/serializer.py,sha256=WDZfqJr2LMlf36fUW2CmWc2aREc63553_y_GRP2-qYc,826
|
|
@@ -36,11 +40,11 @@ pyview/vendor/ibis/errors.py,sha256=gtRX3LjkdWEP4NaX8HXL_6OU2fCX16IBBSiGMG5wiY4,
|
|
|
36
40
|
pyview/vendor/ibis/filters.py,sha256=M36KS6dlzfsb2NmHkbVuo8gJbiQ6aQjcHzXxHwZ3Afw,7042
|
|
37
41
|
pyview/vendor/ibis/loaders.py,sha256=NYW7_hlC7TRPDau37bgiOCsvsBgIPpUEpb1NbroUVUA,3457
|
|
38
42
|
pyview/vendor/ibis/nodes.py,sha256=TgFt4q5MrVW3gC3PVitrs2LyXKllRveooM7XKydNATk,25617
|
|
39
|
-
pyview/vendor/ibis/template.py,sha256=
|
|
43
|
+
pyview/vendor/ibis/template.py,sha256=6XJXnztw87CrOaKeW3e18LL0fNM8AI6AaK_QgMdb7ew,2353
|
|
40
44
|
pyview/vendor/ibis/tree.py,sha256=hg8f-fKHeo6DE8R-QgAhdvEaZ8rKyz7p0nGwPy0CBTs,2509
|
|
41
45
|
pyview/vendor/ibis/utils.py,sha256=nLSaxPR9vMphzV9qinlz_Iurv9c49Ps6Knv8vyNlewU,2768
|
|
42
|
-
pyview/ws_handler.py,sha256=
|
|
43
|
-
pyview_web-0.1.
|
|
44
|
-
pyview_web-0.1.
|
|
45
|
-
pyview_web-0.1.
|
|
46
|
-
pyview_web-0.1.
|
|
46
|
+
pyview/ws_handler.py,sha256=8iFKEse4TUaBzHF4xAcZiOqXaAgv4yB2CT0BKoW2Ny4,9319
|
|
47
|
+
pyview_web-0.2.1.dist-info/LICENSE,sha256=M_bADaBm9_MV9llX3lCicksLhwk3eZUjA2srE0uUWr0,1071
|
|
48
|
+
pyview_web-0.2.1.dist-info/METADATA,sha256=Od48u4VsUdsrfxHx0er1SGZNUgX4J5PBwI-9eIlQzm4,5256
|
|
49
|
+
pyview_web-0.2.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
50
|
+
pyview_web-0.2.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|