pyweber 1.2.0.dev20260331__tar.gz → 1.2.0.dev20260404__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.
Files changed (86) hide show
  1. {pyweber-1.2.0.dev20260331/pyweber.egg-info → pyweber-1.2.0.dev20260404}/PKG-INFO +1 -1
  2. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyproject.toml +1 -1
  3. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/connection/websocket.py +37 -5
  4. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/core/element.py +3 -0
  5. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/models/element.py +36 -0
  6. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/models/field.py +6 -5
  7. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/models/field_storage.py +4 -1
  8. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/models/file.py +2 -1
  9. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/models/routes.py +36 -7
  10. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/models/ws_message.py +6 -1
  11. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/pyweber/pyweber.py +12 -9
  12. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/static/js.js +87 -22
  13. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404/pyweber.egg-info}/PKG-INFO +1 -1
  14. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/LICENSE +0 -0
  15. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/MANIFEST.in +0 -0
  16. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/README.md +0 -0
  17. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/__init__.py +0 -0
  18. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/admin/index.html +0 -0
  19. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/admin/src/script.js +0 -0
  20. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/admin/src/style.css +0 -0
  21. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/cli/__init__.py +0 -0
  22. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/cli/commands.py +0 -0
  23. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/components/__init__.py +0 -0
  24. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/components/form.py +0 -0
  25. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/components/general.py +0 -0
  26. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/components/input.py +0 -0
  27. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/config/__init__.py +0 -0
  28. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/config/config.py +0 -0
  29. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/connection/__init__.py +0 -0
  30. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/connection/http.py +0 -0
  31. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/connection/reload.py +0 -0
  32. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/connection/selector.py +0 -0
  33. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/connection/session.py +0 -0
  34. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/core/__init__.py +0 -0
  35. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/core/events.py +0 -0
  36. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/core/template.py +0 -0
  37. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/core/window.py +0 -0
  38. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/models/__init__.py +0 -0
  39. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/models/cookies.py +0 -0
  40. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/models/create_app.py +0 -0
  41. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/models/error_pages.py +0 -0
  42. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/models/headers.py +0 -0
  43. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/models/middleware.py +0 -0
  44. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/models/openapi.py +0 -0
  45. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/models/request.py +0 -0
  46. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/models/response.py +0 -0
  47. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/models/run.py +0 -0
  48. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/models/task_manager.py +0 -0
  49. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/models/template_diff.py +0 -0
  50. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/pyweber/__init__.py +0 -0
  51. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/static/.gitignore +0 -0
  52. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/static/config.toml +0 -0
  53. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/static/css.css +0 -0
  54. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/static/docs.html +0 -0
  55. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/static/favicon/android-chrome-192x192.png +0 -0
  56. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/static/favicon/android-chrome-512x512.png +0 -0
  57. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/static/favicon/apple-touch-icon.png +0 -0
  58. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/static/favicon/favicon-16x16.png +0 -0
  59. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/static/favicon/favicon-32x32.png +0 -0
  60. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/static/favicon/favicon.ico +0 -0
  61. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/static/favicon/pyweber.png +0 -0
  62. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/static/favicon/site.webmanifest +0 -0
  63. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/static/handlers.js +0 -0
  64. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/static/html.html +0 -0
  65. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/static/html401.html +0 -0
  66. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/static/html404.html +0 -0
  67. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/static/html500.html +0 -0
  68. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/static/loading.html +0 -0
  69. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/static/main.py +0 -0
  70. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/static/pyweber.css +0 -0
  71. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/static/update.py +0 -0
  72. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/utils/__init__.py +0 -0
  73. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/utils/exceptions.py +0 -0
  74. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/utils/loads.py +0 -0
  75. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/utils/types.py +0 -0
  76. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber/utils/utils.py +0 -0
  77. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber.egg-info/SOURCES.txt +0 -0
  78. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber.egg-info/dependency_links.txt +0 -0
  79. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber.egg-info/entry_points.txt +0 -0
  80. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber.egg-info/requires.txt +0 -0
  81. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/pyweber.egg-info/top_level.txt +0 -0
  82. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/setup.cfg +0 -0
  83. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/tests/test_config.py +0 -0
  84. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/tests/test_cookies.py +0 -0
  85. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/tests/test_response.py +0 -0
  86. {pyweber-1.2.0.dev20260331 → pyweber-1.2.0.dev20260404}/tests/test_template_diff.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyweber
