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.
Files changed (123) hide show
  1. {pyweber-1.6.0.dev0/pyweber.egg-info → pyweber-1.6.0.dev1}/PKG-INFO +1 -1
  2. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyproject.toml +1 -1
  3. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/core/element.py +45 -4
  4. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/core/template.py +10 -9
  5. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/db/cli.py +21 -3
  6. pyweber-1.6.0.dev1/pyweber/models/dom_merge.py +92 -0
  7. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/handoff.py +9 -2
  8. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/ws_message.py +11 -2
  9. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/services/template_service.py +33 -1
  10. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1/pyweber.egg-info}/PKG-INFO +1 -1
  11. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber.egg-info/SOURCES.txt +1 -0
  12. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/LICENSE +0 -0
  13. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/MANIFEST.in +0 -0
  14. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/NOTICE +0 -0
  15. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/README.md +0 -0
  16. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/__init__.py +0 -0
  17. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/admin/index.html +0 -0
  18. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/admin/src/script.js +0 -0
  19. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/admin/src/style.css +0 -0
  20. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/auth/__init__.py +0 -0
  21. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/auth/decorators.py +0 -0
  22. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/auth/passwords.py +0 -0
  23. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/auth/rbac.py +0 -0
  24. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/auth/session.py +0 -0
  25. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/cli/__init__.py +0 -0
  26. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/cli/commands.py +0 -0
  27. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/components/__init__.py +0 -0
  28. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/components/form.py +0 -0
  29. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/components/general.py +0 -0
  30. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/components/input.py +0 -0
  31. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/config/__init__.py +0 -0
  32. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/config/config.py +0 -0
  33. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/connection/__init__.py +0 -0
  34. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/connection/http.py +0 -0
  35. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/connection/reload.py +0 -0
  36. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/connection/selector.py +0 -0
  37. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/connection/session.py +0 -0
  38. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/connection/websocket.py +0 -0
  39. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/core/__init__.py +0 -0
  40. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/core/events.py +0 -0
  41. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/core/html_parser.py +0 -0
  42. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/core/window.py +0 -0
  43. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/db/__init__.py +0 -0
  44. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/db/config.py +0 -0
  45. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/db/engine.py +0 -0
  46. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/db/model.py +0 -0
  47. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/db/query.py +0 -0
  48. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/db/session.py +0 -0
  49. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/__init__.py +0 -0
  50. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/context.py +0 -0
  51. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/cookies.py +0 -0
  52. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/create_app.py +0 -0
  53. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/element.py +0 -0
  54. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/error_pages.py +0 -0
  55. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/field.py +0 -0
  56. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/field_storage.py +0 -0
  57. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/file.py +0 -0
  58. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/file_stream.py +0 -0
  59. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/headers.py +0 -0
  60. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/middleware.py +0 -0
  61. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/openapi.py +0 -0
  62. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/rate_limit.py +0 -0
  63. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/request.py +0 -0
  64. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/response.py +0 -0
  65. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/routes.py +0 -0
  66. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/run.py +0 -0
  67. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/security.py +0 -0
  68. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/stream_stats.py +0 -0
  69. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/strem_stats.py +0 -0
  70. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/task_manager.py +0 -0
  71. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/models/template_diff.py +0 -0
  72. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/pyweber/__init__.py +0 -0
  73. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/pyweber/pyweber.py +0 -0
  74. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/services/__init__.py +0 -0
  75. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/services/openapi_setup.py +0 -0
  76. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/services/response_pipeline.py +0 -0
  77. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/services/static_files.py +0 -0
  78. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/session_store/__init__.py +0 -0
  79. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/.gitignore +0 -0
  80. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/config.toml +0 -0
  81. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/css.css +0 -0
  82. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/docs.html +0 -0
  83. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/favicon/android-chrome-192x192.png +0 -0
  84. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/favicon/android-chrome-512x512.png +0 -0
  85. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/favicon/apple-touch-icon.png +0 -0
  86. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/favicon/favicon-16x16.png +0 -0
  87. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/favicon/favicon-32x32.png +0 -0
  88. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/favicon/favicon.ico +0 -0
  89. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/favicon/pyweber.png +0 -0
  90. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/favicon/site.webmanifest +0 -0
  91. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/handlers.js +0 -0
  92. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/html.html +0 -0
  93. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/html401.html +0 -0
  94. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/html404.html +0 -0
  95. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/html500.html +0 -0
  96. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/js.js +0 -0
  97. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/loading.html +0 -0
  98. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/main.py +0 -0
  99. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/pyweber.css +0 -0
  100. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/static/update.py +0 -0
  101. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/testing/__init__.py +0 -0
  102. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/testing/client.py +0 -0
  103. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/utils/__init__.py +0 -0
  104. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/utils/async_utils.py +0 -0
  105. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/utils/deprecation.py +0 -0
  106. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/utils/exceptions.py +0 -0
  107. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/utils/icons.py +0 -0
  108. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/utils/loads.py +0 -0
  109. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/utils/mime.py +0 -0
  110. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/utils/security.py +0 -0
  111. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/utils/types.py +0 -0
  112. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber/utils/utils.py +0 -0
  113. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber.egg-info/dependency_links.txt +0 -0
  114. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber.egg-info/entry_points.txt +0 -0
  115. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber.egg-info/requires.txt +0 -0
  116. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/pyweber.egg-info/top_level.txt +0 -0
  117. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/setup.cfg +0 -0
  118. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/tests/test_config.py +0 -0
  119. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/tests/test_context.py +0 -0
  120. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/tests/test_cookies.py +0 -0
  121. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/tests/test_public_api.py +0 -0
  122. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/tests/test_response.py +0 -0
  123. {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev1}/tests/test_template_diff.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyweber
3
- Version: 1.6.0.dev0
3
+ Version: 1.6.0.dev1
4
4
  Summary: A lightweight Python framework for building and managing web applications.
5
5
  Author-email: DevPythonMZ <pypi.dev@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pyweber"
7
- version = "1.6.0.dev0"
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
- cln = Element(
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
- **element.kwargs
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 = element.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
- cln.childs.append(child.clone)
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
- tpl = Template(
279
- template=self.template,
280
- status_code=self.status_code,
281
- title=self.title,
282
- include_uuid=self.__include_uuid,
283
- **self.kwargs
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.root = self.root.clone
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.clone(),
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.clone()
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 not keep_server:
67
- session_template.root = session_template.parse_html(html=client_html)
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 Template(template=template.to_html(), title=title)
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],
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyweber
3
- Version: 1.6.0.dev0
3
+ Version: 1.6.0.dev1
4
4
  Summary: A lightweight Python framework for building and managing web applications.
5
5
  Author-email: DevPythonMZ <pypi.dev@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -49,6 +49,7 @@ pyweber/models/__init__.py
49
49
  pyweber/models/context.py
50
50
  pyweber/models/cookies.py
51
51
  pyweber/models/create_app.py
52
+ pyweber/models/dom_merge.py
52
53
  pyweber/models/element.py
53
54
  pyweber/models/error_pages.py
54
55
  pyweber/models/field.py
File without changes
File without changes
File without changes
File without changes
File without changes