pyview-web 0.0.25__py3-none-any.whl → 0.2.0__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 CHANGED
@@ -1,7 +1,12 @@
1
1
  from pyview.live_view import LiveView
2
- from pyview.live_socket import LiveViewSocket
2
+ from pyview.live_socket import (
3
+ LiveViewSocket,
4
+ is_connected,
5
+ ConnectedLiveViewSocket,
6
+ UnconnectedSocket,
7
+ )
3
8
  from pyview.pyview import PyView, defaultRootTemplate
4
- from pyview.js import js
9
+ from pyview.js import JsCommand
5
10
  from pyview.pyview import RootTemplateContext, RootTemplate
6
11
 
7
12
  __all__ = [
@@ -9,7 +14,9 @@ __all__ = [
9
14
  "LiveViewSocket",
10
15
  "PyView",
11
16
  "defaultRootTemplate",
12
- "js",
17
+ "JsCommand",
13
18
  "RootTemplateContext",
14
19
  "RootTemplate",
20
+ "is_connected",
21
+ "ConnectedLiveViewSocket",
15
22
  ]
@@ -1,4 +1,4 @@
1
- from typing import TypeVar, Any, Generic, Optional, Callable
1
+ from typing import TypeVar, Any, Generic, Optional
2
2
  from pydantic import BaseModel, ValidationError
3
3
  from dataclasses import dataclass
4
4
  from types import SimpleNamespace
@@ -8,7 +8,7 @@ Base = TypeVar("Base", bound=BaseModel)
8
8
 
9
9
  @dataclass
10
10
  class ChangeSet(Generic[Base]):
11
- cls: Callable[..., Base]
11
+ cls: type[Base]
12
12
  changes: dict[str, Any]
13
13
  errors: dict[str, Any]
14
14
  valid: bool
@@ -29,7 +29,7 @@ class ChangeSet(Generic[Base]):
29
29
 
30
30
  @property
31
31
  def fields(self) -> list[str]:
32
- return self.cls.__fields__.keys()
32
+ return list(self.cls.model_fields)
33
33
 
34
34
  def save(self, payload: dict[str, Any]) -> Optional[Base]:
35
35
  self.errors = {}
@@ -58,5 +58,5 @@ class ChangeSet(Generic[Base]):
58
58
  self.valid = False
59
59
 
60
60
 
61
- def change_set(cls: Callable[..., Base]) -> ChangeSet[Base]:
61
+ def change_set(cls: type[Base]) -> ChangeSet[Base]:
62
62
  return ChangeSet(cls, {}, {}, False)
pyview/js.py CHANGED
@@ -1,34 +1,120 @@
1
1
  import json
2
- from typing import Union
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
- JsArgs = Union[tuple[str, str], tuple[str, str, list[str]]]
8
+ @context_processor
9
+ def add_js(meta):
10
+ return {"js": JsCommands([])}
7
11
 
8
12
 
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)
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
- 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
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
- if len(self.names) > 0:
32
- opts["names"] = self.names
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
- return json.dumps([[self.cmd, opts]])
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
- self.routes[path] = lv
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
- lv = self.routes.get(path)
14
- if not lv and path.endswith("/"):
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
- if not lv:
18
- raise ValueError("No LiveView found for path: " + path)
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
- return lv()
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
@@ -1,12 +1,22 @@
1
1
  from __future__ import annotations
2
2
  from starlette.websockets import WebSocket
3
3
  import json
4
- from typing import Any, TypeVar, Generic, TYPE_CHECKING, Optional
4
+ from typing import (
5
+ Any,
6
+ TypeVar,
7
+ Generic,
8
+ TYPE_CHECKING,
9
+ Optional,
10
+ Union,
11
+ TypeAlias,
12
+ TypeGuard,
13
+ )
5
14
  from urllib.parse import urlencode
6
15
  from apscheduler.schedulers.asyncio import AsyncIOScheduler
7
16
  from pyview.vendor.flet.pubsub import PubSubHub, PubSub
8
17
  from pyview.events import InfoEvent
9
18
  from pyview.uploads import UploadConstraints, UploadConfig, UploadManager
