pyweber 1.2.0.dev20260424__tar.gz → 1.2.0.dev20260426__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.dev20260424/pyweber.egg-info → pyweber-1.2.0.dev20260426}/PKG-INFO +1 -1
  2. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyproject.toml +1 -1
  3. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/core/element.py +220 -77
  4. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/core/template.py +39 -104
  5. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/cookies.py +8 -8
  6. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/element.py +31 -16
  7. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/response.py +2 -2
  8. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/routes.py +9 -1
  9. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/pyweber/pyweber.py +5 -3
  10. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426/pyweber.egg-info}/PKG-INFO +1 -1
  11. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/LICENSE +0 -0
  12. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/MANIFEST.in +0 -0
  13. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/README.md +0 -0
  14. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/__init__.py +0 -0
  15. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/admin/index.html +0 -0
  16. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/admin/src/script.js +0 -0
  17. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/admin/src/style.css +0 -0
  18. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/cli/__init__.py +0 -0
  19. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/cli/commands.py +0 -0
  20. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/components/__init__.py +0 -0
  21. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/components/form.py +0 -0
  22. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/components/general.py +0 -0
  23. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/components/input.py +0 -0
  24. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/config/__init__.py +0 -0
  25. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/config/config.py +0 -0
  26. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/connection/__init__.py +0 -0
  27. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/connection/http.py +0 -0
  28. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/connection/reload.py +0 -0
  29. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/connection/selector.py +0 -0
  30. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/connection/session.py +0 -0
  31. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/connection/websocket.py +0 -0
  32. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/core/__init__.py +0 -0
  33. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/core/events.py +0 -0
  34. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/core/window.py +0 -0
  35. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/__init__.py +0 -0
  36. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/create_app.py +0 -0
  37. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/error_pages.py +0 -0
  38. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/field.py +0 -0
  39. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/field_storage.py +0 -0
  40. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/file.py +0 -0
  41. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/file_stream.py +0 -0
  42. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/headers.py +0 -0
  43. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/middleware.py +0 -0
  44. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/openapi.py +0 -0
  45. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/request.py +0 -0
  46. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/run.py +0 -0
  47. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/strem_stats.py +0 -0
  48. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/task_manager.py +0 -0
  49. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/template_diff.py +0 -0
  50. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/ws_message.py +0 -0
  51. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/pyweber/__init__.py +0 -0
  52. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/.gitignore +0 -0
  53. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/config.toml +0 -0
  54. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/css.css +0 -0
  55. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/docs.html +0 -0
  56. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/favicon/android-chrome-192x192.png +0 -0
  57. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/favicon/android-chrome-512x512.png +0 -0
  58. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/favicon/apple-touch-icon.png +0 -0
  59. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/favicon/favicon-16x16.png +0 -0
  60. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/favicon/favicon-32x32.png +0 -0
  61. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/favicon/favicon.ico +0 -0
  62. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/favicon/pyweber.png +0 -0
  63. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/favicon/site.webmanifest +0 -0
  64. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/handlers.js +0 -0
  65. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/html.html +0 -0
  66. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/html401.html +0 -0
  67. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/html404.html +0 -0
  68. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/html500.html +0 -0
  69. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/js.js +0 -0
  70. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/loading.html +0 -0
  71. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/main.py +0 -0
  72. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/pyweber.css +0 -0
  73. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/update.py +0 -0
  74. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/utils/__init__.py +0 -0
  75. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/utils/exceptions.py +0 -0
  76. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/utils/loads.py +0 -0
  77. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/utils/types.py +0 -0
  78. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/utils/utils.py +0 -0
  79. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber.egg-info/SOURCES.txt +0 -0
  80. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber.egg-info/dependency_links.txt +0 -0
  81. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber.egg-info/entry_points.txt +0 -0
  82. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber.egg-info/requires.txt +0 -0
  83. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber.egg-info/top_level.txt +0 -0
  84. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/setup.cfg +0 -0
  85. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/tests/test_config.py +0 -0
  86. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/tests/test_cookies.py +0 -0
  87. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/tests/test_response.py +0 -0
  88. {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/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.dev20260424
3
+ Version: 1.2.0.dev20260426
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.dev20260424"
7
+ version = "1.2.0.dev20260426"
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,4 +1,7 @@
1
+ import re
1
2
  from uuid import uuid4
3
+ import lxml.html as HTMLPARSER
4
+ from lxml.html import fromstring
2
5
  from typing import Union, Any, Literal
3
6
  from pyweber.utils.types import HTMLTag, GetBy
4
7
  from pyweber.models.file import File
@@ -8,6 +11,8 @@ from pyweber.models.element import (
8
11
  ChildElements
9
12
  )
10
13
 
14
+ SEARCH_MODE = Literal['exact', 'regex', 'contains', 'startswith', 'endswith']
15
+
11
16
  class Element(ElementConstrutor): # pragma: no cover
12
17
  def __init__(
13
18
  self,
@@ -23,6 +28,7 @@ class Element(ElementConstrutor): # pragma: no cover
23
28
  data: Any = None,
24
29
  sanitize: bool = False,
25
30
  files: list[File] = None,
31
+ include_uuid: bool = True,
26
32
  **kwargs: str
27
33
  ):
28
34
  super().__init__(
@@ -37,72 +43,73 @@ class Element(ElementConstrutor): # pragma: no cover
37
43
  events=events,
38
44
  sanitize=sanitize,
39
45
  files=files,
46
+ include_uuid=include_uuid,
40
47
  **kwargs
41
48
  )
42
49
  self.uuid = getattr(self, 'uuid', None) or str(uuid4())
43
50
  self.data = data
44
51
  self.__element_methods: dict[str, dict[str, Any]] = {}
45
-
52
+
46
53
  @property
47
54
  def parent(self):
48
55
  return self.__parent
49
-
56
+
50
57
  @parent.setter
51
58
  def parent(self, value: 'Element'):
52
59
  if value is None:
53
60
  self.__parent = None
54
61
  return
55
-
62
+
56
63
  if not isinstance(value, Element):
57
64
  raise TypeError("Parent must be an Element instance")
58
65
 
59
66
  self.__parent = value
60
-
67
+
61
68
  @property
62
69
  def childs(self):
63
70
  return self.__childs
64
-
71
+
65
72
  @childs.setter
66
73
  def childs(self, value: ChildElements):
67
74
  if not isinstance(value, (list, ChildElements)):
68
75
  raise TypeError(f"Children must be a ChildElements instances, but got {type(value).__name__}")
69
-
76
+
70
77
  if isinstance(value, list):
71
78
  value = ChildElements(self).extend(value)
72
79
 
73
80
  value = self.__render_dynamic_elements(childs=value)
74
-
81
+
75
82
  self.__childs = value
76
-
83
+
77
84
  @property
78
85
  def index(self) -> Union[int, None]:
79
86
  return self.parent.childs.index(self) if self.parent else None
80
-
87
+
81
88
  def first_child(self) -> Union['Element', None]:
82
89
  return self.childs[0] if self.childs else None
83
-
90
+
84
91
  def last_child(self) -> Union['Element', None]:
85
92
  return self.childs[-1] if self.childs else None
86
-
93
+
87
94
  def previous_child(self) -> Union['Element', None]:
88
95
  if self.parent:
89
96
  return self.parent.childs[self.index-1] if len(self.parent.childs) > 0 else None
90
-
97
+
91
98
  def next_child(self) ->Union['Element', None]:
92
99
  if self.parent:
93
100
  return self.parent.childs[self.index+1] if len(self.parent.childs) >= self.index+1 else None
94
-
101
+
95
102
  def add_child(self, child: 'Element'):
96
103
  if not isinstance(child, Element):
97
104
  raise TypeError("Child must be Element instances")
98
-
105
+
99
106
  self.__childs.append(child)
100
107
  child.parent = self
101
-
108
+
102
109
  def remove_child(self, child: 'Element'):
103
110
  if child not in self.__childs:
104
111
  raise IndexError('Child not defined for this parent Element')
105
-
112
+
106
113
  self.__childs.remove(child)
107
114
  child.parent = None
108
115
 
@@ -111,11 +118,11 @@ class Element(ElementConstrutor): # pragma: no cover
111
118
  raise TypeError(f'Index must be a integer, but you got {type(index).__name__}')
112
119
 
113
120
  return self.__childs.pop(index)
114
-
121
+
115
122
  def remove(self):
116
123
  if self.parent:
117
124
  self.parent.remove_child(self)
118
-
125
+
119
126
  def focus(self):
120
127
  self.set_selection_range(self.selection_end, self.selection_end)
121
128
  self.__set_element_methods(method='focus')
@@ -125,99 +132,150 @@ class Element(ElementConstrutor): # pragma: no cover
125
132
 
126
133
  def select(self):
127
134
  self.__set_element_methods(method='select')
128
-
135
+
129
136
  def click(self):
130
137
  self.__set_element_methods(method='click')
131
-
138
+
132
139
  def scroll_into_view(
133
140
  self,
134
141
  behavior: Literal['instant', 'smooth'] = 'instant',
135
142
  block: Literal['start', 'center', 'end'] = 'start'
136
143
  ):
137
144
  self.__set_element_methods(method='scrollIntoView', behavior=behavior, block=block)
138
-
145
+
139
146
  def set_selection_range(self, start: int, end: str):
140
147
  self.__set_element_methods(method='setSelectionRange', start=start, end=end)
141
-
142
- def getElement(self, by: GetBy, value: str, element: 'Element' = None) -> 'Element':
143
- results = self.getElements(by=by, value=value, element=element)
148
+
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)
144
151
 
