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.
- {pyweber-1.2.0.dev20260425/pyweber.egg-info → pyweber-1.2.0.dev20260427}/PKG-INFO +1 -1
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyproject.toml +1 -1
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/core/element.py +93 -31
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/core/template.py +34 -12
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/cookies.py +8 -8
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/element.py +11 -1
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/request.py +35 -32
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/response.py +3 -3
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/routes.py +23 -5
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/pyweber/pyweber.py +18 -7
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/js.js +8 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427/pyweber.egg-info}/PKG-INFO +1 -1
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/LICENSE +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/MANIFEST.in +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/README.md +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/__init__.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/admin/index.html +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/admin/src/script.js +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/admin/src/style.css +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/cli/__init__.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/cli/commands.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/components/__init__.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/components/form.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/components/general.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/components/input.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/config/__init__.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/config/config.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/connection/__init__.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/connection/http.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/connection/reload.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/connection/selector.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/connection/session.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/connection/websocket.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/core/__init__.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/core/events.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/core/window.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/__init__.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/create_app.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/error_pages.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/field.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/field_storage.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/file.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/file_stream.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/headers.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/middleware.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/openapi.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/run.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/strem_stats.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/task_manager.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/template_diff.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/models/ws_message.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/pyweber/__init__.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/.gitignore +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/config.toml +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/css.css +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/docs.html +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/favicon/android-chrome-192x192.png +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/favicon/android-chrome-512x512.png +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/favicon/apple-touch-icon.png +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/favicon/favicon-16x16.png +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/favicon/favicon-32x32.png +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/favicon/favicon.ico +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/favicon/pyweber.png +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/favicon/site.webmanifest +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/handlers.js +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/html.html +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/html401.html +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/html404.html +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/html500.html +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/loading.html +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/main.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/pyweber.css +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/update.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/utils/__init__.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/utils/exceptions.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/utils/loads.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/utils/types.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/utils/utils.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber.egg-info/SOURCES.txt +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber.egg-info/dependency_links.txt +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber.egg-info/entry_points.txt +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber.egg-info/requires.txt +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber.egg-info/top_level.txt +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/setup.cfg +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/tests/test_config.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/tests/test_cookies.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/tests/test_response.py +0 -0
- {pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/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.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(
|
|
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
|
|
229
|
+
if matches(element.classes, value):
|
|
160
230
|
results.append(element)
|
|
161
231
|
|
|
162
232
|
elif by in ['attrs', 'style']:
|
|
163
|
-
|
|
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)
|
|
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=
|
|
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 = (
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
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
|
|
@@ -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:
|
|
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.
|
|
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):
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
callback=state_result.callback,
|
|
484
|
-
|
|
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
|
-
|
|
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
|
-
**
|
|
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}`, {
|
|
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.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/favicon/apple-touch-icon.png
RENAMED
|
File without changes
|
{pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/favicon/favicon-16x16.png
RENAMED
|
File without changes
|
{pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/pyweber/static/favicon/favicon-32x32.png
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/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
|
{pyweber-1.2.0.dev20260425 → pyweber-1.2.0.dev20260427}/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
|