19
+ from pyview.meta import PyViewMeta
10
20
  import datetime
11
21
 
12
22
 
@@ -21,10 +31,14 @@ pub_sub_hub = PubSubHub()
21
31
  T = TypeVar("T")
22
32
 
23
33
 
34
+ def is_connected(socket: LiveViewSocket[T]) -> TypeGuard["ConnectedLiveViewSocket[T]"]:
35
+ return socket.connected
36
+
37
+
24
38
  class UnconnectedSocket(Generic[T]):
25
39
  context: T
26
- connected: bool = False
27
40
  live_title: Optional[str] = None
41
+ connected: bool = False
28
42
 
29
43
  def allow_upload(
30
44
  self, upload_name: str, constraints: UploadConstraints
@@ -32,7 +46,7 @@ class UnconnectedSocket(Generic[T]):
32
46
  return UploadConfig(name=upload_name, constraints=constraints)
33
47
 
34
48
 
35
- class LiveViewSocket(Generic[T]):
49
+ class ConnectedLiveViewSocket(Generic[T]):
36
50
  context: T
37
51
  live_title: Optional[str] = None
38
52
  pending_events: list[tuple[str, Any]]
@@ -49,6 +63,10 @@ class LiveViewSocket(Generic[T]):
49
63
  self.pending_events = []
50
64
  self.upload_manager = UploadManager()
51
65
 
66
+ @property
67
+ def meta(self) -> PyViewMeta:
68
+ return PyViewMeta()
69
+
52
70
  async def subscribe(self, topic: str):
53
71
  await self.pub_sub.subscribe_topic_async(topic, self._topic_callback_internal)
54
72
 
@@ -81,12 +99,12 @@ class LiveViewSocket(Generic[T]):
81
99
 
82
100
  async def send_info(self, event: InfoEvent):
83
101
  await self.liveview.handle_info(event, self)
84
- r = await self.liveview.render(self.context)
102
+ r = await self.liveview.render(self.context, self.meta)
85
103
  resp = [None, None, self.topic, "diff", self.diff(r.tree())]
86
104
 
87
105
  try:
88
106
  await self.websocket.send_text(json.dumps(resp))
89
- except Exception as e:
107
+ except Exception:
90
108
  for id in self.scheduled_jobs:
91
109
  print("Removing job", id)
92
110
  scheduler.remove_job(id)
@@ -143,3 +161,6 @@ class LiveViewSocket(Generic[T]):
143
161
  await self.liveview.disconnect(self)
144
162
  except Exception:
145
163
  pass
164
+
165
+
166
+ LiveViewSocket: TypeAlias = Union[ConnectedLiveViewSocket[T], UnconnectedSocket[T]]
pyview/live_view.py CHANGED
@@ -1,5 +1,5 @@
1
1
  from typing import TypeVar, Generic, Optional, Union, Any
2
- from .live_socket import LiveViewSocket, UnconnectedSocket
2
+ from .live_socket import LiveViewSocket, ConnectedLiveViewSocket
3
3
  from pyview.template import (
4
4
  LiveTemplate,
5
5
  template_file,
@@ -8,11 +8,11 @@ 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")
14
15
 
15
- AnySocket = Union[LiveViewSocket[T], UnconnectedSocket[T]]
16
16
  Session = dict[str, Any]
17
17
 
18
18
  # TODO: ideally this would always be a ParseResult, but we need to update push_patch
@@ -23,26 +23,26 @@ class LiveView(Generic[T]):
23
23
  def __init__(self):
24
24
  pass
25
25
 
26
- async def mount(self, socket: AnySocket, session: Session):
26
+ async def mount(self, socket: LiveViewSocket[T], session: Session):
27
27
  pass
28
28
 
29
- async def handle_event(self, event, payload, socket: LiveViewSocket[T]):
29
+ async def handle_event(self, event, payload, socket: ConnectedLiveViewSocket[T]):
30
30
  pass
31
31
 
32
- async def handle_info(self, event: InfoEvent, socket: LiveViewSocket[T]):
32
+ async def handle_info(self, event: InfoEvent, socket: ConnectedLiveViewSocket[T]):
33
33
  pass
34
34
 
35
- async def handle_params(self, url: URL, params, socket: AnySocket):
35
+ async def handle_params(self, url: URL, params, socket: LiveViewSocket[T]):
36
36
  pass
37
37
 
38
- async def disconnect(self, socket: LiveViewSocket[T]):
38
+ async def disconnect(self, socket: ConnectedLiveViewSocket[T]):
39
39
  pass
40
40
 
41
- async def render(self, assigns: T) -> RenderedContent:
41
+ async def render(self, assigns: T, meta: PyViewMeta) -> RenderedContent:
42
42
  html_render = _find_render(self)
43
43
 
44
44
  if html_render:
45
- return LiveRender(html_render, assigns)
45
+ return LiveRender(html_render, assigns, meta)
46
46
 
47
47
  raise NotImplementedError()
48
48
 
pyview/meta.py ADDED
@@ -0,0 +1,6 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class PyViewMeta:
6
+ pass
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
- 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
@@ -1,16 +1,22 @@
1
1
  from pyview.vendor.ibis import Template
2
- from typing import Any, Union, Protocol, Optional
3
- from dataclasses import asdict
2
+ from typing import Any, Union, Protocol, Optional, ClassVar
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
- class DictConvertable(Protocol):
9
- def asdict(self) -> dict[str, Any]:
10
- ...
10
+ class DataclassInstance(Protocol):
11
+ __dataclass_fields__: ClassVar[dict[str, Field[Any]]]
12
+
11
13
 
14
+ Assigns = Union[dict[str, Any], DataclassInstance]
12
15
 
13
- Assigns = Union[dict[str, Any], DictConvertable]
16
+
17
+ # TODO: should we still support this?
18
+ class DictConvertable(Protocol):
19
+ def asdict(self) -> dict[str, Any]: ...
14
20
 
15
21
 
16
22
  class LiveTemplate:
@@ -19,45 +25,47 @@ class LiveTemplate:
19
25
  def __init__(self, template: Template):
20
26
  self.t = template
21
27
 
22
- def tree(self, assigns: Assigns) -> dict[str, Any]:
28
+ def tree(self, assigns: Assigns, meta: PyViewMeta) -> dict[str, Any]:
23
29
  if not isinstance(assigns, dict):
24
30
  assigns = serialize(assigns)
25
- return self.t.tree(assigns)
31
+ additional_context = apply_context_processors(meta)
32
+ return self.t.tree(additional_context | assigns)
26
33
 
27
- def render(self, assigns: Assigns) -> str:
34
+ def render(self, assigns: Assigns, meta: PyViewMeta) -> str:
28
35
  if not isinstance(assigns, dict):
29
36
  assigns = asdict(assigns)
30
- return self.t.render(assigns)
37
+ additional_context = apply_context_processors(meta)
38
+ return self.t.render(additional_context | assigns)
31
39
 
32
- def text(self, assigns: Assigns) -> str:
33
- return self.render(assigns)
40
+ def text(self, assigns: Assigns, meta: PyViewMeta) -> str:
41
+ return self.render(assigns, meta)
34
42
 
35
43
  def debug(self) -> str:
36
44
  return self.t.root_node.to_str()
37
45
 
38
46
 
39
47
  class RenderedContent(Protocol):
40
- def tree(self) -> dict[str, Any]:
41
- ...
48
+ def tree(self) -> dict[str, Any]: ...
42
49
 
43
- def text(self) -> str:
44
- ...
50
+ def text(self) -> str: ...
45
51
 
46
52
 
47
53
  class LiveRender:
48
- def __init__(self, template: LiveTemplate, assigns: Any):
54
+ def __init__(self, template: LiveTemplate, assigns: Any, meta: PyViewMeta):
49
55
  self.template = template
50
56
  self.assigns = assigns
57
+ self.meta = meta
51
58
 
52
59
  def tree(self) -> dict[str, Any]:
53
- return self.template.tree(self.assigns)
60
+ return self.template.tree(self.assigns, self.meta)
54
61
 
55
62
  def text(self) -> str:
56
- return self.template.text(self.assigns)
63
+ return self.template.text(self.assigns, self.meta)
57
64
 
58
65
 
59
66
  _cache = {}
60
67
 
68
+
61
69
  def template_file(filename: str) -> Optional[LiveTemplate]:
62
70
  """Renders a template file with the given assigns."""
63
71
  if not os.path.isfile(filename):
@@ -70,6 +78,6 @@ def template_file(filename: str) -> Optional[LiveTemplate]:
70
78
  return cached_template
71
79
 
72
80
  with open(filename, "r") as f:
73
- t = LiveTemplate(Template(f.read()))
81
+ t = LiveTemplate(Template(f.read(), template_id=filename))
74
82
  _cache[filename] = (mtime, t)
75
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)
pyview/ws_handler.py CHANGED
@@ -2,7 +2,7 @@ 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
5
- from pyview.live_socket import LiveViewSocket
5
+ from pyview.live_socket import ConnectedLiveViewSocket, LiveViewSocket
6
6
  from pyview.live_routes import LiveViewLookup
7
7
  from pyview.csrf import validate_csrf_token
8
8
  from pyview.session import deserialize_session
@@ -42,16 +42,22 @@ 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
- socket = LiveViewSocket(websocket, topic, lv)
47
+ socket = ConnectedLiveViewSocket(websocket, topic, lv)
48
48
 
49
49
  session = {}
50
50
  if "session" in payload:
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
 
@@ -75,7 +81,7 @@ class LiveSocketHandler:
75
81
  self.sessions -= 1
76
82
 
77
83
  async def handle_connected(
78
- self, myJoinId, socket: LiveViewSocket, prev_rendered: dict[str, Any]
84
+ self, myJoinId, socket: ConnectedLiveViewSocket, prev_rendered: dict[str, Any]
79
85
  ):
80
86
  while True:
81
87
  message = await socket.websocket.receive()
@@ -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
@@ -235,8 +254,8 @@ class LiveSocketHandler:
235
254
  )