145
152
  return results[0] if results else None
146
-
147
- def getElements(self, by: GetBy, value: str, element: 'Element' = None) -> list['Element']:
153
+
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
+
148
222
  element = element or self
149
223
  results: list['Element'] = []
150
224
 
151
225
  if isinstance(by, GetBy):
152
226
  by: str = by.value
153
-
227
+
154
228
  if by == 'classes':
155
- if set(value.split()) <= set(element.classes):
229
+ if matches(element.classes, value):
156
230
  results.append(element)
157
-
158
- elif by in ['attrs', 'style']:
159
- conditions: list[str] = [pair.strip() for pair in value.split(';') if pair.strip()]
160
- has = False
161
-
162
- el: dict[str, str] = getattr(element, by, {})
163
- for condition in conditions:
164
- key, _, val = condition.partition(':')
165
231
 
166
- if not val: key, _, val = condition.partition('=')
167
-
168
- if not val:
169
- if key.strip() in el:
170
- has = True
171
-
172
- elif key.strip() in el and el.get(key.strip(), None) == val.strip():
173
- has = True
174
-
175
- if has:
232
+ elif by in ['attrs', 'style']:
233
+ if matches(getattr(element, by, {}), value):
176
234
  results.append(element)
177
-
178
- elif getattr(element, by, None) == value:
235
+
236
+ elif matches(getattr(element, by, None), value):
179
237
  results.append(element)
