pyview-web 0.3.0__py3-none-any.whl → 0.8.0a2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. pyview/__init__.py +16 -6
  2. pyview/assets/js/app.js +1 -0
  3. pyview/assets/js/uploaders.js +221 -0
  4. pyview/assets/package-lock.json +16 -14
  5. pyview/assets/package.json +2 -2
  6. pyview/async_stream_runner.py +2 -1
  7. pyview/auth/__init__.py +3 -1
  8. pyview/auth/provider.py +6 -6
  9. pyview/auth/required.py +7 -10
  10. pyview/binding/__init__.py +47 -0
  11. pyview/binding/binder.py +134 -0
  12. pyview/binding/context.py +33 -0
  13. pyview/binding/converters.py +191 -0
  14. pyview/binding/helpers.py +78 -0
  15. pyview/binding/injectables.py +119 -0
  16. pyview/binding/params.py +105 -0
  17. pyview/binding/result.py +32 -0
  18. pyview/changesets/__init__.py +2 -0
  19. pyview/changesets/changesets.py +8 -3
  20. pyview/cli/commands/create_view.py +4 -3
  21. pyview/cli/main.py +1 -1
  22. pyview/components/__init__.py +72 -0
  23. pyview/components/base.py +212 -0
  24. pyview/components/lifecycle.py +85 -0
  25. pyview/components/manager.py +366 -0
  26. pyview/components/renderer.py +14 -0
  27. pyview/components/slots.py +73 -0
  28. pyview/csrf.py +4 -2
  29. pyview/events/AutoEventDispatch.py +98 -0
  30. pyview/events/BaseEventHandler.py +51 -8
  31. pyview/events/__init__.py +2 -1
  32. pyview/instrumentation/__init__.py +3 -3
  33. pyview/instrumentation/interfaces.py +57 -33
  34. pyview/instrumentation/noop.py +21 -18
  35. pyview/js.py +20 -23
  36. pyview/live_routes.py +5 -3
  37. pyview/live_socket.py +167 -44
  38. pyview/live_view.py +24 -12
  39. pyview/meta.py +14 -2
  40. pyview/phx_message.py +7 -8
  41. pyview/playground/__init__.py +10 -0
  42. pyview/playground/builder.py +118 -0
  43. pyview/playground/favicon.py +39 -0
  44. pyview/pyview.py +54 -20
  45. pyview/session.py +2 -0
  46. pyview/static/assets/app.js +2088 -806
  47. pyview/static/assets/uploaders.js +221 -0
  48. pyview/stream.py +308 -0
  49. pyview/template/__init__.py +11 -1
  50. pyview/template/live_template.py +12 -8
  51. pyview/template/live_view_template.py +338 -0
  52. pyview/template/render_diff.py +33 -7
  53. pyview/template/root_template.py +21 -9
  54. pyview/template/serializer.py +2 -5
  55. pyview/template/template_view.py +170 -0
  56. pyview/template/utils.py +3 -2
  57. pyview/uploads.py +344 -55
  58. pyview/vendor/flet/pubsub/__init__.py +3 -1
  59. pyview/vendor/flet/pubsub/pub_sub.py +10 -18
  60. pyview/vendor/ibis/__init__.py +3 -7
  61. pyview/vendor/ibis/compiler.py +25 -32
  62. pyview/vendor/ibis/context.py +13 -15
  63. pyview/vendor/ibis/errors.py +0 -6
  64. pyview/vendor/ibis/filters.py +70 -76
  65. pyview/vendor/ibis/loaders.py +6 -7
  66. pyview/vendor/ibis/nodes.py +40 -42
  67. pyview/vendor/ibis/template.py +4 -5
  68. pyview/vendor/ibis/tree.py +62 -3
  69. pyview/vendor/ibis/utils.py +14 -15
  70. pyview/ws_handler.py +116 -86
  71. {pyview_web-0.3.0.dist-info → pyview_web-0.8.0a2.dist-info}/METADATA +39 -33
  72. pyview_web-0.8.0a2.dist-info/RECORD +80 -0
  73. pyview_web-0.8.0a2.dist-info/WHEEL +4 -0
  74. pyview_web-0.8.0a2.dist-info/entry_points.txt +3 -0
  75. pyview_web-0.3.0.dist-info/LICENSE +0 -21
  76. pyview_web-0.3.0.dist-info/RECORD +0 -58
  77. pyview_web-0.3.0.dist-info/WHEEL +0 -4
  78. pyview_web-0.3.0.dist-info/entry_points.txt +0 -3
@@ -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 uuid
7
- from urllib.parse import parse_qs, urlparse
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.meta import PyViewMeta
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 .ws_handler import LiveSocketHandler
17
- from .live_view import LiveView
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.handle_params(urlparse(url._url), merged_params, s)
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
- r = await lv.render(s.context, PyViewMeta())
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),
pyview/session.py CHANGED
@@ -1,5 +1,7 @@
1
1
  from typing import Any, cast
2
+
2
3
  from itsdangerous import URLSafeSerializer
4
+
3
5
  from pyview.secret import get_secret
4
6
 
5
7