236
255
 
237
256
 
238
- async def _render(socket: LiveViewSocket):
239
- rendered = (await socket.liveview.render(socket.context)).tree()
257
+ async def _render(socket: ConnectedLiveViewSocket):
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,17 +1,15 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: pyview-web
3
- Version: 0.0.25
3
+ Version: 0.2.0
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
9
8
  Author-email: ogrodnek@gmail.com
10
- Requires-Python: >=3.9,<3.12
9
+ Requires-Python: >=3.10,<3.13
11
10
  Classifier: Development Status :: 4 - Beta
12
11
  Classifier: Environment :: Web Environment
13
12
  Classifier: Framework :: AsyncIO
14
- Classifier: Framework :: FastAPI
15
13
  Classifier: Framework :: Pydantic
16
14
  Classifier: Intended Audience :: Developers
17
15
  Classifier: Intended Audience :: Information Technology
@@ -20,9 +18,9 @@ Classifier: License :: OSI Approved :: MIT License
20
18
  Classifier: Operating System :: OS Independent
21
19
  Classifier: Programming Language :: Python
22
20
  Classifier: Programming Language :: Python :: 3
23
- Classifier: Programming Language :: Python :: 3.9
24
21
  Classifier: Programming Language :: Python :: 3.10
25
22
  Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