180
-
238
+
181
239
  if element.childs:
182
240
  for child in element.childs:
183
- results.extend(self.getElements(by=by, value=value, element=child))
184
-
241
+ results.extend(self.getElements(by=by, value=value, element=child, search_mode=search_mode))
242
+
185
243
  return results
186
-
187
- def querySelector(self, selector: str, element: 'Element' = None) -> 'Element':
188
- results = self.querySelectorAll(selector=selector, element=element)
244
+
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)
189
247
 
190
248
  return results[0] if results else None
191
-
192
- def querySelectorAll(self, selector: str, element: 'Element' = None) -> list['Element']:
249
+
250
+ def querySelectorAll(self, selector: str, element: 'Element' = None, search_mode: SEARCH_MODE = 'exact') -> list['Element']:
193
251
  element = element or self
194
252
  results: list['Element'] = []
195
253
 
196
254
  if selector.startswith('.'):
197
255
  classes = ' '.join(selector.split('.')).strip()
198
- return self.getElements(by=GetBy.classes, value=classes)
199
-
256
+ return self.getElements(by=GetBy.classes, value=classes, search_mode=search_mode)
257
+
200
258
  elif selector.startswith('#'):
201
259
  if selector[1:].strip() == element.id:
202
260
  results.append(element)
203
-
261
+
204
262
  elif selector.startswith('['):
