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.
- flet_web/__init__.py +9 -0
- flet_web/fastapi/README.md +146 -0
- flet_web/fastapi/__init__.py +6 -0
- flet_web/fastapi/app.py +121 -0
- flet_web/fastapi/flet_app.py +430 -0
- flet_web/fastapi/flet_app_manager.py +167 -0
- flet_web/fastapi/flet_fastapi.py +128 -0
- flet_web/fastapi/flet_oauth.py +66 -0
- flet_web/fastapi/flet_static_files.py +188 -0
- flet_web/fastapi/flet_upload.py +95 -0
- flet_web/fastapi/oauth_state.py +11 -0
- flet_web/fastapi/serve_fastapi_web_app.py +94 -0
- flet_web/patch_index.py +112 -0
- flet_web/uploads.py +54 -0
- flet_web/version.py +1 -0
- flet_web/web/.last_build_id +1 -0
- flet_web/web/assets/AssetManifest.bin +1 -0
- flet_web/web/assets/AssetManifest.bin.json +1 -0
- flet_web/web/assets/AssetManifest.json +1 -0
- flet_web/web/assets/FontManifest.json +1 -0
- flet_web/web/assets/NOTICES +37378 -0
- flet_web/web/assets/fonts/MaterialIcons-Regular.otf +0 -0
- flet_web/web/assets/packages/cupertino_icons/assets/CupertinoIcons.ttf +0 -0
- flet_web/web/assets/packages/flutter_map/lib/assets/flutter_map_logo.png +0 -0
- flet_web/web/assets/packages/media_kit/assets/web/hls1.4.10.js +2 -0
- flet_web/web/assets/packages/record_web/assets/js/record.fixwebmduration.js +507 -0
- flet_web/web/assets/packages/record_web/assets/js/record.worklet.js +400 -0
- flet_web/web/assets/packages/wakelock_plus/assets/no_sleep.js +230 -0
- flet_web/web/assets/shaders/ink_sparkle.frag +126 -0
- flet_web/web/favicon.png +0 -0
- flet_web/web/flutter.js +4 -0
- flet_web/web/flutter_bootstrap.js +31 -0
- flet_web/web/flutter_service_worker.js +214 -0
- flet_web/web/icons/apple-touch-icon-192.png +0 -0
- flet_web/web/icons/icon-192.png +0 -0
- flet_web/web/icons/icon-512.png +0 -0
- flet_web/web/icons/icon-maskable-192.png +0 -0
- flet_web/web/icons/icon-maskable-512.png +0 -0
- flet_web/web/icons/loading-animation.png +0 -0
- flet_web/web/index.html +99 -0
- flet_web/web/main.dart.js +233348 -0
- flet_web/web/manifest.json +35 -0
- flet_web/web/python-worker.js +47 -0
- flet_web/web/python.js +28 -0
- flet_web/web/version.json +1 -0
- flet_web-0.25.0.dist-info/METADATA +27 -0
- flet_web-0.25.0.dist-info/RECORD +48 -0
- flet_web-0.25.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from contextlib import asynccontextmanager
|
|
3
|
+
from typing import (
|
|
4
|
+
Any,
|
|
5
|
+
Awaitable,
|
|
6
|
+
Callable,
|
|
7
|
+
Coroutine,
|
|
8
|
+
Dict,
|
|
9
|
+
List,
|
|
10
|
+
Optional,
|
|
11
|
+
Sequence,
|
|
12
|
+
Type,
|
|
13
|
+
Union,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
import fastapi
|
|
17
|
+
import flet_web.fastapi
|
|
18
|
+
from fastapi.datastructures import Default
|
|
19
|
+
from fastapi.params import Depends
|
|
20
|
+
from fastapi.utils import generate_unique_id
|
|
21
|
+
from starlette.middleware import Middleware
|
|
22
|
+
from starlette.requests import Request
|
|
23
|
+
from starlette.responses import JSONResponse, Response
|
|
24
|
+
from starlette.routing import BaseRoute
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class FastAPI(fastapi.FastAPI):
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
*,
|
|
31
|
+
debug: bool = False,
|
|
32
|
+
routes: Optional[List[BaseRoute]] = None,
|
|
33
|
+
title: str = "FastAPI",
|
|
34
|
+
summary: Optional[str] = None,
|
|
35
|
+
description: str = "",
|
|
36
|
+
version: str = "0.1.0",
|
|
37
|
+
openapi_url: Optional[str] = "/openapi.json",
|
|
38
|
+
openapi_tags: Optional[List[Dict[str, Any]]] = None,
|
|
39
|
+
servers: Optional[List[Dict[str, Union[str, Any]]]] = None,
|
|
40
|
+
dependencies: Optional[Sequence[Depends]] = None,
|
|
41
|
+
default_response_class: Type[Response] = Default(JSONResponse),
|
|
42
|
+
redirect_slashes: bool = True,
|
|
43
|
+
docs_url: Optional[str] = "/docs",
|
|
44
|
+
redoc_url: Optional[str] = "/redoc",
|
|
45
|
+
swagger_ui_oauth2_redirect_url: Optional[str] = "/docs/oauth2-redirect",
|
|
46
|
+
swagger_ui_init_oauth: Optional[Dict[str, Any]] = None,
|
|
47
|
+
middleware: Optional[Sequence[Middleware]] = None,
|
|
48
|
+
exception_handlers: Optional[
|
|
49
|
+
Dict[
|
|
50
|
+
Union[int, Type[Exception]],
|
|
51
|
+
Callable[[Request, Any], Coroutine[Any, Any, Response]],
|
|
52
|
+
]
|
|
53
|
+
] = None,
|
|
54
|
+
on_startup: Optional[Sequence[Callable[[], Optional[Awaitable]]]] = None,
|
|
55
|
+
on_shutdown: Optional[Sequence[Callable[[], Optional[Awaitable]]]] = None,
|
|
56
|
+
terms_of_service: Optional[str] = None,
|
|
57
|
+
contact: Optional[Dict[str, Union[str, Any]]] = None,
|
|
58
|
+
license_info: Optional[Dict[str, Union[str, Any]]] = None,
|
|
59
|
+
openapi_prefix: str = "",
|
|
60
|
+
root_path: str = "",
|
|
61
|
+
root_path_in_servers: bool = True,
|
|
62
|
+
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
|
63
|
+
callbacks: Optional[List[BaseRoute]] = None,
|
|
64
|
+
webhooks: Optional[fastapi.routing.APIRouter] = None,
|
|
65
|
+
deprecated: Optional[bool] = None,
|
|
66
|
+
include_in_schema: bool = True,
|
|
67
|
+
swagger_ui_parameters: Optional[Dict[str, Any]] = None,
|
|
68
|
+
generate_unique_id_function: Callable[
|
|
69
|
+
[fastapi.routing.APIRoute], str
|
|
70
|
+
] = Default(generate_unique_id),
|
|
71
|
+
**extra: Any,
|
|
72
|
+
) -> None:
|
|
73
|
+
@asynccontextmanager
|
|
74
|
+
async def lifespan(app: fastapi.FastAPI):
|
|
75
|
+
await flet_web.fastapi.app_manager.start()
|
|
76
|
+
if on_startup:
|
|
77
|
+
for h in on_startup:
|
|
78
|
+
if asyncio.iscoroutinefunction(h):
|
|
79
|
+
await h()
|
|
80
|
+
else:
|
|
81
|
+
h()
|
|
82
|
+
|
|
83
|
+
yield
|
|
84
|
+
if on_shutdown:
|
|
85
|
+
for h in on_shutdown:
|
|
86
|
+
if asyncio.iscoroutinefunction(h):
|
|
87
|
+
await h()
|
|
88
|
+
else:
|
|
89
|
+
h()
|
|
90
|
+
await flet_web.fastapi.app_manager.shutdown()
|
|
91
|
+
|
|
92
|
+
super().__init__(
|
|
93
|
+
debug=debug,
|
|
94
|
+
routes=routes,
|
|
95
|
+
title=title,
|
|
96
|
+
summary=summary,
|
|
97
|
+
description=description,
|
|
98
|
+
version=version,
|
|
99
|
+
openapi_url=openapi_url,
|
|
100
|
+
openapi_tags=openapi_tags,
|
|
101
|
+
servers=servers,
|
|
102
|
+
dependencies=dependencies,
|
|
103
|
+
default_response_class=default_response_class,
|
|
104
|
+
redirect_slashes=redirect_slashes,
|
|
105
|
+
docs_url=docs_url,
|
|
106
|
+
redoc_url=redoc_url,
|
|
107
|
+
swagger_ui_oauth2_redirect_url=swagger_ui_oauth2_redirect_url,
|
|
108
|
+
swagger_ui_init_oauth=swagger_ui_init_oauth,
|
|
109
|
+
middleware=middleware,
|
|
110
|
+
exception_handlers=exception_handlers,
|
|
111
|
+
# on_startup=on_startup,
|
|
112
|
+
# on_shutdown=on_shutdown,
|
|
113
|
+
lifespan=lifespan,
|
|
114
|
+
terms_of_service=terms_of_service,
|
|
115
|
+
contact=contact,
|
|
116
|
+
license_info=license_info,
|
|
117
|
+
openapi_prefix=openapi_prefix,
|
|
118
|
+
root_path=root_path,
|
|
119
|
+
root_path_in_servers=root_path_in_servers,
|
|
120
|
+
responses=responses,
|
|
121
|
+
callbacks=callbacks,
|
|
122
|
+
webhooks=webhooks,
|
|
123
|
+
deprecated=deprecated,
|
|
124
|
+
include_in_schema=include_in_schema,
|
|
125
|
+
swagger_ui_parameters=swagger_ui_parameters,
|
|
126
|
+
generate_unique_id_function=generate_unique_id_function,
|
|
127
|
+
**extra,
|
|
128
|
+
)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from fastapi import HTTPException, Request
|
|
2
|
+
from fastapi.responses import HTMLResponse, RedirectResponse
|
|
3
|
+
from flet_web.fastapi.flet_app_manager import app_manager
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FletOAuth:
|
|
7
|
+
"""
|
|
8
|
+
HTTP handler for OAuth callback.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def __init__(self) -> None:
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
async def handle(self, request: Request):
|
|
15
|
+
"""
|
|
16
|
+
Handle OAuth callback request.
|
|
17
|
+
|
|
18
|
+
Request must contain minimum `code` and `state` query parameters.
|
|
19
|
+
|
|
20
|
+
Returns either redirect to a Flet page or a HTML page with further instructions.
|
|
21
|
+
"""
|
|
22
|
+
state_id = request.query_params.get("state")
|
|
23
|
+
|
|
24
|
+
if not state_id:
|
|
25
|
+
raise HTTPException(status_code=400, detail="Invalid state")
|
|
26
|
+
|
|
27
|
+
state = app_manager.retrieve_state(state_id)
|
|
28
|
+
|
|
29
|
+
if not state:
|
|
30
|
+
raise HTTPException(status_code=400, detail="Invalid state")
|
|
31
|
+
|
|
32
|
+
session = await app_manager.get_session(state.session_id)
|
|
33
|
+
if not session:
|
|
34
|
+
raise HTTPException(status_code=500, detail="Session not found")
|
|
35
|
+
|
|
36
|
+
await session._authorize_callback_async(
|
|
37
|
+
{
|
|
38
|
+
"state": state_id,
|
|
39
|
+
"code": request.query_params.get("code"),
|
|
40
|
+
"error": request.query_params.get("error"),
|
|
41
|
+
"error_description": request.query_params.get("error_description"),
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
if state.complete_page_url:
|
|
46
|
+
return RedirectResponse(state.complete_page_url)
|
|
47
|
+
else:
|
|
48
|
+
html_content = (
|
|
49
|
+
state.complete_page_html
|
|
50
|
+
if state.complete_page_html
|
|
51
|
+
else f"""
|
|
52
|
+
<!DOCTYPE html>
|
|
53
|
+
<html>
|
|
54
|
+
<head>
|
|
55
|
+
<title>Signed in successfully</title>
|
|
56
|
+
</head>
|
|
57
|
+
<body>
|
|
58
|
+
<script type="text/javascript">
|
|
59
|
+
window.close();
|
|
60
|
+
</script>
|
|
61
|
+
<p>You've been successfully signed in! You can close this tab or window now.</p>
|
|
62
|
+
</body>
|
|
63
|
+
</html>
|
|
64
|
+
"""
|
|
65
|
+
)
|
|
66
|
+
return HTMLResponse(content=html_content, status_code=200)
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import tempfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional, Tuple
|
|
7
|
+
|
|
8
|
+
import flet_web.fastapi as flet_fastapi
|
|
9
|
+
from fastapi.staticfiles import StaticFiles
|
|
10
|
+
from flet.core.types import WebRenderer
|
|
11
|
+
from flet.utils import Once, get_bool_env_var
|
|
12
|
+
from flet_web import get_package_web_dir, patch_index_html, patch_manifest_json
|
|
13
|
+
from flet_web.fastapi.flet_app_manager import app_manager
|
|
14
|
+
from starlette.types import Receive, Scope, Send
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(flet_fastapi.__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FletStaticFiles(StaticFiles):
|
|
20
|
+
"""
|
|
21
|
+
Serve Flet app static files.
|
|
22
|
+
|
|
23
|
+
Parameters:
|
|
24
|
+
|
|
25
|
+
* `app_mount_path` (str) - absolute URL of Flet app. Default is `/`.
|
|
26
|
+
* `assets_dir` (str, optional) - an absolute path to app's assets directory.
|
|
27
|
+
* `app_name` (str, optional) - PWA application name.
|
|
28
|
+
* `app_short_name` (str, optional) - PWA application short name.
|
|
29
|
+
* `app_description` (str, optional) - PWA application description.
|
|
30
|
+
* `web_renderer` (WebRenderer) - web renderer defaulting to `WebRenderer.CANVAS_KIT`.
|
|
31
|
+
* `use_color_emoji` (bool) - whether to load a font with color emoji. Default is `False`.
|
|
32
|
+
* `route_url_strategy` (str) - routing URL strategy: `path` (default) or `hash`.
|
|
33
|
+
* `websocket_endpoint_path` (str, optional) - absolute URL of Flet app WebSocket handler. Default is `{app_mount_path}/ws`.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
proxy_path: Optional[str] = None,
|
|
39
|
+
assets_dir: Optional[str] = None,
|
|
40
|
+
app_name: Optional[str] = None,
|
|
41
|
+
app_short_name: Optional[str] = None,
|
|
42
|
+
app_description: Optional[str] = None,
|
|
43
|
+
web_renderer: WebRenderer = WebRenderer.CANVAS_KIT,
|
|
44
|
+
use_color_emoji: bool = False,
|
|
45
|
+
route_url_strategy: str = "path",
|
|
46
|
+
websocket_endpoint_path: Optional[str] = None,
|
|
47
|
+
) -> None:
|
|
48
|
+
self.index = "index.html"
|
|
49
|
+
self.manifest_json = "manifest.json"
|
|
50
|
+
self.__proxy_path = proxy_path
|
|
51
|
+
self.__assets_dir = assets_dir
|
|
52
|
+
self.__app_name = app_name
|
|
53
|
+
self.__app_short_name = app_short_name
|
|
54
|
+
self.__app_description = app_description
|
|
55
|
+
self.__web_renderer = web_renderer
|
|
56
|
+
self.__use_color_emoji = use_color_emoji
|
|
57
|
+
self.__route_url_strategy = route_url_strategy
|
|
58
|
+
self.__websocket_endpoint_path = websocket_endpoint_path
|
|
59
|
+
self.__once = Once()
|
|
60
|
+
|
|
61
|
+
env_web_renderer = os.getenv("FLET_WEB_RENDERER")
|
|
62
|
+
if env_web_renderer:
|
|
63
|
+
self.__web_renderer = WebRenderer(env_web_renderer)
|
|
64
|
+
|
|
65
|
+
env_use_color_emoji = get_bool_env_var("FLET_WEB_USE_COLOR_EMOJI")
|
|
66
|
+
if env_use_color_emoji is not None:
|
|
67
|
+
self.__use_color_emoji = env_use_color_emoji
|
|
68
|
+
|
|
69
|
+
env_route_url_strategy = os.getenv("FLET_WEB_ROUTE_URL_STRATEGY")
|
|
70
|
+
if env_route_url_strategy:
|
|
71
|
+
self.__route_url_strategy = env_route_url_strategy
|
|
72
|
+
|
|
73
|
+
logger.info(f"Web renderer configured: {self.__web_renderer}")
|
|
74
|
+
logger.info(f"Use color emoji: {self.__use_color_emoji}")
|
|
75
|
+
logger.info(f"Route URL strategy configured: {self.__route_url_strategy}")
|
|
76
|
+
|
|
77
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
78
|
+
await self.__once.do(self.__config, scope["root_path"])
|
|
79
|
+
await super().__call__(scope, receive, send)
|
|
80
|
+
|
|
81
|
+
def lookup_path(self, path: str) -> Tuple[str, Optional[os.stat_result]]:
|
|
82
|
+
"""Returns the index file when no match is found.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
path (str): Resource path.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
[tuple[str, os.stat_result]]: Always retuens a full path and stat result.
|
|
89
|
+
"""
|
|
90
|
+
logger.debug(f"StaticFiles.lookup_path: {self.__app_mount_path} {path}")
|
|
91
|
+
full_path, stat_result = super().lookup_path(path)
|
|
92
|
+
|
|
93
|
+
# if a file cannot be found
|
|
94
|
+
if stat_result is None:
|
|
95
|
+
return super().lookup_path(self.index)
|
|
96
|
+
|
|
97
|
+
return full_path, stat_result
|
|
98
|
+
|
|
99
|
+
async def __config(self, root_path: str):
|
|
100
|
+
if self.__proxy_path:
|
|
101
|
+
self.__app_mount_path = self.__proxy_path + root_path
|
|
102
|
+
else:
|
|
103
|
+
self.__app_mount_path = root_path
|
|
104
|
+
|
|
105
|
+
# where modified index.html is stored
|
|
106
|
+
temp_dir = tempfile.mkdtemp()
|
|
107
|
+
app_manager.add_temp_dir(temp_dir)
|
|
108
|
+
logger.info(f"Temp dir created for patched index and manifest: {temp_dir}")
|
|
109
|
+
|
|
110
|
+
# "standard" web files
|
|
111
|
+
web_dir = get_package_web_dir()
|
|
112
|
+
logger.info(f"Web root: {web_dir}")
|
|
113
|
+
|
|
114
|
+
if not os.path.exists(web_dir):
|
|
115
|
+
raise Exception(f"Web root path not found: {web_dir}")
|
|
116
|
+
|
|
117
|
+
# user-defined assets
|
|
118
|
+
if self.__assets_dir:
|
|
119
|
+
if not Path(self.__assets_dir).is_absolute():
|
|
120
|
+
logger.warning("assets_dir must be absolute path.")
|
|
121
|
+
self.__assets_dir = None
|
|
122
|
+
elif not os.path.exists(self.__assets_dir):
|
|
123
|
+
logger.info(f"assets_dir does not exist: {self.__assets_dir}")
|
|
124
|
+
self.__assets_dir = None
|
|
125
|
+
|
|
126
|
+
logger.info(f"Assets dir: {self.__assets_dir}")
|
|
127
|
+
|
|
128
|
+
# copy index.html from assets_dir or web_dir
|
|
129
|
+
if self.__assets_dir and os.path.exists(
|
|
130
|
+
os.path.join(self.__assets_dir, self.index)
|
|
131
|
+
):
|
|
132
|
+
shutil.copyfile(
|
|
133
|
+
os.path.join(self.__assets_dir, self.index),
|
|
134
|
+
os.path.join(temp_dir, self.index),
|
|
135
|
+
)
|
|
136
|
+
else:
|
|
137
|
+
shutil.copyfile(
|
|
138
|
+
os.path.join(web_dir, self.index),
|
|
139
|
+
os.path.join(temp_dir, self.index),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# copy manifest.json from assets_dir or web_dir
|
|
143
|
+
if self.__assets_dir and os.path.exists(
|
|
144
|
+
os.path.join(self.__assets_dir, self.manifest_json)
|
|
145
|
+
):
|
|
146
|
+
shutil.copyfile(
|
|
147
|
+
os.path.join(self.__assets_dir, self.manifest_json),
|
|
148
|
+
os.path.join(temp_dir, self.manifest_json),
|
|
149
|
+
)
|
|
150
|
+
else:
|
|
151
|
+
shutil.copyfile(
|
|
152
|
+
os.path.join(web_dir, self.manifest_json),
|
|
153
|
+
os.path.join(temp_dir, self.manifest_json),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
ws_path = self.__websocket_endpoint_path
|
|
157
|
+
if not ws_path:
|
|
158
|
+
ws_path = self.__app_mount_path.strip("/")
|
|
159
|
+
ws_path = f"{'' if ws_path == '' else '/'}{ws_path}/ws"
|
|
160
|
+
|
|
161
|
+
# replace variables in index.html and manifest.json
|
|
162
|
+
patch_index_html(
|
|
163
|
+
index_path=os.path.join(temp_dir, self.index),
|
|
164
|
+
base_href=self.__app_mount_path,
|
|
165
|
+
websocket_endpoint_path=ws_path,
|
|
166
|
+
app_name=self.__app_name,
|
|
167
|
+
app_description=self.__app_description,
|
|
168
|
+
web_renderer=WebRenderer(self.__web_renderer),
|
|
169
|
+
use_color_emoji=self.__use_color_emoji,
|
|
170
|
+
route_url_strategy=self.__route_url_strategy,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
patch_manifest_json(
|
|
174
|
+
manifest_path=os.path.join(temp_dir, self.manifest_json),
|
|
175
|
+
app_name=self.__app_name,
|
|
176
|
+
app_short_name=self.__app_short_name,
|
|
177
|
+
app_description=self.__app_description,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# set html=True to resolve the index even when no
|
|
181
|
+
# the base path is passed in
|
|
182
|
+
super().__init__(directory=temp_dir, packages=None, html=True, check_dir=True)
|
|
183
|
+
|
|
184
|
+
# add the rest of dirs
|
|
185
|
+
if self.__assets_dir:
|
|
186
|
+
self.all_directories.append(self.__assets_dir)
|
|
187
|
+
|
|
188
|
+
self.all_directories.append(web_dir)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import flet_web.fastapi as flet_fastapi
|
|
7
|
+
from anyio import open_file
|
|
8
|
+
from fastapi import Request
|
|
9
|
+
from flet_web.uploads import build_upload_query_string, get_upload_signature
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(flet_fastapi.__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FletUpload:
|
|
15
|
+
"""
|
|
16
|
+
Flet app uploads handler.
|
|
17
|
+
|
|
18
|
+
Parameters:
|
|
19
|
+
|
|
20
|
+
* `upload_dir` (str) - an absolute path to a directory with uploaded files.
|
|
21
|
+
* `max_upload_size` (str, int) - maximum size of a single upload, bytes. Unlimited if `None`.
|
|
22
|
+
* `secret_key` (str, optional) - secret key to sign and verify upload requests.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
upload_dir: str,
|
|
28
|
+
max_upload_size: Optional[int] = None,
|
|
29
|
+
secret_key: Optional[str] = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
self.__upload_dir = os.path.realpath(upload_dir)
|
|
32
|
+
self.__max_upload_size = max_upload_size
|
|
33
|
+
|
|
34
|
+
env_max_upload_size = os.getenv("FLET_MAX_UPLOAD_SIZE")
|
|
35
|
+
if env_max_upload_size:
|
|
36
|
+
self.__max_upload_size = int(env_max_upload_size)
|
|
37
|
+
|
|
38
|
+
self.__secret_key = secret_key
|
|
39
|
+
|
|
40
|
+
env_upload_secret_key = os.getenv("FLET_SECRET_KEY")
|
|
41
|
+
if env_upload_secret_key:
|
|
42
|
+
self.__secret_key = env_upload_secret_key
|
|
43
|
+
|
|
44
|
+
logger.info(f"Upload path configured: {self.__upload_dir}")
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
Handle file upload.
|
|
48
|
+
|
|
49
|
+
Upload must be an non-encoded (raw) file in the requst body.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
async def handle(self, request: Request):
|
|
53
|
+
file_name = request.query_params["f"]
|
|
54
|
+
expire_str = request.query_params["e"]
|
|
55
|
+
signature = request.query_params["s"]
|
|
56
|
+
|
|
57
|
+
if not file_name or not expire_str or not signature:
|
|
58
|
+
raise Exception("Invalid request")
|
|
59
|
+
|
|
60
|
+
expire_date = datetime.fromisoformat(expire_str)
|
|
61
|
+
|
|
62
|
+
# verify signature
|
|
63
|
+
query_string = build_upload_query_string(file_name, expire_date)
|
|
64
|
+
if (
|
|
65
|
+
get_upload_signature(
|
|
66
|
+
request.url.path, query_string, expire_date, self.__secret_key
|
|
67
|
+
)
|
|
68
|
+
!= signature
|
|
69
|
+
):
|
|
70
|
+
raise Exception("Invalid request")
|
|
71
|
+
|
|
72
|
+
# check expiration date
|
|
73
|
+
if datetime.now(timezone.utc) >= expire_date:
|
|
74
|
+
raise Exception("Invalid request")
|
|
75
|
+
|
|
76
|
+
# build/validate dest path
|
|
77
|
+
joined_path = os.path.join(self.__upload_dir, file_name)
|
|
78
|
+
full_path = os.path.realpath(joined_path)
|
|
79
|
+
if os.path.commonpath([full_path, self.__upload_dir]) != self.__upload_dir:
|
|
80
|
+
raise Exception("Invalid request")
|
|
81
|
+
|
|
82
|
+
# create directory if not exists
|
|
83
|
+
dest_dir = os.path.dirname(full_path)
|
|
84
|
+
os.makedirs(dest_dir, exist_ok=True)
|
|
85
|
+
|
|
86
|
+
# upload file
|
|
87
|
+
size = 0
|
|
88
|
+
async with await open_file(full_path, "wb") as f:
|
|
89
|
+
async for chunk in request.stream():
|
|
90
|
+
size += len(chunk)
|
|
91
|
+
if self.__max_upload_size and size > self.__max_upload_size:
|
|
92
|
+
raise Exception(
|
|
93
|
+
f"Max upload size reached: {self.__max_upload_size}"
|
|
94
|
+
)
|
|
95
|
+
await f.write(chunk)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclasses.dataclass
|
|
7
|
+
class OAuthState:
|
|
8
|
+
session_id: str
|
|
9
|
+
expires_at: datetime
|
|
10
|
+
complete_page_url: Optional[str] = dataclasses.field(default=None)
|
|
11
|
+
complete_page_html: Optional[str] = dataclasses.field(default=None)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import uvicorn
|
|
6
|
+
from flet.core.types import WebRenderer
|
|
7
|
+
|
|
8
|
+
import flet_web.fastapi
|
|
9
|
+
import flet_web.fastapi as flet_fastapi
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(flet_fastapi.__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class WebServerHandle:
|
|
15
|
+
def __init__(self, page_url: str, server: uvicorn.Server) -> None:
|
|
16
|
+
self.page_url = page_url
|
|
17
|
+
self.server = server
|
|
18
|
+
|
|
19
|
+
async def close(self):
|
|
20
|
+
logger.info("Closing Flet web server...")
|
|
21
|
+
await self.server.shutdown()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_fastapi_web_app(
|
|
25
|
+
session_handler,
|
|
26
|
+
page_name: str,
|
|
27
|
+
assets_dir,
|
|
28
|
+
upload_dir,
|
|
29
|
+
web_renderer: Optional[WebRenderer],
|
|
30
|
+
use_color_emoji,
|
|
31
|
+
route_url_strategy,
|
|
32
|
+
):
|
|
33
|
+
web_path = f"/{page_name.strip('/')}"
|
|
34
|
+
app = flet_web.fastapi.FastAPI()
|
|
35
|
+
app.mount(
|
|
36
|
+
web_path,
|
|
37
|
+
flet_web.fastapi.app(
|
|
38
|
+
session_handler,
|
|
39
|
+
upload_dir=upload_dir,
|
|
40
|
+
assets_dir=assets_dir,
|
|
41
|
+
web_renderer=web_renderer if web_renderer else WebRenderer.AUTO,
|
|
42
|
+
use_color_emoji=use_color_emoji,
|
|
43
|
+
route_url_strategy=route_url_strategy,
|
|
44
|
+
),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return app
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async def serve_fastapi_web_app(
|
|
51
|
+
session_handler,
|
|
52
|
+
host,
|
|
53
|
+
url_host,
|
|
54
|
+
port,
|
|
55
|
+
page_name: str,
|
|
56
|
+
assets_dir,
|
|
57
|
+
upload_dir,
|
|
58
|
+
web_renderer: Optional[WebRenderer],
|
|
59
|
+
use_color_emoji,
|
|
60
|
+
route_url_strategy,
|
|
61
|
+
blocking,
|
|
62
|
+
on_startup,
|
|
63
|
+
log_level,
|
|
64
|
+
):
|
|
65
|
+
|
|
66
|
+
web_path = f"/{page_name.strip('/')}"
|
|
67
|
+
page_url = f"http://{url_host}:{port}{web_path if web_path != '/' else ''}"
|
|
68
|
+
|
|
69
|
+
def startup():
|
|
70
|
+
if on_startup:
|
|
71
|
+
on_startup(page_url)
|
|
72
|
+
|
|
73
|
+
app = flet_web.fastapi.FastAPI(on_startup=[startup])
|
|
74
|
+
|
|
75
|
+
app.mount(
|
|
76
|
+
web_path,
|
|
77
|
+
flet_web.fastapi.app(
|
|
78
|
+
session_handler,
|
|
79
|
+
upload_dir=upload_dir,
|
|
80
|
+
assets_dir=assets_dir,
|
|
81
|
+
web_renderer=web_renderer if web_renderer else WebRenderer.AUTO,
|
|
82
|
+
use_color_emoji=use_color_emoji,
|
|
83
|
+
route_url_strategy=route_url_strategy,
|
|
84
|
+
),
|
|
85
|
+
)
|
|
86
|
+
config = uvicorn.Config(app, host=host, port=port, log_level=log_level)
|
|
87
|
+
server = uvicorn.Server(config)
|
|
88
|
+
|
|
89
|
+
if blocking:
|
|
90
|
+
await server.serve()
|
|
91
|
+
else:
|
|
92
|
+
asyncio.create_task(server.serve())
|
|
93
|
+
|
|
94
|
+
return WebServerHandle(page_url=page_url, server=server)
|
flet_web/patch_index.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from flet.core.types import WebRenderer
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def patch_index_html(
|
|
10
|
+
index_path: str,
|
|
11
|
+
base_href: str,
|
|
12
|
+
websocket_endpoint_path: Optional[str] = None,
|
|
13
|
+
app_name: Optional[str] = None,
|
|
14
|
+
app_description: Optional[str] = None,
|
|
15
|
+
pyodide: bool = False,
|
|
16
|
+
pyodide_pre: bool = False,
|
|
17
|
+
pyodide_script_path: str = "",
|
|
18
|
+
web_renderer: WebRenderer = WebRenderer.AUTO,
|
|
19
|
+
use_color_emoji: bool = False,
|
|
20
|
+
route_url_strategy: str = "path",
|
|
21
|
+
):
|
|
22
|
+
with open(index_path, "r") as f:
|
|
23
|
+
index = f.read()
|
|
24
|
+
|
|
25
|
+
if pyodide and pyodide_script_path:
|
|
26
|
+
module_name = Path(pyodide_script_path).stem
|
|
27
|
+
pyodideCode = f"""
|
|
28
|
+
<script>
|
|
29
|
+
var micropipIncludePre = {str(pyodide_pre).lower()};
|
|
30
|
+
var pythonModuleName = "{module_name}";
|
|
31
|
+
</script>
|
|
32
|
+
<script src="python.js"></script>
|
|
33
|
+
"""
|
|
34
|
+
index = index.replace("<!-- pyodideCode -->", pyodideCode)
|
|
35
|
+
index = index.replace("%FLET_WEB_PYODIDE%", str(pyodide).lower())
|
|
36
|
+
index = index.replace(
|
|
37
|
+
"<!-- webRenderer -->",
|
|
38
|
+
f'<script>webRenderer="{web_renderer.value}";</script>',
|
|
39
|
+
)
|
|
40
|
+
index = index.replace(
|
|
41
|
+
"<!-- useColorEmoji -->",
|
|
42
|
+
f"<script>useColorEmoji={str(use_color_emoji).lower()};</script>",
|
|
43
|
+
)
|
|
44
|
+
index = index.replace("%FLET_ROUTE_URL_STRATEGY%", route_url_strategy)
|
|
45
|
+
|
|
46
|
+
if base_href:
|
|
47
|
+
base_url = base_href.strip("/").strip()
|
|
48
|
+
index = index.replace(
|
|
49
|
+
'<base href="/">',
|
|
50
|
+
'<base href="{}">'.format(
|
|
51
|
+
"/" if base_url == "" else "/{}/".format(base_url)
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
if websocket_endpoint_path:
|
|
55
|
+
index = re.sub(
|
|
56
|
+
r"\<meta name=\"flet-websocket-endpoint-path\" content=\"(.+)\">",
|
|
57
|
+
r'<meta name="flet-websocket-endpoint-path" content="{}">'.format(
|
|
58
|
+
websocket_endpoint_path
|
|
59
|
+
),
|
|
60
|
+
index,
|
|
61
|
+
)
|
|
62
|
+
if app_name:
|
|
63
|
+
index = re.sub(
|
|
64
|
+
r"\<meta name=\"apple-mobile-web-app-title\" content=\"(.+)\">",
|
|
65
|
+
r'<meta name="apple-mobile-web-app-title" content="{}">'.format(app_name),
|
|
66
|
+
index,
|
|
67
|
+
)
|
|
68
|
+
index = re.sub(
|
|
69
|
+
r"\<title>(.+)</title>",
|
|
70
|
+
r"<title>{}</title>".format(app_name),
|
|
71
|
+
index,
|
|
72
|
+
)
|
|
73
|
+
if app_description:
|
|
74
|
+
index = re.sub(
|
|
75
|
+
r"\<meta name=\"description\" content=\"(.+)\">",
|
|
76
|
+
r'<meta name="description" content="{}">'.format(app_description),
|
|
77
|
+
index,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
with open(index_path, "w") as f:
|
|
81
|
+
f.write(index)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def patch_manifest_json(
|
|
85
|
+
manifest_path: str,
|
|
86
|
+
app_name: Optional[str] = None,
|
|
87
|
+
app_short_name: Optional[str] = None,
|
|
88
|
+
app_description: Optional[str] = None,
|
|
89
|
+
background_color: Optional[str] = None,
|
|
90
|
+
theme_color: Optional[str] = None,
|
|
91
|
+
):
|
|
92
|
+
with open(manifest_path, "r") as f:
|
|
93
|
+
manifest = json.loads(f.read())
|
|
94
|
+
|
|
95
|
+
if app_name:
|
|
96
|
+
manifest["name"] = app_name
|
|
97
|
+
manifest["short_name"] = app_name
|
|
98
|
+
|
|
99
|
+
if app_short_name:
|
|
100
|
+
manifest["short_name"] = app_short_name
|
|
101
|
+
|
|
102
|
+
if app_description:
|
|
103
|
+
manifest["description"] = app_description
|
|
104
|
+
|
|
105
|
+
if background_color:
|
|
106
|
+
manifest["background_color"] = background_color
|
|
107
|
+
|
|
108
|
+
if theme_color:
|
|
109
|
+
manifest["theme_color"] = theme_color
|
|
110
|
+
|
|
111
|
+
with open(manifest_path, "w") as f:
|
|
112
|
+
f.write(json.dumps(manifest, indent=2))
|