3
- Version: 1.2.0.dev20260331
3
+ Version: 1.2.0.dev20260404
4
4
  Summary: A lightweight Python framework for building and managing web applications.
5
5
  Author-email: DevPythonMZ <pypi.dev@gmail.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pyweber"
7
- version = "1.2.0.dev20260331"
7
+ version = "1.2.0.dev20260404"
8
8
  description = "A lightweight Python framework for building and managing web applications."
9
9
  readme = "README.md"
10
10
  authors = [
@@ -34,7 +34,8 @@ def need_message_keys(): # pragma: no cover
34
34
  'window_data',
35
35
  'window_response',
36
36
  'window_event',
37
- 'sessionId'
37
+ 'sessionId',
38
+ 'file_content'
38
39
  ]
39
40
 
40
41
  def event_is_running(message: wsMessage, task_manager: TaskManager) -> bool: # pragma: no cover
@@ -264,6 +265,7 @@ class BaseWebsockets: # pragma: no cover
264
265
  self.task_manager = TaskManager()
265
266
  self.ws_connections: dict[str, Union[WebsocketServer, Callable[..., Any]]] = {}
266
267
  self._window_response_future: dict[str, asyncio.Future] = {}
268
+ self._file_content_future: dict[str, asyncio.Future] = {}
267
269
  self.old_template: 'Template' = None
268
270
  self.app = app
269
271
 
@@ -275,9 +277,8 @@ class BaseWebsockets: # pragma: no cover
275
277
  assert isinstance(value, dict)
276
278
  self.__window_response = value
277
279
 