205
263
  sel = selector.removeprefix('[').removesuffix(']')
206
- return self.getElements(by=GetBy.attrs, value=sel)
207
-
264
+ return self.getElements(by=GetBy.attrs, value=sel, search_mode=search_mode)
265
+
208
266
  else:
209
267
  if selector.strip() == element.tag:
210
268
  results.append(element)
211
-
269
+
212
270
  for child in element.childs:
213
- results.extend(self.querySelectorAll(selector=selector, element=child))
271
+ results.extend(self.querySelectorAll(selector=selector, element=child, search_mode=search_mode))
214
272
 
215
273
  return results
216
-
274
+
217
275
  @property
218
276
  def clone(self):
219
277
  from pyweber.core.events import TemplateEvents
220
-
278
+
221
279
  element = self
222
280
 
223
281
  cln = Element(
@@ -240,20 +298,20 @@ class Element(ElementConstrutor): # pragma: no cover
240
298
  cln.childs.append(child.clone)
241
299
 
242
300
  return cln
243
-
301
+
244
302
  def get_element_methods(self):
245
303
  return self.__element_methods
246
-
304
+
247
305
  def __set_element_methods(self, method: str, **kwargs):
248
306
  self.__element_methods[method] = kwargs
249
-
307
+
250
308
  def remove_element_methods(self, method: Any = None):
251
309
  if not method:
252
310
  self.__element_methods.clear()
253
311
  return
254
-
312
+
255
313
  self.__element_methods.pop(method, None)
256
-
314
+
257
315
  def __render_dynamic_elements(self, childs: ChildElements):
258
316
  new_childs: ChildElements = ChildElements(self)
259
317
  if childs:
@@ -261,7 +319,7 @@ class Element(ElementConstrutor): # pragma: no cover
261
319
  if isinstance(child, str):
262
320
  if not child.startswith('{{') or not child.endswith('}}'):
263
321
  raise ValueError("{} must be starts with '{{' and ends with '}}'".format(child))
264
-
322
+
265
323
  key = child.removeprefix('{{').removesuffix('}}').strip()
266
324
  element = self.kwargs.get(key, None)
267
325
 
@@ -270,18 +328,103 @@ class Element(ElementConstrutor): # pragma: no cover
270
328
 
271
329
  elif isinstance(element, ElementConstrutor):
272
330
  new_childs.append(element)
273
-
331
+
274
332
  elif isinstance(child, ElementConstrutor):
275
333
  new_childs.append(child)
276
-
334
+
277
335
  else:
278
336
  raise TypeError(f'all childs must be str or Element instances, but got {type(child).__name__}')
279
-
337
+
280
338
  return new_childs
281
339
 
340
+ @classmethod
341
+ def from_html(cls, html: str, include_uuid: bool = True, **kwargs):
342
+ HtmlElement = fromstring(html.strip())
343
+ return cls._create_element(HTMLElement=HtmlElement, parent=None, include_uuid=include_uuid, **kwargs)
344
+
345
+ @classmethod
346
+ def _create_element(cls, HTMLElement, parent=None, include_uuid: bool = True, **kwargs):
347
+
348
+ def gettail(val):
349
+ try: return val.strip()
350
+ except: return val
351
+
352
+ name = 'comment' if isinstance(HTMLElement, HTMLPARSER.HtmlComment) else HTMLElement.tag
353
+ attrib = HTMLElement.attrib
354
+
355
+ id_ = cls.render_dynamic_values(attrib.pop('id', None), **kwargs)
356
+ class_str = cls.render_dynamic_values(attrib.pop('class', None), **kwargs)
357
+ classes = class_str.split() if class_str else []
358
+
359
+ style_str = cls.render_dynamic_values(attrib.pop('style', None), **kwargs)
360
+ style_dict = {}
361
+ if style_str:
362
+ for pair in [s.strip() for s in style_str.split(';') if s.strip()]:
363
+ if ':' in pair:
364
+ k, v = pair.split(':', 1)
365
+ style_dict[k.strip()] = v.strip()
366
+
367
+ uuid = attrib.pop('uuid', None)
368
+ value = cls.render_dynamic_values(attrib.pop('value', None), **kwargs)
369
+
370
+ content = cls.render_dynamic_values(HTMLElement.text or None, **kwargs)
371
+
372
+ events_dict = {k[1:]: attrib.pop(k) for k in list(attrib) if k.startswith('_on')}
373
+ event_obj = TemplateEvents()
374
+ for key, event in events_dict.items():
375
+ if hasattr(event_obj, key): setattr(event_obj, key, event)
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
+
382
+ element = cls(
383
+ tag=name,
384
+ id=id_,
385
+ classes=classes,
386
+ value=value,
387
+ content=content,
388
+ events=event_obj,
389
+ style=style_dict,
390
+ attrs=attrs,
391
+ childs=None,
392
+ sanitize=False,
393
+ files=[],
394
+ include_uuid=include_uuid,
395
+ **kwargs
396
+ )
397
+ element.parent = parent
398
+ element.uuid = uuid
399
+
400
+ if element.tag == 'select' and value:
401
+ __xyza = value
402
+
403
+ for child in HTMLElement.getchildren():
404
+ child_el = cls._create_element(
405
+ HTMLElement=child,
406
+ parent=element,
407
+ include_uuid=include_uuid,
408
+ **kwargs
409
+ )
410
+ element.childs.append(child_el)
411
+ get_tail = gettail(child.tail)
412
+
413
+ if get_tail:
414
+ uuid_child = "{{" + child_el.uuid + "}}"
415
+ element.content = cls.render_dynamic_values(
416
+ content=(element.content or '') + f"{uuid_child} {get_tail}",
417
+ **kwargs
418
+ )
419
+
420
+ if element.tag == 'select' and element.childs:
421
+ try: element.value = __xyza
422
+ except: pass
423
+ return element
424
+
282
425
  def update(self):
283
426
  raise NotImplementedError
284
-
427
+
285
428
  def __deepy_clone(self, obj):
286
429
  if isinstance(obj, list):
287
430
  return [self.__deepy_clone(item) for item in obj]
@@ -289,6 +432,6 @@ class Element(ElementConstrutor): # pragma: no cover
289
432
  return {chave: self.__deepy_clone(valor) for chave, valor in obj.items()}
290
433
  else:
291
434
  return obj
292
-
435
+
293
436
  def __str__(self):
294
- return self.to_html()
437
+ return self.to_html()
@@ -1,10 +1,6 @@
1
1
  import os
2
- import re
3
2
  from uuid import uuid4
4
- import lxml.html as HTMLPARSER
5
- from lxml.etree import Element as LXML_Element
6
- from pyweber.core.events import TemplateEvents
7
- from pyweber.core.element import Element
3
+ from pyweber.core.element import Element, SEARCH_MODE
8
4
  from pyweber.utils.loads import LoadStaticFiles
9
5
  from pyweber.config.config import config
10
6
  from pyweber.utils.types import HTTPStatusCode, GetBy
@@ -77,34 +73,54 @@ class Template: # pragma: no cover
77
73
  return str(config['app'].get('icon'))
78
74
 
79
75
  def parse_html(self, html: str = None):
80
- if not html:
81
- html = self.__template
82
-
76
+ if not html: html = self.__template
83
77
  return self.__inject_default_elements(root=self.__parse_html(html=html))
84
78
 
85
79
  def build_html(self, include_doctype: bool = True):
86
- html = self.root.to_html(include_uuid=self.__include_uuid)
80
+ html = self.root.to_html()
87
81
 
88
82
  if include_doctype:
89
83
  html = f'<!DOCTYPE html>\n{html}'
90
84
 
91
85
  return html
92
86
 
93
- 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
+ ):
94
94
  if not element: element = self.root
