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.
- {pyweber-1.2.0.dev20260424/pyweber.egg-info → pyweber-1.2.0.dev20260426}/PKG-INFO +1 -1
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyproject.toml +1 -1
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/core/element.py +220 -77
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/core/template.py +39 -104
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/cookies.py +8 -8
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/element.py +31 -16
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/response.py +2 -2
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/routes.py +9 -1
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/pyweber/pyweber.py +5 -3
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426/pyweber.egg-info}/PKG-INFO +1 -1
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/LICENSE +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/MANIFEST.in +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/README.md +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/__init__.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/admin/index.html +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/admin/src/script.js +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/admin/src/style.css +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/cli/__init__.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/cli/commands.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/components/__init__.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/components/form.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/components/general.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/components/input.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/config/__init__.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/config/config.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/connection/__init__.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/connection/http.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/connection/reload.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/connection/selector.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/connection/session.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/connection/websocket.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/core/__init__.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/core/events.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/core/window.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/__init__.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/create_app.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/error_pages.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/field.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/field_storage.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/file.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/file_stream.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/headers.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/middleware.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/openapi.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/request.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/run.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/strem_stats.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/task_manager.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/template_diff.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/models/ws_message.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/pyweber/__init__.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/.gitignore +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/config.toml +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/css.css +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/docs.html +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/favicon/android-chrome-192x192.png +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/favicon/android-chrome-512x512.png +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/favicon/apple-touch-icon.png +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/favicon/favicon-16x16.png +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/favicon/favicon-32x32.png +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/favicon/favicon.ico +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/favicon/pyweber.png +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/favicon/site.webmanifest +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/handlers.js +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/html.html +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/html401.html +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/html404.html +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/html500.html +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/js.js +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/loading.html +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/main.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/pyweber.css +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/update.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/utils/__init__.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/utils/exceptions.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/utils/loads.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/utils/types.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/utils/utils.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber.egg-info/SOURCES.txt +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber.egg-info/dependency_links.txt +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber.egg-info/entry_points.txt +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber.egg-info/requires.txt +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber.egg-info/top_level.txt +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/setup.cfg +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/tests/test_config.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/tests/test_cookies.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/tests/test_response.py +0 -0
- {pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/tests/test_template_diff.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pyweber"
|
|
7
|
-
version = "1.2.0.
|
|
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(
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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: '
|
|
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.
|
|
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
|
|
436
|
-
uuid_placeholder =
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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:
|
|
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):
|
|
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
|
-
|
|
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
|
-
**
|
|
499
|
+
**kwargs
|
|
498
500
|
)
|
|
499
501
|
|
|
500
502
|
template = state_result.template
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/favicon/apple-touch-icon.png
RENAMED
|
File without changes
|
{pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/favicon/favicon-16x16.png
RENAMED
|
File without changes
|
{pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/favicon/favicon-32x32.png
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber/static/favicon/site.webmanifest
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyweber-1.2.0.dev20260424 → pyweber-1.2.0.dev20260426}/pyweber.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|