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.
- pyview/__init__.py +16 -6
- pyview/assets/js/app.js +1 -0
- pyview/assets/js/uploaders.js +221 -0
- pyview/assets/package-lock.json +16 -14
- pyview/assets/package.json +2 -2
- pyview/async_stream_runner.py +2 -1
- pyview/auth/__init__.py +3 -1
- pyview/auth/provider.py +6 -6
- pyview/auth/required.py +7 -10
- pyview/binding/__init__.py +47 -0
- pyview/binding/binder.py +134 -0
- pyview/binding/context.py +33 -0
- pyview/binding/converters.py +191 -0
- pyview/binding/helpers.py +78 -0
- pyview/binding/injectables.py +119 -0
- pyview/binding/params.py +105 -0
- pyview/binding/result.py +32 -0
- pyview/changesets/__init__.py +2 -0
- pyview/changesets/changesets.py +8 -3
- pyview/cli/commands/create_view.py +4 -3
- pyview/cli/main.py +1 -1
- pyview/components/__init__.py +72 -0
- pyview/components/base.py +212 -0
- pyview/components/lifecycle.py +85 -0
- pyview/components/manager.py +366 -0
- pyview/components/renderer.py +14 -0
- pyview/components/slots.py +73 -0
- pyview/csrf.py +4 -2
- pyview/events/AutoEventDispatch.py +98 -0
- pyview/events/BaseEventHandler.py +51 -8
- pyview/events/__init__.py +2 -1
- pyview/instrumentation/__init__.py +3 -3
- pyview/instrumentation/interfaces.py +57 -33
- pyview/instrumentation/noop.py +21 -18
- pyview/js.py +20 -23
- pyview/live_routes.py +5 -3
- pyview/live_socket.py +167 -44
- pyview/live_view.py +24 -12
- pyview/meta.py +14 -2
- pyview/phx_message.py +7 -8
- pyview/playground/__init__.py +10 -0
- pyview/playground/builder.py +118 -0
- pyview/playground/favicon.py +39 -0
- pyview/pyview.py +54 -20
- pyview/session.py +2 -0
- pyview/static/assets/app.js +2088 -806
- pyview/static/assets/uploaders.js +221 -0
- pyview/stream.py +308 -0
- pyview/template/__init__.py +11 -1
- pyview/template/live_template.py +12 -8
- pyview/template/live_view_template.py +338 -0
- pyview/template/render_diff.py +33 -7
- pyview/template/root_template.py +21 -9
- pyview/template/serializer.py +2 -5
- pyview/template/template_view.py +170 -0
- pyview/template/utils.py +3 -2
- pyview/uploads.py +344 -55
- pyview/vendor/flet/pubsub/__init__.py +3 -1
- pyview/vendor/flet/pubsub/pub_sub.py +10 -18
- pyview/vendor/ibis/__init__.py +3 -7
- pyview/vendor/ibis/compiler.py +25 -32
- pyview/vendor/ibis/context.py +13 -15
- pyview/vendor/ibis/errors.py +0 -6
- pyview/vendor/ibis/filters.py +70 -76
- pyview/vendor/ibis/loaders.py +6 -7
- pyview/vendor/ibis/nodes.py +40 -42
- pyview/vendor/ibis/template.py +4 -5
- pyview/vendor/ibis/tree.py +62 -3
- pyview/vendor/ibis/utils.py +14 -15
- pyview/ws_handler.py +116 -86
- {pyview_web-0.3.0.dist-info → pyview_web-0.8.0a2.dist-info}/METADATA +39 -33
- pyview_web-0.8.0a2.dist-info/RECORD +80 -0
- pyview_web-0.8.0a2.dist-info/WHEEL +4 -0
- pyview_web-0.8.0a2.dist-info/entry_points.txt +3 -0
- pyview_web-0.3.0.dist-info/LICENSE +0 -21
- pyview_web-0.3.0.dist-info/RECORD +0 -58
- pyview_web-0.3.0.dist-info/WHEEL +0 -4
- pyview_web-0.3.0.dist-info/entry_points.txt +0 -3
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Playground helpers for creating single-file PyView examples.
|
|
3
|
+
|
|
4
|
+
Example usage:
|
|
5
|
+
|
|
6
|
+
from pyview.playground import playground
|
|
7
|
+
from pyview import LiveView, LiveViewSocket
|
|
8
|
+
|
|
9
|
+
class CounterView(LiveView):
|
|
10
|
+
async def mount(self, socket: LiveViewSocket, session):
|
|
11
|
+
socket.context = {"count": 0}
|
|
12
|
+
|
|
13
|
+
async def handle_event(self, event, payload, socket: LiveViewSocket):
|
|
14
|
+
if event == "increment":
|
|
15
|
+
socket.context["count"] += 1
|
|
16
|
+
|
|
17
|
+
async def render(self, context, meta):
|
|
18
|
+
return f"<button phx-click='increment'>Count: {context['count']}</button>"
|
|
19
|
+
|
|
20
|
+
# Create app, run with: uvicorn module:app --reload
|
|
21
|
+
app = playground().with_live_view(CounterView).with_title("Counter").build()
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from typing import Optional
|
|
25
|
+
|
|
26
|
+
from markupsafe import Markup
|
|
27
|
+
from starlette.requests import Request
|
|
28
|
+
from starlette.responses import Response
|
|
29
|
+
from starlette.routing import Route
|
|
30
|
+
from starlette.staticfiles import StaticFiles
|
|
31
|
+
|
|
32
|
+
from pyview.live_view import LiveView
|
|
33
|
+
from pyview.playground.favicon import Favicon, generate_favicon_svg
|
|
34
|
+
from pyview.pyview import PyView
|
|
35
|
+
from pyview.template import defaultRootTemplate
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class PlaygroundBuilder:
|
|
39
|
+
"""Fluent builder for creating PyView playground applications."""
|
|
40
|
+
|
|
41
|
+
def __init__(self) -> None:
|
|
42
|
+
self._views: list[tuple[str, type[LiveView]]] = []
|
|
43
|
+
self._css: list[Markup] = []
|
|
44
|
+
self._title: Optional[str] = None
|
|
45
|
+
self._title_suffix: Optional[str] = " | LiveView"
|
|
46
|
+
self._favicon: Optional[Favicon] = Favicon()
|
|
47
|
+
|
|
48
|
+
def with_live_view(self, view: type[LiveView], path: str = "/") -> "PlaygroundBuilder":
|
|
49
|
+
"""Add a LiveView to the application."""
|
|
50
|
+
self._views.append((path, view))
|
|
51
|
+
return self
|
|
52
|
+
|
|
53
|
+
def with_css(self, css: Markup | str) -> "PlaygroundBuilder":
|
|
54
|
+
"""Add CSS/head content. Can be called multiple times to accumulate."""
|
|
55
|
+
self._css.append(Markup(css) if isinstance(css, str) else css)
|
|
56
|
+
return self
|
|
57
|
+
|
|
58
|
+
def with_title(self, title: str, suffix: Optional[str] = " | LiveView") -> "PlaygroundBuilder":
|
|
59
|
+
"""Set the page title and optional suffix."""
|
|
60
|
+
self._title = title
|
|
61
|
+
self._title_suffix = suffix
|
|
62
|
+
return self
|
|
63
|
+
|
|
64
|
+
def with_favicon(self, favicon: Optional[Favicon]) -> "PlaygroundBuilder":
|
|
65
|
+
"""Configure favicon generation. Pass None to disable."""
|
|
66
|
+
self._favicon = favicon
|
|
67
|
+
return self
|
|
68
|
+
|
|
69
|
+
def no_favicon(self) -> "PlaygroundBuilder":
|
|
70
|
+
"""Disable auto-generated favicon."""
|
|
71
|
+
self._favicon = None
|
|
72
|
+
return self
|
|
73
|
+
|
|
74
|
+
def build(self) -> PyView:
|
|
75
|
+
"""Build and return the configured PyView application."""
|
|
76
|
+
if not self._views:
|
|
77
|
+
raise ValueError("Must add at least one LiveView via with_live_view()")
|
|
78
|
+
|
|
79
|
+
app = PyView()
|
|
80
|
+
|
|
81
|
+
# Auto-mount static files
|
|
82
|
+
app.mount("/static", StaticFiles(packages=[("pyview", "static")]), name="static")
|
|
83
|
+
|
|
84
|
+
# Collect CSS entries
|
|
85
|
+
css_parts = list(self._css)
|
|
86
|
+
|
|
87
|
+
# Add favicon route and link tag if favicon config exists and title is set
|
|
88
|
+
if self._favicon is not None and self._title:
|
|
89
|
+
favicon_svg = generate_favicon_svg(
|
|
90
|
+
self._title,
|
|
91
|
+
bg_color=self._favicon.bg_color,
|
|
92
|
+
text_color=self._favicon.text_color,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
async def favicon_route(request: Request) -> Response:
|
|
96
|
+
return Response(content=favicon_svg, media_type="image/svg+xml")
|
|
97
|
+
|
|
98
|
+
app.routes.append(Route("/favicon.svg", favicon_route, methods=["GET"]))
|
|
99
|
+
css_parts.append(Markup('<link rel="icon" href="/favicon.svg" type="image/svg+xml">'))
|
|
100
|
+
|
|
101
|
+
# Configure root template - join all CSS entries
|
|
102
|
+
combined_css = Markup("\n".join(css_parts)) if css_parts else None
|
|
103
|
+
app.rootTemplate = defaultRootTemplate(
|
|
104
|
+
css=combined_css,
|
|
105
|
+
title=self._title,
|
|
106
|
+
title_suffix=self._title_suffix,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Add all LiveViews
|
|
110
|
+
for path, view in self._views:
|
|
111
|
+
app.add_live_view(path, view)
|
|
112
|
+
|
|
113
|
+
return app
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def playground() -> PlaygroundBuilder:
|
|
117
|
+
"""Create a new playground builder."""
|
|
118
|
+
return PlaygroundBuilder()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Favicon generation utilities for PyView playground."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class Favicon:
|
|
8
|
+
"""Configuration for auto-generated favicon."""
|
|
9
|
+
|
|
10
|
+
bg_color: str = "#3b82f6"
|
|
11
|
+
text_color: str = "#ffffff"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def generate_favicon_svg(
|
|
15
|
+
text: str,
|
|
16
|
+
bg_color: str = "#3b82f6",
|
|
17
|
+
text_color: str = "#ffffff",
|
|
18
|
+
) -> str:
|
|
19
|
+
"""Generate a rounded square SVG favicon with initials.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
text: Text to extract initials from (e.g., app title)
|
|
23
|
+
bg_color: Background color (hex)
|
|
24
|
+
text_color: Text color (hex)
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
SVG string
|
|
28
|
+
"""
|
|
29
|
+
words = text.strip().split()
|
|
30
|
+
if len(words) >= 2:
|
|
31
|
+
initials = (words[0][0] + words[1][0]).upper()
|
|
32
|
+
else:
|
|
33
|
+
initials = words[0][0].upper() if words else "A"
|
|
34
|
+
|
|
35
|
+
return f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
|
36
|
+
<rect width="100" height="100" rx="15" fill="{bg_color}"/>
|
|
37
|
+
<text x="50" y="50" font-family="sans-serif" font-size="52" font-weight="bold"
|
|
38
|
+
fill="{text_color}" text-anchor="middle" dominant-baseline="central">{initials}</text>
|
|
39
|
+
</svg>'''
|
pyview/pyview.py
CHANGED
|
@@ -1,27 +1,32 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from contextlib import asynccontextmanager
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from urllib.parse import parse_qs, urlparse
|
|
5
|
+
|
|
1
6
|
from starlette.applications import Starlette
|
|
2
|
-
from starlette.responses import HTMLResponse
|
|
3
7
|
from starlette.middleware.gzip import GZipMiddleware
|
|
4
|
-
from starlette.routing import Route, WebSocketRoute
|
|
5
8
|
from starlette.requests import Request
|
|
6
|
-
import
|
|
7
|
-
from
|
|
8
|
-
from typing import Optional
|
|
9
|
+
from starlette.responses import HTMLResponse
|
|
10
|
+
from starlette.routing import Route, WebSocketRoute
|
|
9
11
|
|
|
10
|
-
from pyview.live_socket import UnconnectedSocket
|
|
11
|
-
from pyview.csrf import generate_csrf_token
|
|
12
|
-
from pyview.session import serialize_session
|
|
13
12
|
from pyview.auth import AuthProviderFactory
|
|
14
|
-
from pyview.
|
|
13
|
+
from pyview.binding import call_handle_params
|
|
14
|
+
from pyview.components.lifecycle import run_nested_component_lifecycle
|
|
15
|
+
from pyview.csrf import generate_csrf_token
|
|
15
16
|
from pyview.instrumentation import InstrumentationProvider, NoOpInstrumentation
|
|
16
|
-
from .
|
|
17
|
-
from .
|
|
17
|
+
from pyview.live_socket import UnconnectedSocket
|
|
18
|
+
from pyview.meta import PyViewMeta
|
|
19
|
+
from pyview.session import serialize_session
|
|
20
|
+
|
|
18
21
|
from .live_routes import LiveViewLookup
|
|
22
|
+
from .live_view import LiveView
|
|
19
23
|
from .template import (
|
|
20
24
|
RootTemplate,
|
|
21
25
|
RootTemplateContext,
|
|
22
26
|
defaultRootTemplate,
|
|
23
27
|
find_associated_css,
|
|
24
28
|
)
|
|
29
|
+
from .ws_handler import LiveSocketHandler
|
|
25
30
|
|
|
26
31
|
|
|
27
32
|
class PyView(Starlette):
|
|
@@ -29,6 +34,10 @@ class PyView(Starlette):
|
|
|
29
34
|
instrumentation: InstrumentationProvider
|
|
30
35
|
|
|
31
36
|
def __init__(self, *args, instrumentation: Optional[InstrumentationProvider] = None, **kwargs):
|
|
37
|
+
# Extract user's lifespan if provided, then always use our composed lifespan
|
|
38
|
+
user_lifespan = kwargs.pop("lifespan", None)
|
|
39
|
+
kwargs["lifespan"] = self._create_lifespan(user_lifespan)
|
|
40
|
+
|
|
32
41
|
super().__init__(*args, **kwargs)
|
|
33
42
|
self.rootTemplate = defaultRootTemplate()
|
|
34
43
|
self.instrumentation = instrumentation or NoOpInstrumentation()
|
|
@@ -38,20 +47,40 @@ class PyView(Starlette):
|
|
|
38
47
|
self.routes.append(WebSocketRoute("/live/websocket", self.live_handler.handle))
|
|
39
48
|
self.add_middleware(GZipMiddleware)
|
|
40
49
|
|
|
50
|
+
def _create_lifespan(self, user_lifespan=None):
|
|
51
|
+
"""Create the lifespan context manager for proper startup/shutdown.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
user_lifespan: Optional user-provided lifespan context manager to wrap
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
@asynccontextmanager
|
|
58
|
+
async def lifespan(app):
|
|
59
|
+
# Startup: Start the scheduler
|
|
60
|
+
app.live_handler.start_scheduler()
|
|
61
|
+
|
|
62
|
+
# Run user's lifespan if they provided one
|
|
63
|
+
if user_lifespan:
|
|
64
|
+
async with user_lifespan(app):
|
|
65
|
+
yield
|
|
66
|
+
else:
|
|
67
|
+
yield
|
|
68
|
+
|
|
69
|
+
# Shutdown: Stop the scheduler
|
|
70
|
+
await app.live_handler.shutdown_scheduler()
|
|
71
|
+
|
|
72
|
+
return lifespan
|
|
73
|
+
|
|
41
74
|
def add_live_view(self, path: str, view: type[LiveView]):
|
|
42
75
|
async def lv(request: Request):
|
|
43
|
-
return await liveview_container(
|
|
44
|
-
self.rootTemplate, self.view_lookup, request
|
|
45
|
-
)
|
|
76
|
+
return await liveview_container(self.rootTemplate, self.view_lookup, request)
|
|
46
77
|
|
|
47
78
|
self.view_lookup.add(path, view)
|
|
48
79
|
auth = AuthProviderFactory.get(view)
|
|
49
80
|
self.routes.append(Route(path, auth.wrap(lv), methods=["GET"]))
|
|
50
81
|
|
|
51
82
|
|
|
52
|
-
async def liveview_container(
|
|
53
|
-
template: RootTemplate, view_lookup: LiveViewLookup, request: Request
|
|
54
|
-
):
|
|
83
|
+
async def liveview_container(template: RootTemplate, view_lookup: LiveViewLookup, request: Request):
|
|
55
84
|
url = request.url
|
|
56
85
|
path = url.path
|
|
57
86
|
lv, path_params = view_lookup.get(path)
|
|
@@ -69,9 +98,14 @@ async def liveview_container(
|
|
|
69
98
|
merged_params = {**query_params, **path_params}
|
|
70
99
|
|
|
71
100
|
# Pass merged parameters to handle_params
|
|
72
|
-
await lv
|
|
101
|
+
await call_handle_params(lv, urlparse(url._url), merged_params, s)
|
|
102
|
+
|
|
103
|
+
# Pass socket to meta for component registration
|
|
104
|
+
meta = PyViewMeta(socket=s)
|
|
105
|
+
r = await lv.render(s.context, meta)
|
|
73
106
|
|
|
74
|
-
|
|
107
|
+
# Run component lifecycle, including nested components
|
|
108
|
+
await run_nested_component_lifecycle(s, meta)
|
|
75
109
|
|
|
76
110
|
liveview_css = find_associated_css(lv)
|
|
77
111
|
|
|
@@ -79,7 +113,7 @@ async def liveview_container(
|
|
|
79
113
|
|
|
80
114
|
context: RootTemplateContext = {
|
|
81
115
|
"id": id,
|
|
82
|
-
"content": r.text(),
|
|
116
|
+
"content": r.text(socket=s), # Pass socket for ComponentMarker resolution
|
|
83
117
|
"title": s.live_title,
|
|
84
118
|
"csrf_token": generate_csrf_token("lv:phx-" + id),
|
|
85
119
|
"session": serialize_session(session),
|