26
24
  Classifier: Programming Language :: Python :: 3 :: Only
27
25
  Classifier: Topic :: Internet
28
26
  Classifier: Topic :: Internet :: WWW/HTTP
@@ -36,10 +34,11 @@ Requires-Dist: APScheduler (==3.9.1.post1)
36
34
  Requires-Dist: itsdangerous (>=2.1.2,<3.0.0)
37
35
  Requires-Dist: markupsafe (>=2.1.2,<3.0.0)
38
36
  Requires-Dist: psutil (>=5.9.4,<6.0.0)
39
- Requires-Dist: pydantic (>=2.7.1,<3.0.0)
40
- Requires-Dist: starlette (==0.37.2)
41
- Requires-Dist: uvicorn (==0.20.0)
37
+ Requires-Dist: pydantic (>=2.9.2,<3.0.0)
38
+ Requires-Dist: starlette (==0.40.0)
39
+ Requires-Dist: uvicorn (==0.30.6)
42
40
  Requires-Dist: wsproto (==1.2.0)
41
+ Project-URL: Homepage, https://pyview.rocks
43
42
  Project-URL: Repository, https://github.com/ogrodnek/pyview
44
43
  Description-Content-Type: text/markdown
45
44
 
@@ -1,4 +1,4 @@
1
- pyview/__init__.py,sha256=Pk-RgSkIG6fkLH3CwrF5tptoq7n72w0O73k2vAW4CAQ,374
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
@@ -6,20 +6,22 @@ pyview/auth/__init__.py,sha256=vMlirETRhD4va61NOzwg8VY8ep9wVOF96GznJGBmzD0,109
6
6
  pyview/auth/provider.py,sha256=fwriy2JZcOStutVXD-8VlMPAFXjILCM0l08lhTgmuyE,935