95
- return element.getElement(by=by, value=value)
96
-
97
- 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
+ ):
98
104
  if not element: element = self.root
99
- return element.getElements(by=by, value=value)
100
-
101
- 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
+ ):
102
113
  if element is None: element = self.__root
103
- return element.querySelector(selector=selector)
104
-
105
- 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]:
106
122
  if element is None: element = self.__root
107
- return element.querySelectorAll(selector=selector)
123
+ return element.querySelectorAll(selector=selector, search_mode=search_mode)
108
124
 
109
125
  def __parse_html(self, html: str) -> Element:
110
126
  if not html.replace('<!DOCTYPE html>', '').strip().startswith('<html'):
@@ -112,94 +128,13 @@ class Template: # pragma: no cover
112
128
  html = f'<body>{html}</body>'
113
129
  html = f'<html>{html}</html>'
114
130
 
115
- root: HTMLPARSER.HtmlElement = HTMLPARSER.fromstring(html=html)
116
-
117
- if root.find(path='head') is None:
118
- root.insert(0, LXML_Element('head'))
131
+ element = Element.from_html(html=html, include_uuid=self.__include_uuid, **self.kwargs)
119
132
 
120
- return self.__create_element(HTMLElement=root)
121
-
122
- def __create_element(self, HTMLElement: HTMLPARSER.HtmlElement, parent: Element = None):
123
- def gettail(html_element: str | None):
124
- try:
125
- return html_element.strip()
126
- except:
127
- return html_element
128
-
129
- if isinstance(HTMLElement, HTMLPARSER.HtmlComment):
130
- name = 'comment'
131
- else:
132
- name = HTMLElement.tag
133
-
134
- id = self.__render_dynamic_values(content=HTMLElement.attrib.pop('id', None), include_uuid=self.__include_uuid)
135
-
136
- class_str = self.__render_dynamic_values(content=HTMLElement.attrib.pop('class', None), include_uuid=self.__include_uuid)
137
- classes = class_str.split() if class_str else []
138
-
139
- style_str: str = self.__render_dynamic_values(content=HTMLElement.attrib.pop('style', None), include_uuid=self.__include_uuid)
140
- style_dict = {}
141
-
142
- if style_str:
143
- style_pair = [s.strip() for s in style_str.split(';') if s.strip()]
144
-
145
- for pair in style_pair:
146
- if ':' in pair:
147
- key, value = pair.split(':', 1)
148
- style_dict[key.strip()] = value.strip()
149
-
150
- parent = parent
151
- uuid = HTMLElement.attrib.pop('uuid', None)
152
- value = self.__render_dynamic_values(content=HTMLElement.attrib.pop('value', None), include_uuid=self.__include_uuid)
153
- content: str = self.__render_dynamic_values(content=HTMLElement.text if HTMLElement.text else None, include_uuid=self.__include_uuid)
154
- events_dict = {key[1:]: HTMLElement.attrib.pop(key) for key in HTMLElement.attrib if key.startswith('_on')}
155
- childrens: list[HTMLPARSER.HtmlElement] = HTMLElement.getchildren()
156
-
157
- event_obj = TemplateEvents()
158
- for key, event in events_dict.items():
159
- if hasattr(event_obj, key):
160
- setattr(event_obj, key, event)
161
-
162
- element = Element(
163
- tag=name,
164
- id=id,
165
- classes=classes,
166
- value=value,
167
- content=content,
168
- events=event_obj,
169
- style=style_dict,
170
- attrs=dict(HTMLElement.attrib),
171
- **self.kwargs
172
- )
173
- element.parent = parent
174
- element.template = self
175
- element.uuid = uuid
176
-
177
- if parent:
178
- if gettail(HTMLElement.tail):
179
- parent.content = parent.content or ''
180
- parent.content += f"{{{element.uuid}}} {gettail(HTMLElement.tail)}"
181
-
182
- for child in childrens:
183
- element.childs.append(self.__create_element(child, element))
133
+ if element.tag == 'html':
134
+ if not element.querySelector('head'): element.childs.insert(0, Element('head'))
184
135
 
