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