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
pyview/ws_handler.py
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
|
-
from typing import Optional
|
|
2
1
|
import json
|
|
3
2
|
import logging
|
|
3
|
+
from contextlib import suppress
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from urllib.parse import parse_qs, urlparse
|
|
6
|
+
|
|
7
|
+
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|
4
8
|
from starlette.websockets import WebSocket, WebSocketDisconnect
|
|
5
|
-
|
|
6
|
-
from pyview.live_socket import ConnectedLiveViewSocket, LiveViewSocket
|
|
7
|
-
from pyview.live_routes import LiveViewLookup
|
|
8
|
-
from pyview.csrf import validate_csrf_token
|
|
9
|
-
from pyview.session import deserialize_session
|
|
9
|
+
|
|
10
10
|
from pyview.auth import AuthProviderFactory
|
|
11
|
-
from pyview.
|
|
11
|
+
from pyview.binding import call_handle_event, call_handle_params
|
|
12
|
+
from pyview.csrf import validate_csrf_token
|
|
12
13
|
from pyview.instrumentation import InstrumentationProvider
|
|
13
|
-
from
|
|
14
|
+
from pyview.live_routes import LiveViewLookup
|
|
15
|
+
from pyview.live_socket import ConnectedLiveViewSocket, LiveViewSocket
|
|
16
|
+
from pyview.phx_message import parse_message
|
|
17
|
+
from pyview.session import deserialize_session
|
|
14
18
|
|
|
15
19
|
logger = logging.getLogger(__name__)
|
|
16
20
|
|
|
21
|
+
# Must match phoenix_live_view version in pyview/assets/package.json
|
|
22
|
+
PHOENIX_LIVEVIEW_VERSION = "0.20.17"
|
|
23
|
+
|
|
17
24
|
|
|
18
25
|
class AuthException(Exception):
|
|
19
26
|
pass
|
|
@@ -21,34 +28,25 @@ class AuthException(Exception):
|
|
|
21
28
|
|
|
22
29
|
class LiveSocketMetrics:
|
|
23
30
|
"""Container for LiveSocket instrumentation metrics."""
|
|
24
|
-
|
|
31
|
+
|
|
25
32
|
def __init__(self, instrumentation: InstrumentationProvider):
|
|
26
33
|
self.active_connections = instrumentation.create_updown_counter(
|
|
27
|
-
"pyview.websocket.active_connections",
|
|
28
|
-
"Number of active WebSocket connections"
|
|
34
|
+
"pyview.websocket.active_connections", "Number of active WebSocket connections"
|
|
29
35
|
)
|
|
30
36
|
self.mounts = instrumentation.create_counter(
|
|
31
|
-
"pyview.liveview.mounts",
|
|
32
|
-
"Total number of LiveView mounts"
|
|
37
|
+
"pyview.liveview.mounts", "Total number of LiveView mounts"
|
|
33
38
|
)
|
|
34
39
|
self.events_processed = instrumentation.create_counter(
|
|
35
|
-
"pyview.events.processed",
|
|
36
|
-
"Total number of events processed"
|
|
40
|
+
"pyview.events.processed", "Total number of events processed"
|
|
37
41
|
)
|
|
38
42
|
self.event_duration = instrumentation.create_histogram(
|
|
39
|
-
"pyview.events.duration",
|
|
40
|
-
"Event processing duration",
|
|
41
|
-
unit="s"
|
|
43
|
+
"pyview.events.duration", "Event processing duration", unit="s"
|
|
42
44
|
)
|
|
43
45
|
self.message_size = instrumentation.create_histogram(
|
|
44
|
-
"pyview.websocket.message_size",
|
|
45
|
-
"WebSocket message size in bytes",
|
|
46
|
-
unit="bytes"
|
|
46
|
+
"pyview.websocket.message_size", "WebSocket message size in bytes", unit="bytes"
|
|
47
47
|
)
|
|
48
48
|
self.render_duration = instrumentation.create_histogram(
|
|
49
|
-
"pyview.render.duration",
|
|
50
|
-
"Template render duration",
|
|
51
|
-
unit="s"
|
|
49
|
+
"pyview.render.duration", "Template render duration", unit="s"
|
|
52
50
|
)
|
|
53
51
|
|
|
54
52
|
|
|
@@ -60,7 +58,19 @@ class LiveSocketHandler:
|
|
|
60
58
|
self.manager = ConnectionManager()
|
|
61
59
|
self.sessions = 0
|
|
62
60
|
self.scheduler = AsyncIOScheduler()
|
|
63
|
-
self.
|
|
61
|
+
self._scheduler_started = False
|
|
62
|
+
|
|
63
|
+
def start_scheduler(self):
|
|
64
|
+
"""Start the scheduler. Called during app startup in async context."""
|
|
65
|
+
if not self._scheduler_started:
|
|
66
|
+
self.scheduler.start()
|
|
67
|
+
self._scheduler_started = True
|
|
68
|
+
|
|
69
|
+
async def shutdown_scheduler(self):
|
|
70
|
+
"""Shutdown the scheduler. Called during app shutdown."""
|
|
71
|
+
if self._scheduler_started:
|
|
72
|
+
self.scheduler.shutdown(wait=False)
|
|
73
|
+
self._scheduler_started = False
|
|
64
74
|
|
|
65
75
|
async def check_auth(self, websocket: WebSocket, lv):
|
|
66
76
|
if not await AuthProviderFactory.get(lv).has_required_auth(websocket):
|
|
@@ -68,7 +78,7 @@ class LiveSocketHandler:
|
|
|
68
78
|
|
|
69
79
|
async def handle(self, websocket: WebSocket):
|
|
70
80
|
await self.manager.connect(websocket)
|
|
71
|
-
|
|
81
|
+
|
|
72
82
|
# Track active connections
|
|
73
83
|
self.metrics.active_connections.add(1)
|
|
74
84
|
self.sessions += 1
|
|
@@ -77,7 +87,7 @@ class LiveSocketHandler:
|
|
|
77
87
|
|
|
78
88
|
try:
|
|
79
89
|
data = await websocket.receive_text()
|
|
80
|
-
[joinRef,
|
|
90
|
+
[joinRef, messageRef, topic, event, payload] = json.loads(data)
|
|
81
91
|
if event == "phx_join":
|
|
82
92
|
if not validate_csrf_token(payload["params"]["_csrf_token"], topic):
|
|
83
93
|
raise AuthException("Invalid CSRF token")
|
|
@@ -87,7 +97,9 @@ class LiveSocketHandler:
|
|
|
87
97
|
url = urlparse(payload["url"])
|
|
88
98
|
lv, path_params = self.routes.get(url.path)
|
|
89
99
|
await self.check_auth(websocket, lv)
|
|
90
|
-
socket = ConnectedLiveViewSocket(
|
|
100
|
+
socket = ConnectedLiveViewSocket(
|
|
101
|
+
websocket, topic, lv, self.scheduler, self.instrumentation
|
|
102
|
+
)
|
|
91
103
|
|
|
92
104
|
session = {}
|
|
93
105
|
if "session" in payload:
|
|
@@ -96,7 +108,7 @@ class LiveSocketHandler:
|
|
|
96
108
|
# Track mount
|
|
97
109
|
view_name = lv.__class__.__name__
|
|
98
110
|
self.metrics.mounts.add(1, {"view": view_name})
|
|
99
|
-
|
|
111
|
+
|
|
100
112
|
await lv.mount(socket, session)
|
|
101
113
|
|
|
102
114
|
# Parse query parameters and merge with path parameters
|
|
@@ -104,17 +116,23 @@ class LiveSocketHandler:
|
|
|
104
116
|
merged_params = {**query_params, **path_params}
|
|
105
117
|
|
|
106
118
|
# Pass merged parameters to handle_params
|
|
107
|
-
await lv
|
|
119
|
+
await call_handle_params(lv, url, merged_params, socket)
|
|
108
120
|
|
|
109
121
|
rendered = await _render(socket)
|
|
110
122
|
socket.prev_rendered = rendered
|
|
111
123
|
|
|
112
124
|
resp = [
|
|
113
125
|
joinRef,
|
|
114
|
-
|
|
126
|
+
messageRef,
|
|
115
127
|
topic,
|
|
116
128
|
"phx_reply",
|
|
117
|
-
{
|
|
129
|
+
{
|
|
130
|
+
"response": {
|
|
131
|
+
"rendered": rendered,
|
|
132
|
+
"liveview_version": PHOENIX_LIVEVIEW_VERSION,
|
|
133
|
+
},
|
|
134
|
+
"status": "ok",
|
|
135
|
+
},
|
|
118
136
|
]
|
|
119
137
|
|
|
120
138
|
await self.manager.send_personal_message(json.dumps(resp), websocket)
|
|
@@ -138,19 +156,17 @@ class LiveSocketHandler:
|
|
|
138
156
|
async def handle_connected(self, myJoinId, socket: ConnectedLiveViewSocket):
|
|
139
157
|
while True:
|
|
140
158
|
message = await socket.websocket.receive()
|
|
141
|
-
[joinRef,
|
|
159
|
+
[joinRef, messageRef, topic, event, payload] = parse_message(message)
|
|
142
160
|
|
|
143
161
|
if event == "heartbeat":
|
|
144
162
|
resp = [
|
|
145
163
|
None,
|
|
146
|
-
|
|
164
|
+
messageRef,
|
|
147
165
|
"phoenix",
|
|
148
166
|
"phx_reply",
|
|
149
167
|
{"response": {}, "status": "ok"},
|
|
150
168
|
]
|
|
151
|
-
await self.manager.send_personal_message(
|
|
152
|
-
json.dumps(resp), socket.websocket
|
|
153
|
-
)
|
|
169
|
+
await self.manager.send_personal_message(json.dumps(resp), socket.websocket)
|
|
154
170
|
continue
|
|
155
171
|
|
|
156
172
|
if event == "event":
|
|
@@ -163,20 +179,36 @@ class LiveSocketHandler:
|
|
|
163
179
|
# Track event metrics
|
|
164
180
|
event_name = payload["event"]
|
|
165
181
|
view_name = socket.liveview.__class__.__name__
|
|
182
|
+
|
|
183
|
+
# Check if event is targeted at a component (via phx-target={cid})
|
|
184
|
+
target_cid = payload.get("cid")
|
|
185
|
+
|
|
166
186
|
self.metrics.events_processed.add(1, {"event": event_name, "view": view_name})
|
|
167
|
-
|
|
187
|
+
|
|
168
188
|
# Time event processing
|
|
169
|
-
with self.instrumentation.time_histogram(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
189
|
+
with self.instrumentation.time_histogram(
|
|
190
|
+
"pyview.events.duration", {"event": event_name, "view": view_name}
|
|
191
|
+
):
|
|
192
|
+
if target_cid is not None:
|
|
193
|
+
# Validate CID type - must be an integer
|
|
194
|
+
if not isinstance(target_cid, int):
|
|
195
|
+
logger.warning(
|
|
196
|
+
f"Invalid cid type for event '{event_name}': {type(target_cid).__name__}"
|
|
197
|
+
)
|
|
198
|
+
else:
|
|
199
|
+
# Route event to component
|
|
200
|
+
await socket.components.handle_event(target_cid, event_name, value)
|
|
201
|
+
else:
|
|
202
|
+
# Route event to LiveView (default behavior)
|
|
203
|
+
await call_handle_event(socket.liveview, event_name, value, socket)
|
|
204
|
+
|
|
173
205
|
# Time rendering
|
|
174
|
-
with self.instrumentation.time_histogram(
|
|
206
|
+
with self.instrumentation.time_histogram(
|
|
207
|
+
"pyview.render.duration", {"view": view_name}
|
|
208
|
+
):
|
|
175
209
|
rendered = await _render(socket)
|
|
176
210
|
|
|
177
|
-
hook_events =
|
|
178
|
-
{} if not socket.pending_events else {"e": socket.pending_events}
|
|
179
|
-
)
|
|
211
|
+
hook_events = {} if not socket.pending_events else {"e": socket.pending_events}
|
|
180
212
|
|
|
181
213
|
diff = socket.diff(rendered)
|
|
182
214
|
|
|
@@ -184,7 +216,7 @@ class LiveSocketHandler:
|
|
|
184
216
|
|
|
185
217
|
resp = [
|
|
186
218
|
joinRef,
|
|
187
|
-
|
|
219
|
+
messageRef,
|
|
188
220
|
topic,
|
|
189
221
|
"phx_reply",
|
|
190
222
|
{"response": {"diff": diff | hook_events}, "status": "ok"},
|
|
@@ -203,33 +235,29 @@ class LiveSocketHandler:
|
|
|
203
235
|
path_params = {}
|
|
204
236
|
|
|
205
237
|
# We need to get path params for the new URL
|
|
206
|
-
|
|
238
|
+
with suppress(ValueError):
|
|
207
239
|
# TODO: I don't think this is actually going to work...
|
|
208
240
|
_, path_params = self.routes.get(url.path)
|
|
209
|
-
except ValueError:
|
|
210
|
-
pass # Handle case where the path doesn't match any route
|
|
211
241
|
|
|
212
242
|
merged_params = {**query_params, **path_params}
|
|
213
243
|
|
|
214
|
-
await lv
|
|
244
|
+
await call_handle_params(lv, url, merged_params, socket)
|
|
215
245
|
rendered = await _render(socket)
|
|
216
246
|
diff = socket.diff(rendered)
|
|
217
247
|
|
|
218
248
|
resp = [
|
|
219
249
|
joinRef,
|
|
220
|
-
|
|
250
|
+
messageRef,
|
|
221
251
|
topic,
|
|
222
252
|
"phx_reply",
|
|
223
253
|
{"response": {"diff": diff}, "status": "ok"},
|
|
224
254
|
]
|
|
225
|
-
await self.manager.send_personal_message(
|
|
226
|
-
json.dumps(resp), socket.websocket
|
|
227
|
-
)
|
|
255
|
+
await self.manager.send_personal_message(json.dumps(resp), socket.websocket)
|
|
228
256
|
continue
|
|
229
257
|
|
|
230
258
|
if event == "allow_upload":
|
|
231
|
-
allow_upload_response = socket.upload_manager.process_allow_upload(
|
|
232
|
-
payload
|
|
259
|
+
allow_upload_response = await socket.upload_manager.process_allow_upload(
|
|
260
|
+
payload, socket.context
|
|
233
261
|
)
|
|
234
262
|
|
|
235
263
|
rendered = await _render(socket)
|
|
@@ -237,7 +265,7 @@ class LiveSocketHandler:
|
|
|
237
265
|
|
|
238
266
|
resp = [
|
|
239
267
|
joinRef,
|
|
240
|
-
|
|
268
|
+
messageRef,
|
|
241
269
|
topic,
|
|
242
270
|
"phx_reply",
|
|
243
271
|
{
|
|
@@ -246,9 +274,7 @@ class LiveSocketHandler:
|
|
|
246
274
|
},
|
|
247
275
|
]
|
|
248
276
|
|
|
249
|
-
await self.manager.send_personal_message(
|
|
250
|
-
json.dumps(resp), socket.websocket
|
|
251
|
-
)
|
|
277
|
+
await self.manager.send_personal_message(json.dumps(resp), socket.websocket)
|
|
252
278
|
continue
|
|
253
279
|
|
|
254
280
|
# file upload or navigation
|
|
@@ -260,20 +286,22 @@ class LiveSocketHandler:
|
|
|
260
286
|
|
|
261
287
|
resp = [
|
|
262
288
|
joinRef,
|
|
263
|
-
|
|
289
|
+
messageRef,
|
|
264
290
|
topic,
|
|
265
291
|
"phx_reply",
|
|
266
292
|
{"response": {}, "status": "ok"},
|
|
267
293
|
]
|
|
268
294
|
|
|
269
|
-
await self.manager.send_personal_message(
|
|
270
|
-
json.dumps(resp), socket.websocket
|
|
271
|
-
)
|
|
295
|
+
await self.manager.send_personal_message(json.dumps(resp), socket.websocket)
|
|
272
296
|
else:
|
|
273
297
|
# This is a navigation join (topic starts with "lv:")
|
|
274
298
|
# Navigation payload has 'redirect' field instead of 'url'
|
|
275
299
|
url_str_raw = payload.get("redirect") or payload.get("url")
|
|
276
|
-
url_str: str =
|
|
300
|
+
url_str: str = (
|
|
301
|
+
url_str_raw.decode("utf-8")
|
|
302
|
+
if isinstance(url_str_raw, bytes)
|
|
303
|
+
else str(url_str_raw)
|
|
304
|
+
)
|
|
277
305
|
url = urlparse(url_str)
|
|
278
306
|
lv, path_params = self.routes.get(url.path)
|
|
279
307
|
await self.check_auth(socket.websocket, lv)
|
|
@@ -293,29 +321,33 @@ class LiveSocketHandler:
|
|
|
293
321
|
query_params = parse_qs(url.query)
|
|
294
322
|
merged_params = {**query_params, **path_params}
|
|
295
323
|
|
|
296
|
-
await lv
|
|
324
|
+
await call_handle_params(lv, url, merged_params, socket)
|
|
297
325
|
|
|
298
326
|
rendered = await _render(socket)
|
|
299
327
|
socket.prev_rendered = rendered
|
|
300
328
|
|
|
301
329
|
resp = [
|
|
302
330
|
joinRef,
|
|
303
|
-
|
|
331
|
+
messageRef,
|
|
304
332
|
topic,
|
|
305
333
|
"phx_reply",
|
|
306
|
-
{
|
|
334
|
+
{
|
|
335
|
+
"response": {
|
|
336
|
+
"rendered": rendered,
|
|
337
|
+
"liveview_version": PHOENIX_LIVEVIEW_VERSION,
|
|
338
|
+
},
|
|
339
|
+
"status": "ok",
|
|
340
|
+
},
|
|
307
341
|
]
|
|
308
342
|
|
|
309
|
-
await self.manager.send_personal_message(
|
|
310
|
-
json.dumps(resp), socket.websocket
|
|
311
|
-
)
|
|
343
|
+
await self.manager.send_personal_message(json.dumps(resp), socket.websocket)
|
|
312
344
|
|
|
313
345
|
if event == "chunk":
|
|
314
346
|
socket.upload_manager.add_chunk(joinRef, payload) # type: ignore
|
|
315
347
|
|
|
316
348
|
resp = [
|
|
317
349
|
joinRef,
|
|
318
|
-
|
|
350
|
+
messageRef,
|
|
319
351
|
topic,
|
|
320
352
|
"phx_reply",
|
|
321
353
|
{"response": {}, "status": "ok"},
|
|
@@ -335,26 +367,26 @@ class LiveSocketHandler:
|
|
|
335
367
|
socket.websocket,
|
|
336
368
|
)
|
|
337
369
|
|
|
338
|
-
await self.manager.send_personal_message(
|
|
339
|
-
json.dumps(resp), socket.websocket
|
|
340
|
-
)
|
|
370
|
+
await self.manager.send_personal_message(json.dumps(resp), socket.websocket)
|
|
341
371
|
|
|
342
372
|
if event == "progress":
|
|
343
|
-
|
|
373
|
+
# Trigger progress callback BEFORE updating progress (which may consume the entry)
|
|
374
|
+
await socket.upload_manager.trigger_progress_callback_if_exists(payload, socket)
|
|
375
|
+
|
|
376
|
+
await socket.upload_manager.update_progress(joinRef, payload, socket)
|
|
377
|
+
|
|
344
378
|
rendered = await _render(socket)
|
|
345
379
|
diff = socket.diff(rendered)
|
|
346
380
|
|
|
347
381
|
resp = [
|
|
348
382
|
joinRef,
|
|
349
|
-
|
|
383
|
+
messageRef,
|
|
350
384
|
topic,
|
|
351
385
|
"phx_reply",
|
|
352
386
|
{"response": {"diff": diff}, "status": "ok"},
|
|
353
387
|
]
|
|
354
388
|
|
|
355
|
-
await self.manager.send_personal_message(
|
|
356
|
-
json.dumps(resp), socket.websocket
|
|
357
|
-
)
|
|
389
|
+
await self.manager.send_personal_message(json.dumps(resp), socket.websocket)
|
|
358
390
|
|
|
359
391
|
if event == "phx_leave":
|
|
360
392
|
# Handle LiveView navigation - clean up current LiveView
|
|
@@ -362,20 +394,18 @@ class LiveSocketHandler:
|
|
|
362
394
|
|
|
363
395
|
resp = [
|
|
364
396
|
joinRef,
|
|
365
|
-
|
|
397
|
+
messageRef,
|
|
366
398
|
topic,
|
|
367
399
|
"phx_reply",
|
|
368
400
|
{"response": {}, "status": "ok"},
|
|
369
401
|
]
|
|
370
|
-
await self.manager.send_personal_message(
|
|
371
|
-
json.dumps(resp), socket.websocket
|
|
372
|
-
)
|
|
402
|
+
await self.manager.send_personal_message(json.dumps(resp), socket.websocket)
|
|
373
403
|
# Continue to wait for next phx_join
|
|
374
404
|
continue
|
|
375
405
|
|
|
376
406
|
|
|
377
407
|
async def _render(socket: ConnectedLiveViewSocket):
|
|
378
|
-
rendered =
|
|
408
|
+
rendered = await socket.render_with_components()
|
|
379
409
|
|
|
380
410
|
if socket.live_title:
|
|
381
411
|
rendered["t"] = socket.live_title
|
|
@@ -1,42 +1,43 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pyview-web
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0a2
|
|
4
4
|
Summary: LiveView in Python
|
|
5
|
-
License: MIT
|
|
6
5
|
Keywords: web,api,LiveView
|
|
7
6
|
Author: Larry Ogrodnek
|
|
8
|
-
Author-email: ogrodnek@gmail.com
|
|
9
|
-
|
|
7
|
+
Author-email: Larry Ogrodnek <ogrodnek@gmail.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Intended Audience :: Information Technology
|
|
10
|
+
Classifier: Intended Audience :: System Administrators
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python
|
|
14
|
+
Classifier: Topic :: Internet
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
18
|
+
Classifier: Topic :: Software Development
|
|
19
|
+
Classifier: Typing :: Typed
|
|
10
20
|
Classifier: Development Status :: 4 - Beta
|
|
11
21
|
Classifier: Environment :: Web Environment
|
|
12
22
|
Classifier: Framework :: AsyncIO
|
|
13
23
|
Classifier: Framework :: Pydantic
|
|
14
24
|
Classifier: Intended Audience :: Developers
|
|
15
|
-
Classifier: Intended Audience :: Information Technology
|
|
16
|
-
Classifier: Intended Audience :: System Administrators
|
|
17
25
|
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
-
Classifier:
|
|
19
|
-
Classifier: Programming Language :: Python
|
|
20
|
-
Classifier: Programming Language :: Python :: 3
|
|
26
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
27
|
Classifier: Programming Language :: Python :: 3.11
|
|
22
28
|
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
-
Classifier: Programming Language :: Python :: 3
|
|
24
|
-
Classifier:
|
|
25
|
-
Classifier: Topic :: Internet :: WWW/HTTP
|
|
29
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
30
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
26
31
|
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
27
|
-
Classifier: Topic ::
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
Requires-Dist:
|
|
33
|
-
Requires-Dist:
|
|
34
|
-
Requires-Dist:
|
|
35
|
-
Requires-
|
|
36
|
-
Requires-Dist: pydantic (>=2.9.2,<3.0.0)
|
|
37
|
-
Requires-Dist: starlette (==0.47.1)
|
|
38
|
-
Requires-Dist: uvicorn (==0.34.3)
|
|
39
|
-
Requires-Dist: wsproto (==1.2.0)
|
|
32
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
33
|
+
Requires-Dist: starlette>=0.50.0,<0.51
|
|
34
|
+
Requires-Dist: wsproto>=1.3.0,<2
|
|
35
|
+
Requires-Dist: apscheduler>=3.11.0,<4
|
|
36
|
+
Requires-Dist: markupsafe>=3.0.2,<4
|
|
37
|
+
Requires-Dist: itsdangerous>=2.2.0,<3
|
|
38
|
+
Requires-Dist: pydantic>=2.11,<3
|
|
39
|
+
Requires-Dist: click>=8.1.7,<9
|
|
40
|
+
Requires-Python: >=3.11, <3.15
|
|
40
41
|
Project-URL: Homepage, https://pyview.rocks
|
|
41
42
|
Project-URL: Repository, https://github.com/ogrodnek/pyview
|
|
42
43
|
Description-Content-Type: text/markdown
|
|
@@ -49,6 +50,8 @@ Description-Content-Type: text/markdown
|
|
|
49
50
|
|
|
50
51
|
PyView enables dynamic, real-time web apps, using server-rendered HTML.
|
|
51
52
|
|
|
53
|
+
**Documentation**: <a href="https://pyview.rocks" target="_blank">https://pyview.rocks</a>
|
|
54
|
+
|
|
52
55
|
**Source Code**: <a href="https://github.com/ogrodnek/pyview" target="_blank">https://github.com/ogrodnek/pyview</a>
|
|
53
56
|
|
|
54
57
|
# Installation
|
|
@@ -134,28 +137,31 @@ PyView is in the very early stages of active development. Please check it out an
|
|
|
134
137
|
## Setup
|
|
135
138
|
|
|
136
139
|
```
|
|
137
|
-
|
|
140
|
+
uv sync
|
|
138
141
|
```
|
|
139
142
|
|
|
140
143
|
## Running
|
|
141
144
|
|
|
142
145
|
```
|
|
143
|
-
|
|
146
|
+
uv run uvicorn examples.app:app --reload
|
|
144
147
|
```
|
|
145
148
|
|
|
146
149
|
Then go to http://localhost:8000/
|
|
147
150
|
|
|
148
|
-
###
|
|
151
|
+
### uv Install
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
brew install uv
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
or
|
|
149
158
|
|
|
150
159
|
```
|
|
151
|
-
|
|
152
|
-
pipx install poetry
|
|
153
|
-
pipx ensurepath
|
|
160
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
154
161
|
```
|
|
155
162
|
|
|
156
|
-
(see https://
|
|
163
|
+
(see https://docs.astral.sh/uv/getting-started/installation/ for more details)
|
|
157
164
|
|
|
158
165
|
# License
|
|
159
166
|
|
|
160
167
|
PyView is licensed under the [MIT License](LICENSE).
|
|
161
|
-
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
pyview/__init__.py,sha256=83FVbEmqTCpMe58y-cbMuRq5MYIi6Bd54NxZ5YUKv5M,826
|
|
2
|
+
pyview/assets/js/app.js,sha256=8Y3mGEf6KeqBUSzyYFalnzD6U_r5hhU332RyQSXwW0w,2561
|
|
3
|
+
pyview/assets/js/uploaders.js,sha256=fKqvmGSfM_dIalcldqy_Zd-4Jv_7ruucfC6hdoPW2QQ,8249
|
|
4
|
+
pyview/assets/package-lock.json,sha256=ArlO7NA-pXSjXra4rOsZdHc7kONt5iQ3-Ki-cME9HuE,2448
|
|
5
|
+
pyview/assets/package.json,sha256=xbpFeJX3NRAtvJKM9NGRYFzwAUl5MBIMXrqv-KvIjSQ,146
|
|
6
|
+
pyview/async_stream_runner.py,sha256=ReCBAZChiLv1q7_6pOApew0w99M0g1EpaL7GsYsJtrs,2290
|
|
7
|
+
pyview/auth/__init__.py,sha256=wAKd3tU43P8ap9NaTkCceZRHDIXWLgciTRmf-C8UGJ4,196
|
|
8
|
+
pyview/auth/provider.py,sha256=6BQnBNoREI4xwvDmX2kp0rQrBlAU30oOQgo0LoPYKrE,935
|
|
9
|
+
pyview/auth/required.py,sha256=8Pk-elflWfr0hGb5ULHpI7zxL1K0j0Vyp75IX2yBuNo,1305
|
|
10
|
+
pyview/binding/__init__.py,sha256=GGssy0LZMu_UcXv89HzL86wYAbPrC1go0PhBX7hjeAs,1497
|
|
11
|
+
pyview/binding/binder.py,sha256=SF-tRmNZEBmiB5Dnz2_qb3OnUoqL5bc4dZa9PvBcjVw,4908
|
|
12
|
+
pyview/binding/context.py,sha256=SdjBSsahVneDifC2S8zWf2NA1JIvvOi4PXpQNvw-kdo,967
|
|
13
|
+
pyview/binding/converters.py,sha256=hemZxn45f7Wq6cpjmzYQpfR4xQSAOMvtK_Jcnyx6YiU,6902
|
|
14
|
+
pyview/binding/helpers.py,sha256=lVmtKA0UKuJpCtB3TfOnS0-fot7KzMh2dDc28G11YKA,2076
|
|
15
|
+
pyview/binding/injectables.py,sha256=GNZK0BG91fDejro5V59U6odNZDIhMZ6G01nKmL13LC4,3776
|
|
16
|
+
pyview/binding/params.py,sha256=OQGR1w58zF7rhmlKQsWM53XljiI2BDOUyfRA_YHehzA,3317
|
|
17
|
+
pyview/binding/result.py,sha256=VMfwi74f4RN3olatPjSLeuIJwZR9D1BrMC1HcO9nKU8,696
|
|
18
|
+
pyview/changesets/__init__.py,sha256=f7KeFc1crh9O142NdyxYdlHqgBWZRNkI20FTvBC0fBs,85
|
|
19
|
+
pyview/changesets/changesets.py,sha256=4SxUQsQaQHp_3IsS2iHvGryJCEG8Z6icaskU5f9lH7g,1898
|
|
20
|
+
pyview/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
+
pyview/cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
+
pyview/cli/commands/create_view.py,sha256=4onWH8oD5tyMRoNkEr5WQpN2jCscRoqswum1AomQt28,5691
|
|
23
|
+
pyview/cli/main.py,sha256=2R5NT_KBEkpevBzKRdwFcPaNCarfo8pkUWs2Pbg_hYI,297
|
|
24
|
+
pyview/components/__init__.py,sha256=thUGqC7mDhgdk9Y8OKiyElw8RVcnjpjSmts2Al3QkvY,1953
|
|
25
|
+
pyview/components/base.py,sha256=gJmTSpj4DCl06-e_1YW__1lFNBGM6qr5WFmQxyJrlFQ,7526
|
|
26
|
+
pyview/components/lifecycle.py,sha256=1NEo2BC1I-t2bUs9ZWElTgBtd6XEu445lZl9HTnCcZk,3271
|
|
27
|
+
pyview/components/manager.py,sha256=FwCCxqy1UO7diifNO5SXgR2fRQgW0RbFaUZtzMkY3co,12758
|
|
28
|
+
pyview/components/renderer.py,sha256=8dCegiXAFHvNSXHu_LyNAYgCeWiDs3-k29mlPt95QRQ,386
|
|
29
|
+
pyview/components/slots.py,sha256=fUhL_HPM9AYXJfbNMu_0wNKlycCcTNK1wnAkxvFz_dM,2099
|
|
30
|
+
pyview/csrf.py,sha256=u8Hjs07lgBU9_UCax4k2B7mrGcrBiVmFB7MaS0WErVE,890
|
|
31
|
+
pyview/events/AutoEventDispatch.py,sha256=MEVg0qQ0r_Dvycw5055w6VKt_ppGp3pNCRiRtN3tCC0,3598
|
|
32
|
+
pyview/events/BaseEventHandler.py,sha256=1mOD302mJgmhltUC9vnBvmB678hY0rA9k8t3Sxpr_AU,3494
|
|
33
|
+
pyview/events/__init__.py,sha256=zJ7KA0VnKqg7VpA2ZUdX4GuBhPMJdwnL_hcAP1oOCSc,226
|
|
34
|
+
pyview/events/info_event.py,sha256=JOwf3KDodHkmH1MzqTD8sPxs0zbI4t8Ff0rLjwRSe2Y,358
|
|
35
|
+
pyview/instrumentation/__init__.py,sha256=CwDbCR3TLWW_obOfxbw0x2b8oljWxsUq5gljvoGygXs,376
|
|
36
|
+
pyview/instrumentation/interfaces.py,sha256=pjXFFLSKQqV7pfIwVQhkJJFYwjLk6_F8zbthFFaDlKk,6650
|
|
37
|
+
pyview/instrumentation/noop.py,sha256=Qn8GubOY4gRn9QpD1R2lyuKfIKtShtR8qD2AYNnVXhs,2901
|
|
38
|
+
pyview/js.py,sha256=OMiiOdVKVQVs2GCZpH2pZ6XKHx6C9ed5UW3QSRj13Y8,3641
|
|
39
|
+
pyview/live_routes.py,sha256=JWEpTr-o3CGsFmaWxKLUyOvZEPu9b7WMOd89MWneYwI,1752
|
|
40
|
+
pyview/live_socket.py,sha256=gs89yw33k1T_RbgJu41cF7wTsixb_pzVT2n7qKqz3EM,11048
|
|
41
|
+
pyview/live_view.py,sha256=7XFkksdPo9LNCrDsDBFpQrJS98KRMcDrx20QBiNfDts,1769
|
|
42
|
+
pyview/meta.py,sha256=KvLEPhaLDyAU5gwun0YEoK9c81GOIfqtwy_ELw2eP5s,518
|
|
43
|
+
pyview/phx_message.py,sha256=uBOjKwpJ76cVhh9FU8XXCWxX78N1XFBZX5NuhMQAt6M,2009
|
|
44
|
+
pyview/playground/__init__.py,sha256=iyDYeMBYUlzeHmIzX6KTNiiGd-NU_b4s3bMvYjr1zfc,254
|
|
45
|
+
pyview/playground/builder.py,sha256=2FOoOI4b_iN1iqnC_L60NBodhNpKhp7iXKSkm4SaRKc,4198
|
|
46
|
+
pyview/playground/favicon.py,sha256=t_GsEDWra0VI0zzeNgMEoml24CSWoJPW5jcYC7quAow,1110
|
|
47
|
+
pyview/pyview.py,sha256=SfDyeDZXvr6WIwEWirsbAfE2Jcu6Xm02IjHTiW6KIbU,4266
|
|
48
|
+
pyview/secret.py,sha256=HbaNpGAkFs4uxMVAmk9HwE3FIehg7dmwEOlED7C9moM,363
|
|
49
|
+
pyview/session.py,sha256=fYhWDThlMcCcLE15V6ieGhf7QvDnsjPQIozFEJY_vYE,434
|
|
50
|
+
pyview/static/assets/app.js,sha256=1WrDZ64rk-7MwiPzoo-8TXuiSzpPEioeDynMgZLiyzg,246443
|
|
51
|
+
pyview/static/assets/uploaders.js,sha256=fKqvmGSfM_dIalcldqy_Zd-4Jv_7ruucfC6hdoPW2QQ,8249
|
|
52
|
+
pyview/stream.py,sha256=rXTgLUpVe8cCc6Ito5BVi5wtmfwcry130ApAbc55a_M,9988
|
|
53
|
+
pyview/template/__init__.py,sha256=V_wnJnFswkNXbBfQsvqleWHn0m1tmA1BCqwlHHh1pTc,820
|
|
54
|
+
pyview/template/context_processor.py,sha256=y07t7mhL7XjZNbwHnTTyXJvYhXabtuTukDScycAFjVc,312
|
|
55
|
+
pyview/template/live_template.py,sha256=rBcQFqXWYjHCBvCdxc9m0-melYprXdbZcL8E6kqKi0Q,2637
|
|
56
|
+
pyview/template/live_view_template.py,sha256=bolhzMQPEE5hptt2VMFxqtcli_dkgdL5hUKiu82Tkf4,13093
|
|
57
|
+
pyview/template/render_diff.py,sha256=wclePCkMznuf1IEmuRa-QGam5rLhw4k49egTl-rLnYU,2589
|
|
58
|
+
pyview/template/root_template.py,sha256=h2jBIKXqqNH6A2igEQEKv4i9YwUYEhac7p9YfvyqScQ,2355
|
|
59
|
+
pyview/template/serializer.py,sha256=eWfAWJsiU6CnQD8g2kfT_U9VPiadLmZ8Zd1CIvfZlI0,760
|
|
60
|
+
pyview/template/template_view.py,sha256=zIVEwy0utnG-UZnsicQuo06m7NATA0pdAc108reaP4c,6263
|
|
61
|
+
pyview/template/utils.py,sha256=UmHTWkdoExFEqFpHpeuFkub0kTkcIZ1PfOh4r7lRrmw,638
|
|
62
|
+
pyview/uploads.py,sha256=3Hako81iDOd1DmrohL9iiCPmxLh37_3Ubomw9w2TlQY,21352
|
|
63
|
+
pyview/vendor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
64
|
+
pyview/vendor/flet/pubsub/__init__.py,sha256=JbAT43vsmIvKADm2MKsHGnP19DQxiOQUjQidIpvByEs,74
|
|
65
|
+
pyview/vendor/flet/pubsub/pub_sub.py,sha256=Pp-q1bniQTS6kjXYhradBuaeiqyZxSSEh9zhk4WDDhk,10098
|
|
66
|
+
pyview/vendor/ibis/__init__.py,sha256=qUba6YQ03HVKQmjLP-ySAhj4CHt4VIZVv1aGUuBefXw,478
|
|
67
|
+
pyview/vendor/ibis/compiler.py,sha256=CV2X1wytDaKaUUDsyErKaN5biErd3T3tU7_fTgM0luA,7176
|
|
68
|
+
pyview/vendor/ibis/context.py,sha256=1nh77iz_3CJgU_nOIO3EjvP9tWB09zNdRz62nr_7Hk8,3644
|
|
69
|
+
pyview/vendor/ibis/errors.py,sha256=wcF8G8z0zSdjzDILdpqDCuoZT7FQRf3PY8NKpVAThQU,1525
|
|
70
|
+
pyview/vendor/ibis/filters.py,sha256=gk1Yyg_qPsmB4BxCS-hBsmOQHiUL8KTzESA0Expkads,6942
|
|
71
|
+
pyview/vendor/ibis/loaders.py,sha256=t8t6bz-lxkJ8LHskrjiNQrMrARM6KxLHmT9vvg2C5yU,3478
|
|
72
|
+
pyview/vendor/ibis/nodes.py,sha256=dlLukFNt9-ZH06F-AylViwNGV3HwC0R3arAzdZ9M35E,25793
|
|
73
|
+
pyview/vendor/ibis/template.py,sha256=wXO4-qZz_vebNYU7bOvPj8fMhdAfuAgyLtUSL-JyIlk,2331
|
|
74
|
+
pyview/vendor/ibis/tree.py,sha256=2V9wu_knSrIHwwBAsWhyX1EsB9FfKNpDm7L08cfcJNo,4484
|
|
75
|
+
pyview/vendor/ibis/utils.py,sha256=CzDWjQ-Q3EE7BSsGbWZN59J427tCsrK1EdIjkgUY74E,2777
|
|
76
|
+
pyview/ws_handler.py,sha256=uagt-Qpwn8wkEp06WJRSukXNRlwZe2BR77OpklfPq30,16133
|
|
77
|
+
pyview_web-0.8.0a2.dist-info/WHEEL,sha256=RRVLqVugUmFOqBedBFAmA4bsgFcROUBiSUKlERi0Hcg,79
|
|
78
|
+
pyview_web-0.8.0a2.dist-info/entry_points.txt,sha256=wKRXhN-7FFJRgSKWdPZadd4_j83P_nuY8-jFq3kBsjY,44
|
|
79
|
+
pyview_web-0.8.0a2.dist-info/METADATA,sha256=pJ9ZwbQQGsJ4zawri805wNkIrrPKI_3aF5Bp4862-Bc,5371
|
|
80
|
+
pyview_web-0.8.0a2.dist-info/RECORD,,
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2023 Larry Ogrodnek
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|