185
136
  return element
186
137
 
187
- def __render_dynamic_values(self, content: str, include_uuid: bool = True):
188
- if content:
189
- pattern = r'\{\{(.*?)\}\}'
190
- result = re.findall(pattern, content)
191
-
192
- if result:
193
- for r in result:
194
- value = self.kwargs.get(r.strip(), None)
195
- if value is not None:
196
- if isinstance(value, Element):
197
- value = self.to_html(element=value, include_uuid=include_uuid)
198
-
199
- content = content.replace("{{" + r + "}}", str(value))
200
-
201
- return content
202
-
203
138
  def __read_file(self, file_path: str) -> str:
204
139
  from pyweber.models.error_pages import ErrorPages
205
140
  if file_path.endswith('.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
@@ -16,6 +16,7 @@ from pyweber.utils.types import (
16
16
  )
17
17
 
18
18
  from pyweber.models.file import File
19
+ from questionary import checkbox
19
20
 
20
21
  if TYPE_CHECKING: # pragma: no cover
21
22
  from pyweber.core.template import Template
@@ -68,8 +69,10 @@ class ElementConstrutor: # pragma: no cover
68
69
  events: TemplateEvents,
69
70
  sanitize: bool,
70
71
  files: list[File],
72
+ include_uuid: bool,
71
73
  **kwargs: str
72
74
  ):