278
- async def message_handler(self, message: wsMessage):
279
-
280
- event_handler = EventConstrutor(
280
+ def event_handler(self, message: wsMessage):
281
+ return EventConstrutor(
281
282
  target_id=message.target_uuid,
282
283
  current_target_id=message.current_target_uuid,
283
284
  app=self.app,
@@ -287,6 +288,10 @@ class BaseWebsockets: # pragma: no cover
287
288
  event_data=message.event_data,
288
289
  event_type=message.type
289
290
  ).build_event()
291
+
292
+ async def message_handler(self, message: wsMessage):
293
+
294
+ event_handler = self.event_handler(message)
290
295
 
291
296
  if message.event_ref == 'document':
292
297
  if event_handler.current_target:
@@ -414,6 +419,25 @@ class BaseWebsockets: # pragma: no cover
414
419
  if future and not future.done():
415
420
  future.set_result(response)
416
421
 
422
+ async def get_file_content(self, timeout: int, file_id: str):
423
+ if file_id not in self._file_content_future:
424
+ self._file_content_future[file_id] = asyncio.get_event_loop().create_future()
425
+ try:
426
+ result = await asyncio.wait_for(self._file_content_future[file_id], timeout)
427
+ return result
428
+ except asyncio.TimeoutError:
429
+ return None
430
+ finally:
431
+ self._file_content_future.pop(file_id, None)
432
+
433
+ async def set_file_content(self, response: dict, file_id: str):
434
+ self.file_content = response
435
+
436
+ future = self._file_content_future.get(file_id)
437
+
438
+ if future and not future.done():
439
+ future.set_result(response)
440
+
417
441
  async def get_template_diff(self, session: Session):
418
442
 
419
443
  diff = TemplateDiff()
@@ -516,7 +540,9 @@ class WebsocketManager(BaseWebsockets): # pragma: no cover
516
540
  if not message.get('template') and message.get('window_data'):
517
541
  return {}
518
542
 
519
- if message.get('route') not in self.app.list_routes:
543
+ route = message.get('route', '')
544
+ route = route[:-1] if route.endswith('/') and len(route) > 1 else route
545
+ if route not in self.app.list_routes:
520
546
  return {}
521
547
 
522
548
  return message
@@ -533,6 +559,9 @@ class WebsocketManager(BaseWebsockets): # pragma: no cover
533
559
 
534
560
  message = wsMessage(raw_message=raw_message, app=self.app, ws=self)
535
561
 
562
+ if message.file_content:
563
+ await self.set_file_content(message.file_content, message.file_content.get('file_id'))
564
+
536
565
  if message.window_response:
537
566
  await self.set_window_response(message.window_response, message.session_id)
538
567
 
@@ -591,6 +620,9 @@ class WebsocketManager(BaseWebsockets): # pragma: no cover
591
620
 
592
621
  if message.window_response:
593
622
  await self.set_window_response(message.window_response, message.session_id)
623
+
624
+ if message.file_content:
625
+ await self.set_file_content(message.file_content, message.session_id)
594
626
 
595
627
  sync_template = await self.get_sync_template(message=message)
596
628
 
@@ -278,6 +278,9 @@ class Element(ElementConstrutor): # pragma: no cover
278
278
  raise TypeError(f'all childs must be str or Element instances, but got {type(child).__name__}')
279
279
 
280
280
  return new_childs
281
+
282
+ def update(self):
283
+ raise NotImplementedError
281
284
 
282
285
  def __deepy_clone(self, obj):
283
286
  if isinstance(obj, list):
@@ -1,5 +1,6 @@
1
1
  from uuid import uuid4
2
2
  import re
3
+ import inspect
3
4
 
4
5
  from typing import (
5
6
  TYPE_CHECKING,
@@ -20,6 +21,7 @@ from pyweber.models.file import File
20
21
  if TYPE_CHECKING: # pragma: no cover
21
22
  from pyweber.core.template import Template
22
23
  from pyweber.core.element import Element
24
+ from pyweber.connection.websocket import WebsocketManager
23
25
 
24
26
  class ChildElements(list['Element']): # pragma: no cover
25
27
  def __init__(self, parent: 'Element'):
@@ -117,6 +119,40 @@ class ElementConstrutor: # pragma: no cover
117
119
  def template(self, value: 'Template'):
118
120
  self.__template = value
119
121
 
122
+ async def stream(
123
+ self,
124
+ file: File,
125
+ ws_server: WebsocketManager,
126
+ session_id: str,
127
+ speed_max: int = 8192,
128
+ timeout: float = 5,
129
+ updater_event: Callable[..., Any] = None
130
+ ):
131
+ parts = file.size // speed_max + 1
132
+ content = b''
133
+
134
+ for part in range(parts):
135
+ await ws_server.send_message(
136
+ data={'read_file': file.file_id, 'start': speed_max * part, 'end': speed_max * (part+1)},
137
+ session_id=session_id
138
+ )
139
+
140
+ response = await ws_server.get_file_content(timeout=timeout, file_id=file.file_id)
141
+
142
+ if isinstance(updater_event, Callable):
143
+ if inspect.iscoroutinefunction(updater_event):
144
+ await updater_event(**response)
145
+ else:
146
+ updater_event(**response)
147
+
148
+ if response.get('status') == 'sucess':
149
+ content += bytes(response.get('data').values())
150
+
151
+ if file.size == len(content):
152
+ return content
153
+
154
+ raise ValueError('File content incomplete.')
155
+
120
156
  @property
121
157
  def files(self):
122
158
  if self.tag == 'input' and self.attrs.get('type', None) == 'file':
@@ -1,9 +1,6 @@
1
1
  from dataclasses import dataclass
2
2
  from typing import Optional
3
3
 
4
- from dataclasses import dataclass
5
- from typing import Optional
6
-
7
4
 
8
5
  class Field: # pragma: no cover
9
6
  def __init__(
@@ -11,15 +8,19 @@ class Field: # pragma: no cover
11
8
  name: Optional[str] = None,
12
9
  filename: Optional[str] = None,
13
10
  value: Optional[bytes] = b'',
14
- content_type: Optional[str] = None
11
+ content_type: Optional[str] = None,
12
+ field_id: Optional[str] = None,
13
+ size: Optional[int] = 0
15
14
  ):
16
15
  self.name = name
17
16
  self.filename = filename
18
17
  self.__value = value
19
18
  self.content_type = content_type
19
+ self.field_id = field_id
20
+ self.size = size
20
21
 
21
22
  @property
22
23
  def value(self): return bytes(self.__value)
23
24
 
24
25
  def __repr__(self):
25
- return f"Field(name={self.name}, value_length={len(self.value)})"
26
+ return f"Field(name={self.name}, value_length={self.size})"
@@ -1,4 +1,5 @@
1
1
  import re
2
+ from uuid import uuid4
2
3
  from pyweber.models.field import Field
3
4
 
4
5
  class FieldStorage: # pragma: no cover
@@ -51,13 +52,14 @@ class FieldStorage: # pragma: no cover
51
52
  name = re.search(r"name=\"(.+)\"", keys)
52
53
  filename = re.search(r"filename=\"(.+)\"", keys)
53
54
  content_type = re.search(r"Content-Type: (.+)", keys)
54
- field = Field()
55
+ field = Field(field_id=str(uuid4()))
55
56
 
56
57
  if filename:
57
58
  field.content_type = content_type.group(1)
58
59
  field.name = name.group(1).split('";')[0]
59
60
  field.filename = filename.group(1)
60
61
  field.value = v
62
+ field.size = len(field.value)
61
63
 
62
64
  fids.append(field)
63
65
  elif name:
@@ -65,6 +67,7 @@ class FieldStorage: # pragma: no cover
65
67
  field.content_type = None
66
68
  field.value = v.decode()
67
69
  field.filename = None
70
+ field.size = len(field.value)
68
71
  fids.append(field)
69
72
 
70
73
  return fids
@@ -4,8 +4,9 @@ class File: # pragma: no cover
4
4
  def __init__(self, field: Field):
5
5
  self.filename = field.filename
6
6
  self.content = field.value
7
- self.size = len(self.content)
7
+ self.size = field.size
8
8
  self.content_type = field.content_type
9
+ self.file_id = field.field_id
9
10
 
10
11
  def __len__(self):
11
12
  return self.size
@@ -77,10 +77,12 @@ class Route: # pragma: no cover
77
77
  title: str = '',
78
78
  process_response: bool = True,
79
79
  callback: Callable[..., Any] = None,
80
+ query_paramns: list[str] = None,
80
81
  **kwargs
81
82
  ):
82
83
  self.group = group
83
84
  self.route = route
85
+ self.query_params = query_paramns
84
86
  self.template = template
85
87
  self.methods = methods or self.default_method()
86
88
  self.name = name
@@ -92,6 +94,17 @@ class Route: # pragma: no cover
92
94
  self.callback = callback
93
95
  self.kwargs = kwargs
94
96
 
97
+ @property
98
+ def query_params(self):
99
+ return self.__query_params
100
+
101
+ @query_params.setter
102
+ def query_params(self, value: list[str]):
103
+ if value and not isinstance(value, list):
104
+ raise ValueError("Query Paraments isn't not valid")
105
+
106
+ self.__query_params = value
107
+
95
108
  @property
96
109
  def callback(self): return self.__callback
97
110
 
@@ -297,12 +310,20 @@ class RouteManager: # pragma: no cover
297
310
  self.__route_names: dict[str, str] = {}
298
311
  self.groups: list[str] = []
299
312
 
313
+
314
+ def __partition__(self, route: str, spliter: str):
315
+ p, _, q = route.partition(spliter)
316
+ return p, q
317
+
318
+ def __get_query_params__(self, params_str: str):
319
+ return [val.split('=',1)[0] for val in params_str.split('&')] if params_str else []
320
+
300
321
  def is_redirected(self, route: str) -> bool:
301
322
  return self.get_redirected_route(route=route) is not None
302
323
 
303
324
  @property
304
325
  def default_group(self): return Route.default_group()
305
-
326
+
306
327
  @property
307
328
  def list_routes(self) -> list[str]:
308
329
  return [route.full_route for route in self.__routes.values() if '_pyweber' not in str(route.route)]
@@ -326,8 +347,17 @@ class RouteManager: # pragma: no cover
326
347
  def to_route(self, target: str, status_code: int = 302, **kwargs):
327
348
  if not self.is_redirect_status_code(status_code=status_code):
328
349
  raise ValueError(f'status code {status_code} is invalid Redirect HttpStatusCode')
350
+
351
+ route = self.get_route_by_name(target)
352
+
353
+ if not route:
354
+ route = self.get_route_by_path(target)
355
+
356
+ if route:
357
+ _, kwd = self.resolve_path(target)
358
+
359
+ kwargs = {**kwargs, **kwd}
329
360
 
330
- route = self.get_route_by_name(name=target) or self.get_route_by_path(route=target)
331
361
  if route:
332
362
  return RedirectRoute(route=route, status_code=status_code, **kwargs)
333
363
 
@@ -370,10 +400,6 @@ class RouteManager: # pragma: no cover
370
400
 
371
401
  else:
372
402
  response = handler(**kwargs)
373
-
374
- # self.__routes.get(route).template = response
375
-
376
- # return self.__routes.get(route).template
377
403
  return response
378
404
 
379
405
  self.add_route(
@@ -406,6 +432,8 @@ class RouteManager: # pragma: no cover
406
432
  process_response: bool = True,
407
433
  **kwargs
408
434
  ):
435
+ route, query_params = self.__partition__(route, '/?')
436
+
409
437
  group = self.get_group(group=group)
410
438
  if self.full_route(route=route, group=group) in self.__routes:
411
439
  raise RouteAlreadyExistError(route=route)
@@ -429,7 +457,8 @@ class RouteManager: # pragma: no cover
429
457
  content_type=content_type,
430
458
  title=title,
431
459
  process_response=process_response,
432
- callback=handler
460
+ callback=handler,
461
+ query_paramns=self.__get_query_params__(query_params)
433
462
  )