7
7
  pyview/auth/required.py,sha256=ZtNmLFth9nK39RxDiJkSzArXwS5Cvr55MUAzfJ1F2e0,1418
8
8
  pyview/changesets/__init__.py,sha256=55CLari2JHZtwy4hapHe7CqUyKjcP4dkM_t5d3CY2gU,46
9
- pyview/changesets/changesets.py,sha256=B1q1nXwI2iuZZQpE3P2T0PpwI21PHjqcsuIQmkKPCvI,1747
9
+ pyview/changesets/changesets.py,sha256=hImmvB_jS6RyLr5Mas5L7DO_0d805jR3c41LKJlnNL4,1720
10
10
  pyview/csrf.py,sha256=VIURva9EJqXXYGC7engweh3SwDQCnHlhV2zWdcdnFqc,789
11
11
  pyview/events.py,sha256=Zv8G2F1XeXUk1wrnfomeFfxB0OPYmHdjSvxRjQew3No,125
12
- pyview/js.py,sha256=4OnPEfBfuvmekeQlm9444As4PLR22zLMIyyzQIIkmls,751
13
- pyview/live_routes.py,sha256=tsKFh2gmH2BWsjsZQZErzRp_-KiAZcn4lFKNLRIN5Nc,498
14
- pyview/live_socket.py,sha256=6SLEkEBzK-zIUNh_5j_OG5t6IHGTDNCpGXk7D7SMNJ4,4370
15
- pyview/live_view.py,sha256=A0vCCvHUy39_eEhRzDbYMEDzgpRqseZPjCnBAMjogxw,1406
12
+ pyview/js.py,sha256=E6HMsUfXQjrcLqYq26ieeYuzTjBeZqfJwwOm3uSR4ME,3498
13
+ pyview/live_routes.py,sha256=IN2Jmy8b1umcfx1R7ZgFXHZNbYDJp_kLIbADtDJknPM,1749
14
+ pyview/live_socket.py,sha256=D8fg7UQSPCVEboxohbDAZuK7J1xBEEoDycVFUuDfZxI,4767
15
+ pyview/live_view.py,sha256=mwAp7jiABSZCBgYF-GLQCB7zcJ7Wpz9cuC84zjzsp2U,1455
16
+ pyview/meta.py,sha256=01Z-qldB9jrewmIJHQpUqyIhuHodQGgCvpuY9YM5R6c,74
16
17
  pyview/phx_message.py,sha256=DUdPfl6tlw9K0FNXJ35ehq03JGgynvwA_JItHQ_dxMQ,2007
17
- pyview/pyview.py,sha256=xy8on2f-chU4JWVy6zGTDqjP8BW-kOoi16voNIgWRg4,2478
18
+ pyview/pyview.py,sha256=UuAeHdmrcmu3q681NR8IVQ1-LcMBnWyT1vIuQdrLlhY,2666
18
19
  pyview/secret.py,sha256=HbaNpGAkFs4uxMVAmk9HwE3FIehg7dmwEOlED7C9moM,363
19
20
  pyview/session.py,sha256=nC8ExyVwfCgQfx9T-aJGyFhr2C7jsrEY_QFkaXtP28U,432
20
21
  pyview/static/assets/app.js,sha256=QoXfdcOCYwVYJftvjsIIVwFye7onaOJMxRpalyYqoMU,200029
