pyweber 1.2.0.dev20260425__tar.gz → 1.2.0.dev20260427__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 (88) hide show
  1. {pyweber-1.2.0.dev20260425/pyweber.egg-info → pyweber-1.2.0.dev20260427}/PKG-INFO +1 -1
  2. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyproject.toml +1 -1
  3. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/core/element.py +93 -31
  4. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/core/template.py +34 -12
  5. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/cookies.py +8 -8
  6. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/element.py +11 -1
  7. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/request.py +35 -32
  8. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/response.py +3 -3
  9. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/routes.py +23 -5
  10. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/pyweber/pyweber.py +18 -7
  11. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/js.js +8 -0
  12. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427/pyweber.egg-info}/PKG-INFO +1 -1
  13. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/LICENSE +0 -0
  14. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/MANIFEST.in +0 -0
  15. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/README.md +0 -0
  16. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/__init__.py +0 -0
  17. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/admin/index.html +0 -0
  18. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/admin/src/script.js +0 -0
  19. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/admin/src/style.css +0 -0
  20. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/cli/__init__.py +0 -0
  21. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/cli/commands.py +0 -0
  22. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/components/__init__.py +0 -0
  23. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/components/form.py +0 -0
  24. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/components/general.py +0 -0
  25. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/components/input.py +0 -0
  26. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/config/__init__.py +0 -0
  27. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/config/config.py +0 -0
  28. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/connection/__init__.py +0 -0
  29. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/connection/http.py +0 -0
  30. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/connection/reload.py +0 -0
  31. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/connection/selector.py +0 -0
  32. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/connection/session.py +0 -0
  33. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/connection/websocket.py +0 -0
  34. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/core/__init__.py +0 -0
  35. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/core/events.py +0 -0
  36. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/core/window.py +0 -0
  37. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/__init__.py +0 -0
  38. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/create_app.py +0 -0
  39. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/error_pages.py +0 -0
  40. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/field.py +0 -0
  41. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/field_storage.py +0 -0
  42. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/file.py +0 -0
  43. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/file_stream.py +0 -0
  44. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/headers.py +0 -0
  45. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/middleware.py +0 -0
  46. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/openapi.py +0 -0
  47. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/run.py +0 -0
  48. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/strem_stats.py +0 -0
  49. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/task_manager.py +0 -0
  50. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/template_diff.py +0 -0
  51. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/ws_message.py +0 -0
  52. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/pyweber/__init__.py +0 -0
  53. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/.gitignore +0 -0
  54. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/config.toml +0 -0
  55. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/css.css +0 -0
  56. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/docs.html +0 -0
  57. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/favicon/android-chrome-192x192.png +0 -0
  58. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/favicon/android-chrome-512x512.png +0 -0
  59. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/favicon/apple-touch-icon.png +0 -0
  60. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/favicon/favicon-16x16.png +0 -0
  61. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/favicon/favicon-32x32.png +0 -0
  62. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/favicon/favicon.ico +0 -0
  63. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/favicon/pyweber.png +0 -0
  64. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/favicon/site.webmanifest +0 -0
  65. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/handlers.js +0 -0
  66. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/html.html +0 -0
  67. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/html401.html +0 -0
  68. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/html404.html +0 -0
  69. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/html500.html +0 -0
  70. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/loading.html +0 -0
  71. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/main.py +0 -0
  72. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/pyweber.css +0 -0
  73. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/update.py +0 -0
  74. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/utils/__init__.py +0 -0
  75. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/utils/exceptions.py +0 -0
  76. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/utils/loads.py +0 -0
  77. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/utils/types.py +0 -0
  78. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/utils/utils.py +0 -0
  79. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber.egg-info/SOURCES.txt +0 -0
  80. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber.egg-info/dependency_links.txt +0 -0
  81. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber.egg-info/entry_points.txt +0 -0
  82. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber.egg-info/requires.txt +0 -0
  83. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber.egg-info/top_level.txt +0 -0
  84. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/setup.cfg +0 -0
  85. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/tests/test_config.py +0 -0
  86. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/tests/test_cookies.py +0 -0
  87. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/tests/test_response.py +0 -0
  88. {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/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.dev20260425
3
+ Version: 1.2.0.dev20260427
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.dev20260425"
7
+ version = "1.2.0.dev20260427"
8
8
  description = "A lightweight Python framework for building and managing web applications."
9
9
  readme = "README.md"
10
10
  authors = [{ name = "DevPythonMZ", email = "pypi.dev@gmail.com" }]
@@ -1,3 +1,4 @@
1
+ import re
1
2
  from uuid import uuid4
2
3
  import lxml.html as HTMLPARSER
3
4
  from lxml.html import fromstring
@@ -10,6 +11,8 @@ from pyweber.models.element import (
10
11
  ChildElements
11
12
  )
12
13
 
14
+ SEARCH_MODE = Literal['exact', 'regex', 'contains', 'startswith', 'endswith']
15
+
13
16
  class Element(ElementConstrutor): # pragma: no cover
14
17
  def __init__(
15
18
  self,
@@ -143,12 +146,79 @@ class Element(ElementConstrutor): # pragma: no cover
143
146
  def set_selection_range(self, start: int, end: str):
144
147
  self.__set_element_methods(method='setSelectionRange', start=start, end=end)
145
148
 
146
- def getElement(self, by: GetBy, value: str, element: 'Element' = None) -> 'Element':
147
- results = self.getElements(by=by, value=value, element=element)
149
+ def getElement(self, by: GetBy, value: str, element: 'Element' = None, search_mode: SEARCH_MODE = 'exact') -> 'Element':
150
+ results = self.getElements(by=by, value=value, element=element, search_mode=search_mode)
148
151
 
149
152
  return results[0] if results else None
150
153
 
151
- def getElements(self, by: GetBy, value: str, element: 'Element' = None) -> list['Element']:
154
+ def getElements(
155
+ self,
156
+ by: GetBy,
157
+ value: str,
158
+ element: 'Element' = None,
159
+ search_mode: SEARCH_MODE = 'exact'
160
+ ) -> list['Element']:
161
+
162
+ def matches(target, val) -> bool:
163
+ if target is None: return False
164
+
165
+ # Lista (classes)
166
+ if isinstance(target, list):
167
+ search_classes = val.split() # classes que o utilizador passou
168
+ match search_mode:
169
+ case 'exact': return set(search_classes) <= set(target)
170
+ case 'regex': return all(any(re.search(cls, t) for t in target) for cls in search_classes)
171
+ case 'contains': return all(any(cls in t for t in target) for cls in search_classes)
172
+ case 'startswith': return all(any(t.startswith(cls) for t in target) for cls in search_classes)
173
+ case 'endswith': return all(any(t.endswith(cls) for t in target) for cls in search_classes)
174
+
175
+ # Dict (attrs, style)
176
+ if isinstance(target, dict):
177
+ conditions = [pair.strip() for pair in val.split(';') if pair.strip()]
178
+
179
+ match search_mode:
180
+ case 'exact':
181
+ for condition in conditions:
182
+ key, _, v = condition.partition(':')
183
+ if not v: key, _, v = condition.partition('=')
184
+ if not v:
185
+ if key.strip() not in target: return False
186
+ elif target.get(key.strip()) != v.strip():
187
+ return False
188
+ return True
189
+
190
+ case 'regex':
191
+ return all(
192
+ any(re.search(cond, k) or re.search(cond, str(v)) for k, v in target.items())
193
+ for cond in conditions
194
+ )
195
+ case 'contains':
196
+ return all(
197
+ any(cond in k or cond in str(v) for k, v in target.items())
198
+ for cond in conditions
199
+ )
200
+ case 'startswith':
201
+ return all(
202
+ any(k.startswith(cond) or str(v).startswith(cond) for k, v in target.items())
203
+ for cond in conditions
204
+ )
205
+ case 'endswith':
206
+ return all(
207
+ any(k.endswith(cond) or str(v).endswith(cond) for k, v in target.items())
208
+ for cond in conditions
209
+ )
210
+
211
+ # String (id, tag, content, etc)
212
+ target = str(target)
213
+ match search_mode:
214
+ case 'exact': return target == val
215
+ case 'regex': return bool(re.search(val, target))
216
+ case 'contains': return val in target
217
+ case 'startswith': return target.startswith(val)
218
+ case 'endswith': return target.endswith(val)
219
+
220
+ return False
221
+
152
222
  element = element or self
153
223
  results: list['Element'] = []
154
224
 
@@ -156,50 +226,34 @@ class Element(ElementConstrutor): # pragma: no cover
156
226
  by: str = by.value
157
227
 
158
228
  if by == 'classes':
159
- if set(value.split()) <= set(element.classes):
229
+ if matches(element.classes, value):
160
230
  results.append(element)
161
231
 
162
232
  elif by in ['attrs', 'style']:
163
- conditions: list[str] = [pair.strip() for pair in value.split(';') if pair.strip()]
164
- has = False
165
-
166
- el: dict[str, str] = getattr(element, by, {})
167
- for condition in conditions:
168
- key, _, val = condition.partition(':')
169
-
170
- if not val: key, _, val = condition.partition('=')
171
-
172
- if not val:
173
- if key.strip() in el:
174
- has = True
175
-
176
- elif key.strip() in el and el.get(key.strip(), None) == val.strip():
177
- has = True
178
-
179
- if has:
233
+ if matches(getattr(element, by, {}), value):
180
234
  results.append(element)
181
235
 
182
- elif getattr(element, by, None) == value:
236
+ elif matches(getattr(element, by, None), value):
183
237
  results.append(element)
184
238
 
185
239
  if element.childs:
186
240
  for child in element.childs:
187
- results.extend(self.getElements(by=by, value=value, element=child))
241
+ results.extend(self.getElements(by=by, value=value, element=child, search_mode=search_mode))
188
242
 
189
243
  return results
190
244
 
191
- def querySelector(self, selector: str, element: 'Element' = None) -> 'Element':
192
- results = self.querySelectorAll(selector=selector, element=element)
245
+ def querySelector(self, selector: str, element: 'Element' = None, search_mode: SEARCH_MODE = 'exact') -> 'Element':
246
+ results = self.querySelectorAll(selector=selector, element=element, search_mode=search_mode)
193
247
 
194
248
  return results[0] if results else None
195
249
 
196
- def querySelectorAll(self, selector: str, element: 'Element' = None) -> list['Element']:
250
+ def querySelectorAll(self, selector: str, element: 'Element' = None, search_mode: SEARCH_MODE = 'exact') -> list['Element']:
197
251
  element = element or self
198
252
  results: list['Element'] = []
199
253
 
200
254
  if selector.startswith('.'):
201
255
  classes = ' '.join(selector.split('.')).strip()
202
- return self.getElements(by=GetBy.classes, value=classes)
256
+ return self.getElements(by=GetBy.classes, value=classes, search_mode=search_mode)
203
257
 
204
258
  elif selector.startswith('#'):
205
259
  if selector[1:].strip() == element.id:
@@ -207,14 +261,14 @@ class Element(ElementConstrutor): # pragma: no cover
207
261
 
208
262
  elif selector.startswith('['):
209
263
  sel = selector.removeprefix('[').removesuffix(']')
210
- return self.getElements(by=GetBy.attrs, value=sel)
264
+ return self.getElements(by=GetBy.attrs, value=sel, search_mode=search_mode)
211
265
 
212
266
  else:
213
267
  if selector.strip() == element.tag:
214
268
  results.append(element)
215
269
 
216
270
  for child in element.childs:
217
- results.extend(self.querySelectorAll(selector=selector, element=child))
271
+ results.extend(self.querySelectorAll(selector=selector, element=child, search_mode=search_mode))
218
272
 
219
273
  return results
220
274
 
@@ -320,6 +374,11 @@ class Element(ElementConstrutor): # pragma: no cover
320
374
  for key, event in events_dict.items():
321
375
  if hasattr(event_obj, key): setattr(event_obj, key, event)
322
376
 
377
+ attrs = {
378
+ cls.render_dynamic_values(c, **kwargs): cls.render_dynamic_values(v, **kwargs)
379
+ for c, v in attrib.items()
380
+ }
381
+
323
382
  element = cls(
324
383
  tag=name,
325
384
  id=id_,
@@ -328,7 +387,7 @@ class Element(ElementConstrutor): # pragma: no cover
328
387
  content=content,
329
388
  events=event_obj,
330
389
  style=style_dict,
331
- attrs=dict(attrib),
390
+ attrs=attrs,
332
391
  childs=None,
333
392
  sanitize=False,
334
393
  files=[],
@@ -353,7 +412,10 @@ class Element(ElementConstrutor): # pragma: no cover
353
412
 
354
413
  if get_tail:
355
414
  uuid_child = "{{" + child_el.uuid + "}}"
356
- element.content = (element.content or '') + f"{uuid_child} {get_tail}"
415
+ element.content = cls.render_dynamic_values(
416
+ content=(element.content or '') + f"{uuid_child} {get_tail}",
417
+ **kwargs
418
+ )
357
419
 
358
420
  if element.tag == 'select' and element.childs:
359
421
  try: element.value = __xyza
@@ -1,6 +1,6 @@
1
1
  import os
2
2
  from uuid import uuid4
3
- from pyweber.core.element import Element
3
+ from pyweber.core.element import Element, SEARCH_MODE
4
4
  from pyweber.utils.loads import LoadStaticFiles
5
5
  from pyweber.config.config import config
6
6
  from pyweber.utils.types import HTTPStatusCode, GetBy
@@ -84,21 +84,43 @@ class Template: # pragma: no cover
84
84
 
85
85
  return html
86
86
 
87
- def getElement(self, by: GetBy, value: str, element: Element = None):
87
+ def getElement(
88
+ self,
89
+ by: GetBy,
90
+ value: str,
91
+ element: Element = None,
92
+ search_mode: SEARCH_MODE='exact'
93
+ ):
88
94
  if not element: element = self.root
89
- return element.getElement(by=by, value=value)
90
-
91
- def getElements(self, by: GetBy, value: str, element: Element = None):
95
+ return element.getElement(by=by, value=value, search_mode=search_mode)
96
+
97
+ def getElements(
98
+ self,
99
+ by: GetBy,
100
+ value: str,
101
+ element: Element = None,
102
+ search_mode: SEARCH_MODE='exact'
103
+ ):
92
104
  if not element: element = self.root
93
- return element.getElements(by=by, value=value)
94
-
95
- def querySelector(self, selector: str, element: Element = None):
105
+ return element.getElements(by=by, value=value, search_mode=search_mode)
106
+
107
+ def querySelector(
108
+ self,
109
+ selector: str,
110
+ element: Element = None,
111
+ search_mode: SEARCH_MODE='exact'
112
+ ):
96
113
  if element is None: element = self.__root
97
- return element.querySelector(selector=selector)
98
-
99
- def querySelectorAll(self, selector: str, element: Element = None) -> list[Element]:
114
+ return element.querySelector(selector=selector, search_mode=search_mode)
115
+
116
+ def querySelectorAll(
117
+ self,
118
+ selector: str,
119
+ element: Element = None,
120
+ search_mode: SEARCH_MODE='exact'
121
+ ) -> list[Element]:
100
122
  if element is None: element = self.__root
101
- return element.querySelectorAll(selector=selector)
123
+ return element.querySelectorAll(selector=selector, search_mode=search_mode)
102
124
 
103
125
  def __parse_html(self, html: str) -> Element:
104
126
  if not html.replace('<!DOCTYPE html>', '').strip().startswith('<html'):
@@ -2,12 +2,12 @@ from datetime import datetime, timezone, timedelta
2
2
 
3
3
  class CookieManager:
4
4
  def __init__(self):
5
- self.__cookies: list[str] = []
6
-
5
+ self.__cookies: dict[str, str] = {}
6
+
7
7
  @property
8
8
  def cookies(self):
9
9
  return self.__cookies
10
-
10
+
11
11
  def set_cookie(
12
12
  self,
13
13
  cookie_name: str,
@@ -25,10 +25,10 @@ class CookieManager:
25
25
 
26
26
  if httponly:
27
27
  cookie += ' HttpOnly;'
28
-
28
+
29
29
  if secure:
30
30
  cookie += ' Secure;'
31
-
31
+
32
32
  if samesite:
33
33
  if samesite not in ['Strict', 'Lax']:
34
34
  raise ValueError("SameSite is not valid. Please use one of: ['Strict', 'Lax']")
@@ -54,9 +54,9 @@ class CookieManager:
54
54
 
55
55
  expires_str = expires.strftime("%a, %d %b %Y %H:%M:%S GMT")
56
56
  cookie += f' Expires={expires_str};'
57
-
57
+
58
58
  if isinstance(max_age, (int, float)) and max_age > 0:
59
59
  cookie += f' Max-Age={max_age};'
60
-
60
+
61
61
  if cookie not in self.__cookies:
62
- self.__cookies.append(cookie)
62
+ self.__cookies[cookie_name] = cookie
@@ -307,6 +307,17 @@ class ElementConstrutor: # pragma: no cover
307
307
  def content(self):
308
308
  return self.__content
309
309
 
310
+ @property
311
+ def text_content(self):
312
+ uuids = re.findall(pattern=r'\{\{(.*?)\}\}', string=self.content or '')
313
+ child_uuids = {child.uuid for child in self.childs if child.uuid in uuids}
314
+ result = self.content or ''
315
+
316
+ for uuid in child_uuids:
317
+ u_ = "{{" + uuid + "}}"
318
+ result = result.replace(u_, '')
319
+ return result.strip()
320
+
310
321
  @content.setter
311
322
  def content(self, value: str):
312
323
  if value is None:
@@ -471,7 +482,6 @@ class ElementConstrutor: # pragma: no cover
471
482
  value = self.to_html(element=value)
472
483
 
473
484
  content = content.replace("{{" + r + "}}", str(value))
474
-
475
485
  return content
476
486
 
477
487
  def create_event_id(self, event: Union[Callable, str], type: str, element_id: str = None):
@@ -42,24 +42,24 @@ class Request: # pragma: no cover
42
42
  self.__raw_request_asgi = headers
43
43
  self.__raw_body = body or b''
44
44
  self.__raw_headers: list[tuple[bytes, bytes]] = headers.get('headers', [])
45
-
45
+
46
46
  else:
47
47
  raise TypeError('Request type does not valid')
48
-
48
+
49
49
  self.client_info = client_info
50
50
  self.__additions_headers()
51
-
51
+
52
52
  @property
53
53
  def request_mode(self):
54
54
  return self.__request_mode
55
-
55
+
56
56
  @request_mode.setter
57
57
  def request_mode(self, value):
58
58
  if not isinstance(value, RequestMode):
59
59
  raise TypeError('Request mode does not valid')
60
-
60
+
61
61
  self.__request_mode = value
62
-
62
+
63
63
  @property
64
64
  def client_info(self): return self.__client_info
65
65
 
@@ -67,63 +67,63 @@ class Request: # pragma: no cover
67
67
  def client_info(self, value: ClientInfo):
68
68
  if value and not isinstance(value, ClientInfo):
69
69
  raise TypeError('client_info must be a ClientInfo instances')
70
-
70
+
71
71
  self.__client_info = value or ClientInfo(host=None, port=0)
72
-
72
+
73
73
  @property
74
74
  def raw_headers(self):
75
75
  if self.request_mode.value == 'asgi':
76
76
  return self.__raw_request_asgi
77
-
77
+
78
78
  return self.__raw_request_wsgi
79
-
79
+
80
80
  @property
81
81
  def raw_body(self):
82
82
  return self.__raw_body
83
-
83
+
84
84
  @property
85
85
  def host(self):
86
86
  return self.headers.get('host')
87
-
87
+
88
88
  @property
89
89
  def port(self):
90
90
  try:
91
91
  return int(self.headers.get('host', '0').split(':')[-1])
92
92
  except:
93
93
  return 0
94
-
94
+
95
95
  @property
96
96
  def content_length(self):
97
97
  return int(self.headers.get('content-length'))
98
-
98
+
99
99
  @property
100
100
  def content_type(self):
101
101
  return self.headers.get('content-type', '')
102
-
102
+
103
103
  @property
104
104
  def user_agent(self):
105
105
  return self.headers.get('user-agent')
106
-
106
+
107
107
  @property
108
108
  def origin(self):
109
109
  return self.headers.get('origin')
110
-
110
+
111
111
  @property
112
112
  def referrer(self):
113
113
  return self.headers.get('referrer')
114
-
114
+
115
115
  @property
116
116
  def accept(self):
117
117
  return [val.strip().split(';') for val in self.headers.get('accept', '').split(',') if val]
118
-
118
+
119
119
  @property
120
120
  def accept_encoding(self):
121
121
  return [val.strip().split(';') for val in self.headers.get('accept-encoding', '').split(',') if val]
122
-
122
+
123
123
  @property
124
124
  def accept_language(self):
125
125
  return [val.strip().split(';') for val in self.headers.get('accept-language', '').split(',') if val]
126
-
126
+
127
127
  @property
128
128
  def cookies(self):
129
129
  return {cookie.split('=')[0]: cookie.split('=')[-1] for cookie in self.headers.get('cookie', '').split(';') if cookie}
@@ -136,9 +136,9 @@ class Request: # pragma: no cover
136
136
  def headers(self):
137
137
  if self.request_mode.value == 'asgi':
138
138
  return {header[0].decode(): header[1].decode() for header in self.__raw_headers}
139
-
139
+
140
140
  return self.__parse_headers_wsgi()
141
-
141
+
142
142
  @property
143
143
  def body(self) -> Union[dict[str, Union[list[File], str]]]:
144
144
  if self.content_type == ContentTypes.json.value:
@@ -148,15 +148,18 @@ class Request: # pragma: no cover
148
148
  elif ContentTypes.form_data.value in self.content_type:
149
149
  return self.__parse_form_data()
150
150
  return {'body': self.__raw_body}
151
-
151
+
152
152
  @property
153
153
  def first_line(self):
154
154
  if self.request_mode.value == 'wsgi':
155
155
  return self.__raw_headers.split(self.__line_splitter, 1)[0].strip()
156
-
156
+
157
157
  full_path = f"{self.path}?{'&'.join(['{key}={value}'.format(key=key, value=value) for key, value in self.query_params.items()])}" if self.query_params else self.path
158
158
  return f"{self.method} {full_path} {self.scheme}"
159
159
 
160
+ @property
161
+ def full_path(self): return self.first_line.split(' ', 2)[1].strip()
162
+
160
163
  def __parse_form_data(self):
161
164
  fs = FieldStorage(self.content_type, callbacks=self.__raw_body)
162
165
  body: dict[str, list[File] | str] = {}
@@ -174,10 +177,10 @@ class Request: # pragma: no cover
174
177
 
175
178
  body[field.name].append(field.value)
176
179
  else:
177
- body[field.name] = field.value
178
-
180
+ body[field.name] = field.value
181
+
179
182
  return body
180
-
183
+
181
184
  def __additions_headers(self):
182
185
  if self.request_mode.value == 'asgi':
183
186
  self.method: str = self.raw_headers.get('method')
@@ -193,17 +196,17 @@ class Request: # pragma: no cover
193
196
  self.query_params = {
194
197
  key: ';'.join(val) for key, val in parse_qs(line_info[1].split('?', 1)[-1]).items() if val
195
198
  } if len(line_info) >= 2 else {}
196
-
199
+
197
200
  def __parse_headers_wsgi(self) -> dict[str, str]:
198
201
  return {header.split(':', 1)[0].strip().lower(): header.split(':', 1)[-1].strip() for header in self.__raw_headers.split(self.__line_splitter)[1::]}
199
-
202
+
200
203
  @property
201
204
  def __line_splitter(self):
202
205
  return '\r\n'
203
-
206
+
204
207
  @property
205
208
  def request_parts_splitter(self):
206
209
  return '\r\n\r\n'
207
210
 
208
211
  def __repr__(self):
209
- return f"Request(method={self.method}, mode={self.request_mode})"
212
+ return f"Request(method={self.method}, mode={self.request_mode})"
@@ -10,7 +10,7 @@ class Response:
10
10
  request: Request,
11
11
  response_content: bytes,
12
12
  code: int,
13
- cookies: list[str],
13
+ cookies: dict[str, str],
14
14
  response_type: ContentTypes,
15
15
  route: str
16
16
  ):
@@ -30,7 +30,7 @@ class Response:
30
30
  "Server": 'Pyweber/1.0',
31
31
  "Date": datetime.now(timezone.utc).strftime("%a, %d %b %Y %H:%M:%S GMT"),
32
32
  "Set-Cookie": cookies,
33
- "Request-Path": request.path,
33
+ "Request-Path": request.full_path,
34
34
  "Response-Path": route,
35
35
  "Access-Control-Allow-Origin": request.origin,
36
36
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
@@ -153,7 +153,7 @@ class Response:
153
153
  for key, value in self.headers.items():
154
154
  if key == 'Set-Cookie':
155
155
  for cookie in value:
156
- response += f'{key}: {cookie}\r\n'
156
+ response += f'{key}: {value[cookie]}\r\n'
157
157
 
158
158
  elif key == 'Response':
159
159
  pass
@@ -109,7 +109,12 @@ class Route: # pragma: no cover
109
109
  self.__callback = callback or self.template if callable(self.template) else lambda **kwargs: self.template
110
110
 
111
111
  @property
112
- def full_route(self): return f"/{self.group.removeprefix('__')}{self.route}" if self.group != self.default_group() else self.route
112
+ def full_route(self):
113
+ return f"/{self.group.removeprefix('__')}{self.route}" if self.group != self.default_group() else self.route
114
+
115
+ @property
116
+ def full_route_with_params(self):
117
+ return f"{self.full_route}{('?' + self.route_with_params.split('?',1)[-1] if self.query_params else '')}"
113
118
 
114
119
  @property
115
120
  def middlewares(self): return self.__middlewares
@@ -330,6 +335,7 @@ class Route: # pragma: no cover
330
335
  f'group={self.group}, '
331
336
  f'route={self.route}, '
332
337
  f'full_route={self.full_route}, '
338
+ f'route_with_params={self.route_with_params}, '
333
339
  f'name={self.name}, '
334
340
  f'methods={self.methods}, '
335
341
  f'status_code={self.status_code})'
@@ -542,6 +548,7 @@ class RouteManager: # pragma: no cover
542
548
  if follow_redirect in [True, 1] and self.is_redirected(route=path):
543
549
  redirect_route = self.get_redirected_route(route=path)
544
550
  return redirect_route.route
551
+
545
552
  return self.__routes.get(path)
546
553
 
547
554
  def get_route_by_name(self, name: str):
@@ -589,14 +596,25 @@ class RouteManager: # pragma: no cover
589
596
  def __resolve_path__(route: str, list_routes: dict[str, Route | RedirectRoute]):
590
597
  kwargs: dict[str, str] = {}
591
598
 
599
+ # Separa path dos query params antes de qualquer processamento
600
+ clean_route, _, query_string = route.partition('?')
601
+
602
+ # Parse dos query params
603
+ query_params: dict[str, str] = {}
604
+ if query_string:
605
+ for pair in query_string.split('&'):
606
+ key, _, val = pair.partition('=')
607
+ if key:
608
+ query_params[key] = val
609
+
592
610
  for path in list_routes:
593
611
  l_route = path.strip('/').split('/')
594
- r_route = route.strip('/').split('/')
612
+ r_route = clean_route.strip('/').split('/') # usa o path limpo
595
613
 
596
614
  if len(l_route) != len(r_route):
597
615
  continue
598
616
 
599
- if '{' in path and len(route) == 1:
617
+ if '{' in path and len(clean_route) == 1:
600
618
  continue
601
619
 
602
620
  match = True
@@ -611,9 +629,9 @@ class RouteManager: # pragma: no cover
611
629
  break
612
630
 
613
631
  if match:
614
- return path, kwargs
632
+ return path, {**kwargs, **query_params} # merge kwargs + query_params
615
633
 
616
- return route, kwargs
634
+ return clean_route, query_params
617
635
 
618
636
  @staticmethod
619
637
  def inspect_function(callback: Callable):
@@ -476,25 +476,28 @@ class Pyweber(
476
476
  try:
477
477
  template = state_result.template
478
478
 
479
+ kwargs = {**self.request.body, **self.request.query_params, 'request': self.request} if self.request else {}
479
480
  while callable(template) or isinstance(template, RedirectRoute):
481
+ kwargs = {**kwargs, **state_result.kwargs}
482
+
480
483
  if callable(template):
481
- request_params = {**self.request.body, **self.request.query_params, 'request': self.request} if self.request else {}
482
- kwargs = OpenApiProcessor.prepare_callback_kwargs(
483
- callback=state_result.callback,
484
- **{**state_result.kwargs, **request_params}
485
- )
484
+ kwargs = {
485
+ **kwargs,
486
+ **OpenApiProcessor.prepare_callback_kwargs(callback=state_result.callback, **kwargs)
487
+ }
486
488
 
487
489
  template = await template(**kwargs) if inspect.iscoroutinefunction(template) else template(**kwargs)
488
490
 
489
491
  if isinstance(template, RedirectRoute):
490
- redirect_path = self.build_route(route=template.route.full_route, **{**state_result.kwargs, **template.kwargs})
492
+ kwargs = {**kwargs, **template.kwargs}
493
+ redirect_path = self.build_route(route=template.route.full_route_with_params, **kwargs)
491
494
 
492
495
  self._check_recursion(route=redirect_path)
493
496
  state_result = await self._process_redirect_route(
494
497
  state=state_result,
495
498
  redirect_route=template,
496
499
  redirect_path=redirect_path,
497
- **state_result.kwargs
500
+ **kwargs
498
501
  )
499
502
 
500
503
  template = state_result.template
@@ -568,6 +571,14 @@ class Pyweber(
568
571
  methods=['post'],
569
572
  content_type=ContentTypes.json
570
573
  ),
574
+ Route(
575
+ route='/_pyweber/check-cookies',
576
+ template={'message': 'OK'},
577
+ methods=['get'],
578
+ title='Get Cookies',
579
+ process_response=False,
580
+ content_type=ContentTypes.json
581
+ ),
571
582
  Route(
572
583
  route='/docs',
573
584
  template=StaticFilePath.pyweber_docs.value,
@@ -117,6 +117,7 @@ function connectWebSocket() {
117
117
  }
118
118
 
119
119
  if (data.open) {
120
+ await update_cookies();
120
121
  data.open.new_page
121
122
  ? window.open(data.open.path, '_blank')
122
123
  : (window.location.href = data.open.path);
@@ -232,6 +233,13 @@ function connectWebSocket() {
232
233
  return socket;
233
234
  }
234
235
 
236
+ async function update_cookies() {
237
+ await fetch(`/_pyweber/check-cookies`, {
238
+ method: 'GET',
239
+ headers: { 'Content-Type': 'application/json' }
240
+ });
241
+ }
242
+
235
243
  // ─── Envio de ficheiro via HTTP (mantém binário puro, sem compressão JSON) ────
236
244
  async function send_file_chunk(file_id, status, data) {
237
245
  await fetch(`/_pyweber/file_chunk?file_id=${file_id}&status=${status}`, {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyweber
3
- Version: 1.2.0.dev20260425
3
+ Version: 1.2.0.dev20260427
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