75
+ self.include_uuid = include_uuid
73
76
  self.sanitize = sanitize
74
77
  self.kwargs = kwargs
75
78
  self.tag = tag
@@ -147,9 +150,7 @@ class ElementConstrutor: # pragma: no cover
147
150
 
148
151
  @uuid.setter
149
152
  def uuid(self, value: str):
150
- if not value:
151
- self.__uuid = str(uuid4())
152
- return
153
+ if not value: value = str(uuid4())
153
154
 
154
155
  self.__uuid = value.strip()
155
156
 
@@ -306,6 +307,17 @@ class ElementConstrutor: # pragma: no cover
306
307
  def content(self):
307
308
  return self.__content
308
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
+
309
321
  @content.setter
310
322
  def content(self, value: str):
311
323
  if value is None:
@@ -347,16 +359,21 @@ class ElementConstrutor: # pragma: no cover
347
359
  if self.tag == 'textarea':
348
360
  self.content = value
349
361
 
350
- if self.tag == 'select':
362
+ elif self.tag == 'select':
351
363
  self.__value = None
352
364
  if hasattr(self, 'childs') and self.childs:
353
365
  for child in self.childs:
354
366
  if child.tag == 'option':
355
- if child.value == value:
367
+ if child.value == value or child.content == value:
356
368
  child.set_attr('selected', '')
357
369
  else:
358
370
  child.remove_attr('selected')
359
371
 
372
+ elif self.get_attr('type', None) == 'checkbox':
373
+ self.__value = None
374
+ if value == 'on':
375
+ self.set_attr('checked', '')
376
+
360
377
  @property
361
378
  def events(self):
362
379
  return self.__events
@@ -383,7 +400,7 @@ class ElementConstrutor: # pragma: no cover
383
400
 
384
401
  setattr(self.__events, event_type.value, None)
385
402
 
386
- def to_html(self, element: 'ElementConstrutor' = None, indent: int = 0, include_uuid: bool = True):
403
+ def to_html(self, element: 'Element' = None, indent: int = 0):
387
404
  if not element:
388
405
  element = self
389
406
 
@@ -391,7 +408,7 @@ class ElementConstrutor: # pragma: no cover
391
408
  raise TypeError(f'element must be an Element instances, but got {type(element).__name__}')
392
409
 
393
410
  indentation = ' ' * indent
394
- uuid_attribute = f' uuid="{element.uuid}"' if include_uuid else ""
411
+ uuid_attribute = f' uuid="{element.uuid}"' if self.include_uuid else ""
395
412
  html = f'{indentation}<{element.tag}{uuid_attribute}' if element.tag != 'comment' else f'{indentation}<!--'
396
413
 
397
414
  if element.id:
@@ -425,15 +442,15 @@ class ElementConstrutor: # pragma: no cover
425
442
  if element.tag != 'comment':
426
443
  html += '>'
427
444
 
428
- final_content = str((self.__render_dynamic_values(content=element.content, include_uuid=include_uuid) or ''))
445
+ final_content = str((self.render_dynamic_values(content=element.content, **self.kwargs) or ''))
429
446
  has_children = bool(element.childs)
