pyweber 1.6.0.dev0__tar.gz → 1.6.0.dev3__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.dev3}/PKG-INFO +12 -3
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/README.md +10 -2
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyproject.toml +3 -1
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/__init__.py +18 -1
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/connection/session.py +14 -3
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/connection/websocket.py +240 -122
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/core/element.py +66 -8
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/core/events.py +70 -7
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/core/template.py +77 -9
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/core/window.py +5 -2
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/db/cli.py +21 -3
- pyweber-1.6.0.dev3/pyweber/models/dom_merge.py +92 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/models/element.py +11 -8
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/models/handoff.py +9 -2
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/models/ws_message.py +28 -4
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/pyweber/pyweber.py +17 -3
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/services/template_service.py +33 -1
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/static/config.toml +3 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/static/js.js +78 -4
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/utils/security.py +78 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3/pyweber.egg-info}/PKG-INFO +12 -3
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber.egg-info/SOURCES.txt +1 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/LICENSE +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/MANIFEST.in +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/NOTICE +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/admin/index.html +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/admin/src/script.js +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/admin/src/style.css +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/auth/__init__.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/auth/decorators.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/auth/passwords.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/auth/rbac.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/auth/session.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/cli/__init__.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/cli/commands.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/components/__init__.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/components/form.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/components/general.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/components/input.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/config/__init__.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/config/config.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/connection/__init__.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/connection/http.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/connection/reload.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/connection/selector.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/core/__init__.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/core/html_parser.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/db/__init__.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/db/config.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/db/engine.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/db/model.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/db/query.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/db/session.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/models/__init__.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/models/context.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/models/cookies.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/models/create_app.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/models/error_pages.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/models/field.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/models/field_storage.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/models/file.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/models/file_stream.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/models/headers.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/models/middleware.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/models/openapi.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/models/rate_limit.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/models/request.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/models/response.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/models/routes.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/models/run.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/models/security.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/models/stream_stats.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/models/strem_stats.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/models/task_manager.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/models/template_diff.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/pyweber/__init__.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/services/__init__.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/services/openapi_setup.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/services/response_pipeline.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/services/static_files.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/session_store/__init__.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/static/.gitignore +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/static/css.css +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/static/docs.html +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/static/favicon/android-chrome-192x192.png +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/static/favicon/android-chrome-512x512.png +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/static/favicon/apple-touch-icon.png +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/static/favicon/favicon-16x16.png +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/static/favicon/favicon-32x32.png +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/static/favicon/favicon.ico +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/static/favicon/pyweber.png +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/static/favicon/site.webmanifest +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/static/handlers.js +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/static/html.html +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/static/html401.html +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/static/html404.html +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/static/html500.html +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/static/loading.html +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/static/main.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/static/pyweber.css +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/static/update.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/testing/__init__.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/testing/client.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/utils/__init__.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/utils/async_utils.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/utils/deprecation.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/utils/exceptions.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/utils/icons.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/utils/loads.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/utils/mime.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/utils/types.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber/utils/utils.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber.egg-info/dependency_links.txt +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber.egg-info/entry_points.txt +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber.egg-info/requires.txt +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/pyweber.egg-info/top_level.txt +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/setup.cfg +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/tests/test_config.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/tests/test_context.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/tests/test_cookies.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/tests/test_public_api.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/tests/test_response.py +0 -0
- {pyweber-1.6.0.dev0 → pyweber-1.6.0.dev3}/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.
|
|
3
|
+
Version: 1.6.0.dev3
|
|
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
|
|
@@ -10,6 +10,7 @@ Project-URL: Tracker, https://github.com/pyweber/pyweber/issues
|
|
|
10
10
|
Project-URL: PyPI, https://pypi.org/project/pyweber/
|
|
11
11
|
Project-URL: YouTube, https://youtube.com/@devpythonMZ
|
|
12
12
|
Project-URL: Examples, https://github.com/pyweber-examples
|
|
13
|
+
Project-URL: Security, https://pyweber.dev/guides/supported-versions/
|
|
13
14
|
Requires-Python: >=3.10
|
|
14
15
|
Description-Content-Type: text/markdown
|
|
15
16
|
License-File: LICENSE
|
|
@@ -55,17 +56,25 @@ Pyweber is a lightweight Python web framework designed to create dynamic, reacti
|
|
|
55
56
|
- **Intuitive DOM Manipulation**: Query and modify elements with familiar selectors
|
|
56
57
|
- **Event-Driven Programming**: Handle user interactions with Python event handlers
|
|
57
58
|
- **WebSocket Integration**: Real-time communication between client and server
|
|
58
|
-
- **Template Handoff (1.3.0)**: WebSocket sessions reuse the HTTP-rendered template — handlers are not executed twice on connect
|
|
59
|
+
- **Template Handoff (1.3.0+)**: WebSocket sessions reuse the HTTP-rendered template — handlers are not executed twice on connect
|
|
60
|
+
- **Identity-preserving sync (1.6.0+)**: `self` / subclass element refs stay valid after WS connect
|
|
61
|
+
- **Optional ORM & Redis sessions (1.6.0+)**: `pyweber[db]`, `pyweber[redis]`
|
|
62
|
+
- **Auth & RBAC (1.5+)**: signed login cookie, roles/permissions
|
|
59
63
|
- **Minimal Configuration**: Get started quickly with sensible defaults
|
|
60
64
|
|
|
61
65
|
## Quick Start
|
|
62
66
|
|
|
63
67
|
### Installation
|
|
64
68
|
|
|
69
|
+
!!! danger "Avoid ≤ 1.3.1"
|
|
70
|
+
Older lines are **not recommended** for production. Use a current release:
|
|
71
|
+
|
|
65
72
|
```bash
|
|
66
|
-
pip install pyweber
|
|
73
|
+
pip install -U 'pyweber>=1.5'
|
|
67
74
|
```
|
|
68
75
|
|
|
76
|
+
See [Supported versions](https://pyweber.dev/guides/supported-versions/) (docs) for yank / security notes. `pyproject.toml` cannot warn when someone pins an old wheel — that requires [PyPI yanking](https://docs.pypi.org/project-management/yanking/).
|
|
77
|
+
|
|
69
78
|
### Create a Project
|
|
70
79
|
|
|
71
80
|
```bash
|
|
@@ -14,17 +14,25 @@ Pyweber is a lightweight Python web framework designed to create dynamic, reacti
|
|
|
14
14
|
- **Intuitive DOM Manipulation**: Query and modify elements with familiar selectors
|
|
15
15
|
- **Event-Driven Programming**: Handle user interactions with Python event handlers
|
|
16
16
|
- **WebSocket Integration**: Real-time communication between client and server
|
|
17
|
-
- **Template Handoff (1.3.0)**: WebSocket sessions reuse the HTTP-rendered template — handlers are not executed twice on connect
|
|
17
|
+
- **Template Handoff (1.3.0+)**: WebSocket sessions reuse the HTTP-rendered template — handlers are not executed twice on connect
|
|
18
|
+
- **Identity-preserving sync (1.6.0+)**: `self` / subclass element refs stay valid after WS connect
|
|
19
|
+
- **Optional ORM & Redis sessions (1.6.0+)**: `pyweber[db]`, `pyweber[redis]`
|
|
20
|
+
- **Auth & RBAC (1.5+)**: signed login cookie, roles/permissions
|
|
18
21
|
- **Minimal Configuration**: Get started quickly with sensible defaults
|
|
19
22
|
|
|
20
23
|
## Quick Start
|
|
21
24
|
|
|
22
25
|
### Installation
|
|
23
26
|
|
|
27
|
+
!!! danger "Avoid ≤ 1.3.1"
|
|
28
|
+
Older lines are **not recommended** for production. Use a current release:
|
|
29
|
+
|
|
24
30
|
```bash
|
|
25
|
-
pip install pyweber
|
|
31
|
+
pip install -U 'pyweber>=1.5'
|
|
26
32
|
```
|
|
27
33
|
|
|
34
|
+
See [Supported versions](https://pyweber.dev/guides/supported-versions/) (docs) for yank / security notes. `pyproject.toml` cannot warn when someone pins an old wheel — that requires [PyPI yanking](https://docs.pypi.org/project-management/yanking/).
|
|
35
|
+
|
|
28
36
|
### Create a Project
|
|
29
37
|
|
|
30
38
|
```bash
|
|
@@ -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.dev3"
|
|
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" }]
|
|
@@ -18,6 +18,8 @@ Tracker = "https://github.com/pyweber/pyweber/issues"
|
|
|
18
18
|
PyPI = "https://pypi.org/project/pyweber/"
|
|
19
19
|
YouTube = "https://youtube.com/@devpythonMZ"
|
|
20
20
|
Examples = "https://github.com/pyweber-examples"
|
|
21
|
+
# Supported versions / security policy (≤1.3.1 not recommended; yank on PyPI — not via this file)
|
|
22
|
+
Security = "https://pyweber.dev/guides/supported-versions/"
|
|
21
23
|
|
|
22
24
|
[project.scripts]
|
|
23
25
|
pyweber = "pyweber.cli.commands:app"
|
|
@@ -105,7 +105,6 @@ from .utils.types import (
|
|
|
105
105
|
EventType,
|
|
106
106
|
HTMLTag,
|
|
107
107
|
HTTPStatusCode,
|
|
108
|
-
JWTAlgorithms,
|
|
109
108
|
NonSelfClosingHTMLTags,
|
|
110
109
|
WebSocketStatusCode,
|
|
111
110
|
WindowEventType,
|
|
@@ -305,3 +304,21 @@ __all__ = [
|
|
|
305
304
|
'Label',
|
|
306
305
|
'TextArea',
|
|
307
306
|
]
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def __getattr__(name: str):
|
|
310
|
+
if name == 'JWTAlgorithms':
|
|
311
|
+
from pyweber.utils.deprecation import warn_deprecated
|
|
312
|
+
from pyweber.utils.types import JWTAlgorithms as _JWTAlgorithms
|
|
313
|
+
|
|
314
|
+
warn_deprecated(
|
|
315
|
+
'pyweber.JWTAlgorithms',
|
|
316
|
+
alternative=(
|
|
317
|
+
'pyjwt (or similar) for encode/decode/verify; '
|
|
318
|
+
'pyweber.auth for session login. '
|
|
319
|
+
'JWTAlgorithms remains an algorithm-name enum only until 2.0'
|
|
320
|
+
),
|
|
321
|
+
removal='2.0',
|
|
322
|
+
)
|
|
323
|
+
return _JWTAlgorithms
|
|
324
|
+
raise AttributeError(f'module {__name__!r} has no attribute {name!r}')
|
|
@@ -21,7 +21,12 @@ class Session:
|
|
|
21
21
|
self.session_id = session_id
|
|
22
22
|
self.create_at = time()
|
|
23
23
|
self.current_route = current_route
|
|
24
|
-
|
|
24
|
+
# Must be a distinct snapshot for TemplateDiff; aliasing template makes
|
|
25
|
+
# every e.update() produce an empty diff (mutations visible on both sides).
|
|
26
|
+
try:
|
|
27
|
+
self.old_template = template.clone()
|
|
28
|
+
except Exception:
|
|
29
|
+
self.old_template = None
|
|
25
30
|
|
|
26
31
|
|
|
27
32
|
class SessionManager:
|
|
@@ -57,8 +62,14 @@ class SessionManager:
|
|
|
57
62
|
self._schedule_persist(session)
|
|
58
63
|
|
|
59
64
|
def remove_session(self, session_id: str):
|
|
60
|
-
|
|
61
|
-
|
|
65
|
+
session = self.sessions.pop(session_id, None)
|
|
66
|
+
if session is not None:
|
|
67
|
+
try:
|
|
68
|
+
from pyweber.core.events import cleanup_template_events
|
|
69
|
+
cleanup_template_events(getattr(session, 'template', None))
|
|
70
|
+
cleanup_template_events(getattr(session, 'old_template', None))
|
|
71
|
+
except Exception as exc:
|
|
72
|
+
logger.debug('EventBook cleanup skipped: %s', exc)
|
|
62
73
|
self._schedule_delete(session_id)
|
|
63
74
|
|
|
64
75
|
def get_session(self, session_id: str):
|
|
@@ -20,7 +20,7 @@ from pyweber.utils.security import (
|
|
|
20
20
|
from pyweber.connection.session import sessions, Session
|
|
21
21
|
from pyweber.models.template_diff import TemplateDiff
|
|
22
22
|
from pyweber.models.task_manager import TaskManager
|
|
23
|
-
from pyweber.core.events import EventConstrutor
|
|
23
|
+
from pyweber.core.events import EventConstrutor
|
|
24
24
|
from pyweber.models.context import set_current_window, reset_current_window
|
|
25
25
|
|
|
26
26
|
if TYPE_CHECKING:
|
|
@@ -29,6 +29,7 @@ if TYPE_CHECKING:
|
|
|
29
29
|
from pyweber.core.window import Window
|
|
30
30
|
|
|
31
31
|
def need_message_keys():
|
|
32
|
+
"""Keys the browser always includes in ``getEventData`` payloads."""
|
|
32
33
|
return [
|
|
33
34
|
'type',
|
|
34
35
|
'event_ref',
|
|
@@ -46,11 +47,38 @@ def need_message_keys():
|
|
|
46
47
|
'handoffToken',
|
|
47
48
|
]
|
|
48
49
|
|
|
50
|
+
|
|
51
|
+
def _ws_message_is_actionable(message: dict) -> bool:
|
|
52
|
+
"""True when the payload can create a session or drive an event/update.
|
|
53
|
+
|
|
54
|
+
The old gate rejected *any* message without ``template`` when the session
|
|
55
|
+
was unknown — that dropped clicks, window responses, and handshakes that
|
|
56
|
+
omit outerHTML (``includeTemplate: false``).
|
|
57
|
+
"""
|
|
58
|
+
if message.get('template'):
|
|
59
|
+
return True
|
|
60
|
+
if message.get('handoffToken'):
|
|
61
|
+
return True
|
|
62
|
+
if message.get('type') and message.get('event_ref'):
|
|
63
|
+
return True
|
|
64
|
+
if message.get('window_response'):
|
|
65
|
+
return True
|
|
66
|
+
if message.get('file_content'):
|
|
67
|
+
return True
|
|
68
|
+
session_id = message.get('sessionId')
|
|
69
|
+
return bool(session_id and session_id in sessions.all_sessions)
|
|
70
|
+
|
|
49
71
|
def event_is_running(message: wsMessage, task_manager: TaskManager) -> bool:
|
|
50
72
|
"""Check if event is running"""
|
|
51
73
|
if message.session_id in task_manager.active_handlers_async:
|
|
52
74
|
template = sessions.get_session(session_id=message.session_id).template
|
|
53
|
-
|
|
75
|
+
element = template.getElement(by='uuid', value=message.target_uuid)
|
|
76
|
+
handler = element.events.__dict__.get(f'on{message.type}') if element else None
|
|
77
|
+
|
|
78
|
+
if not callable(handler):
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
event_id = f'event_{id(handler)}'
|
|
54
82
|
|
|
55
83
|
if event_id in task_manager.active_handlers_async[message.session_id]:
|
|
56
84
|
return True
|
|
@@ -150,6 +178,7 @@ class WebsocketServer:
|
|
|
150
178
|
current_opcode = None
|
|
151
179
|
is_coro = inspect.iscoroutinefunction(message_handler)
|
|
152
180
|
timeout = 60 * 60
|
|
181
|
+
consumer_task: asyncio.Task | None = None
|
|
153
182
|
|
|
154
183
|
try:
|
|
155
184
|
while True:
|
|
@@ -174,14 +203,19 @@ class WebsocketServer:
|
|
|
174
203
|
else self.__all_message
|
|
175
204
|
)
|
|
176
205
|
self.__messages.append(decoded)
|
|
177
|
-
|
|
178
|
-
if is_coro:
|
|
179
|
-
asyncio.create_task(message_handler(self))
|
|
180
|
-
else:
|
|
181
|
-
message_handler(self)
|
|
182
|
-
|
|
183
206
|
self.__all_message = b''
|
|
184
207
|
|
|
208
|
+
# One long-lived consumer (async for). Spawning a
|
|
209
|
+
# task per frame made sync ``for`` exit after the
|
|
210
|
+
# first drain and raced multiple consumers.
|
|
211
|
+
if consumer_task is None or consumer_task.done():
|
|
212
|
+
if is_coro:
|
|
213
|
+
consumer_task = asyncio.create_task(
|
|
214
|
+
message_handler(self)
|
|
215
|
+
)
|
|
216
|
+
else:
|
|
217
|
+
message_handler(self)
|
|
218
|
+
|
|
185
219
|
elif current_opcode == 8:
|
|
186
220
|
break
|
|
187
221
|
|
|
@@ -209,6 +243,12 @@ class WebsocketServer:
|
|
|
209
243
|
raise e
|
|
210
244
|
|
|
211
245
|
finally:
|
|
246
|
+
if consumer_task is not None and not consumer_task.done():
|
|
247
|
+
consumer_task.cancel()
|
|
248
|
+
try:
|
|
249
|
+
await consumer_task
|
|
250
|
+
except (asyncio.CancelledError, Exception):
|
|
251
|
+
pass
|
|
212
252
|
await self.close()
|
|
213
253
|
PrintLine(text=f'Connection {self.id} closed.')
|
|
214
254
|
|
|
@@ -290,12 +330,13 @@ class BaseWebsockets:
|
|
|
290
330
|
self.__window_response = value
|
|
291
331
|
|
|
292
332
|
def event_handler(self, message: wsMessage):
|
|
333
|
+
session = sessions.get_session(session_id=message.session_id)
|
|
293
334
|
return EventConstrutor(
|
|
294
335
|
target_id=message.target_uuid,
|
|
295
336
|
current_target_id=message.current_target_uuid,
|
|
296
337
|
app=self.app,
|
|
297
338
|
ws=self,
|
|
298
|
-
session=
|
|
339
|
+
session=session,
|
|
299
340
|
route=message.route,
|
|
300
341
|
event_data=message.event_data,
|
|
301
342
|
event_type=message.type
|
|
@@ -304,14 +345,28 @@ class BaseWebsockets:
|
|
|
304
345
|
async def message_handler(self, message: wsMessage):
|
|
305
346
|
token = set_current_window(message.window)
|
|
306
347
|
try:
|
|
348
|
+
if sessions.get_session(session_id=message.session_id) is None:
|
|
349
|
+
return
|
|
350
|
+
|
|
307
351
|
event_handler = self.event_handler(message)
|
|
308
352
|
|
|
309
353
|
if message.event_ref == 'document':
|
|
310
354
|
if event_handler.current_target:
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
355
|
+
raw = event_handler.current_target.events.__dict__.get(f'on{message.type}')
|
|
356
|
+
handler: Callable[..., Any] | None = None
|
|
357
|
+
event_id: str | None = None
|
|
358
|
+
|
|
359
|
+
if callable(raw):
|
|
360
|
+
handler = raw
|
|
361
|
+
event_id = f'event_{id(handler)}'
|
|
362
|
+
elif isinstance(raw, str):
|
|
363
|
+
from pyweber.core.events import EventBook
|
|
364
|
+
entry = EventBook.get(raw)
|
|
365
|
+
if entry and callable(entry.get('event')):
|
|
366
|
+
handler = entry['event']
|
|
367
|
+
event_id = raw
|
|
368
|
+
|
|
369
|
+
if handler and event_id:
|
|
315
370
|
if inspect.iscoroutinefunction(handler):
|
|
316
371
|
if event_id not in self.task_manager.active_handlers_async.get(message.session_id, {}):
|
|
317
372
|
await self.task_manager.create_task_async(
|
|
@@ -355,10 +410,7 @@ class BaseWebsockets:
|
|
|
355
410
|
session = sessions.get_session(session_id=session_id)
|
|
356
411
|
current_template: 'Template' = data.get('template', None)
|
|
357
412
|
|
|
358
|
-
if current_template:
|
|
359
|
-
updated_template = current_template.clone()
|
|
360
|
-
# session.template = updated_template
|
|
361
|
-
|
|
413
|
+
if current_template and session is not None:
|
|
362
414
|
data['template'] = await self.get_template_diff(
|
|
363
415
|
session=session
|
|
364
416
|
)
|
|
@@ -499,13 +551,27 @@ class BaseWebsockets:
|
|
|
499
551
|
|
|
500
552
|
diff = TemplateDiff()
|
|
501
553
|
|
|
554
|
+
if session.old_template is None:
|
|
555
|
+
try:
|
|
556
|
+
session.old_template = session.template.clone()
|
|
557
|
+
except Exception:
|
|
558
|
+
# No baseline → empty diff rather than crashing the WS send path
|
|
559
|
+
return {}
|
|
560
|
+
|
|
502
561
|
for tag in ['head', 'body']:
|
|
562
|
+
old_el = session.old_template.querySelector(tag)
|
|
563
|
+
new_el = session.template.querySelector(tag)
|
|
564
|
+
if old_el is None or new_el is None:
|
|
565
|
+
continue
|
|
503
566
|
diff.track_differences(
|
|
504
|
-
new_element=
|
|
505
|
-
old_element=
|
|
567
|
+
new_element=new_el,
|
|
568
|
+
old_element=old_el,
|
|
506
569
|
)
|
|
507
570
|
|
|
508
|
-
|
|
571
|
+
try:
|
|
572
|
+
session.old_template = session.template.clone()
|
|
573
|
+
except Exception as exc:
|
|
574
|
+
PrintLine(text=f'template clone after diff failed: {exc}', level='ERROR')
|
|
509
575
|
|
|
510
576
|
return diff.differences
|
|
511
577
|
|
|
@@ -518,18 +584,33 @@ class WebsocketManager(BaseWebsockets):
|
|
|
518
584
|
self.ws_connections[connection.id] = connection
|
|
519
585
|
|
|
520
586
|
def remove_connection(self, id: str):
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
587
|
+
conn = self.ws_connections.pop(id, None)
|
|
588
|
+
if conn is None:
|
|
589
|
+
return
|
|
590
|
+
try:
|
|
591
|
+
loop = asyncio.get_running_loop()
|
|
592
|
+
loop.create_task(conn.close())
|
|
593
|
+
except RuntimeError:
|
|
594
|
+
try:
|
|
595
|
+
conn.client.close()
|
|
596
|
+
except Exception:
|
|
597
|
+
pass
|
|
524
598
|
|
|
525
599
|
def remove_all(self):
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
600
|
+
ids = list(self.ws_connections.keys())
|
|
601
|
+
for conn_id in ids:
|
|
602
|
+
self.remove_connection(conn_id)
|
|
529
603
|
|
|
530
604
|
def send_all(self, message: bytes):
|
|
531
|
-
for
|
|
532
|
-
|
|
605
|
+
for conn in list(self.ws_connections.values()):
|
|
606
|
+
try:
|
|
607
|
+
loop = asyncio.get_running_loop()
|
|
608
|
+
loop.create_task(conn.send(message=message))
|
|
609
|
+
except RuntimeError:
|
|
610
|
+
try:
|
|
611
|
+
asyncio.run(conn.send(message=message))
|
|
612
|
+
except Exception:
|
|
613
|
+
pass
|
|
533
614
|
|
|
534
615
|
def get_connection(self, id: str):
|
|
535
616
|
return self.ws_connections.get(id, None)
|
|
@@ -549,12 +630,109 @@ class WebsocketManager(BaseWebsockets):
|
|
|
549
630
|
# Ignore client-supplied session_id when cookie is missing/invalid
|
|
550
631
|
return generate_session_id()
|
|
551
632
|
|
|
633
|
+
def bind_session_id(
|
|
634
|
+
self,
|
|
635
|
+
*,
|
|
636
|
+
client_session_id: str | None,
|
|
637
|
+
connection_id: str | None,
|
|
638
|
+
cookies: dict[str, str] | None,
|
|
639
|
+
) -> tuple[str, bool]:
|
|
640
|
+
"""Resolve the session for this WS connection.
|
|
641
|
+
|
|
642
|
+
Prefer an id already bound on the socket (``connection_id``). That stops
|
|
643
|
+
the handshake race where a second frame still has ``sessionId: null`` and
|
|
644
|
+
would otherwise mint a new session + ``clone_template`` (new UUIDs → no
|
|
645
|
+
event handlers match the DOM).
|
|
646
|
+
|
|
647
|
+
Returns ``(session_id, is_new)``.
|
|
648
|
+
"""
|
|
649
|
+
if connection_id and connection_id in sessions.all_sessions:
|
|
650
|
+
return connection_id, False
|
|
651
|
+
|
|
652
|
+
if client_session_id and client_session_id in sessions.all_sessions:
|
|
653
|
+
return client_session_id, False
|
|
654
|
+
|
|
655
|
+
resolved = self.get_session_id(
|
|
656
|
+
session_id=client_session_id,
|
|
657
|
+
cookies=cookies or {},
|
|
658
|
+
)
|
|
659
|
+
if resolved in sessions.all_sessions:
|
|
660
|
+
return resolved, False
|
|
661
|
+
|
|
662
|
+
# Keep a stable id for this socket even before add_session runs
|
|
663
|
+
if connection_id:
|
|
664
|
+
return connection_id, True
|
|
665
|
+
|
|
666
|
+
return resolved, True
|
|
667
|
+
|
|
668
|
+
async def _ensure_session_and_template(
|
|
669
|
+
self,
|
|
670
|
+
message: wsMessage,
|
|
671
|
+
*,
|
|
672
|
+
connection_id: str | None,
|
|
673
|
+
cookies: dict[str, str] | None,
|
|
674
|
+
send_target,
|
|
675
|
+
) -> tuple[str, 'Template', bool]:
|
|
676
|
+
"""Bind session id *before* template sync, then create/reuse session."""
|
|
677
|
+
session_id, is_new = self.bind_session_id(
|
|
678
|
+
client_session_id=message.session_id,
|
|
679
|
+
connection_id=connection_id,
|
|
680
|
+
cookies=cookies,
|
|
681
|
+
)
|
|
682
|
+
message.session_id = session_id
|
|
683
|
+
|
|
684
|
+
sync_template = await self.get_sync_template(message=message)
|
|
685
|
+
|
|
686
|
+
if is_new:
|
|
687
|
+
self.add_session(
|
|
688
|
+
session_id=session_id,
|
|
689
|
+
template=sync_template,
|
|
690
|
+
window=message.window,
|
|
691
|
+
route=message.route,
|
|
692
|
+
)
|
|
693
|
+
self.ws_connections[session_id] = send_target
|
|
694
|
+
await self.send_message(
|
|
695
|
+
data={
|
|
696
|
+
'setSessionId': session_id,
|
|
697
|
+
'windowEvents': message.window.get_all_event_ids,
|
|
698
|
+
},
|
|
699
|
+
session_id=session_id,
|
|
700
|
+
)
|
|
701
|
+
else:
|
|
702
|
+
self.ws_connections[session_id] = send_target
|
|
703
|
+
if message.get_value(key='template'):
|
|
704
|
+
self.update_session(
|
|
705
|
+
session_id=session_id,
|
|
706
|
+
template=sync_template,
|
|
707
|
+
window=message.window,
|
|
708
|
+
route=message.route,
|
|
709
|
+
)
|
|
710
|
+
session = sessions.get_session(session_id=session_id)
|
|
711
|
+
if session:
|
|
712
|
+
try:
|
|
713
|
+
session.old_template = sync_template.clone()
|
|
714
|
+
except Exception as exc:
|
|
715
|
+
PrintLine(
|
|
716
|
+
text=f'template clone for diff failed: {exc}',
|
|
717
|
+
level='ERROR',
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
return session_id, sync_template, is_new
|
|
721
|
+
|
|
552
722
|
async def get_sync_template(self, message: wsMessage):
|
|
553
723
|
assert isinstance(message, wsMessage)
|
|
554
724
|
sync_template = await message.template
|
|
555
725
|
session = sessions.get_session(session_id=message.session_id)
|
|
556
726
|
if session:
|
|
557
|
-
|
|
727
|
+
try:
|
|
728
|
+
session.old_template = sync_template.clone()
|
|
729
|
+
except Exception as exc:
|
|
730
|
+
# Never drop the WS event path because old_template clone failed
|
|
731
|
+
# (historically Form/Input attrs setters broke Element.clone).
|
|
732
|
+
PrintLine(text=f'template clone for diff failed: {exc}', level='ERROR')
|
|
733
|
+
# Do not alias template (that yields empty diffs after mutations).
|
|
734
|
+
if session.old_template is sync_template:
|
|
735
|
+
session.old_template = None
|
|
558
736
|
return sync_template
|
|
559
737
|
|
|
560
738
|
def add_session(self, session_id: str, template: 'Template', window: 'Window', route: str):
|
|
@@ -578,7 +756,14 @@ class WebsocketManager(BaseWebsockets):
|
|
|
578
756
|
sessions.remove_session(session_id=session_id)
|
|
579
757
|
|
|
580
758
|
if session_id in self.ws_connections:
|
|
581
|
-
|
|
759
|
+
conn = self.ws_connections.pop(session_id)
|
|
760
|
+
try:
|
|
761
|
+
await conn.close()
|
|
762
|
+
except Exception:
|
|
763
|
+
try:
|
|
764
|
+
conn.client.close()
|
|
765
|
+
except Exception:
|
|
766
|
+
pass
|
|
582
767
|
|
|
583
768
|
try:
|
|
584
769
|
await self.task_manager.cancel_session_handlers(session_id=session_id)
|
|
@@ -596,19 +781,19 @@ class WebsocketManager(BaseWebsockets):
|
|
|
596
781
|
if not isinstance(message, dict):
|
|
597
782
|
return {}
|
|
598
783
|
|
|
599
|
-
|
|
784
|
+
# Require the JS contract keys (values may be null). Extra keys are
|
|
785
|
+
# ignored — do not invert this into an allowlist over message.keys()
|
|
786
|
+
# (that rejects nothing useful and confuses debugging).
|
|
787
|
+
required = need_message_keys()
|
|
788
|
+
if not all(key in message for key in required):
|
|
600
789
|
return {}
|
|
601
790
|
|
|
602
|
-
|
|
603
|
-
if (
|
|
604
|
-
not message.get('template')
|
|
605
|
-
and message.get('window_data')
|
|
606
|
-
and (not session_id or session_id not in sessions.all_sessions)
|
|
607
|
-
):
|
|
791
|
+
if not _ws_message_is_actionable(message):
|
|
608
792
|
return {}
|
|
609
793
|
|
|
610
|
-
route = message.get('route', '')
|
|
794
|
+
route = message.get('route', '') or ''
|
|
611
795
|
route = route[:-1] if route.endswith('/') and len(route) > 1 else route
|
|
796
|
+
message['route'] = route
|
|
612
797
|
if route not in self.app.list_routes:
|
|
613
798
|
return {}
|
|
614
799
|
|
|
@@ -618,7 +803,7 @@ class WebsocketManager(BaseWebsockets):
|
|
|
618
803
|
return {}
|
|
619
804
|
|
|
620
805
|
async def ws_handler_wsgi(self, ws_server: WebsocketServer):
|
|
621
|
-
for message in ws_server:
|
|
806
|
+
async for message in ws_server:
|
|
622
807
|
raw_message = self.process_ws_message_handler(message=message)
|
|
623
808
|
|
|
624
809
|
if not raw_message:
|
|
@@ -632,54 +817,19 @@ class WebsocketManager(BaseWebsockets):
|
|
|
632
817
|
if message.window_response:
|
|
633
818
|
await self.set_window_response(message.window_response, message.session_id)
|
|
634
819
|
|
|
635
|
-
sync_template = await self.
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
session_id not in sessions.all_sessions
|
|
641
|
-
for session_id in [message.session_id, ws_server.id]
|
|
642
|
-
)
|
|
820
|
+
session_id, sync_template, _is_new = await self._ensure_session_and_template(
|
|
821
|
+
message,
|
|
822
|
+
connection_id=ws_server.id,
|
|
823
|
+
cookies=getattr(ws_server, 'cookies', {}) or {},
|
|
824
|
+
send_target=ws_server,
|
|
643
825
|
)
|
|
644
|
-
|
|
645
|
-
if is_new_session:
|
|
646
|
-
ws_server.id = self.get_session_id(
|
|
647
|
-
session_id=message.session_id,
|
|
648
|
-
cookies=getattr(ws_server, 'cookies', {}) or {},
|
|
649
|
-
)
|
|
650
|
-
self.ws_connections[ws_server.id] = ws_server
|
|
651
|
-
|
|
652
|
-
self.add_session(
|
|
653
|
-
session_id=ws_server.id,
|
|
654
|
-
template=sync_template,
|
|
655
|
-
window=message.window,
|
|
656
|
-
route=message.route
|
|
657
|
-
)
|
|
658
|
-
|
|
659
|
-
await self.send_message(
|
|
660
|
-
data={
|
|
661
|
-
'setSessionId': ws_server.id,
|
|
662
|
-
'windowEvents': message.window.get_all_event_ids
|
|
663
|
-
},
|
|
664
|
-
session_id=ws_server.id
|
|
665
|
-
)
|
|
666
|
-
|
|
667
|
-
elif message.get_value(key='template'):
|
|
668
|
-
self.update_session(
|
|
669
|
-
session_id=ws_server.id,
|
|
670
|
-
template=sync_template,
|
|
671
|
-
window=message.window,
|
|
672
|
-
route=message.route
|
|
673
|
-
)
|
|
674
|
-
session = sessions.get_session(session_id=ws_server.id)
|
|
675
|
-
if session:
|
|
676
|
-
session.old_template = sync_template.clone()
|
|
826
|
+
ws_server.id = session_id
|
|
677
827
|
|
|
678
828
|
if message.type and message.event_ref and not event_is_running(
|
|
679
829
|
message=message, task_manager=self.task_manager
|
|
680
830
|
):
|
|
681
831
|
self.update_session(
|
|
682
|
-
session_id=
|
|
832
|
+
session_id=session_id,
|
|
683
833
|
template=sync_template,
|
|
684
834
|
window=message.window,
|
|
685
835
|
route=message.route
|
|
@@ -711,55 +861,23 @@ class WebsocketManager(BaseWebsockets):
|
|
|
711
861
|
if message.file_content:
|
|
712
862
|
await self.set_file_content(message.file_content, message.session_id)
|
|
713
863
|
|
|
714
|
-
sync_template = await self.
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
864
|
+
session_id, sync_template, _is_new = await self._ensure_session_and_template(
|
|
865
|
+
message,
|
|
866
|
+
connection_id=ws_connection,
|
|
867
|
+
cookies=handshake_cookies,
|
|
868
|
+
send_target=send,
|
|
719
869
|
)
|
|
870
|
+
ws_connection = session_id
|
|
720
871
|
|
|
721
|
-
if
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
cookies=handshake_cookies,
|
|
725
|
-
)
|
|
726
|
-
|
|
727
|
-
self.add_session(
|
|
728
|
-
session_id=ws_connection,
|
|
729
|
-
template=sync_template,
|
|
730
|
-
window=message.window,
|
|
731
|
-
route=message.route
|
|
732
|
-
)
|
|
733
|
-
|
|
734
|
-
self.ws_connections[ws_connection] = send
|
|
735
|
-
|
|
736
|
-
await self.send_message(
|
|
737
|
-
data={
|
|
738
|
-
'setSessionId': ws_connection,
|
|
739
|
-
'windowEvents': message.window.get_all_event_ids,
|
|
740
|
-
},
|
|
741
|
-
session_id=ws_connection
|
|
742
|
-
)
|
|
743
|
-
|
|
744
|
-
elif message.get_value(key='template'):
|
|
745
|
-
self.update_session(
|
|
746
|
-
session_id=ws_connection,
|
|
747
|
-
template=sync_template,
|
|
748
|
-
window=message.window,
|
|
749
|
-
route=message.route
|
|
750
|
-
)
|
|
751
|
-
session = sessions.get_session(session_id=ws_connection)
|
|
752
|
-
if session:
|
|
753
|
-
session.old_template = sync_template.clone()
|
|
754
|
-
|
|
755
|
-
if message.type and message.event_ref and not event_is_running(message=message, task_manager=self.task_manager):
|
|
872
|
+
if message.type and message.event_ref and not event_is_running(
|
|
873
|
+
message=message, task_manager=self.task_manager
|
|
874
|
+
):
|
|
756
875
|
self.update_session(
|
|
757
876
|
session_id=ws_connection,
|
|
758
877
|
template=sync_template,
|
|
759
878
|
window=message.window,
|
|
760
879
|
route=message.route
|
|
761
880
|
)
|
|
762
|
-
|
|
763
881
|
await self.message_handler(message=message)
|
|
764
882
|
else:
|
|
765
883
|
break
|