pyview-web 0.4.0__py3-none-any.whl → 0.4.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pyview-web might be problematic. Click here for more details.
- pyview/live_socket.py +39 -4
- pyview/uploads.py +59 -8
- pyview/ws_handler.py +4 -0
- {pyview_web-0.4.0.dist-info → pyview_web-0.4.3.dist-info}/METADATA +3 -2
- {pyview_web-0.4.0.dist-info → pyview_web-0.4.3.dist-info}/RECORD +8 -8
- {pyview_web-0.4.0.dist-info → pyview_web-0.4.3.dist-info}/WHEEL +1 -1
- {pyview_web-0.4.0.dist-info → pyview_web-0.4.3.dist-info}/entry_points.txt +0 -0
- {pyview_web-0.4.0.dist-info → pyview_web-0.4.3.dist-info/licenses}/LICENSE +0 -0
pyview/live_socket.py
CHANGED
|
@@ -11,6 +11,7 @@ from typing import (
|
|
|
11
11
|
Union,
|
|
12
12
|
TypeAlias,
|
|
13
13
|
TypeGuard,
|
|
14
|
+
Callable,
|
|
14
15
|
)
|
|
15
16
|
from urllib.parse import urlencode
|
|
16
17
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|
@@ -46,9 +47,18 @@ class UnconnectedSocket(Generic[T]):
|
|
|
46
47
|
connected: bool = False
|
|
47
48
|
|
|
48
49
|
def allow_upload(
|
|
49
|
-
self,
|
|
50
|
+
self,
|
|
51
|
+
upload_name: str,
|
|
52
|
+
constraints: UploadConstraints,
|
|
53
|
+
auto_upload: bool = False,
|
|
54
|
+
progress: Optional[Callable] = None,
|
|
50
55
|
) -> UploadConfig:
|
|
51
|
-
return UploadConfig(
|
|
56
|
+
return UploadConfig(
|
|
57
|
+
name=upload_name,
|
|
58
|
+
constraints=constraints,
|
|
59
|
+
autoUpload=auto_upload,
|
|
60
|
+
progress_callback=progress,
|
|
61
|
+
)
|
|
52
62
|
|
|
53
63
|
|
|
54
64
|
class ConnectedLiveViewSocket(Generic[T]):
|
|
@@ -192,13 +202,38 @@ class ConnectedLiveViewSocket(Generic[T]):
|
|
|
192
202
|
except Exception:
|
|
193
203
|
logger.warning("Error sending navigation message", exc_info=True)
|
|
194
204
|
|
|
205
|
+
async def redirect(self, path: str, params: dict[str, Any] = {}):
|
|
206
|
+
"""Redirect to a new location with full page reload"""
|
|
207
|
+
to = path
|
|
208
|
+
if params:
|
|
209
|
+
to = to + "?" + urlencode(params)
|
|
210
|
+
|
|
211
|
+
message = [
|
|
212
|
+
None,
|
|
213
|
+
None,
|
|
214
|
+
self.topic,
|
|
215
|
+
"redirect",
|
|
216
|
+
{"to": to},
|
|
217
|
+
]
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
await self.websocket.send_text(json.dumps(message))
|
|
221
|
+
except Exception:
|
|
222
|
+
logger.warning("Error sending redirect message", exc_info=True)
|
|
223
|
+
|
|
195
224
|
async def push_event(self, event: str, value: dict[str, Any]):
|
|
196
225
|
self.pending_events.append((event, value))
|
|
197
226
|
|
|
198
227
|
def allow_upload(
|
|
199
|
-
self,
|
|
228
|
+
self,
|
|
229
|
+
upload_name: str,
|
|
230
|
+
constraints: UploadConstraints,
|
|
231
|
+
auto_upload: bool = False,
|
|
232
|
+
progress: Optional[Callable] = None,
|
|
200
233
|
) -> UploadConfig:
|
|
201
|
-
return self.upload_manager.allow_upload(
|
|
234
|
+
return self.upload_manager.allow_upload(
|
|
235
|
+
upload_name, constraints, auto_upload, progress
|
|
236
|
+
)
|
|
202
237
|
|
|
203
238
|
async def close(self):
|
|
204
239
|
self.connected = False
|
pyview/uploads.py
CHANGED
|
@@ -2,7 +2,7 @@ import datetime
|
|
|
2
2
|
import uuid
|
|
3
3
|
import logging
|
|
4
4
|
from pydantic import BaseModel, Field
|
|
5
|
-
from typing import Optional, Any, Literal, Generator
|
|
5
|
+
from typing import Optional, Any, Literal, Generator, Callable
|
|
6
6
|
from dataclasses import dataclass, field
|
|
7
7
|
from contextlib import contextmanager
|
|
8
8
|
import os
|
|
@@ -105,6 +105,7 @@ class UploadConfig(BaseModel):
|
|
|
105
105
|
errors: list[ConstraintViolation] = Field(default_factory=list)
|
|
106
106
|
autoUpload: bool = False
|
|
107
107
|
constraints: UploadConstraints = Field(default_factory=UploadConstraints)
|
|
108
|
+
progress_callback: Optional[Callable] = None
|
|
108
109
|
|
|
109
110
|
uploads: ActiveUploads = Field(default_factory=ActiveUploads)
|
|
110
111
|
|
|
@@ -135,8 +136,9 @@ class UploadConfig(BaseModel):
|
|
|
135
136
|
self.errors.append(ConstraintViolation(ref=self.ref, code="too_many_files"))
|
|
136
137
|
|
|
137
138
|
def update_progress(self, ref: str, progress: int):
|
|
138
|
-
self.entries_by_ref
|
|
139
|
-
|
|
139
|
+
if ref in self.entries_by_ref:
|
|
140
|
+
self.entries_by_ref[ref].progress = progress
|
|
141
|
+
self.entries_by_ref[ref].done = progress == 100
|
|
140
142
|
|
|
141
143
|
@contextmanager
|
|
142
144
|
def consume_uploads(self) -> Generator[list["ActiveUpload"], None, None]:
|
|
@@ -152,6 +154,34 @@ class UploadConfig(BaseModel):
|
|
|
152
154
|
self.uploads = ActiveUploads()
|
|
153
155
|
self.entries_by_ref = {}
|
|
154
156
|
|
|
157
|
+
@contextmanager
|
|
158
|
+
def consume_upload_entry(self, entry_ref: str) -> Generator[Optional["ActiveUpload"], None, None]:
|
|
159
|
+
"""Consume a single upload entry by its ref"""
|
|
160
|
+
upload = None
|
|
161
|
+
join_ref = None
|
|
162
|
+
|
|
163
|
+
# Find the join_ref for this entry
|
|
164
|
+
for jr, active_upload in self.uploads.uploads.items():
|
|
165
|
+
if active_upload.entry.ref == entry_ref:
|
|
166
|
+
upload = active_upload
|
|
167
|
+
join_ref = jr
|
|
168
|
+
break
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
yield upload
|
|
172
|
+
finally:
|
|
173
|
+
if upload and join_ref:
|
|
174
|
+
try:
|
|
175
|
+
upload.close()
|
|
176
|
+
except Exception:
|
|
177
|
+
logger.warning("Error closing upload entry", exc_info=True)
|
|
178
|
+
|
|
179
|
+
# Remove only this specific upload
|
|
180
|
+
if join_ref in self.uploads.uploads:
|
|
181
|
+
del self.uploads.uploads[join_ref]
|
|
182
|
+
if entry_ref in self.entries_by_ref:
|
|
183
|
+
del self.entries_by_ref[entry_ref]
|
|
184
|
+
|
|
155
185
|
def close(self):
|
|
156
186
|
self.uploads.close()
|
|
157
187
|
|
|
@@ -165,9 +195,9 @@ class UploadManager:
|
|
|
165
195
|
self.upload_config_join_refs = {}
|
|
166
196
|
|
|
167
197
|
def allow_upload(
|
|
168
|
-
self, upload_name: str, constraints: UploadConstraints
|
|
198
|
+
self, upload_name: str, constraints: UploadConstraints, auto_upload: bool = False, progress: Optional[Callable] = None
|
|
169
199
|
) -> UploadConfig:
|
|
170
|
-
config = UploadConfig(name=upload_name, constraints=constraints)
|
|
200
|
+
config = UploadConfig(name=upload_name, constraints=constraints, autoUpload=auto_upload, progress_callback=progress)
|
|
171
201
|
self.upload_configs[upload_name] = config
|
|
172
202
|
return config
|
|
173
203
|
|
|
@@ -242,13 +272,33 @@ class UploadManager:
|
|
|
242
272
|
config.update_progress(entry_ref, progress)
|
|
243
273
|
|
|
244
274
|
if progress == 100:
|
|
245
|
-
|
|
246
|
-
|
|
275
|
+
try:
|
|
276
|
+
joinRef_to_remove = config.uploads.join_ref_for_entry(entry_ref)
|
|
277
|
+
if joinRef_to_remove in self.upload_config_join_refs:
|
|
278
|
+
del self.upload_config_join_refs[joinRef_to_remove]
|
|
279
|
+
except (IndexError, KeyError):
|
|
280
|
+
# Entry might have already been consumed and removed
|
|
281
|
+
pass
|
|
247
282
|
|
|
248
283
|
def no_progress(self, joinRef) -> bool:
|
|
249
284
|
config = self.upload_config_join_refs[joinRef]
|
|
250
285
|
return config.uploads.no_progress()
|
|
251
286
|
|
|
287
|
+
async def trigger_progress_callback_if_exists(self, payload: dict[str, Any], socket):
|
|
288
|
+
"""Trigger progress callback if one exists for this upload config"""
|
|
289
|
+
upload_config_ref = payload["ref"]
|
|
290
|
+
config = self.config_for_ref(upload_config_ref)
|
|
291
|
+
|
|
292
|
+
if config and config.progress_callback:
|
|
293
|
+
entry_ref = payload["entry_ref"]
|
|
294
|
+
if entry_ref in config.entries_by_ref:
|
|
295
|
+
entry = config.entries_by_ref[entry_ref]
|
|
296
|
+
# Update entry progress before calling callback
|
|
297
|
+
progress = int(payload["progress"])
|
|
298
|
+
entry.progress = progress
|
|
299
|
+
entry.done = progress == 100
|
|
300
|
+
await config.progress_callback(entry, socket)
|
|
301
|
+
|
|
252
302
|
def close(self):
|
|
253
303
|
for config in self.upload_configs.values():
|
|
254
304
|
config.close()
|
|
@@ -272,6 +322,7 @@ def live_file_input(config: Optional[UploadConfig]) -> Markup:
|
|
|
272
322
|
accepted = ",".join(config.constraints.accept)
|
|
273
323
|
accept = f'accept="{accepted}"' if accepted else ""
|
|
274
324
|
multiple = "multiple" if config.constraints.max_files > 1 else ""
|
|
325
|
+
auto_upload = "data-phx-auto-upload" if config.autoUpload else ""
|
|
275
326
|
|
|
276
327
|
return Markup(
|
|
277
328
|
f"""
|
|
@@ -281,7 +332,7 @@ def live_file_input(config: Optional[UploadConfig]) -> Markup:
|
|
|
281
332
|
data-phx-done-refs="{done_refs}"
|
|
282
333
|
data-phx-preflighted-refs="{preflighted_refs}"
|
|
283
334
|
data-phx-update="ignore" phx-hook="Phoenix.LiveFileUpload"
|
|
284
|
-
{accept} {multiple}>
|
|
335
|
+
{accept} {multiple} {auto_upload}>
|
|
285
336
|
</input>
|
|
286
337
|
"""
|
|
287
338
|
)
|
pyview/ws_handler.py
CHANGED
|
@@ -340,7 +340,11 @@ class LiveSocketHandler:
|
|
|
340
340
|
)
|
|
341
341
|
|
|
342
342
|
if event == "progress":
|
|
343
|
+
# Trigger progress callback BEFORE updating progress (which may consume the entry)
|
|
344
|
+
await socket.upload_manager.trigger_progress_callback_if_exists(payload, socket)
|
|
345
|
+
|
|
343
346
|
socket.upload_manager.update_progress(joinRef, payload)
|
|
347
|
+
|
|
344
348
|
rendered = await _render(socket)
|
|
345
349
|
diff = socket.diff(rendered)
|
|
346
350
|
|
|
@@ -21,7 +21,7 @@ pyview/instrumentation/interfaces.py,sha256=AhVDM_vzETWtM-wfOXaM13K2OgdL0H8lu5wh
|
|
|
21
21
|
pyview/instrumentation/noop.py,sha256=VP8UjiI--A7KWqnSFh7PMG7MqY0Z9ddQjBYVW7iHZa0,2941
|
|
22
22
|
pyview/js.py,sha256=E6HMsUfXQjrcLqYq26ieeYuzTjBeZqfJwwOm3uSR4ME,3498
|
|
23
23
|
pyview/live_routes.py,sha256=IN2Jmy8b1umcfx1R7ZgFXHZNbYDJp_kLIbADtDJknPM,1749
|
|
24
|
-
pyview/live_socket.py,sha256=
|
|
24
|
+
pyview/live_socket.py,sha256=SjN2TSYHnMgmzjE3rVZkNBxtZ0EA3Wx3frg7tx85YBY,7655
|
|
25
25
|
pyview/live_view.py,sha256=mwAp7jiABSZCBgYF-GLQCB7zcJ7Wpz9cuC84zjzsp2U,1455
|
|
26
26
|
pyview/meta.py,sha256=01Z-qldB9jrewmIJHQpUqyIhuHodQGgCvpuY9YM5R6c,74
|
|
27
27
|
pyview/phx_message.py,sha256=DUdPfl6tlw9K0FNXJ35ehq03JGgynvwA_JItHQ_dxMQ,2007
|
|
@@ -36,7 +36,7 @@ pyview/template/render_diff.py,sha256=1P-OgtcGb0Y-zJ9uUH3bKWX-qQTHBa4jgg73qJD7eg
|
|
|
36
36
|
pyview/template/root_template.py,sha256=zCUs1bt8R7qynhBE0tTSEYfdkGtbeKNmPhwzRiFNdsI,2031
|
|
37
37
|
pyview/template/serializer.py,sha256=WDZfqJr2LMlf36fUW2CmWc2aREc63553_y_GRP2-qYc,826
|
|
38
38
|
pyview/template/utils.py,sha256=S8593UjUJztUrtC3h1EL9MxQp5uH7rFDTNkv9C6A_xU,642
|
|
39
|
-
pyview/uploads.py,sha256=
|
|
39
|
+
pyview/uploads.py,sha256=G8zcVoTfWUjFs4nW3XZj60PqWujWTw2Rs3WOyyo52nY,12077
|
|
40
40
|
pyview/vendor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
41
41
|
pyview/vendor/flet/pubsub/__init__.py,sha256=JSPCeKB26b5E-IVHNRvVHrlf_CBGDLCulE9ADrostGs,39
|
|
42
42
|
pyview/vendor/flet/pubsub/pub_sub.py,sha256=gpdruSxKQBqL7_Dtxo4vETm1kM0YH7S299msw2oyUoE,10184
|
|
@@ -50,9 +50,9 @@ pyview/vendor/ibis/nodes.py,sha256=TgFt4q5MrVW3gC3PVitrs2LyXKllRveooM7XKydNATk,2
|
|
|
50
50
|
pyview/vendor/ibis/template.py,sha256=6XJXnztw87CrOaKeW3e18LL0fNM8AI6AaK_QgMdb7ew,2353
|
|
51
51
|
pyview/vendor/ibis/tree.py,sha256=hg8f-fKHeo6DE8R-QgAhdvEaZ8rKyz7p0nGwPy0CBTs,2509
|
|
52
52
|
pyview/vendor/ibis/utils.py,sha256=nLSaxPR9vMphzV9qinlz_Iurv9c49Ps6Knv8vyNlewU,2768
|
|
53
|
-
pyview/ws_handler.py,sha256=
|
|
54
|
-
pyview_web-0.4.
|
|
55
|
-
pyview_web-0.4.
|
|
56
|
-
pyview_web-0.4.
|
|
57
|
-
pyview_web-0.4.
|
|
58
|
-
pyview_web-0.4.
|
|
53
|
+
pyview/ws_handler.py,sha256=f6iI4vvLesepI9tCTHLyRBbdKCGPS6Gzj_UUjfIrPT8,14722
|
|
54
|
+
pyview_web-0.4.3.dist-info/METADATA,sha256=uqYhaTBqJQgHBz5s2S33GfRp7BeVPNEmAuaQcitesNk,5280
|
|
55
|
+
pyview_web-0.4.3.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
56
|
+
pyview_web-0.4.3.dist-info/entry_points.txt,sha256=GAT-ic-VYmmSMUSUVKdV1bp4w-vgEeVP-XzElvarQ9U,42
|
|
57
|
+
pyview_web-0.4.3.dist-info/licenses/LICENSE,sha256=M_bADaBm9_MV9llX3lCicksLhwk3eZUjA2srE0uUWr0,1071
|
|
58
|
+
pyview_web-0.4.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|