21
- pyview/template/__init__.py,sha256=c5hLRfsF2fDOz8aOsoOgoCeBV6VBzdqN_Ktg3mYPw8A,509
22
- pyview/template/live_template.py,sha256=wSKyBw7ejpUY5qXUZdE36Jeeix8Of0CUq8eZdQwxXyg,1864
22
+ pyview/template/__init__.py,sha256=0goMpA8-TCKcwHbhjvAgbPYnY929vBrwjc701t9RIQw,583
23
+ pyview/template/context_processor.py,sha256=y07t7mhL7XjZNbwHnTTyXJvYhXabtuTukDScycAFjVc,312
24
+ pyview/template/live_template.py,sha256=m8_1TCFGfpVkXyZOIWN6a3ksvsewPlo8vTzzPGDyEU0,2408
23
25
  pyview/template/render_diff.py,sha256=v7EVmn8oJdh809N0vnSLK8OiDs1BOpErF36y4VUo9ew,1214
24
26
  pyview/template/root_template.py,sha256=zCUs1bt8R7qynhBE0tTSEYfdkGtbeKNmPhwzRiFNdsI,2031
25
27
  pyview/template/serializer.py,sha256=WDZfqJr2LMlf36fUW2CmWc2aREc63553_y_GRP2-qYc,826
@@ -36,11 +38,11 @@ pyview/vendor/ibis/errors.py,sha256=gtRX3LjkdWEP4NaX8HXL_6OU2fCX16IBBSiGMG5wiY4,
36
38
  pyview/vendor/ibis/filters.py,sha256=M36KS6dlzfsb2NmHkbVuo8gJbiQ6aQjcHzXxHwZ3Afw,7042
37
39
  pyview/vendor/ibis/loaders.py,sha256=NYW7_hlC7TRPDau37bgiOCsvsBgIPpUEpb1NbroUVUA,3457
38
40
  pyview/vendor/ibis/nodes.py,sha256=TgFt4q5MrVW3gC3PVitrs2LyXKllRveooM7XKydNATk,25617
39
- pyview/vendor/ibis/template.py,sha256=IX9z-Ig13yJyRnMqtB52eiRLe002qdIxnfa7fYEXLqM,2314
41
+ pyview/vendor/ibis/template.py,sha256=6XJXnztw87CrOaKeW3e18LL0fNM8AI6AaK_QgMdb7ew,2353
40
42
  pyview/vendor/ibis/tree.py,sha256=hg8f-fKHeo6DE8R-QgAhdvEaZ8rKyz7p0nGwPy0CBTs,2509
41
43
  pyview/vendor/ibis/utils.py,sha256=nLSaxPR9vMphzV9qinlz_Iurv9c49Ps6Knv8vyNlewU,2768
42
- pyview/ws_handler.py,sha256=Vi5aIoxz_Z9qOEcA5fgxsSoJtLz7n_ytbJ8vZiWDvGc,8473
43
- pyview_web-0.0.25.dist-info/LICENSE,sha256=M_bADaBm9_MV9llX3lCicksLhwk3eZUjA2srE0uUWr0,1071
44
- pyview_web-0.0.25.dist-info/METADATA,sha256=-UhlfnbHQaDx-Elu2KFZRxrV2brUwuJ2GAwFfrRWRGU,5276
45
- pyview_web-0.0.25.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
46
- pyview_web-0.0.25.dist-info/RECORD,,
44
+ pyview/ws_handler.py,sha256=8iFKEse4TUaBzHF4xAcZiOqXaAgv4yB2CT0BKoW2Ny4,9319
45
+ pyview_web-0.2.0.dist-info/LICENSE,sha256=M_bADaBm9_MV9llX3lCicksLhwk3eZUjA2srE0uUWr0,1071
46
+ pyview_web-0.2.0.dist-info/METADATA,sha256=jWEXUjbxfTB87xaKUG2_NAnDB9Yz7BQ40Evr0bNGVK0,5256
47
+ pyview_web-0.2.0.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
48
+ pyview_web-0.2.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.0
2
+ Generator: poetry-core 2.1.2
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any