434
463
 
435
464
  self.__routes[_route.full_route] = _route
@@ -30,6 +30,10 @@ class wsMessage: # pragma: no cover
30
30
  @property
31
31
  def window_response(self) -> dict[str, (str, int)]:
32
32
  return self.__raw_message.get('window_response', {})
33
+
34
+ @property
35
+ def file_content(self) -> dict[str, Any]:
36
+ return self.__raw_message.get('file_content', {})
33
37
 
34
38
  def get_value(self, key: str):
35
39
  return self.__raw_message.get(key, None)
@@ -113,7 +117,8 @@ class wsMessage: # pragma: no cover
113
117
  name=None,
114
118
  filename=value.get('name'),
115
119
  content_type=value.get('content_type'),
116
- value=bytes(value.get('content'))
120
+ field_id=value.get('file_id'),
121
+ size=int(value.get('size'))
117
122
  )
118
123
  ) for value in values.get('value', []) if isinstance(value, dict)
119
124
  ])
@@ -163,14 +163,15 @@ class Pyweber(
163
163
  content_type=ContentTypes.html,
164
164
  redirect_path=request.path,
165
165
  callback=None,
166
- kwargs={}
166
+ kwargs=request.query_params
167
167
  )
168
168
  )
169
169
 
170
170
  else:
171
171
  template_result = await self.get_template(
172
172
  route=request.path,
173
- method=request.method
173
+ method=request.method,
174
+ **request.query_params
174
175
  )
175
176
 
176
177
  content_result = self.template_to_bytes(
@@ -286,8 +287,10 @@ class Pyweber(
286
287
  return ContentTypes.unkown
287
288
  return ContentTypes.html
288
289
 
289
- async def get_template(self, route: str, method: str = 'GET'):
290
- path, kwargs = self.resolve_path(route=route)
290
+ async def get_template(self, route: str, method: str = 'GET', **kwargs):
291
+ path, kwd = self.resolve_path(route=route)
292
+
293
+ kwargs = {**kwargs, **kwd}
291
294
 
292
295
  state_result = StateResult(
293
296
  template=None,
@@ -417,17 +420,17 @@ class Pyweber(
417
420
 
418
421
  while callable(template) or isinstance(template, RedirectRoute):
419
422
  if callable(template):
420
- request_body = self.request.body if self.request else {}
423
+ request_params = {**self.request.body, **self.request.query_params} if self.request else {}
421
424
  kwargs = OpenApiProcessor.prepare_callback_kwargs(
422
425
  callback=state_result.callback,
423
- **{**state_result.kwargs, **request_body}
426
+ **{**state_result.kwargs, **request_params}
424
427
  )
425
428
 
426
429
  template = await template(**kwargs) if inspect.iscoroutinefunction(template) else template(**kwargs)
427
-
428
- if isinstance(template, RedirectRoute):
429
- redirect_path = self.build_route(route=template.route.full_route, **state_result.kwargs)
430
430
 
431
+ if isinstance(template, RedirectRoute):
432
+ redirect_path = self.build_route(route=template.route.full_route, **{**state_result.kwargs, **template.kwargs})
433
+
431
434
  self._check_recursion(route=redirect_path)
432
435
  state_result = await self._process_redirect_route(
433
436
  state=state_result,
@@ -3,6 +3,8 @@ let socketReady = false;
3
3
  let window_event_received = false;
4
4
  const earyEventsBuffer = [];
5
5
  const activeListeners = [];
6
+ const fileMap = {};
7
+ const fileSignatureMap = {};
6
8
 
7
9
  const EventRef = new Proxy(
8
10
  {DOCUMENT: 'document', WINDOW: 'window'},
@@ -62,8 +64,18 @@ function connectWebSocket() {
62
64
  }
63
65
  };
64
66
 
65
- if (data.documentEvents) {
66
- };
67
+ if (data.read_file) {
68
+ const response = await get_file_content(
69
+ data.read_file,
70
+ data.start,
71
+ data.end
72
+ );
73
+
74
+ const event_data = await getEventData(
75
+ {file_content: response}
76
+ );
77
+ socket.send(JSON.stringify(event_data));
78
+ }
67
79
 
68
80
  if (data.template) {
69
81
  applyDifferences(data.template);
@@ -377,7 +389,7 @@ function fromBase64(base64) {
377
389
  return new TextDecoder().decode(Uint8Array.from(atob(base64), c => c.charCodeAt(0)));
378
390
  };
379
391
 
380
- async function getEventData({type=null, event=null, event_ref=null, window_response=null}) {
392
+ async function getEventData({type=null, event=null, event_ref=null, window_response=null, file_content=null}) {
381
393
  let target;
382
394
 
383
395
  if (event){
@@ -446,6 +458,7 @@ async function getEventData({type=null, event=null, event_ref=null, window_respo
446
458
  window_data: JSON.stringify(getWindowData()),
447
459
  window_response: window_response ? window_response : {},
448
460
  window_event: event_ref === EventRef.WINDOW ? sessionStorage.getItem(type): null,
461
+ file_content: file_content ? file_content : {},
449
462
  sessionId: getsessionId()
450
463
  };
451
464
 
@@ -496,10 +509,70 @@ function getWindowData() {
496
509
  return windowData;
497
510
  }
498
511
 
512
+ async function get_file_content(file_id, start, end) {
513
+ const file = fileMap[file_id];
514
+
515
+ if (!file) {
516
+ return {
517
+ data: 'File not Found',
518
+ file_id: file_id,
519
+ status: 'error'
520
+ };
521
+ };
522
+
523
+ const fileSize = file.size;
524
+ const safeStart = Math.max(0, start);
525
+ const safeEnd = end !== null ? Math.min(end, fileSize) : fileSize;
526
+
527
+ if (safeStart >= fileSize) {
528
+ return {
529
+ data: 'Start index does not gather than filesize',
530
+ start: safeStart,
531
+ end: safeEnd,
532
+ size: fileSize,
533
+ file_id: file_id,
534
+ status: 'error'
535
+ };
536
+ };
537
+
538
+ const chunk = file.slice(safeStart, safeEnd);
539
+ const buffer = await chunk.arrayBuffer();
540
+
541
+ return {
542
+ data: new Uint8Array(buffer),
543
+ start: safeStart,
544
+ end: safeEnd,
545
+ size: fileSize,
546
+ file_id: file_id,
547
+ status: 'sucess'
548
+ };
549
+ }
550
+
551
+ function getOrCreateFileId(file) {
552
+ // 1. tenta por referência
553
+ for (const [id, existingFile] of Object.entries(fileMap)) {
554
+ if (existingFile === file) return id;
555
+ }
556
+
557
+ // 2. tenta por assinatura
558
+ const signature = `${file.name}_${file.size}_${file.lastModified}`;
559
+
560
+ if (fileSignatureMap[signature]) {
561
+ return fileSignatureMap[signature];
562
+ }
563
+
564
+ // 3. cria novo
565
+ const file_id = crypto.randomUUID();
566
+
567
+ fileMap[file_id] = file;
568
+ fileSignatureMap[signature] = file_id;
569
+
570
+ return file_id;
571
+ }
572
+
499
573
  async function getFormValues() {
500
574
  const values = {};
501
575
  const inputs = document.querySelectorAll('input, textarea, select, option');
502
-
503
576
 
504
577
  for (const input of inputs) {
505
578
  const id = input.getAttribute('uuid');
@@ -525,24 +598,16 @@ async function getFormValues() {
525
598
  if (input.files) {
526
599
  const files = Array.from(input.files);
527
600
 
528
- const arquivos = await Promise.all(files.map(file => {
529
- return new Promise(resolve => {
530
- const reader = new FileReader();
531
-
532
- reader.onload = function (e) {
533
- const arraybuffer = e.target.result;
534
- const bytes = new Uint8Array(arraybuffer);
535
-
536
- resolve({
537
- content: Array.from(bytes),
538
- size: file.size,
539
- name: file.name,
540
- content_type: file.type || "application/octet-stream",
541
- });
542
- };
543
- reader.readAsArrayBuffer(file);
544
- })
545
- }));
601
+ const arquivos = files.map(file => {
602
+ const file_id = getOrCreateFileId(file);
603
+
604
+ return {
605
+ size: file.size,
606
+ name: file.name,
607
+ file_id: file_id,
608
+ content_type: file.type || "application/octet-stream",
609
+ }
610
+ });
546
611
 
547
612
  values[id]['value'] = arquivos;
548
613
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyweber
3
- Version: 1.2.0.dev20260331
3
+ Version: 1.2.0.dev20260404
4
4
  Summary: A lightweight Python framework for building and managing web applications.
5
5
  Author-email: DevPythonMZ <pypi.dev@gmail.com>
6
6
  License-Expression: MIT