flet-web 0.70.0.dev6232__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.
Potentially problematic release.
This version of flet-web might be problematic. Click here for more details.
- flet_web-0.70.0.dev6232/PKG-INFO +18 -0
- flet_web-0.70.0.dev6232/README.md +3 -0
- flet_web-0.70.0.dev6232/pyproject.toml +25 -0
- flet_web-0.70.0.dev6232/setup.cfg +4 -0
- flet_web-0.70.0.dev6232/src/flet_web/__init__.py +16 -0
- flet_web-0.70.0.dev6232/src/flet_web/fastapi/__init__.py +8 -0
- flet_web-0.70.0.dev6232/src/flet_web/fastapi/app.py +148 -0
- flet_web-0.70.0.dev6232/src/flet_web/fastapi/flet_app.py +373 -0
- flet_web-0.70.0.dev6232/src/flet_web/fastapi/flet_app_manager.py +146 -0
- flet_web-0.70.0.dev6232/src/flet_web/fastapi/flet_fastapi.py +128 -0
- flet_web-0.70.0.dev6232/src/flet_web/fastapi/flet_oauth.py +80 -0
- flet_web-0.70.0.dev6232/src/flet_web/fastapi/flet_static_files.py +192 -0
- flet_web-0.70.0.dev6232/src/flet_web/fastapi/flet_upload.py +95 -0
- flet_web-0.70.0.dev6232/src/flet_web/fastapi/oauth_state.py +14 -0
- flet_web-0.70.0.dev6232/src/flet_web/fastapi/serve_fastapi_web_app.py +98 -0
- flet_web-0.70.0.dev6232/src/flet_web/patch_index.py +112 -0
- flet_web-0.70.0.dev6232/src/flet_web/uploads.py +54 -0
- flet_web-0.70.0.dev6232/src/flet_web/version.py +1 -0
- flet_web-0.70.0.dev6232/src/flet_web.egg-info/PKG-INFO +18 -0
- flet_web-0.70.0.dev6232/src/flet_web.egg-info/SOURCES.txt +21 -0
- flet_web-0.70.0.dev6232/src/flet_web.egg-info/dependency_links.txt +1 -0
- flet_web-0.70.0.dev6232/src/flet_web.egg-info/requires.txt +3 -0
- flet_web-0.70.0.dev6232/src/flet_web.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flet-web
|
|
3
|
+
Version: 0.70.0.dev6232
|
|
4
|
+
Summary: Flet web client in Flutter.
|
|
5
|
+
Author-email: "Appveyor Systems Inc." <hello@flet.dev>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://flet.dev
|
|
8
|
+
Project-URL: Repository, https://github.com/flet-dev/flet
|
|
9
|
+
Project-URL: Documentation, https://flet.dev/docs
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: flet
|
|
13
|
+
Requires-Dist: fastapi>=0.115.12
|
|
14
|
+
Requires-Dist: uvicorn[standard]>=0.35.0
|
|
15
|
+
|
|
16
|
+
# Flet Web client in Flutter
|
|
17
|
+
|
|
18
|
+
This package contains a compiled Flutter Flet web client.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "flet-web"
|
|
3
|
+
version = "0.70.0.dev6232"
|
|
4
|
+
description = "Flet web client in Flutter."
|
|
5
|
+
authors = [{ name = "Appveyor Systems Inc.", email = "hello@flet.dev" }]
|
|
6
|
+
license = "Apache-2.0"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
requires-python = ">=3.10"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"flet",
|
|
11
|
+
"fastapi >=0.115.12",
|
|
12
|
+
"uvicorn[standard] >=0.35.0"
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
Homepage = "https://flet.dev"
|
|
17
|
+
Repository = "https://github.com/flet-dev/flet"
|
|
18
|
+
Documentation = "https://flet.dev/docs"
|
|
19
|
+
|
|
20
|
+
[tool.setuptools.package-data]
|
|
21
|
+
"flet_web.web" = ["**/*"]
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["setuptools"]
|
|
25
|
+
build-backend = "setuptools.build_meta"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from flet_web.patch_index import (
|
|
5
|
+
patch_font_manifest_json,
|
|
6
|
+
patch_index_html,
|
|
7
|
+
patch_manifest_json,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_package_web_dir():
|
|
12
|
+
web_root_dir = os.environ.get("FLET_WEB_PATH")
|
|
13
|
+
return web_root_dir or str(Path(__file__).parent.joinpath("web"))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__all__ = ["patch_font_manifest_json", "patch_index_html", "patch_manifest_json"]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from flet_web.fastapi.app import app
|
|
2
|
+
from flet_web.fastapi.flet_app import FletApp
|
|
3
|
+
from flet_web.fastapi.flet_app_manager import app_manager
|
|
4
|
+
from flet_web.fastapi.flet_fastapi import FastAPI
|
|
5
|
+
from flet_web.fastapi.flet_static_files import FletStaticFiles
|
|
6
|
+
from flet_web.fastapi.flet_upload import FletUpload
|
|
7
|
+
|
|
8
|
+
__all__ = ["app", "FletApp", "app_manager", "FastAPI", "FletStaticFiles", "FletUpload"]
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
from collections.abc import Awaitable
|
|
4
|
+
from typing import Callable, Optional, Union
|
|
5
|
+
|
|
6
|
+
from fastapi import Request, WebSocket
|
|
7
|
+
from flet.controls.page import Page
|
|
8
|
+
from flet.controls.types import RouteUrlStrategy, WebRenderer
|
|
9
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
10
|
+
|
|
11
|
+
from flet_web.fastapi.flet_app import (
|
|
12
|
+
DEFAULT_FLET_OAUTH_STATE_TIMEOUT,
|
|
13
|
+
DEFAULT_FLET_SESSION_TIMEOUT,
|
|
14
|
+
FletApp,
|
|
15
|
+
app_manager,
|
|
16
|
+
)
|
|
17
|
+
from flet_web.fastapi.flet_fastapi import FastAPI
|
|
18
|
+
from flet_web.fastapi.flet_oauth import FletOAuth
|
|
19
|
+
from flet_web.fastapi.flet_static_files import FletStaticFiles
|
|
20
|
+
from flet_web.fastapi.flet_upload import FletUpload
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def app(
|
|
24
|
+
main: Union[Callable[[Page], Awaitable], Callable[[Page], None]],
|
|
25
|
+
before_main: Union[Callable[[Page], Awaitable], Callable[[Page], None]],
|
|
26
|
+
proxy_path: Optional[str] = None,
|
|
27
|
+
assets_dir: Optional[str] = None,
|
|
28
|
+
app_name: Optional[str] = None,
|
|
29
|
+
app_short_name: Optional[str] = None,
|
|
30
|
+
app_description: Optional[str] = None,
|
|
31
|
+
web_renderer: WebRenderer = WebRenderer.AUTO,
|
|
32
|
+
route_url_strategy: RouteUrlStrategy = RouteUrlStrategy.PATH,
|
|
33
|
+
no_cdn: bool = False,
|
|
34
|
+
upload_dir: Optional[str] = None,
|
|
35
|
+
upload_endpoint_path: Optional[str] = None,
|
|
36
|
+
max_upload_size: Optional[int] = None,
|
|
37
|
+
secret_key: Optional[str] = None,
|
|
38
|
+
session_timeout_seconds: int = DEFAULT_FLET_SESSION_TIMEOUT,
|
|
39
|
+
oauth_state_timeout_seconds: int = DEFAULT_FLET_OAUTH_STATE_TIMEOUT,
|
|
40
|
+
):
|
|
41
|
+
"""
|
|
42
|
+
Mount all Flet FastAPI handlers in one call.
|
|
43
|
+
|
|
44
|
+
Parameters:
|
|
45
|
+
* `main` (function or coroutine) - application entry point - a method
|
|
46
|
+
called for newly connected user. Handler must have 1 parameter: `page` - `Page`
|
|
47
|
+
instance.
|
|
48
|
+
* `before_main` - a function that is called after Page was created, but before
|
|
49
|
+
calling `main`.
|
|
50
|
+
* `assets_dir` (str, optional) - an absolute path to app's assets directory.
|
|
51
|
+
* `app_name` (str, optional) - PWA application name.
|
|
52
|
+
* `app_short_name` (str, optional) - PWA application short name.
|
|
53
|
+
* `app_description` (str, optional) - PWA application description.
|
|
54
|
+
* `web_renderer` (WebRenderer) - web renderer defaulting to `WebRenderer.AUTO`.
|
|
55
|
+
* `route_url_strategy` (str) - routing URL strategy: `path` (default) or `hash`.
|
|
56
|
+
* `no_cdn` (bool) - do not load resources from CDN.
|
|
57
|
+
* `upload_dir` (str) - an absolute path to a directory with uploaded files.
|
|
58
|
+
* `upload_endpoint_path` (str, optional) - absolute URL of upload endpoint,
|
|
59
|
+
e.g. `/upload`.
|
|
60
|
+
* `max_upload_size` (str, int) - maximum size of a single upload, bytes.
|
|
61
|
+
Unlimited if `None`.
|
|
62
|
+
* `secret_key` (str, optional) - secret key to sign and verify upload requests.
|
|
63
|
+
* `session_timeout_seconds` (int, optional)- session lifetime, in seconds, after
|
|
64
|
+
user disconnected.
|
|
65
|
+
* `oauth_state_timeout_seconds` (int, optional) - OAuth state lifetime, in seconds,
|
|
66
|
+
which is a maximum allowed time between starting OAuth flow and redirecting
|
|
67
|
+
to OAuth callback URL.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
env_upload_dir = os.getenv("FLET_UPLOAD_DIR")
|
|
71
|
+
if env_upload_dir:
|
|
72
|
+
upload_dir = env_upload_dir
|
|
73
|
+
|
|
74
|
+
env_websocket_endpoint = os.getenv("FLET_WEBSOCKET_HANDLER_ENDPOINT")
|
|
75
|
+
websocket_endpoint = (
|
|
76
|
+
"ws" if not env_websocket_endpoint else env_websocket_endpoint.strip("/")
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
env_upload_endpoint = os.getenv("FLET_UPLOAD_HANDLER_ENDPOINT")
|
|
80
|
+
upload_endpoint = (
|
|
81
|
+
"upload" if not env_upload_endpoint else env_upload_endpoint.strip("/")
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
env_oauth_callback_endpoint = os.getenv("FLET_OAUTH_CALLBACK_HANDLER_ENDPOINT")
|
|
85
|
+
oauth_callback_endpoint = (
|
|
86
|
+
"oauth_callback"
|
|
87
|
+
if not env_oauth_callback_endpoint
|
|
88
|
+
else env_oauth_callback_endpoint.strip("/")
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
fastapi_app = FastAPI()
|
|
92
|
+
|
|
93
|
+
@fastapi_app.websocket(f"/{websocket_endpoint}")
|
|
94
|
+
async def app_handler(websocket: WebSocket):
|
|
95
|
+
await FletApp(
|
|
96
|
+
loop=asyncio.get_running_loop(),
|
|
97
|
+
executor=app_manager.executor,
|
|
98
|
+
main=main,
|
|
99
|
+
before_main=before_main,
|
|
100
|
+
session_timeout_seconds=session_timeout_seconds,
|
|
101
|
+
oauth_state_timeout_seconds=oauth_state_timeout_seconds,
|
|
102
|
+
upload_endpoint_path=upload_endpoint_path,
|
|
103
|
+
secret_key=secret_key,
|
|
104
|
+
).handle(websocket)
|
|
105
|
+
|
|
106
|
+
if upload_dir:
|
|
107
|
+
|
|
108
|
+
@fastapi_app.put(
|
|
109
|
+
f"/{upload_endpoint_path if upload_endpoint_path else upload_endpoint}"
|
|
110
|
+
)
|
|
111
|
+
async def upload_handler(request: Request):
|
|
112
|
+
await FletUpload(
|
|
113
|
+
upload_dir=upload_dir,
|
|
114
|
+
max_upload_size=max_upload_size,
|
|
115
|
+
secret_key=secret_key,
|
|
116
|
+
).handle(request)
|
|
117
|
+
|
|
118
|
+
@fastapi_app.get(f"/{oauth_callback_endpoint}")
|
|
119
|
+
async def oauth_redirect_handler(request: Request):
|
|
120
|
+
return await FletOAuth().handle(request)
|
|
121
|
+
|
|
122
|
+
fastapi_app.mount(
|
|
123
|
+
path="/",
|
|
124
|
+
app=FletStaticFiles(
|
|
125
|
+
proxy_path=proxy_path,
|
|
126
|
+
assets_dir=assets_dir,
|
|
127
|
+
app_name=app_name,
|
|
128
|
+
app_short_name=app_short_name,
|
|
129
|
+
app_description=app_description,
|
|
130
|
+
web_renderer=web_renderer,
|
|
131
|
+
route_url_strategy=route_url_strategy,
|
|
132
|
+
websocket_endpoint_path=websocket_endpoint,
|
|
133
|
+
no_cdn=no_cdn,
|
|
134
|
+
),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Add middleware for custom headers
|
|
138
|
+
class CustomHeadersMiddleware(BaseHTTPMiddleware):
|
|
139
|
+
async def dispatch(self, request: Request, call_next):
|
|
140
|
+
response = await call_next(request)
|
|
141
|
+
response.headers["Cross-Origin-Opener-Policy"] = "same-origin"
|
|
142
|
+
response.headers["Cross-Origin-Embedder-Policy"] = "require-corp"
|
|
143
|
+
response.headers["Access-Control-Allow-Origin"] = "*"
|
|
144
|
+
return response
|
|
145
|
+
|
|
146
|
+
fastapi_app.add_middleware(CustomHeadersMiddleware)
|
|
147
|
+
|
|
148
|
+
return fastapi_app
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import inspect
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import traceback
|
|
6
|
+
import weakref
|
|
7
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
8
|
+
from datetime import datetime, timedelta, timezone
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
|
|
11
|
+
import msgpack
|
|
12
|
+
from fastapi import WebSocket, WebSocketDisconnect
|
|
13
|
+
|
|
14
|
+
import flet_web.fastapi as flet_fastapi
|
|
15
|
+
from flet.controls.base_control import BaseControl
|
|
16
|
+
from flet.controls.context import _context_page, context
|
|
17
|
+
from flet.controls.exceptions import FletPageDisconnectedException
|
|
18
|
+
from flet.messaging.connection import Connection
|
|
19
|
+
from flet.messaging.protocol import (
|
|
20
|
+
ClientAction,
|
|
21
|
+
ClientMessage,
|
|
22
|
+
ControlEventBody,
|
|
23
|
+
InvokeMethodResponseBody,
|
|
24
|
+
RegisterClientRequestBody,
|
|
25
|
+
RegisterClientResponseBody,
|
|
26
|
+
UpdateControlPropsBody,
|
|
27
|
+
configure_encode_object_for_msgpack,
|
|
28
|
+
decode_ext_from_msgpack,
|
|
29
|
+
)
|
|
30
|
+
from flet.messaging.session import Session
|
|
31
|
+
from flet.utils import random_string, sha1
|
|
32
|
+
from flet_web.fastapi.flet_app_manager import app_manager
|
|
33
|
+
from flet_web.fastapi.oauth_state import OAuthState
|
|
34
|
+
from flet_web.uploads import build_upload_url
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(flet_fastapi.__name__)
|
|
37
|
+
transport_log = logging.getLogger("flet_transport")
|
|
38
|
+
|
|
39
|
+
DEFAULT_FLET_SESSION_TIMEOUT = 3600
|
|
40
|
+
DEFAULT_FLET_OAUTH_STATE_TIMEOUT = 600
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class FletApp(Connection):
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
loop: asyncio.AbstractEventLoop,
|
|
47
|
+
executor: ThreadPoolExecutor,
|
|
48
|
+
main,
|
|
49
|
+
before_main,
|
|
50
|
+
session_timeout_seconds: int = DEFAULT_FLET_SESSION_TIMEOUT,
|
|
51
|
+
oauth_state_timeout_seconds: int = DEFAULT_FLET_OAUTH_STATE_TIMEOUT,
|
|
52
|
+
upload_endpoint_path: Optional[str] = None,
|
|
53
|
+
secret_key: Optional[str] = None,
|
|
54
|
+
):
|
|
55
|
+
"""
|
|
56
|
+
Handle Flet app WebSocket connections.
|
|
57
|
+
|
|
58
|
+
Parameters:
|
|
59
|
+
|
|
60
|
+
* `session_handler` (Coroutine) - application entry point - an async method
|
|
61
|
+
called for newly connected user. Handler coroutine must have
|
|
62
|
+
1 parameter: `page` - `Page` instance.
|
|
63
|
+
* `session_timeout_seconds` (int, optional) - session lifetime, in seconds,
|
|
64
|
+
after user disconnected.
|
|
65
|
+
* `oauth_state_timeout_seconds` (int, optional) - OAuth state lifetime,
|
|
66
|
+
in seconds, which is a maximum allowed time between starting OAuth flow
|
|
67
|
+
and redirecting to OAuth callback URL.
|
|
68
|
+
* `upload_endpoint_path` (str, optional) - absolute URL of upload endpoint,
|
|
69
|
+
e.g. `/upload`.
|
|
70
|
+
* `secret_key` (str, optional) - secret key to sign upload requests.
|
|
71
|
+
"""
|
|
72
|
+
super().__init__()
|
|
73
|
+
self.__id = random_string(8)
|
|
74
|
+
logger.info(f"New FletApp: {self.__id}")
|
|
75
|
+
|
|
76
|
+
self.__session = None
|
|
77
|
+
self.loop = loop
|
|
78
|
+
self.executor = executor
|
|
79
|
+
self.__main = main
|
|
80
|
+
self.__before_main = before_main
|
|
81
|
+
self.__session_timeout_seconds = session_timeout_seconds
|
|
82
|
+
self.__oauth_state_timeout_seconds = oauth_state_timeout_seconds
|
|
83
|
+
self.__running_tasks = set()
|
|
84
|
+
|
|
85
|
+
env_session_timeout_seconds = os.getenv("FLET_SESSION_TIMEOUT")
|
|
86
|
+
if env_session_timeout_seconds:
|
|
87
|
+
self.__session_timeout_seconds = int(env_session_timeout_seconds)
|
|
88
|
+
|
|
89
|
+
env_oauth_state_timeout_seconds = os.getenv("FLET_OAUTH_STATE_TIMEOUT")
|
|
90
|
+
if env_oauth_state_timeout_seconds:
|
|
91
|
+
self.__oauth_state_timeout_seconds = int(env_oauth_state_timeout_seconds)
|
|
92
|
+
|
|
93
|
+
self.__upload_endpoint_path = upload_endpoint_path
|
|
94
|
+
self.__secret_key = secret_key
|
|
95
|
+
|
|
96
|
+
app_id = self.__id
|
|
97
|
+
weakref.finalize(
|
|
98
|
+
self, lambda: logger.debug(f"FletApp was garbage collected: {app_id}")
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
async def handle(self, websocket: WebSocket):
|
|
102
|
+
"""
|
|
103
|
+
Handle WebSocket connection.
|
|
104
|
+
|
|
105
|
+
Parameters:
|
|
106
|
+
|
|
107
|
+
* `websocket` (WebSocket) - Websocket instance.
|
|
108
|
+
"""
|
|
109
|
+
self.__websocket = websocket
|
|
110
|
+
|
|
111
|
+
self.__client_ip = (
|
|
112
|
+
self.__websocket.client.host if self.__websocket.client else ""
|
|
113
|
+
)
|
|
114
|
+
self.__client_user_agent = self.__websocket.headers.get("user-agent", "")
|
|
115
|
+
self.__oauth_state_id = self.__websocket.cookies.get("flet_oauth_state")
|
|
116
|
+
|
|
117
|
+
self.pubsubhub = app_manager.get_pubsubhub(self.__main, loop=self.loop)
|
|
118
|
+
self.page_url = str(websocket.url).rsplit("/", 1)[0]
|
|
119
|
+
self.page_name = websocket.url.path.rsplit("/", 1)[0].lstrip("/")
|
|
120
|
+
|
|
121
|
+
if not self.__upload_endpoint_path:
|
|
122
|
+
self.__upload_endpoint_path = (
|
|
123
|
+
f"{'' if self.page_name == '' else '/'}{self.page_name}/upload"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
await self.__websocket.accept()
|
|
127
|
+
self.__send_queue = asyncio.Queue()
|
|
128
|
+
send_loop_task = asyncio.create_task(self.__send_loop())
|
|
129
|
+
await self.__receive_loop()
|
|
130
|
+
await send_loop_task
|
|
131
|
+
|
|
132
|
+
# disconnect this connection from a session
|
|
133
|
+
await app_manager.disconnect_session(
|
|
134
|
+
self.__get_unique_session_id(self.__session.id),
|
|
135
|
+
self.__session_timeout_seconds,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
async def __on_session_created(self):
|
|
139
|
+
assert self.__session
|
|
140
|
+
logger.info(f"Start session: {self.__session.id}")
|
|
141
|
+
try:
|
|
142
|
+
assert self.__main is not None
|
|
143
|
+
_context_page.set(self.__session.page)
|
|
144
|
+
context.reset_auto_update()
|
|
145
|
+
|
|
146
|
+
if asyncio.iscoroutinefunction(self.__main):
|
|
147
|
+
await self.__main(self.__session.page)
|
|
148
|
+
|
|
149
|
+
elif inspect.isasyncgenfunction(self.__main):
|
|
150
|
+
async for _ in self.__main(self.__session.page):
|
|
151
|
+
await self.__session.after_event(self.__session.page)
|
|
152
|
+
|
|
153
|
+
elif inspect.isgeneratorfunction(self.__main):
|
|
154
|
+
for _ in self.__main(self.__session.page):
|
|
155
|
+
await self.__session.after_event(self.__session.page)
|
|
156
|
+
else:
|
|
157
|
+
self.__main(self.__session.page)
|
|
158
|
+
|
|
159
|
+
await self.__session.after_event(self.__session.page)
|
|
160
|
+
except FletPageDisconnectedException:
|
|
161
|
+
logger.debug(
|
|
162
|
+
"Session handler attempted to update disconnected page: "
|
|
163
|
+
f"{self.__session.id}"
|
|
164
|
+
)
|
|
165
|
+
except BrokenPipeError:
|
|
166
|
+
logger.info(
|
|
167
|
+
"Session handler terminated: "
|
|
168
|
+
f"{self.__session.id if self.__session else ''}"
|
|
169
|
+
)
|
|
170
|
+
except Exception as e:
|
|
171
|
+
print(
|
|
172
|
+
"Unhandled error processing page session: "
|
|
173
|
+
f"{self.__session.id if self.__session else ''}",
|
|
174
|
+
traceback.format_exc(),
|
|
175
|
+
)
|
|
176
|
+
if self.__session:
|
|
177
|
+
self.__session.error(
|
|
178
|
+
f"There was an error while processing your request: {e}"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
async def __send_loop(self):
|
|
182
|
+
assert self.__websocket
|
|
183
|
+
assert self.__send_queue
|
|
184
|
+
while True:
|
|
185
|
+
message = await self.__send_queue.get()
|
|
186
|
+
if message is None:
|
|
187
|
+
break
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
await self.__websocket.send_bytes(message)
|
|
191
|
+
except Exception:
|
|
192
|
+
# re-enqueue the message to repeat it when re-connected
|
|
193
|
+
# self.__send_queue.put_nowait(message)
|
|
194
|
+
raise
|
|
195
|
+
self.__websocket = None
|
|
196
|
+
self.__send_queue = None
|
|
197
|
+
|
|
198
|
+
async def __receive_loop(self):
|
|
199
|
+
assert self.__websocket
|
|
200
|
+
try:
|
|
201
|
+
while True:
|
|
202
|
+
data = await self.__websocket.receive_bytes()
|
|
203
|
+
await self.__on_message(
|
|
204
|
+
msgpack.unpackb(data, ext_hook=decode_ext_from_msgpack)
|
|
205
|
+
)
|
|
206
|
+
except Exception as e:
|
|
207
|
+
if not isinstance(e, WebSocketDisconnect):
|
|
208
|
+
logger.warning(f"Receive loop error: {e}", exc_info=True)
|
|
209
|
+
if self.__session:
|
|
210
|
+
# terminate __send_loop
|
|
211
|
+
await self.__send_queue.put(None)
|
|
212
|
+
|
|
213
|
+
async def __on_message(self, data: Any):
|
|
214
|
+
action = ClientAction(data[0])
|
|
215
|
+
body = data[1]
|
|
216
|
+
transport_log.debug(f"_on_message: {action} {body}")
|
|
217
|
+
task = None
|
|
218
|
+
if action == ClientAction.REGISTER_CLIENT:
|
|
219
|
+
req = RegisterClientRequestBody(**body)
|
|
220
|
+
|
|
221
|
+
new_session = False
|
|
222
|
+
|
|
223
|
+
# try to retrieve existing session
|
|
224
|
+
if req.session_id:
|
|
225
|
+
self.__session = await app_manager.get_session(
|
|
226
|
+
self.__get_unique_session_id(req.session_id)
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
oauth_state = None
|
|
230
|
+
if self.__oauth_state_id:
|
|
231
|
+
oauth_state = app_manager.retrieve_state(self.__oauth_state_id)
|
|
232
|
+
if oauth_state:
|
|
233
|
+
self.__session = await app_manager.get_session(
|
|
234
|
+
oauth_state.session_id
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# re-create session
|
|
238
|
+
if self.__session is None:
|
|
239
|
+
new_session = True
|
|
240
|
+
|
|
241
|
+
# create new session
|
|
242
|
+
self.__session = Session(self)
|
|
243
|
+
|
|
244
|
+
# register session
|
|
245
|
+
await app_manager.add_session(
|
|
246
|
+
self.__get_unique_session_id(self.__session.id),
|
|
247
|
+
self.__session,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
_context_page.set(self.__session.page)
|
|
251
|
+
|
|
252
|
+
original_route = self.__session.page.route
|
|
253
|
+
|
|
254
|
+
# apply page patch
|
|
255
|
+
self.__session.apply_page_patch(req.page)
|
|
256
|
+
|
|
257
|
+
if new_session:
|
|
258
|
+
# update IP and user-agent
|
|
259
|
+
self.__session.page.client_ip = self.__client_ip
|
|
260
|
+
self.__session.page.client_user_agent = self.__client_user_agent
|
|
261
|
+
|
|
262
|
+
# run before_main
|
|
263
|
+
if asyncio.iscoroutinefunction(self.__before_main):
|
|
264
|
+
await self.__before_main(self.__session.page)
|
|
265
|
+
elif callable(self.__before_main):
|
|
266
|
+
self.__before_main(self.__session.page)
|
|
267
|
+
|
|
268
|
+
# register response
|
|
269
|
+
self.send_message(
|
|
270
|
+
ClientMessage(
|
|
271
|
+
ClientAction.REGISTER_CLIENT,
|
|
272
|
+
RegisterClientResponseBody(
|
|
273
|
+
session_id=self.__session.id,
|
|
274
|
+
page_patch=self.__session.get_page_patch()
|
|
275
|
+
if new_session
|
|
276
|
+
else self.__session.page,
|
|
277
|
+
error="",
|
|
278
|
+
),
|
|
279
|
+
)
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# start session
|
|
283
|
+
if new_session:
|
|
284
|
+
asyncio.create_task(self.__on_session_created())
|
|
285
|
+
else:
|
|
286
|
+
await app_manager.reconnect_session(
|
|
287
|
+
self.__get_unique_session_id(self.__session.id), self
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
if (
|
|
291
|
+
self.__session.page.route
|
|
292
|
+
and self.__session.page.route != original_route
|
|
293
|
+
):
|
|
294
|
+
asyncio.create_task(
|
|
295
|
+
self.__session.page._trigger_event(
|
|
296
|
+
"route_change", {"route": self.__session.page.route}
|
|
297
|
+
)
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
if oauth_state:
|
|
301
|
+
await self.__session.page._authorize_callback(
|
|
302
|
+
{
|
|
303
|
+
"state": self.__oauth_state_id,
|
|
304
|
+
"code": oauth_state.code,
|
|
305
|
+
"error": oauth_state.error,
|
|
306
|
+
"error_description": oauth_state.error_description,
|
|
307
|
+
}
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
elif action == ClientAction.CONTROL_EVENT:
|
|
311
|
+
req = ControlEventBody(**body)
|
|
312
|
+
task = asyncio.create_task(
|
|
313
|
+
self.__session.dispatch_event(req.target, req.name, req.data)
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
elif action == ClientAction.UPDATE_CONTROL_PROPS:
|
|
317
|
+
req = UpdateControlPropsBody(**body)
|
|
318
|
+
self.__session.apply_patch(req.id, req.props)
|
|
319
|
+
|
|
320
|
+
elif action == ClientAction.INVOKE_METHOD:
|
|
321
|
+
req = InvokeMethodResponseBody(**body)
|
|
322
|
+
self.__session.handle_invoke_method_results(
|
|
323
|
+
req.control_id, req.call_id, req.result, req.error
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
else:
|
|
327
|
+
# it's something else
|
|
328
|
+
raise Exception(f'Unknown message "{action}": {body}')
|
|
329
|
+
|
|
330
|
+
if task:
|
|
331
|
+
self.__running_tasks.add(task)
|
|
332
|
+
task.add_done_callback(self.__running_tasks.discard)
|
|
333
|
+
|
|
334
|
+
def send_message(self, message: ClientMessage):
|
|
335
|
+
transport_log.debug(f"send_message: {message}")
|
|
336
|
+
m = msgpack.packb(
|
|
337
|
+
[message.action, message.body],
|
|
338
|
+
default=configure_encode_object_for_msgpack(BaseControl),
|
|
339
|
+
)
|
|
340
|
+
self.__send_queue.put_nowait(m)
|
|
341
|
+
|
|
342
|
+
def get_upload_url(self, file_name: str, expires: int) -> str:
|
|
343
|
+
assert self.__upload_endpoint_path, (
|
|
344
|
+
"upload_path should be specified to enable uploads"
|
|
345
|
+
)
|
|
346
|
+
return build_upload_url(
|
|
347
|
+
self.__upload_endpoint_path,
|
|
348
|
+
file_name,
|
|
349
|
+
expires,
|
|
350
|
+
self.__secret_key,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
def oauth_authorize(self, attrs: dict[str, Any]):
|
|
354
|
+
state_id = attrs["state"]
|
|
355
|
+
state = OAuthState(
|
|
356
|
+
session_id=self.__get_unique_session_id(self.__session.id),
|
|
357
|
+
expires_at=datetime.now(timezone.utc)
|
|
358
|
+
+ timedelta(seconds=self.__oauth_state_timeout_seconds),
|
|
359
|
+
complete_page_html=attrs.get("completePageHtml"),
|
|
360
|
+
complete_page_url=attrs.get("completePageUrl"),
|
|
361
|
+
)
|
|
362
|
+
app_manager.store_state(state_id, state)
|
|
363
|
+
|
|
364
|
+
def __get_unique_session_id(self, session_id: str):
|
|
365
|
+
ip = self.__client_ip
|
|
366
|
+
if ip in ["127.0.0.1", "::1"]:
|
|
367
|
+
ip = ""
|
|
368
|
+
client_hash = sha1(f"{ip}{self.__client_user_agent}")
|
|
369
|
+
return f"{self.page_name}_{session_id}_{client_hash}"
|
|
370
|
+
|
|
371
|
+
def dispose(self):
|
|
372
|
+
logger.info(f"Disposing FletApp: {self.__id}")
|
|
373
|
+
self.__session = None
|