430
447
 
431
448
  if has_children or '\n' in final_content:
432
449
  html += '\n'
433
450
 
434
451
  for child in element.childs:
435
- child_html = self.to_html(child, indent + 4, include_uuid=include_uuid)
436
- uuid_placeholder = f'{{{child.uuid}}}'
452
+ child_html = self.to_html(child, indent + 4)
453
+ uuid_placeholder = "{{" + child.uuid + "}}"
437
454
 
438
455
  if uuid_placeholder in final_content:
439
456
  final_content: str = final_content.replace(uuid_placeholder, child_html)
@@ -450,8 +467,8 @@ class ElementConstrutor: # pragma: no cover
450
467
 
451
468
  return html
452
469
 
453
- def __render_dynamic_values(self, content: str, include_uuid: bool = True):
454
-
470
+ @classmethod
471
+ def render_dynamic_values(self, content: str, **kwargs):
455
472
 
456
473
  if content:
457
474
  pattern = r'\{\{(.*?)\}\}'
@@ -459,16 +476,14 @@ class ElementConstrutor: # pragma: no cover
459
476
 
460
477
  if result:
461
478
  for r in result:
462
- value = self.kwargs.get(r.strip(), None)
479
+ value = kwargs.get(r.strip(), None)
463
480
  if value is not None:
464
481
  if isinstance(value, ElementConstrutor):
465
- value = self.to_html(element=value, include_uuid=include_uuid)
482
+ value = self.to_html(element=value)
466
483
 
467
484
  content = content.replace("{{" + r + "}}", str(value))
468
-
469
485
  return content
470
486
 
471
-
472
487
  def create_event_id(self, event: Union[Callable, str], type: str, element_id: str = None):
473
488
  from pyweber.core.events import EventBook
474
489
 
@@ -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
  ):
@@ -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})'
@@ -538,10 +544,12 @@ class RouteManager: # pragma: no cover
538
544
 
539
545
  def get_route_by_path(self, route: str, follow_redirect: bool = True):
540
546
  path, _ = self.resolve_path(route=route)
547
+ path = path.split('?', 1)[0]
541
548
 
542
549
  if follow_redirect in [True, 1] and self.is_redirected(route=path):
543
550
  redirect_route = self.get_redirected_route(route=path)
544
551
  return redirect_route.route
552
+
545
553
  return self.__routes.get(path)
546
554
 
547
555
  def get_route_by_name(self, name: str):
@@ -477,8 +477,9 @@ class Pyweber(
477
477
  template = state_result.template
478
478
 
479
479
  while callable(template) or isinstance(template, RedirectRoute):
480
+ request_params = {**self.request.body, **self.request.query_params, 'request': self.request} if self.request else {}
481
+
480
482
  if callable(template):
481
- request_params = {**self.request.body, **self.request.query_params, 'request': self.request} if self.request else {}
482
483
  kwargs = OpenApiProcessor.prepare_callback_kwargs(
483
484
  callback=state_result.callback,
484
485
  **{**state_result.kwargs, **request_params}
@@ -487,14 +488,15 @@ class Pyweber(
487
488
  template = await template(**kwargs) if inspect.iscoroutinefunction(template) else template(**kwargs)
488
489
 
489
490
  if isinstance(template, RedirectRoute):
490
- redirect_path = self.build_route(route=template.route.full_route, **{**state_result.kwargs, **template.kwargs})
491
+ kwargs = {**state_result.kwargs, **request_params, **template.kwargs}
492
+ redirect_path = self.build_route(route=template.route.full_route_with_params, **kwargs)
491
493
 
492
494
  self._check_recursion(route=redirect_path)
493
495
  state_result = await self._process_redirect_route(
494
496
  state=state_result,
495
497
  redirect_route=template,
496
498
  redirect_path=redirect_path,
497
- **state_result.kwargs
499
+ **kwargs
498
500
  )
499
501
 
500
502
  template = state_result.template
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyweber
3
- Version: 1.2.0.dev20260424
3
+ Version: 1.2.0.dev20260426
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