pyweber 1.6.0.dev0__tar.gz → 1.6.0.dev1__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.6.0.dev0/pyweber.egg-info → pyweber-1.6.0.dev1}/PKG-INFO +1 -1
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyproject.toml +1 -1
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/core/element.py +45 -4
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/core/template.py +10 -9
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/db/cli.py +21 -3
- pyweber-1.6.0.dev1/pyweber/models/dom_merge.py +92 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/handoff.py +9 -2
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/ws_message.py +11 -2
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/services/template_service.py +33 -1
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1/pyweber.egg-info}/PKG-INFO +1 -1
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber.egg-info/SOURCES.txt +1 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/LICENSE +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/MANIFEST.in +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/NOTICE +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/README.md +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/__init__.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/admin/index.html +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/admin/src/script.js +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/admin/src/style.css +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/auth/__init__.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/auth/decorators.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/auth/passwords.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/auth/rbac.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/auth/session.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/cli/__init__.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/cli/commands.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/components/__init__.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/components/form.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/components/general.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/components/input.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/config/__init__.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/config/config.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/connection/__init__.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/connection/http.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/connection/reload.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/connection/selector.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/connection/session.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/connection/websocket.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/core/__init__.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/core/events.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/core/html_parser.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/core/window.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/db/__init__.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/db/config.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/db/engine.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/db/model.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/db/query.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/db/session.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/__init__.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/context.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/cookies.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/create_app.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/element.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/error_pages.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/field.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/field_storage.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/file.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/file_stream.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/headers.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/middleware.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/openapi.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/rate_limit.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/request.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/response.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/routes.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/run.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/security.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/stream_stats.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/strem_stats.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/task_manager.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/template_diff.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/pyweber/__init__.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/pyweber/pyweber.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/services/__init__.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/services/openapi_setup.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/services/response_pipeline.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/services/static_files.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/session_store/__init__.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/.gitignore +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/config.toml +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/css.css +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/docs.html +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/favicon/android-chrome-192x192.png +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/favicon/android-chrome-512x512.png +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/favicon/apple-touch-icon.png +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/favicon/favicon-16x16.png +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/favicon/favicon-32x32.png +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/favicon/favicon.ico +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/favicon/pyweber.png +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/favicon/site.webmanifest +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/handlers.js +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/html.html +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/html401.html +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/html404.html +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/html500.html +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/js.js +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/loading.html +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/main.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/pyweber.css +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/update.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/testing/__init__.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/testing/client.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/utils/__init__.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/utils/async_utils.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/utils/deprecation.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/utils/exceptions.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/utils/icons.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/utils/loads.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/utils/mime.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/utils/security.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/utils/types.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/utils/utils.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber.egg-info/dependency_links.txt +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber.egg-info/entry_points.txt +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber.egg-info/requires.txt +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber.egg-info/top_level.txt +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/setup.cfg +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/tests/test_config.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/tests/test_context.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/tests/test_cookies.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/tests/test_public_api.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/tests/test_response.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/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.6.0.
|
|
7
|
+
version = "1.6.0.dev1"
|
|
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" }]
|
|
@@ -264,8 +264,9 @@ class Element(ElementConstrutor):
|
|
|
264
264
|
from pyweber.core.events import TemplateEvents
|
|
265
265
|
|
|
266
266
|
element = self
|
|
267
|
+
cls = type(self)
|
|
267
268
|
|
|
268
|
-
|
|
269
|
+
init_kwargs = dict(
|
|
269
270
|
tag=element.tag,
|
|
270
271
|
id=element.id,
|
|
271
272
|
content=element.content,
|
|
@@ -274,15 +275,55 @@ class Element(ElementConstrutor):
|
|
|
274
275
|
style=self.__deepy_clone(element.style),
|
|
275
276
|
attrs=self.__deepy_clone(element.attrs),
|
|
276
277
|
events=TemplateEvents(**element.events.__dict__),
|
|
277
|
-
|
|
278
|
+
sanitize=getattr(element, 'sanitize', True),
|
|
279
|
+
files=list(getattr(element, 'files', []) or []),
|
|
280
|
+
include_uuid=getattr(element, 'include_uuid', True),
|
|
281
|
+
**element.kwargs,
|
|
278
282
|
)
|
|
283
|
+
|
|
284
|
+
if cls is Element:
|
|
285
|
+
cln = Element(**init_kwargs)
|
|
286
|
+
else:
|
|
287
|
+
# Avoid subclass __init__ (would rebuild a different tree).
|
|
288
|
+
cln = object.__new__(cls)
|
|
289
|
+
Element.__init__(cln, childs=None, data=getattr(element, 'data', None), **init_kwargs)
|
|
290
|
+
|
|
279
291
|
cln.uuid = element.uuid
|
|
280
|
-
cln.parent =
|
|
292
|
+
cln.parent = None
|
|
281
293
|
cln.template = getattr(element, 'template', None)
|
|
294
|
+
cln.data = getattr(element, 'data', None)
|
|
282
295
|
cln._Element__element_methods = self.__deepy_clone(self.__element_methods)
|
|
283
296
|
|
|
297
|
+
uuid_map: dict[str, Element] = {cln.uuid: cln} if cln.uuid else {}
|
|
284
298
|
for child in element.childs:
|
|
285
|
-
|
|
299
|
+
child_clone = child.clone
|
|
300
|
+
cln.childs.append(child_clone)
|
|
301
|
+
if child_clone.uuid:
|
|
302
|
+
uuid_map[child_clone.uuid] = child_clone
|
|
303
|
+
# Index deep descendants for subclass ref remapping
|
|
304
|
+
stack = list(child_clone.childs)
|
|
305
|
+
while stack:
|
|
306
|
+
node = stack.pop()
|
|
307
|
+
if node.uuid:
|
|
308
|
+
uuid_map[node.uuid] = node
|
|
309
|
+
stack.extend(node.childs)
|
|
310
|
+
|
|
311
|
+
if cls is not Element:
|
|
312
|
+
skip = {
|
|
313
|
+
'kwargs', 'tag', 'id', 'attrs', 'style', 'content', 'value',
|
|
314
|
+
'classes', 'parent', 'data', 'files', 'events', 'childs',
|
|
315
|
+
'include_uuid', 'sanitize', 'uuid', 'template',
|
|
316
|
+
}
|
|
317
|
+
for key, val in element.__dict__.items():
|
|
318
|
+
if key in skip or key.startswith('_Element') or key.startswith('_ElementConstrutor'):
|
|
319
|
+
continue
|
|
320
|
+
if isinstance(val, Element) and val.uuid and val.uuid in uuid_map:
|
|
321
|
+
object.__setattr__(cln, key, uuid_map[val.uuid])
|
|
322
|
+
elif not isinstance(val, Element):
|
|
323
|
+
try:
|
|
324
|
+
object.__setattr__(cln, key, val)
|
|
325
|
+
except AttributeError:
|
|
326
|
+
cln.__dict__[key] = val
|
|
286
327
|
|
|
287
328
|
return cln
|
|
288
329
|
|
|
@@ -275,14 +275,15 @@ class Template:
|
|
|
275
275
|
return root
|
|
276
276
|
|
|
277
277
|
def clone(self):
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
)
|
|
278
|
+
"""Deep-copy the template tree, preserving subclass type when possible."""
|
|
279
|
+
cls = type(self)
|
|
280
|
+
tpl = object.__new__(cls)
|
|
281
|
+
tpl._Template__include_uuid = self._Template__include_uuid
|
|
282
|
+
tpl._Template__template = self._Template__template
|
|
283
|
+
tpl.kwargs = dict(self.kwargs)
|
|
285
284
|
tpl.data = self.data
|
|
286
|
-
tpl.
|
|
287
|
-
|
|
285
|
+
tpl._Template__status_code = self._Template__status_code
|
|
286
|
+
tpl._Template__icon = self._Template__icon
|
|
287
|
+
tpl._Template__title = self._Template__title
|
|
288
|
+
tpl._Template__root = self.root.clone
|
|
288
289
|
return tpl
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Alembic CLI helpers: ``pyweber db init|revision|upgrade|downgrade``."""
|
|
1
|
+
"""Alembic CLI helpers: ``pyweber db init|revision|migrate|upgrade|downgrade``."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -149,7 +149,7 @@ Generic single-database Alembic configuration for PyWeber (async).
|
|
|
149
149
|
|
|
150
150
|
1. Import your models in ``env.py`` so ``Model.metadata`` includes them.
|
|
151
151
|
2. ``pyweber db revision -m "message"``
|
|
152
|
-
3. ``pyweber db upgrade head``
|
|
152
|
+
3. ``pyweber db migrate`` (applies all pending revisions; same as ``upgrade head``)
|
|
153
153
|
'''
|
|
154
154
|
|
|
155
155
|
|
|
@@ -195,6 +195,11 @@ def upgrade(target: str = 'head') -> int:
|
|
|
195
195
|
return _alembic_cmd('upgrade', target)
|
|
196
196
|
|
|
197
197
|
|
|
198
|
+
def migrate(target: str = 'head') -> int:
|
|
199
|
+
"""Apply pending migrations (default: all the way to ``head``)."""
|
|
200
|
+
return upgrade(target)
|
|
201
|
+
|
|
202
|
+
|
|
198
203
|
def downgrade(target: str = '-1') -> int:
|
|
199
204
|
return _alembic_cmd('downgrade', target)
|
|
200
205
|
|
|
@@ -213,6 +218,17 @@ def build_parser(subparsers) -> None:
|
|
|
213
218
|
help='Do not pass --autogenerate to Alembic',
|
|
214
219
|
)
|
|
215
220
|
|
|
221
|
+
mig = db_sub.add_parser(
|
|
222
|
+
'migrate',
|
|
223
|
+
help='Apply all pending migrations (alias for upgrade head)',
|
|
224
|
+
)
|
|
225
|
+
mig.add_argument(
|
|
226
|
+
'target',
|
|
227
|
+
nargs='?',
|
|
228
|
+
default='head',
|
|
229
|
+
help='Revision target (default: head — run everything pending)',
|
|
230
|
+
)
|
|
231
|
+
|
|
216
232
|
up = db_sub.add_parser('upgrade', help='Upgrade to a revision (default: head)')
|
|
217
233
|
up.add_argument('target', nargs='?', default='head')
|
|
218
234
|
|
|
@@ -228,9 +244,11 @@ def handle_db_command(args: argparse.Namespace) -> int:
|
|
|
228
244
|
return 0
|
|
229
245
|
if cmd == 'revision':
|
|
230
246
|
return revision(args.message, autogenerate=not args.no_autogenerate)
|
|
247
|
+
if cmd == 'migrate':
|
|
248
|
+
return migrate(args.target)
|
|
231
249
|
if cmd == 'upgrade':
|
|
232
250
|
return upgrade(args.target)
|
|
233
251
|
if cmd == 'downgrade':
|
|
234
252
|
return downgrade(args.target)
|
|
235
|
-
print('Usage: pyweber db {init,revision,upgrade,downgrade}')
|
|
253
|
+
print('Usage: pyweber db {init,revision,migrate,upgrade,downgrade}')
|
|
236
254
|
return 1
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Merge client DOM HTML into an existing server Element tree by uuid.
|
|
2
|
+
|
|
3
|
+
Preserves Python object identity so bound handlers and ``self`` refs stay valid.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from pyweber.core.element import Element
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def index_elements_by_uuid(root: 'Element') -> dict[str, 'Element']:
|
|
15
|
+
"""Walk ``root`` and map uuid → Element (skips missing/empty uuids)."""
|
|
16
|
+
out: dict[str, Element] = {}
|
|
17
|
+
|
|
18
|
+
def walk(el: 'Element') -> None:
|
|
19
|
+
uid = getattr(el, 'uuid', None)
|
|
20
|
+
if uid:
|
|
21
|
+
out[str(uid)] = el
|
|
22
|
+
for child in el.childs or []:
|
|
23
|
+
walk(child)
|
|
24
|
+
|
|
25
|
+
walk(root)
|
|
26
|
+
return out
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _sync_client_fields(server_el: 'Element', client_el: 'Element') -> None:
|
|
30
|
+
"""Copy browser-authoritative fields onto the existing server element."""
|
|
31
|
+
if client_el.value is not None:
|
|
32
|
+
server_el.value = client_el.value
|
|
33
|
+
|
|
34
|
+
for attr in ('checked', 'selected', 'disabled'):
|
|
35
|
+
if attr in (client_el.attrs or {}):
|
|
36
|
+
server_el.attrs[attr] = client_el.attrs[attr]
|
|
37
|
+
elif attr in (server_el.attrs or {}) and attr not in (client_el.attrs or {}):
|
|
38
|
+
# Client omitted boolean attr → unchecked / not selected
|
|
39
|
+
if server_el.tag in ('input', 'option'):
|
|
40
|
+
server_el.attrs.pop(attr, None)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _is_top_level_client_only(client_el: 'Element', server_map: dict[str, 'Element']) -> bool:
|
|
44
|
+
"""True when this node is new and its parent already exists on the server (or is root)."""
|
|
45
|
+
parent = client_el.parent
|
|
46
|
+
if parent is None:
|
|
47
|
+
return False
|
|
48
|
+
parent_uid = getattr(parent, 'uuid', None)
|
|
49
|
+
if not parent_uid:
|
|
50
|
+
return False
|
|
51
|
+
return str(parent_uid) in server_map
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def merge_client_dom(
|
|
55
|
+
server_root: 'Element',
|
|
56
|
+
client_html: str,
|
|
57
|
+
*,
|
|
58
|
+
include_uuid: bool = True,
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Merge parsed client HTML into ``server_root`` without replacing known nodes.
|
|
61
|
+
|
|
62
|
+
- Matching uuids: update value / form-ish attrs in place.
|
|
63
|
+
- Client-only uuids (e.g. JS inject): graft the new subtree under the matching
|
|
64
|
+
server parent.
|
|
65
|
+
- Server-only uuids: left untouched (next diff can reconcile the browser).
|
|
66
|
+
"""
|
|
67
|
+
from pyweber.core.element import Element
|
|
68
|
+
|
|
69
|
+
if not client_html or not client_html.strip():
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
client_root = Element.from_html(client_html, include_uuid=include_uuid)
|
|
73
|
+
server_map = index_elements_by_uuid(server_root)
|
|
74
|
+
client_map = index_elements_by_uuid(client_root)
|
|
75
|
+
|
|
76
|
+
for uid, server_el in list(server_map.items()):
|
|
77
|
+
client_el = client_map.get(uid)
|
|
78
|
+
if client_el is not None:
|
|
79
|
+
_sync_client_fields(server_el, client_el)
|
|
80
|
+
|
|
81
|
+
for uid, client_el in client_map.items():
|
|
82
|
+
if uid in server_map:
|
|
83
|
+
continue
|
|
84
|
+
if not _is_top_level_client_only(client_el, server_map):
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
parent_uid = str(client_el.parent.uuid)
|
|
88
|
+
server_parent = server_map[parent_uid]
|
|
89
|
+
grafted = client_el.clone
|
|
90
|
+
grafted.parent = None
|
|
91
|
+
server_parent.add_child(grafted)
|
|
92
|
+
server_map.update(index_elements_by_uuid(grafted))
|
|
@@ -18,6 +18,13 @@ class HandoffEntry:
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class TemplateHandoffRegistry:
|
|
21
|
+
"""One-shot HTTP → WS handoff of the **same** Template instance (move on consume).
|
|
22
|
+
|
|
23
|
+
Storing a clone would type-erase subclasses and orphan ``self`` refs used by
|
|
24
|
+
bound event handlers. The HTTP response and the future session share one object
|
|
25
|
+
until ``consume`` moves it into the WebSocket session.
|
|
26
|
+
"""
|
|
27
|
+
|
|
21
28
|
def __init__(self, ttl: int = HANDOFF_TTL_SECONDS):
|
|
22
29
|
self._ttl = ttl
|
|
23
30
|
self._entries: dict[str, HandoffEntry] = {}
|
|
@@ -28,7 +35,7 @@ class TemplateHandoffRegistry:
|
|
|
28
35
|
with self._lock:
|
|
29
36
|
self._purge_expired()
|
|
30
37
|
self._entries[token] = HandoffEntry(
|
|
31
|
-
template=template
|
|
38
|
+
template=template,
|
|
32
39
|
route=route,
|
|
33
40
|
created_at=time(),
|
|
34
41
|
)
|
|
@@ -50,7 +57,7 @@ class TemplateHandoffRegistry:
|
|
|
50
57
|
if time() - entry.created_at > self._ttl:
|
|
51
58
|
return None
|
|
52
59
|
|
|
53
|
-
return entry.template
|
|
60
|
+
return entry.template
|
|
54
61
|
|
|
55
62
|
def clear(self):
|
|
56
63
|
with self._lock:
|
|
@@ -63,8 +63,17 @@ class wsMessage:
|
|
|
63
63
|
not getattr(session_template, 'include_uuid', True)
|
|
64
64
|
or (used_handoff and 'uuid=' not in client_html.lower())
|
|
65
65
|
)
|
|
66
|
-
if
|
|
67
|
-
|
|
66
|
+
if keep_server:
|
|
67
|
+
self.insert_values(element=session_template.root)
|
|
68
|
+
else:
|
|
69
|
+
# Preserve Python object identity (self / bound handlers).
|
|
70
|
+
from pyweber.models.dom_merge import merge_client_dom
|
|
71
|
+
|
|
72
|
+
merge_client_dom(
|
|
73
|
+
session_template.root,
|
|
74
|
+
client_html,
|
|
75
|
+
include_uuid=getattr(session_template, 'include_uuid', True),
|
|
76
|
+
)
|
|
68
77
|
self.insert_values(element=session_template.root)
|
|
69
78
|
|
|
70
79
|
return session_template
|
|
@@ -44,9 +44,41 @@ class TemplateService:
|
|
|
44
44
|
if isinstance(template, Template):
|
|
45
45
|
return template
|
|
46
46
|
if isinstance(template, Element):
|
|
47
|
-
return
|
|
47
|
+
return self._adopt_element_as_template(template, title=title)
|
|
48
48
|
return Template(template=str(template), title=title)
|
|
49
49
|
|
|
50
|
+
def _adopt_element_as_template(self, element: Element, title: str = None) -> Template:
|
|
51
|
+
"""Wrap an Element tree in a Template without HTML round-trip (keeps identity)."""
|
|
52
|
+
from pyweber.config.config import config
|
|
53
|
+
|
|
54
|
+
include_uuid = getattr(element, 'include_uuid', True)
|
|
55
|
+
if element.tag == 'html':
|
|
56
|
+
tpl = object.__new__(Template)
|
|
57
|
+
tpl._Template__include_uuid = include_uuid
|
|
58
|
+
tpl._Template__template = ''
|
|
59
|
+
tpl.kwargs = {}
|
|
60
|
+
tpl.data = getattr(element, 'data', None)
|
|
61
|
+
tpl._Template__status_code = 200
|
|
62
|
+
tpl._Template__icon = str(config['app'].get('icon'))
|
|
63
|
+
tpl._Template__title = title
|
|
64
|
+
tpl._Template__root = element
|
|
65
|
+
if title:
|
|
66
|
+
# Use property to sync <title> when present
|
|
67
|
+
tpl.title = title
|
|
68
|
+
return tpl
|
|
69
|
+
|
|
70
|
+
shell = Template(
|
|
71
|
+
template='<html><head></head><body></body></html>',
|
|
72
|
+
title=title,
|
|
73
|
+
include_uuid=include_uuid,
|
|
74
|
+
)
|
|
75
|
+
body = shell.body
|
|
76
|
+
while body.childs:
|
|
77
|
+
body.childs.pop()
|
|
78
|
+
body.content = None
|
|
79
|
+
body.add_child(element)
|
|
80
|
+
return shell
|
|
81
|
+
|
|
50
82
|
def template_to_bytes(
|
|
51
83
|
self,
|
|
52
84
|
template: Union[Template, Element, dict, list, set, str, bytes],
|
|
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
|
|
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
|
|
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
|