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.

@@ -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,3 @@
1
+ # Flet Web client in Flutter
2
+
3
+ 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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