flet-web 0.25.0.dev3487__py3-none-any.whl

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.

Files changed (52) hide show
  1. flet_web/__init__.py +9 -0
  2. flet_web/fastapi/README.md +146 -0
  3. flet_web/fastapi/__init__.py +6 -0
  4. flet_web/fastapi/app.py +120 -0
  5. flet_web/fastapi/flet_app.py +431 -0
  6. flet_web/fastapi/flet_app_manager.py +166 -0
  7. flet_web/fastapi/flet_fastapi.py +128 -0
  8. flet_web/fastapi/flet_oauth.py +66 -0
  9. flet_web/fastapi/flet_static_files.py +188 -0
  10. flet_web/fastapi/flet_upload.py +95 -0
  11. flet_web/fastapi/oauth_state.py +11 -0
  12. flet_web/fastapi/serve_fastapi_web_app.py +93 -0
  13. flet_web/patch_index.py +104 -0
  14. flet_web/uploads.py +54 -0
  15. flet_web/version.py +5 -0
  16. flet_web/web/.last_build_id +1 -0
  17. flet_web/web/assets/AssetManifest.bin +2 -0
  18. flet_web/web/assets/AssetManifest.bin.json +1 -0
  19. flet_web/web/assets/AssetManifest.json +1 -0
  20. flet_web/web/assets/FontManifest.json +1 -0
  21. flet_web/web/assets/NOTICES +37060 -0
  22. flet_web/web/assets/fonts/MaterialIcons-Regular.otf +0 -0
  23. flet_web/web/assets/packages/cupertino_icons/assets/CupertinoIcons.ttf +0 -0
  24. flet_web/web/assets/packages/flutter_map/lib/assets/flutter_map_logo.png +0 -0
  25. flet_web/web/assets/packages/media_kit/assets/web/hls1.4.10.js +2 -0
  26. flet_web/web/assets/packages/record_web/assets/js/record.fixwebmduration.js +507 -0
  27. flet_web/web/assets/packages/record_web/assets/js/record.worklet.js +400 -0
  28. flet_web/web/assets/packages/wakelock_plus/assets/no_sleep.js +230 -0
  29. flet_web/web/assets/packages/window_manager/images/ic_chrome_close.png +0 -0
  30. flet_web/web/assets/packages/window_manager/images/ic_chrome_maximize.png +0 -0
  31. flet_web/web/assets/packages/window_manager/images/ic_chrome_minimize.png +0 -0
  32. flet_web/web/assets/packages/window_manager/images/ic_chrome_unmaximize.png +0 -0
  33. flet_web/web/assets/shaders/ink_sparkle.frag +126 -0
  34. flet_web/web/favicon.png +0 -0
  35. flet_web/web/flutter.js +4 -0
  36. flet_web/web/flutter_bootstrap.js +28 -0
  37. flet_web/web/flutter_service_worker.js +218 -0
  38. flet_web/web/icons/apple-touch-icon-192.png +0 -0
  39. flet_web/web/icons/icon-192.png +0 -0
  40. flet_web/web/icons/icon-512.png +0 -0
  41. flet_web/web/icons/icon-maskable-192.png +0 -0
  42. flet_web/web/icons/icon-maskable-512.png +0 -0
  43. flet_web/web/icons/loading-animation.png +0 -0
  44. flet_web/web/index.html +99 -0
  45. flet_web/web/main.dart.js +225762 -0
  46. flet_web/web/manifest.json +35 -0
  47. flet_web/web/python-worker.js +47 -0
  48. flet_web/web/python.js +28 -0
  49. flet_web/web/version.json +1 -0
  50. flet_web-0.25.0.dev3487.dist-info/METADATA +25 -0
  51. flet_web-0.25.0.dev3487.dist-info/RECORD +52 -0
  52. flet_web-0.25.0.dev3487.dist-info/WHEEL +4 -0
@@ -0,0 +1,431 @@
1
+ import asyncio
2
+ import copy
3
+ import json
4
+ import logging
5
+ import os
6
+ import traceback
7
+ from datetime import datetime, timedelta, timezone
8
+ from typing import Any, Dict, List, Optional
9
+
10
+ import flet_web.fastapi as flet_fastapi
11
+ from fastapi import WebSocket, WebSocketDisconnect
12
+ from flet_core.event import Event
13
+ from flet_core.local_connection import LocalConnection
14
+ from flet_core.page import Page, PageDisconnectedException
15
+ from flet_core.protocol import (
16
+ ClientActions,
17
+ ClientMessage,
18
+ Command,
19
+ CommandEncoder,
20
+ PageCommandResponsePayload,
21
+ PageCommandsBatchResponsePayload,
22
+ RegisterWebClientRequestPayload,
23
+ )
24
+ from flet_core.pubsub import PubSubHub
25
+ from flet_core.utils import random_string, sha1
26
+ from flet_web.fastapi.flet_app_manager import app_manager
27
+ from flet_web.fastapi.oauth_state import OAuthState
28
+ from flet_web.uploads import build_upload_url
29
+
30
+ logger = logging.getLogger(flet_fastapi.__name__)
31
+
32
+ DEFAULT_FLET_SESSION_TIMEOUT = 3600
33
+ DEFAULT_FLET_OAUTH_STATE_TIMEOUT = 600
34
+
35
+
36
+ class FletApp(LocalConnection):
37
+ def __init__(
38
+ self,
39
+ loop: asyncio.AbstractEventLoop,
40
+ session_handler,
41
+ session_timeout_seconds: int = DEFAULT_FLET_SESSION_TIMEOUT,
42
+ oauth_state_timeout_seconds: int = DEFAULT_FLET_OAUTH_STATE_TIMEOUT,
43
+ upload_endpoint_path: Optional[str] = None,
44
+ secret_key: Optional[str] = None,
45
+ ):
46
+ """
47
+ Handle Flet app WebSocket connections.
48
+
49
+ Parameters:
50
+
51
+ * `session_handler` (Coroutine) - application entry point - an async method called for newly connected user. Handler coroutine must have 1 parameter: `page` - `Page` instance.
52
+ * `session_timeout_seconds` (int, optional) - session lifetime, in seconds, after user disconnected.
53
+ * `oauth_state_timeout_seconds` (int, optional) - OAuth state lifetime, in seconds, which is a maximum allowed time between starting OAuth flow and redirecting to OAuth callback URL.
54
+ * `upload_endpoint_path` (str, optional) - absolute URL of upload endpoint, e.g. `/upload`.
55
+ * `secret_key` (str, optional) - secret key to sign upload requests.
56
+ """
57
+ super().__init__()
58
+ self.__id = random_string(8)
59
+ logger.info(f"New FletApp: {self.__id}")
60
+
61
+ self.__page = None
62
+ self.__loop = loop
63
+ self.__session_handler = session_handler
64
+ self.__session_timeout_seconds = session_timeout_seconds
65
+ self.__oauth_state_timeout_seconds = oauth_state_timeout_seconds
66
+
67
+ env_session_timeout_seconds = os.getenv("FLET_SESSION_TIMEOUT")
68
+ if env_session_timeout_seconds:
69
+ self.__session_timeout_seconds = int(env_session_timeout_seconds)
70
+
71
+ env_oauth_state_timeout_seconds = os.getenv("FLET_OAUTH_STATE_TIMEOUT")
72
+ if env_oauth_state_timeout_seconds:
73
+ self.__oauth_state_timeout_seconds = int(env_oauth_state_timeout_seconds)
74
+
75
+ self.__upload_endpoint_path = upload_endpoint_path
76
+ self.__secret_key = secret_key
77
+
78
+ async def handle(self, websocket: WebSocket):
79
+ """
80
+ Handle WebSocket connection.
81
+
82
+ Parameters:
83
+
84
+ * `websocket` (WebSocket) - Websocket instance.
85
+ """
86
+ self.__websocket = websocket
87
+
88
+ self.client_ip = (
89
+ self.__websocket.client.host if self.__websocket.client else ""
90
+ ).split(":")[0]
91
+ self.client_user_agent = (
92
+ self.__websocket.headers["user-agent"]
93
+ if "user-agent" in self.__websocket.headers
94
+ else ""
95
+ )
96
+
97
+ self.pubsubhub = app_manager.get_pubsubhub(
98
+ self.__session_handler, loop=self.__loop
99
+ )
100
+ self.page_url = str(websocket.url).rsplit("/", 1)[0]
101
+ self.page_name = websocket.url.path.rsplit("/", 1)[0].lstrip("/")
102
+
103
+ if not self.__upload_endpoint_path:
104
+ self.__upload_endpoint_path = (
105
+ f"{'' if self.page_name == '' else '/'}{self.page_name}/upload"
106
+ )
107
+
108
+ await self.__websocket.accept()
109
+ self.__send_queue = asyncio.Queue()
110
+ st = asyncio.create_task(self.__send_loop())
111
+ await self.__receive_loop()
112
+ st.cancel()
113
+
114
+ async def __on_event(self, e):
115
+ session = await app_manager.get_session(
116
+ self.__get_unique_session_id(e.sessionID)
117
+ )
118
+ if session is not None:
119
+ try:
120
+ await session.on_event_async(
121
+ Event(e.eventTarget, e.eventName, e.eventData)
122
+ )
123
+ except PageDisconnectedException:
124
+ logger.debug(
125
+ f"Event handler attempted to update disconnected page: {e.sessionID}"
126
+ )
127
+ if e.eventTarget == "page" and e.eventName == "close":
128
+ logger.info(f"Session closed: {e.sessionID}")
129
+ await app_manager.delete_session(
130
+ self.__get_unique_session_id(e.sessionID)
131
+ )
132
+
133
+ async def __on_session_created(self, session_data):
134
+ logger.info(f"Start session: {session_data.sessionID}")
135
+ session_id = session_data.sessionID
136
+ try:
137
+ assert self.__session_handler is not None
138
+ if asyncio.iscoroutinefunction(self.__session_handler):
139
+ await self.__session_handler(self.__page)
140
+ else:
141
+ # run in thread pool
142
+ await asyncio.get_running_loop().run_in_executor(
143
+ app_manager.executor, self.__session_handler, self.__page
144
+ )
145
+ except PageDisconnectedException:
146
+ logger.debug(
147
+ f"Session handler attempted to update disconnected page: {session_id}"
148
+ )
149
+ except BrokenPipeError:
150
+ logger.info(f"Session handler terminated: {session_id}")
151
+ except Exception as e:
152
+ print(
153
+ f"Unhandled error processing page session {session_id}:",
154
+ traceback.format_exc(),
155
+ )
156
+ assert self.__page
157
+ self.__page.error(f"There was an error while processing your request: {e}")
158
+
159
+ async def __send_loop(self):
160
+ assert self.__websocket
161
+ assert self.__send_queue
162
+ while True:
163
+ message = await self.__send_queue.get()
164
+ try:
165
+ await self.__websocket.send_text(message)
166
+ except Exception:
167
+ # re-enqueue the message to repeat it when re-connected
168
+ self.__send_queue.put_nowait(message)
169
+ raise
170
+
171
+ async def __receive_loop(self):
172
+ assert self.__websocket
173
+ try:
174
+ while True:
175
+ await self.__on_message(await self.__websocket.receive_text())
176
+ except Exception as e:
177
+ if not isinstance(e, WebSocketDisconnect):
178
+ logger.warning(f"Receive loop error: {e}")
179
+ if self.__page:
180
+ await app_manager.disconnect_session(
181
+ self.__get_unique_session_id(self.__page.session_id),
182
+ self.__session_timeout_seconds,
183
+ )
184
+ self.__websocket = None
185
+ self.__send_queue = None
186
+
187
+ async def __on_message(self, data: str):
188
+ logger.debug(f"_on_message: {data}")
189
+ msg_dict = json.loads(data)
190
+ msg = ClientMessage(**msg_dict)
191
+ if msg.action == ClientActions.REGISTER_WEB_CLIENT:
192
+ self._client_details = RegisterWebClientRequestPayload(**msg.payload)
193
+
194
+ new_session = True
195
+ if (
196
+ not self._client_details.sessionId
197
+ or await app_manager.get_session(
198
+ self.__get_unique_session_id(self._client_details.sessionId)
199
+ )
200
+ is None
201
+ ):
202
+ # generate session ID
203
+ self._client_details.sessionId = random_string(16)
204
+
205
+ # create new Page object
206
+ self.__page = Page(
207
+ self,
208
+ self._client_details.sessionId,
209
+ executor=app_manager.executor,
210
+ loop=asyncio.get_running_loop(),
211
+ )
212
+
213
+ # register session
214
+ await app_manager.add_session(
215
+ self.__get_unique_session_id(self._client_details.sessionId),
216
+ self.__page,
217
+ )
218
+ else:
219
+ # existing session
220
+ logger.info(
221
+ f"Existing session requested: {self._client_details.sessionId}"
222
+ )
223
+ self.__page = await app_manager.get_session(
224
+ self.__get_unique_session_id(self._client_details.sessionId)
225
+ )
226
+ new_session = False
227
+
228
+ # update page props
229
+ assert self.__page
230
+ original_route = self.__page.route
231
+ self.__page._set_attr("route", self._client_details.pageRoute, False)
232
+ self.__page._set_attr("pwa", self._client_details.isPWA, False)
233
+ self.__page._set_attr("web", self._client_details.isWeb, False)
234
+ self.__page._set_attr("debug", self._client_details.isDebug, False)
235
+ self.__page._set_attr("platform", self._client_details.platform, False)
236
+ self.__page._set_attr(
237
+ "platformBrightness", self._client_details.platformBrightness, False
238
+ )
239
+ self.__page._set_attr("media", self._client_details.media, False)
240
+ self.__page._set_attr("width", self._client_details.pageWidth, False)
241
+ self.__page._set_attr("height", self._client_details.pageHeight, False)
242
+ self.__page._set_attr(
243
+ "windowWidth", self._client_details.windowWidth, False
244
+ )
245
+ self.__page._set_attr(
246
+ "windowHeight", self._client_details.windowHeight, False
247
+ )
248
+ self.__page._set_attr("windowTop", self._client_details.windowTop, False)
249
+ self.__page._set_attr("windowLeft", self._client_details.windowLeft, False)
250
+ self.__page._set_attr("clientIP", self.client_ip, False)
251
+ self.__page._set_attr("clientUserAgent", self.client_user_agent, False)
252
+
253
+ p = self.__page.snapshot.get("page")
254
+ if not p:
255
+ p = {
256
+ "i": "page",
257
+ "t": "page",
258
+ "p": "",
259
+ "c": [],
260
+ }
261
+ self.__page.snapshot["page"] = p
262
+ self.__page.copy_attrs(p)
263
+
264
+ # send register response
265
+ self.__send(
266
+ self._create_register_web_client_response(controls=self.__page.snapshot)
267
+ )
268
+
269
+ # start session
270
+ if new_session:
271
+ asyncio.create_task(
272
+ self.__on_session_created(self._create_session_handler_arg())
273
+ )
274
+ else:
275
+ await app_manager.reconnect_session(
276
+ self.__get_unique_session_id(self._client_details.sessionId), self
277
+ )
278
+
279
+ if original_route != self.__page.route:
280
+ self.__page.go(self.__page.route)
281
+
282
+ elif msg.action == ClientActions.PAGE_EVENT_FROM_WEB:
283
+ if self.__on_event is not None:
284
+ asyncio.create_task(
285
+ self.__on_event(self._create_page_event_handler_arg(msg))
286
+ )
287
+
288
+ elif msg.action == ClientActions.UPDATE_CONTROL_PROPS:
289
+ if self.__on_event is not None:
290
+ asyncio.create_task(
291
+ self.__on_event(self._create_update_control_props_handler_arg(msg))
292
+ )
293
+ else:
294
+ # it's something else
295
+ raise Exception(f'Unknown message "{msg.action}": {msg.payload}')
296
+
297
+ def _process_get_upload_url_command(self, attrs):
298
+ assert len(attrs) == 2, '"getUploadUrl" command has wrong number of attrs'
299
+ assert (
300
+ self.__upload_endpoint_path
301
+ ), "upload_path should be specified to enable uploads"
302
+ return (
303
+ build_upload_url(
304
+ self.__upload_endpoint_path,
305
+ attrs["file"],
306
+ int(attrs["expires"]),
307
+ self.__secret_key,
308
+ ),
309
+ None,
310
+ )
311
+
312
+ def __process_oauth_authorize_command(self, attrs: Dict[str, Any]):
313
+ state_id = attrs["state"]
314
+ state = OAuthState(
315
+ session_id=self.__get_unique_session_id(self._client_details.sessionId),
316
+ expires_at=datetime.now(timezone.utc)
317
+ + timedelta(seconds=self.__oauth_state_timeout_seconds),
318
+ complete_page_html=attrs.get("completePageHtml", None),
319
+ complete_page_url=attrs.get("completePageUrl", None),
320
+ )
321
+ app_manager.store_state(state_id, state)
322
+ return (
323
+ "",
324
+ None,
325
+ )
326
+
327
+ def _process_add_command(self, command: Command):
328
+ assert self.__page
329
+ result, message = super()._process_add_command(command)
330
+ if message:
331
+ for oc in message.payload.controls:
332
+ control = copy.deepcopy(oc)
333
+ id = control["i"]
334
+ pid = control["p"]
335
+ parent = self.__page.snapshot[pid]
336
+ assert parent, f"parent control not found: {pid}"
337
+ if id not in parent["c"]:
338
+ if "at" in control:
339
+ parent["c"].insert(int(control["at"]), id)
340
+ else:
341
+ parent["c"].append(id)
342
+ self.__page.snapshot[id] = control
343
+ return result, message
344
+
345
+ def _process_set_command(self, values, attrs):
346
+ assert self.__page
347
+ result, message = super()._process_set_command(values, attrs)
348
+ control = self.__page.snapshot.get(values[0])
349
+ if control:
350
+ for k, v in attrs.items():
351
+ control[k] = v
352
+ return result, message
353
+
354
+ def _process_remove_command(self, values):
355
+ assert self.__page
356
+ result, message = super()._process_remove_command(values)
357
+ for id in values:
358
+ control = self.__page.snapshot.get(id)
359
+ assert (
360
+ control is not None
361
+ ), f"_process_remove_command: control with ID '{id}' not found."
362
+ for cid in self.__get_all_descendant_ids(id):
363
+ self.__page.snapshot.pop(cid, None)
364
+ # delete control itself
365
+ self.__page.snapshot.pop(id, None)
366
+ # remove id from parent
367
+ parent = self.__page.snapshot.get(control["p"])
368
+ if parent:
369
+ parent["c"].remove(id)
370
+ return result, message
371
+
372
+ def _process_clean_command(self, values):
373
+ assert self.__page
374
+ result, message = super()._process_clean_command(values)
375
+ for id in values:
376
+ for cid in self.__get_all_descendant_ids(id):
377
+ self.__page.snapshot.pop(cid, None)
378
+ return result, message
379
+
380
+ def __get_all_descendant_ids(self, id):
381
+ assert self.__page
382
+ ids = []
383
+ control = self.__page.snapshot.get(id)
384
+ if control:
385
+ for cid in control["c"]:
386
+ ids.append(cid)
387
+ ids.extend(self.__get_all_descendant_ids(cid))
388
+ return ids
389
+
390
+ def send_command(self, session_id: str, command: Command):
391
+ if command.name == "oauthAuthorize":
392
+ result, message = self.__process_oauth_authorize_command(command.attrs)
393
+ else:
394
+ result, message = self._process_command(command)
395
+ if message:
396
+ self.__send(message)
397
+ return PageCommandResponsePayload(result=result, error="")
398
+
399
+ def send_commands(self, session_id: str, commands: List[Command]):
400
+ results = []
401
+ messages = []
402
+ for command in commands:
403
+ if command.name == "oauthAuthorize":
404
+ result, message = self.__process_oauth_authorize_command(command.attrs)
405
+ else:
406
+ result, message = self._process_command(command)
407
+ if command.name in ["add", "get"]:
408
+ results.append(result)
409
+ if message:
410
+ messages.append(message)
411
+ if len(messages) > 0:
412
+ self.__send(ClientMessage(ClientActions.PAGE_CONTROLS_BATCH, messages))
413
+ return PageCommandsBatchResponsePayload(results=results, error="")
414
+
415
+ def __send(self, message: ClientMessage):
416
+ m = json.dumps(message, cls=CommandEncoder, separators=(",", ":"))
417
+ logger.debug(f"__send: {m}")
418
+ if self.__send_queue:
419
+ self.__loop.call_soon_threadsafe(self.__send_queue.put_nowait, m)
420
+
421
+ def _get_next_control_id(self):
422
+ assert self.__page
423
+ return self.__page.get_next_control_id()
424
+
425
+ def __get_unique_session_id(self, session_id: str):
426
+ client_hash = sha1(f"{self.client_ip}{self.client_user_agent}")
427
+ return f"{self.page_name}_{session_id}_{client_hash}"
428
+
429
+ def dispose(self):
430
+ logger.info(f"Disposing FletApp: {self.__id}")
431
+ self.__page = None
@@ -0,0 +1,166 @@
1
+ import asyncio
2
+ import logging
3
+ import shutil
4
+ import threading
5
+ import traceback
6
+ from concurrent.futures import ThreadPoolExecutor
7
+ from datetime import datetime, timezone
8
+ from typing import Optional
9
+
10
+ import flet_web.fastapi as flet_fastapi
11
+ from flet_core.connection import Connection
12
+ from flet_core.locks import NopeLock
13
+ from flet_core.page import Page
14
+ from flet_core.pubsub.pubsub_hub import PubSubHub
15
+ from flet_core.utils import is_pyodide
16
+ from flet_web.fastapi.oauth_state import OAuthState
17
+
18
+ logger = logging.getLogger(flet_fastapi.__name__)
19
+
20
+
21
+ class FletAppManager:
22
+ """
23
+ Manage application sessions and their lifetime.
24
+ """
25
+
26
+ def __init__(self):
27
+ self.__sessions_lock = asyncio.Lock()
28
+ self.__sessions: dict[str, Page] = {}
29
+ self.__evict_sessions_task = None
30
+ self.__states: dict[str, OAuthState] = {}
31
+ self.__states_lock = threading.Lock() if not is_pyodide() else NopeLock()
32
+ self.__evict_oauth_states_task = None
33
+ self.__temp_dirs = {}
34
+ self.__executor = ThreadPoolExecutor(thread_name_prefix="flet_fastapi")
35
+ self.__pubsubhubs_lock = threading.Lock() if not is_pyodide() else NopeLock()
36
+ self.__pubsubhubs = {}
37
+
38
+ @property
39
+ def executor(self):
40
+ return self.__executor
41
+
42
+ def get_pubsubhub(
43
+ self, session_handler, loop: Optional[asyncio.AbstractEventLoop] = None
44
+ ):
45
+ with self.__pubsubhubs_lock:
46
+ psh = self.__pubsubhubs.get(session_handler, None)
47
+ if psh is None:
48
+ psh = PubSubHub(
49
+ loop=loop or asyncio.get_running_loop(),
50
+ executor=self.__executor,
51
+ )
52
+ self.__pubsubhubs[session_handler] = psh
53
+ return psh
54
+
55
+ async def start(self):
56
+ """
57
+ Background task evicting expired app data. Must be called at FastAPI application startup.
58
+ """
59
+ if not self.__evict_sessions_task:
60
+ logger.info("Starting up Flet App Manager")
61
+ self.__evict_sessions_task = asyncio.create_task(
62
+ self.__evict_expired_sessions()
63
+ )
64
+ self.__evict_oauth_states_task = asyncio.create_task(
65
+ self.__evict_expired_oauth_states()
66
+ )
67
+
68
+ async def shutdown(self):
69
+ """
70
+ Cleanup temporary Flet resources on application shutdown.
71
+ """
72
+ logger.info("Shutting down Flet App Manager")
73
+ self.delete_temp_dirs()
74
+ if self.__evict_sessions_task:
75
+ self.__evict_sessions_task.cancel()
76
+ if self.__evict_oauth_states_task:
77
+ self.__evict_oauth_states_task.cancel()
78
+
79
+ async def get_session(self, session_id: str) -> Optional[Page]:
80
+ async with self.__sessions_lock:
81
+ return self.__sessions.get(session_id)
82
+
83
+ async def add_session(self, session_id: str, conn: Page):
84
+ async with self.__sessions_lock:
85
+ self.__sessions[session_id] = conn
86
+ logger.info(
87
+ f"New session created ({len(self.__sessions)} total): {session_id}"
88
+ )
89
+
90
+ async def reconnect_session(self, session_id: str, conn: Connection):
91
+ logger.info(f"Session reconnected: {session_id}")
92
+ async with self.__sessions_lock:
93
+ if session_id in self.__sessions:
94
+ page = self.__sessions[session_id]
95
+ old_conn = page.connection
96
+ await page._connect(conn)
97
+ if old_conn:
98
+ old_conn.dispose()
99
+
100
+ async def disconnect_session(self, session_id: str, session_timeout_seconds: int):
101
+ logger.info(f"Session disconnected: {session_id}")
102
+ async with self.__sessions_lock:
103
+ if session_id in self.__sessions:
104
+ await self.__sessions[session_id]._disconnect(session_timeout_seconds)
105
+
106
+ async def delete_session(self, session_id: str):
107
+ async with self.__sessions_lock:
108
+ page = self.__sessions.pop(session_id, None)
109
+ total = len(self.__sessions)
110
+ if page is not None:
111
+ logger.info(f"Delete session ({total} left): {session_id}")
112
+ try:
113
+ old_conn = page.connection
114
+ page._close()
115
+ if old_conn:
116
+ old_conn.dispose()
117
+ except Exception as e:
118
+ logger.error(
119
+ f"Error deleting expired session: {e} {traceback.format_exc()}"
120
+ )
121
+
122
+ def store_state(self, state_id: str, state: OAuthState):
123
+ logger.info(f"Store oauth state: {state_id}")
124
+ with self.__states_lock:
125
+ self.__states[state_id] = state
126
+
127
+ def retrieve_state(self, state_id: str) -> Optional[OAuthState]:
128
+ with self.__states_lock:
129
+ return self.__states.pop(state_id, None)
130
+
131
+ def add_temp_dir(self, temp_dir: str):
132
+ self.__temp_dirs[temp_dir] = True
133
+
134
+ async def __evict_expired_sessions(self):
135
+ while True:
136
+ await asyncio.sleep(10)
137
+ session_ids = []
138
+ async with self.__sessions_lock:
139
+ for session_id, page in self.__sessions.items():
140
+ if page.expires_at and datetime.now(timezone.utc) > page.expires_at:
141
+ session_ids.append(session_id)
142
+ for session_id in session_ids:
143
+ await self.delete_session(session_id)
144
+
145
+ async def __evict_expired_oauth_states(self):
146
+ while True:
147
+ await asyncio.sleep(10)
148
+ with self.__states_lock:
149
+ ids = []
150
+ for id, state in self.__states.items():
151
+ if (
152
+ state.expires_at
153
+ and datetime.now(timezone.utc) > state.expires_at
154
+ ):
155
+ ids.append(id)
156
+ for id in ids:
157
+ logger.info(f"Delete expired oauth state: {id}")
158
+ self.retrieve_state(id)
159
+
160
+ def delete_temp_dirs(self):
161
+ for temp_dir in self.__temp_dirs.keys():
162
+ logger.info(f"Deleting temp dir: {temp_dir}")
163
+ shutil.rmtree(temp_dir, ignore_errors=True)
164
+
165
+
166
+ app_manager = FletAppManager()