pyview-web 0.4.0__tar.gz → 0.4.3__tar.gz

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.

Files changed (57) hide show
  1. {pyview_web-0.4.0 → pyview_web-0.4.3}/PKG-INFO +3 -2
  2. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyproject.toml +1 -1
  3. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/live_socket.py +39 -4
  4. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/uploads.py +59 -8
  5. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/ws_handler.py +4 -0
  6. {pyview_web-0.4.0 → pyview_web-0.4.3}/LICENSE +0 -0
  7. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/__init__.py +0 -0
  8. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/assets/js/app.js +0 -0
  9. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/assets/package-lock.json +0 -0
  10. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/assets/package.json +0 -0
  11. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/async_stream_runner.py +0 -0
  12. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/auth/__init__.py +0 -0
  13. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/auth/provider.py +0 -0
  14. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/auth/required.py +0 -0
  15. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/changesets/__init__.py +0 -0
  16. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/changesets/changesets.py +0 -0
  17. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/cli/__init__.py +0 -0
  18. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/cli/commands/__init__.py +0 -0
  19. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/cli/commands/create_view.py +0 -0
  20. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/cli/main.py +0 -0
  21. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/csrf.py +0 -0
  22. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/events/BaseEventHandler.py +0 -0
  23. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/events/__init__.py +0 -0
  24. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/events/info_event.py +0 -0
  25. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/instrumentation/__init__.py +0 -0
  26. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/instrumentation/interfaces.py +0 -0
  27. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/instrumentation/noop.py +0 -0
  28. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/js.py +0 -0
  29. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/live_routes.py +0 -0
  30. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/live_view.py +0 -0
  31. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/meta.py +0 -0
  32. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/phx_message.py +0 -0
  33. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/pyview.py +0 -0
  34. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/secret.py +0 -0
  35. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/session.py +0 -0
  36. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/static/assets/app.js +0 -0
  37. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/template/__init__.py +0 -0
  38. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/template/context_processor.py +0 -0
  39. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/template/live_template.py +0 -0
  40. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/template/render_diff.py +0 -0
  41. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/template/root_template.py +0 -0
  42. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/template/serializer.py +0 -0
  43. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/template/utils.py +0 -0
  44. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/vendor/__init__.py +0 -0
  45. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/vendor/flet/pubsub/__init__.py +0 -0
  46. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/vendor/flet/pubsub/pub_sub.py +0 -0
  47. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/vendor/ibis/__init__.py +0 -0
  48. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/vendor/ibis/compiler.py +0 -0
  49. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/vendor/ibis/context.py +0 -0
  50. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/vendor/ibis/errors.py +0 -0
  51. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/vendor/ibis/filters.py +0 -0
  52. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/vendor/ibis/loaders.py +0 -0
  53. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/vendor/ibis/nodes.py +0 -0
  54. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/vendor/ibis/template.py +0 -0
  55. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/vendor/ibis/tree.py +0 -0
  56. {pyview_web-0.4.0 → pyview_web-0.4.3}/pyview/vendor/ibis/utils.py +0 -0
  57. {pyview_web-0.4.0 → pyview_web-0.4.3}/readme.md +0 -0
@@ -1,8 +1,9 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: pyview-web
3
- Version: 0.4.0
3
+ Version: 0.4.3
4
4
  Summary: LiveView in Python
5
5
  License: MIT
6
+ License-File: LICENSE
6
7
  Keywords: web,api,LiveView
7
8
  Author: Larry Ogrodnek
8
9
  Author-email: ogrodnek@gmail.com
@@ -5,7 +5,7 @@ packages = [
5
5
  { include = "pyview" },
6
6
  ]
7
7
 
8
- version = "0.4.0"
8
+ version = "0.4.3"
9
9
  description = "LiveView in Python"
10
10
  authors = ["Larry Ogrodnek <ogrodnek@gmail.com>"]
11
11
  license = "MIT"
@@ -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, upload_name: str, constraints: UploadConstraints
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(name=upload_name, constraints=constraints)
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, upload_name: str, constraints: UploadConstraints
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(upload_name, constraints)
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
@@ -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[ref].progress = progress
139
- self.entries_by_ref[ref].done = progress == 100
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
- joinRef = config.uploads.join_ref_for_entry(entry_ref)
246
- del self.upload_config_join_refs[joinRef]
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
  )
@@ -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
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes