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
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
- from urllib.parse import urlparse, parse_qs
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.phx_message import parse_message
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 apscheduler.schedulers.asyncio import AsyncIOScheduler
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.scheduler.start()
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, mesageRef, topic, event, payload] = json.loads(data)
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(websocket, topic, lv, self.scheduler, self.instrumentation)
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.handle_params(url, merged_params, socket)
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
- mesageRef,
126
+ messageRef,
115
127
  topic,
116
128
  "phx_reply",
117
- {"response": {"rendered": rendered}, "status": "ok"},
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, mesageRef, topic, event, payload] = parse_message(message)
159
+ [joinRef, messageRef, topic, event, payload] = parse_message(message)
142
160
 
143
161
  if event == "heartbeat":
144
162
  resp = [
145
163
  None,
146
- mesageRef,
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("pyview.events.duration",
170
- {"event": event_name, "view": view_name}):
171
- await socket.liveview.handle_event(event_name, value, socket)
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("pyview.render.duration", {"view": view_name}):
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
- mesageRef,
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
- try:
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.handle_params(url, merged_params, socket)
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
- mesageRef,
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
- mesageRef,
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
- mesageRef,
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 = url_str_raw.decode("utf-8") if isinstance(url_str_raw, bytes) else str(url_str_raw)
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.handle_params(url, merged_params, socket)
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
- mesageRef,
331
+ messageRef,
304
332
  topic,
305
333
  "phx_reply",
306
- {"response": {"rendered": rendered}, "status": "ok"},
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
- mesageRef,
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
- socket.upload_manager.update_progress(joinRef, payload)
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
- mesageRef,
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
- mesageRef,
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 = (await socket.liveview.render(socket.context, socket.meta)).tree()
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.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
- Requires-Python: >=3.11,<3.13
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: Operating System :: OS Independent
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 :: Only
24
- Classifier: Topic :: Internet
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 :: Software Development
28
- Classifier: Topic :: Software Development :: Libraries
29
- Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
30
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
31
- Classifier: Typing :: Typed
32
- Requires-Dist: APScheduler (==3.11.0)
33
- Requires-Dist: click (>=8.1.7,<9.0.0)
34
- Requires-Dist: itsdangerous (>=2.2.0,<3.0.0)
35
- Requires-Dist: markupsafe (>=3.0.2,<4.0.0)
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
- poetry install
140
+ uv sync
138
141
  ```
139
142
 
140
143
  ## Running
141
144
 
142
145
  ```
143
- poetry run uvicorn examples.app:app --reload
146
+ uv run uvicorn examples.app:app --reload
144
147
  ```
145
148
 
146
149
  Then go to http://localhost:8000/
147
150
 
148
- ### Poetry Install
151
+ ### uv Install
152
+
153
+ ```
154
+ brew install uv
155
+ ```
156
+
157
+ or
149
158
 
150
159
  ```
151
- brew install pipx
152
- pipx install poetry
153
- pipx ensurepath
160
+ curl -LsSf https://astral.sh/uv/install.sh | sh
154
161
  ```
155
162
 
156
- (see https://python-poetry.org/docs/#installation for more details)
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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.9.21
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ pv = pyview.cli.main:cli
3
+
@@ -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.