Browse Source

replace request calls with RequestAccessor

dinhlongviolin1 1 month ago
parent
commit
eb547d6ffb

+ 3 - 3
taipy/gui/custom/utils.py

@@ -12,7 +12,7 @@
 import contextlib
 import typing as t
 
-from ..servers import _Server, get_request, has_request_context
+from ..servers import RequestAccessor, _Server, has_request_context
 from ._page import ResourceHandler, _ExternalResourceHandlerManager
 
 
@@ -21,7 +21,7 @@ def is_in_custom_page_context() -> bool:
     resource_handler_id = None
     with contextlib.suppress(Exception):
         if has_request_context():
-            resource_handler_id = get_request().cookies.get(_Server._RESOURCE_HANDLER_ARG, None)
+            resource_handler_id = RequestAccessor.cookies().get(_Server._RESOURCE_HANDLER_ARG, None)
     return resource_handler_id is not None
 
 
@@ -30,5 +30,5 @@ def get_current_resource_handler() -> t.Optional[ResourceHandler]:
     resource_handler_id = None
     with contextlib.suppress(Exception):
         if has_request_context():
-            resource_handler_id = get_request().cookies.get(_Server._RESOURCE_HANDLER_ARG, None)
+            resource_handler_id = RequestAccessor.cookies().get(_Server._RESOURCE_HANDLER_ARG, None)
     return _ExternalResourceHandlerManager().get(resource_handler_id) if resource_handler_id else None

+ 105 - 78
taipy/gui/gui.py

@@ -32,8 +32,9 @@ from urllib.parse import unquote, urlencode, urlparse
 
 import markdown as md_lib
 import tzlocal
-from fastapi import FastAPI
+from fastapi import APIRouter, FastAPI
 from flask import Blueprint, Flask
+from flask.ctx import AppContext
 from werkzeug.utils import secure_filename
 
 import __main__  # noqa: F401
@@ -64,6 +65,7 @@ from .extension.library import Element, ElementLibrary
 from .page import Page
 from .partial import Partial
 from .servers import (
+    RequestAccessor,
     ServerFrameworks,
     _Server,
     create_server,
@@ -645,14 +647,14 @@ class Gui:
 
     def __set_client_id_in_context(self, client_id: t.Optional[str] = None, force=False):
         if not client_id and get_request():
-            client_id = get_request().args.get(Gui.__ARG_CLIENT_ID, "")
+            client_id = RequestAccessor.arg(Gui.__ARG_CLIENT_ID, "")
         if not client_id and (ws_client_id := getattr(get_request_meta(), "ws_client_id", None)):
             client_id = ws_client_id
         if not client_id and force:
             res = self._bindings()._get_or_create_scope("")
             client_id = res[0] if res[1] else None
-        if client_id and get_request():
-            if sid := getattr(get_request(), "sid", None):
+        if client_id:
+            if sid := RequestAccessor.sid():
                 sids = self.__client_id_2_sid.get(client_id, None)
                 if sids is None:
                     sids = set()
@@ -693,9 +695,7 @@ class Gui:
 
     def _handle_disconnect(self):
         _Hooks()._handle_disconnect(self)
-        if (sid := getattr(get_request(), "sid", None)) and (
-            st_to := self._get_config("state_retention_period", 0)
-        ) > 0:
+        if (sid := RequestAccessor.sid()) and (st_to := self._get_config("state_retention_period", 0)) > 0:
             for cl_id, sids in self.__client_id_2_sid.items():
                 if sid in sids:
                     if len(sids) == 1:
@@ -920,7 +920,7 @@ class Gui:
         if len(parts) > 1:
             file_name = parts[-1]
             (dir_path, as_attachment) = self.__get_content_accessor().get_content_path(
-                path[: -len(file_name) - 1], file_name, get_request().args.get("bypass")
+                path[: -len(file_name) - 1], file_name, RequestAccessor.arg("bypass")
             )
             if dir_path:
                 return send_from_directory(str(dir_path), file_name, as_attachment=as_attachment)
@@ -936,7 +936,7 @@ class Gui:
     def __serve_user_content(self, path: str) -> t.Any:
         self.__set_client_id_in_context()
         q_args: t.Dict[str, str] = {}
-        q_args.update(get_request().args)
+        q_args.update(RequestAccessor.args(True))
         q_args.pop(Gui.__ARG_CLIENT_ID, None)
         cb_function: t.Union[t.Callable, str, None] = None
         cb_function_name = None
@@ -1045,17 +1045,17 @@ class Gui:
 
     def __upload_files(self):
         self.__set_client_id_in_context()
-        on_upload_action = get_request().form.get("on_action", None)
-        var_name = t.cast(str, get_request().form.get("var_name", None))
+        on_upload_action = RequestAccessor.form().get("on_action", None)
+        var_name = t.cast(str, RequestAccessor.form().get("var_name", None))
         if not var_name and not on_upload_action:
             _warn("upload files: No var name")
             return ("upload files: No var name", 400)
-        context = get_request().form.get("context", None)
-        upload_data = get_request().form.get("upload_data", None)
-        multiple = "multiple" in get_request().form and get_request().form["multiple"] == "True"
+        context = RequestAccessor.form().get("context", None)
+        upload_data = RequestAccessor.form().get("upload_data", None)
+        multiple = "multiple" in RequestAccessor.form() and RequestAccessor.form()["multiple"] == "True"
 
         # File parsing and checks
-        file = get_request().files.get("blob", None)
+        file = RequestAccessor.files().get("blob", None)
         if not file:
             _warn("upload files: No file part")
             return ("upload files: No file part", 400)
@@ -1066,15 +1066,15 @@ class Gui:
             return ("upload files: No selected file", 400)
 
         # Path parsing and checks
-        path = get_request().form.get("path", "")
+        path = RequestAccessor.form().get("path", "")
         suffix = ""
         complete = True
         part = 0
 
-        if "total" in get_request().form:
-            total = int(get_request().form["total"])
-            if total > 1 and "part" in get_request().form:
-                part = int(get_request().form["part"])
+        if "total" in RequestAccessor.form():
+            total = int(RequestAccessor.form()["total"])
+            if total > 1 and "part" in RequestAccessor.form():
+                part = int(RequestAccessor.form()["part"])
                 suffix = f".part.{part}"
                 complete = part == total - 1
 
@@ -1554,7 +1554,7 @@ class Gui:
     def __get_ws_receiver(self, send_back_only=False) -> t.Union[t.List[str], t.Any, None]:
         if self._bindings()._is_single_client():
             return None
-        sid = getattr(get_request(), "sid", None) if get_request() else None
+        sid = RequestAccessor.sid()
         sids = self.__get_sids(self._get_client_id())
         if sid:
             sids.add(sid)
@@ -1719,13 +1719,12 @@ class Gui:
             args (Optional[Sequence]): The remaining arguments, as a List or a Tuple.
             module_context (Optional[str]): The name of the module that will be used.
         """  # noqa: E501
-        this_sid = None
-        if get_request():
+        this_sid = RequestAccessor.sid()
+        if this_sid:
             # avoid messing with the client_id => Set(ws id)
-            this_sid = getattr(get_request(), "sid", None)
-            get_request().sid = None  # type: ignore[attr-defined]
+            RequestAccessor.set_sid(None)
         try:
-            with self.get_server_instance().app_context():
+            with self.get_app_context():
                 setattr(get_request_meta(), Gui.__ARG_CLIENT_ID, state_id)
                 with self._set_module_context(module_context):
                     if not _is_function(callback):
@@ -1742,7 +1741,7 @@ class Gui:
                 )
         finally:
             if this_sid:
-                get_request().sid = this_sid  # type: ignore[attr-defined]
+                RequestAccessor.set_sid(this_sid)  # type: ignore[attr-defined]
         return None
 
     def broadcast_callback(
@@ -2570,7 +2569,11 @@ class Gui:
         nav_page = page_name
         if hasattr(self, "on_navigate") and _is_function(self.on_navigate):
             try:
-                params = get_request().args.to_dict() if hasattr(get_request(), "args") else {}
+                params = (
+                    RequestAccessor.args(True)
+                    if (hasattr(get_request(), "args") or hasattr(get_request(), "query_params"))
+                    else {}
+                )
                 params.pop("client_id", None)
                 params.pop("v", None)
                 nav_page = self._call_function_with_state(
@@ -2610,7 +2613,7 @@ class Gui:
         """Handle the bindings of custom page variables"""
         if not isinstance(page, CustomPage):
             return
-        with self.get_server_instance().app_context() if has_server_context() else contextlib.nullcontext():  # type: ignore[attr-defined]
+        with self.get_app_context() if has_server_context() else contextlib.nullcontext():  # type: ignore[attr-defined]
             self.__set_client_id_in_context(client_id)
             with self._set_locals_context(page._get_module_name()):
                 for k, v in self._get_locals_bind().items():
@@ -2693,9 +2696,14 @@ class Gui:
             The server instance used.
         """
         if hasattr(self, "_server"):
-            return t.cast(Flask, self._server.get_server_instance())
+            return self._server.get_server_instance()
         raise RuntimeError("get_server_instance() cannot be invoked before run() has been called.")
 
+    def get_app_context(self) -> t.Union[AppContext, contextlib.nullcontext]:
+        if get_server_type() == "flask":
+            return self.get_server_instance().app_context()
+        return contextlib.nullcontext()
+
     def _get_port(self) -> int:
         return self._server.get_port()
 
@@ -2808,7 +2816,7 @@ class Gui:
             _TaipyLogger._get_logger().info(f" * NGROK Public Url: {self._ngrok[0].public_url}")
 
     def __bind_default_function(self):
-        with self.get_server_instance().app_context():
+        with self.get_app_context():
             if additional_pages := _Hooks()._get_additional_pages():
                 # add page context for additional pages so that they can be managed by the variable directory
                 for page in additional_pages:
@@ -2826,14 +2834,6 @@ class Gui:
             self.__bind_local_func("on_user_content")
 
     def __register_blueprint(self):
-        server_type = get_server_type()
-        if server_type == "flask":
-            self.__register_flask_blueprint()
-        if server_type == "fastapi":
-            self.__register_fastapi_blueprint()
-
-    def __register_flask_blueprint(self):
-        flask_blueprint: t.List[Blueprint] = []
         # add en empty main page if it is not defined
         if Gui.__root_page_name not in self._config.routes:
             new_page = _Page()
@@ -2842,27 +2842,6 @@ class Gui:
             self._config.pages.append(new_page)
             self._config.routes.append(Gui.__root_page_name)
 
-        pages_bp = Blueprint("taipy_pages", __name__)
-        flask_blueprint.append(pages_bp)
-
-        # server URL Rule for taipy images
-        images_bp = Blueprint("taipy_images", __name__)
-        images_bp.add_url_rule(f"/{Gui.__CONTENT_ROOT}/<path:path>", view_func=self.__serve_content)
-        flask_blueprint.append(images_bp)
-
-        # server URL for uploaded files
-        upload_bp = Blueprint("taipy_upload", __name__)
-        upload_bp.add_url_rule(f"/{Gui.__UPLOAD_URL}", view_func=self.__upload_files, methods=["POST"])
-        flask_blueprint.append(upload_bp)
-
-        # server URL for user content
-        user_content_bp = Blueprint("taipy_user_content", __name__)
-        user_content_bp.add_url_rule(f"/{Gui.__USER_CONTENT_URL}/<path:path>", view_func=self.__serve_user_content)
-        flask_blueprint.append(user_content_bp)
-
-        # server URL for extension resources
-        extension_bp = Blueprint("taipy_extensions", __name__)
-        extension_bp.add_url_rule(f"/{Gui._EXTENSION_ROOT}/<path:path>", view_func=self.__serve_extension)
         scripts = [
             s if bool(urlparse(s).netloc) else f"{Gui._EXTENSION_ROOT}/{name}/{s}{lib.get_query(s)}"
             for name, libs in Gui.__extensions.items()
@@ -2885,14 +2864,47 @@ class Gui:
         if self.__script_files:
             scripts.extend(self.__script_files)
 
-        flask_blueprint.append(extension_bp)
+        server_type = get_server_type()
+        if server_type == "flask":
+            self.__register_flask_blueprint(styles, scripts)
+        if server_type == "fastapi":
+            self.__register_fastapi_blueprint(styles, scripts)
+
+    def __register_flask_blueprint(self, styles: t.List[str], scripts: t.List[str]):
+        flask_blueprint: t.List[Blueprint] = []
+
+        pages_bp = Blueprint("taipy_pages", __name__)
+        # Run parse markdown to force variables binding at runtime
+        pages_bp.add_url_rule(f"/{Gui.__JSX_URL}/<path:page_name>", view_func=self.__render_page)
+
+        # server URL Rule for flask rendered react-router
+        pages_bp.add_url_rule(f"/{Gui.__INIT_URL}", view_func=self.__init_route)
+        flask_blueprint.append(pages_bp)
 
-        _webapp_path = self._get_webapp_path()
+        # server URL Rule for taipy images
+        images_bp = Blueprint("taipy_images", __name__)
+        images_bp.add_url_rule(f"/{Gui.__CONTENT_ROOT}/<path:path>", view_func=self.__serve_content)
+        flask_blueprint.append(images_bp)
+
+        # server URL for uploaded files
+        upload_bp = Blueprint("taipy_upload", __name__)
+        upload_bp.add_url_rule(f"/{Gui.__UPLOAD_URL}", view_func=self.__upload_files, methods=["POST"])
+        flask_blueprint.append(upload_bp)
+
+        # server URL for user content
+        user_content_bp = Blueprint("taipy_user_content", __name__)
+        user_content_bp.add_url_rule(f"/{Gui.__USER_CONTENT_URL}/<path:path>", view_func=self.__serve_user_content)
+        flask_blueprint.append(user_content_bp)
+
+        # server URL for extension resources
+        extension_bp = Blueprint("taipy_extensions", __name__)
+        extension_bp.add_url_rule(f"/{Gui._EXTENSION_ROOT}/<path:path>", view_func=self.__serve_extension)
+        flask_blueprint.append(extension_bp)
 
         flask_blueprint.append(
             self._server._get_default_blueprint(
-                static_folder=_webapp_path,
-                template_folder=_webapp_path,
+                static_folder=self._get_webapp_path(),
+                template_folder=self._get_webapp_path(),
                 title=self._get_config("title", "Taipy App"),
                 favicon=self._get_config("favicon", Gui.__DEFAULT_FAVICON_URL),
                 root_margin=self._get_config("margin", None),
@@ -2906,20 +2918,36 @@ class Gui:
             )
         )
 
-        # Run parse markdown to force variables binding at runtime
-        pages_bp.add_url_rule(f"/{Gui.__JSX_URL}/<path:page_name>", view_func=self.__render_page)
-
-        # server URL Rule for flask rendered react-router
-        pages_bp.add_url_rule(f"/{Gui.__INIT_URL}", view_func=self.__init_route)
-
         _Hooks()._add_external_blueprint(self, __name__)
 
         # Register Flask Blueprint if available
         for bp in flask_blueprint:
             t.cast(Flask, self._server.get_server_instance()).register_blueprint(bp)
 
-    def __register_fastapi_blueprint(self):
-        pass
+    def __register_fastapi_blueprint(self, styles: t.List[str], scripts: t.List[str]):
+        fastapi_router: t.List[APIRouter] = []
+
+        fastapi_router.append(
+            self._server._get_default_blueprint(
+                static_folder=self._get_webapp_path(),
+                template_folder=self._get_webapp_path(),
+                title=self._get_config("title", "Taipy App"),
+                favicon=self._get_config("favicon", Gui.__DEFAULT_FAVICON_URL),
+                root_margin=self._get_config("margin", None),
+                scripts=scripts,
+                styles=styles,
+                version=self.__get_version(),
+                client_config=self.__get_client_config(),
+                watermark=self._get_config("watermark", None),
+                css_vars=self.__get_css_vars(),
+                base_url=self._get_config("base_url", "/"),
+            )
+        )
+
+        _Hooks()._add_external_blueprint(self, __name__)
+
+        for router in fastapi_router:
+            t.cast(FastAPI, self._server.get_server_instance()).include_router(router)
 
     def _get_accessor(self):
         if self.__accessors is None:
@@ -3180,17 +3208,16 @@ class Gui:
     def __do_fire_event(
         self, event_name: str, client_id: t.Optional[str] = None, payload: t.Optional[t.Dict[str, t.Any]] = None
     ):
-        this_sid = None
-        if get_request():
+        this_sid = RequestAccessor.sid()
+        if this_sid:
             # avoid messing with the client_id => Set(ws id)
-            this_sid = getattr(get_request(), "sid", None)
-            get_request().sid = None  # type: ignore[attr-defined]
+            RequestAccessor.set_sid(None)
 
         try:
-            with self.get_server_instance().app_context(), self.__event_manager:
+            with self.get_app_context(), self.__event_manager:
                 if client_id:
                     setattr(get_request_meta(), Gui.__ARG_CLIENT_ID, client_id)
                 _Hooks()._fire_event(event_name, client_id, payload)
         finally:
             if this_sid:
-                get_request().sid = this_sid  # type: ignore[attr-defined]
+                RequestAccessor.set_sid(this_sid)

+ 3 - 1
taipy/gui/servers/__init__.py

@@ -9,6 +9,7 @@
 # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 # specific language governing permissions and limitations under the License.
 
+from .request import RequestAccessor
 from .server import ServerFrameworks, _Server
 from .utils import (
     create_server,
@@ -24,6 +25,8 @@ from .utils import (
 )
 
 __all__ = [
+    "RequestAccessor",
+    "ServerFrameworks",
     "_Server",
     "create_server",
     "get_request",
@@ -35,5 +38,4 @@ __all__ = [
     "send_file",
     "send_from_directory",
     "set_server_type",
-    "ServerFrameworks",
 ]

+ 3 - 2
taipy/gui/servers/fastapi/request.py

@@ -15,5 +15,6 @@ from typing import Optional
 from fastapi import Request
 from flask.ctx import _AppCtxGlobals
 
-request: ContextVar[Optional[Request]] = ContextVar("request")
-request_meta: ContextVar[Optional[_AppCtxGlobals]] = ContextVar("request_meta")
+request: ContextVar[Optional[Request]] = ContextVar("request", default=None)
+request_meta: ContextVar[Optional[_AppCtxGlobals]] = ContextVar("request_meta", default=None)
+sid: ContextVar[Optional[str]] = ContextVar("sid", default=None)

+ 120 - 92
taipy/gui/servers/fastapi/server.py

@@ -10,6 +10,7 @@
 # specific language governing permissions and limitations under the License.
 
 import asyncio
+import json
 import os
 import pathlib
 import typing as t
@@ -28,12 +29,13 @@ from starlette.middleware.base import BaseHTTPMiddleware
 import __main__
 from taipy.common.logger._taipy_logger import _TaipyLogger
 
-from ..._renderers.json import _TaipyJsonAdapter
+from ..._renderers.json import _TaipyJsonAdapter, _TaipyJsonEncoder
 from ...config import ServerConfig
 from ...custom._page import _ExternalResourceHandlerManager
 from ..server import _Server
 from .request import request as request_context
 from .request import request_meta
+from .request import sid as sid_context
 from .utils import send_from_directory
 
 if t.TYPE_CHECKING:
@@ -56,6 +58,29 @@ def get_request_meta_sync():
     request_meta.set(None)
 
 
+@contextmanager
+def set_request_sync(request: Request):
+    request_context.set(request)
+    yield
+    request_context.set(None)
+
+
+@contextmanager
+def set_sid_sync(sid: str):
+    sid_context.set(sid)
+    yield
+    sid_context.set(None)
+
+
+# json dumps + loads for socketio
+def custom_json_dumps(obj):
+    return _TaipyJsonAdapter().parse(obj)
+
+
+def custom_json_loads(s):
+    return json.loads(s)
+
+
 class CleanupMiddleware(BaseHTTPMiddleware):
     async def dispatch(self, request: Request, call_next):
         response = await call_next(request)
@@ -83,8 +108,10 @@ class FastAPIServer(_Server):
         self._port: t.Optional[int] = None
 
         # server setup
-        self._server = server or FastAPI()
-        self._ws = socketio.AsyncServer()
+        self._server = server or FastAPI(json_encoder=_TaipyJsonEncoder)
+        self._ws = socketio.AsyncServer(
+            json={"dumps": custom_json_dumps, "loads": custom_json_loads}, async_mode="asgi"
+        )
         self._server.mount("/", socketio.ASGIApp(self._ws, other_asgi_app=self._server))
 
         # registering middleware
@@ -92,7 +119,7 @@ class FastAPIServer(_Server):
 
         self._server.add_middleware(
             CORSMiddleware,
-            allow_origins=["*"],  # Change this to your frontend domain for security
+            allow_origins=["*"],
             allow_credentials=True,
             allow_methods=["*"],
             allow_headers=["*"],
@@ -101,12 +128,12 @@ class FastAPIServer(_Server):
         # Define your event handlers and routes
         @self._ws.event
         def connect(sid, environ):
-            with get_request_meta_sync():
+            with get_request_meta_sync(), set_sid_sync(sid):
                 gui._handle_connect()
 
         @self._ws.on("message")  # type: ignore
         def handle_message(sid, message):
-            with get_request_meta_sync():
+            with get_request_meta_sync(), set_sid_sync(sid):
                 if "status" in message:
                     _TaipyLogger._get_logger().info(message["status"])
                 elif "type" in message:
@@ -114,10 +141,10 @@ class FastAPIServer(_Server):
 
         @self._ws.event
         def disconnect(sid):
-            with get_request_meta_sync():
+            with get_request_meta_sync(), set_sid_sync(sid):
                 gui._handle_disconnect()
 
-    def _get_default_router(
+    def _get_default_blueprint(
         self,
         static_folder: str,
         template_folder: str,
@@ -139,95 +166,96 @@ class FastAPIServer(_Server):
         @taipy_router.get("/", response_class=HTMLResponse)
         @taipy_router.get("/{path:path}", response_class=HTMLResponse)
         def my_index(request: Request, path: str = "", request_meta: _AppCtxGlobals = Depends(get_request_meta)):  # noqa: B008
-            resource_handler_id = request.cookies.get("_RESOURCE_HANDLER_ARG", None)
-            if resource_handler_id is not None:
-                resource_handler = _ExternalResourceHandlerManager().get(resource_handler_id)
-                if resource_handler is None:
-                    reload_html = """
-                        <html>
-                            <head><style>body {background-color: black; margin: 0;}</style></head>
-                            <body><script>location.reload();</script></body>
-                        </html>
-                    """
-                    response = HTMLResponse(content=reload_html, status_code=400)
-                    response.set_cookie(
-                        "_RESOURCE_HANDLER_ARG",
-                        "",
-                        secure=request.url.scheme == "https",
-                        httponly=True,
-                        expires=0,
-                        path="/",
-                    )
-                    return response
-                try:
-                    return resource_handler.get_resources(path, static_folder, base_url)
-                except Exception as e:
-                    raise HTTPException(
-                        status_code=500, detail="Can't get resources from custom resource handler"
-                    ) from e
-
-            if not path or path == "index.html" or "." not in path:
-                try:
-                    return templates.TemplateResponse(
-                        "index.html",
-                        {
-                            "title": title,
-                            "favicon": f"{favicon}?version={version}",
-                            "root_margin": root_margin,
-                            "watermark": watermark,
-                            "config": client_config,
-                            "scripts": scripts,
-                            "styles": styles,
-                            "version": version,
-                            "css_vars": css_vars,
-                            "base_url": base_url,
-                        },
-                    )
-                except Exception:
-                    raise HTTPException(  # noqa: B904
-                        status_code=500,
-                        detail="Something is wrong with the taipy-gui front-end installation. Check that the js bundle has been properly built.",  # noqa: E501
-                    )
-
-            if path == "taipy.status.json":
-                return JSONResponse(content=self._gui._serve_status(pathlib.Path(template_folder) / path))
-
-            if (file_path := str(os.path.normpath((base_path := static_folder + os.path.sep) + path))).startswith(
-                base_path
-            ) and os.path.isfile(file_path):
-                return send_from_directory(base_path, path)
-            # use the path mapping to detect and find resources
-            for k, v in self._path_mapping.items():
+            with set_request_sync(request), get_request_meta_sync():
+                resource_handler_id = request.cookies.get("_RESOURCE_HANDLER_ARG", None)
+                if resource_handler_id is not None:
+                    resource_handler = _ExternalResourceHandlerManager().get(resource_handler_id)
+                    if resource_handler is None:
+                        reload_html = """
+                            <html>
+                                <head><style>body {background-color: black; margin: 0;}</style></head>
+                                <body><script>location.reload();</script></body>
+                            </html>
+                        """
+                        response = HTMLResponse(content=reload_html, status_code=400)
+                        response.set_cookie(
+                            "_RESOURCE_HANDLER_ARG",
+                            "",
+                            secure=request.url.scheme == "https",
+                            httponly=True,
+                            expires=0,
+                            path="/",
+                        )
+                        return response
+                    try:
+                        return resource_handler.get_resources(path, static_folder, base_url)
+                    except Exception as e:
+                        raise HTTPException(
+                            status_code=500, detail="Can't get resources from custom resource handler"
+                        ) from e
+
+                if not path or path == "index.html" or "." not in path:
+                    try:
+                        return templates.TemplateResponse(
+                            "index.html",
+                            {
+                                "title": title,
+                                "favicon": f"{favicon}?version={version}",
+                                "root_margin": root_margin,
+                                "watermark": watermark,
+                                "config": client_config,
+                                "scripts": scripts,
+                                "styles": styles,
+                                "version": version,
+                                "css_vars": css_vars,
+                                "base_url": base_url,
+                            },
+                        )
+                    except Exception:
+                        raise HTTPException(  # noqa: B904
+                            status_code=500,
+                            detail="Something is wrong with the taipy-gui front-end installation. Check that the js bundle has been properly built.",  # noqa: E501
+                        )
+
+                if path == "taipy.status.json":
+                    return JSONResponse(content=self._gui._serve_status(pathlib.Path(template_folder) / path))
+
+                if (file_path := str(os.path.normpath((base_path := static_folder + os.path.sep) + path))).startswith(
+                    base_path
+                ) and os.path.isfile(file_path):
+                    return send_from_directory(base_path, path)
+                # use the path mapping to detect and find resources
+                for k, v in self._path_mapping.items():
+                    if (
+                        path.startswith(f"{k}/")
+                        and (
+                            file_path := str(os.path.normpath((base_path := v + os.path.sep) + path[len(k) + 1 :]))
+                        ).startswith(base_path)
+                        and os.path.isfile(file_path)
+                    ):
+                        return send_from_directory(base_path, path[len(k) + 1 :])
                 if (
-                    path.startswith(f"{k}/")
+                    hasattr(__main__, "__file__")
                     and (
-                        file_path := str(os.path.normpath((base_path := v + os.path.sep) + path[len(k) + 1 :]))
+                        file_path := str(
+                            os.path.normpath((base_path := os.path.dirname(__main__.__file__) + os.path.sep) + path)
+                        )
+                    ).startswith(base_path)
+                    and os.path.isfile(file_path)
+                    and not self._is_ignored(file_path)
+                ):
+                    return send_from_directory(base_path, path)
+                if (
+                    (
+                        file_path := str(os.path.normpath((base_path := self._gui._root_dir + os.path.sep) + path))  # type: ignore[attr-defined]
                     ).startswith(base_path)
                     and os.path.isfile(file_path)
+                    and not self._is_ignored(file_path)
                 ):
-                    return send_from_directory(base_path, path[len(k) + 1 :])
-            if (
-                hasattr(__main__, "__file__")
-                and (
-                    file_path := str(
-                        os.path.normpath((base_path := os.path.dirname(__main__.__file__) + os.path.sep) + path)
-                    )
-                ).startswith(base_path)
-                and os.path.isfile(file_path)
-                and not self._is_ignored(file_path)
-            ):
-                return send_from_directory(base_path, path)
-            if (
-                (
-                    file_path := str(os.path.normpath((base_path := self._gui._root_dir + os.path.sep) + path))  # type: ignore[attr-defined]
-                ).startswith(base_path)
-                and os.path.isfile(file_path)
-                and not self._is_ignored(file_path)
-            ):
-                return send_from_directory(base_path, path)
-
-            # Default error return for unmatched paths
-            raise HTTPException(status_code=404, detail="")
+                    return send_from_directory(base_path, path)
+
+                # Default error return for unmatched paths
+                raise HTTPException(status_code=404, detail="")
 
         return taipy_router
 

+ 79 - 0
taipy/gui/servers/request.py

@@ -8,3 +8,82 @@
 # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 # specific language governing permissions and limitations under the License.
+
+import asyncio
+import typing as t
+
+from flask import has_request_context
+from flask import request as flask_request
+from starlette.datastructures import QueryParams
+from werkzeug.datastructures import ImmutableMultiDict
+
+from .fastapi.request import request as fastapi_request
+from .fastapi.request import sid as fastapi_sid
+from .server import server_type
+
+
+class RequestAccessor:
+    @staticmethod
+    def args(to_dict=False) -> t.Union[ImmutableMultiDict[t.Any, t.Any], QueryParams, t.Dict[str, str]]:
+        if server_type.get() == "flask":
+            return flask_request.args if to_dict is False else flask_request.args.to_dict(flat=True)
+        elif server_type.get() == "fastapi":
+            fastapi_r = fastapi_request.get()
+            return (
+                {}
+                if fastapi_r is None
+                else fastapi_r.query_params
+                if to_dict is False
+                else dict(fastapi_r.query_params)
+            )
+        return {}
+
+    @staticmethod
+    def arg(key, default=None):
+        if server_type.get() == "flask":
+            return flask_request.args.get(key, default)
+        elif server_type.get() == "fastapi":
+            fastapi_r = fastapi_request.get()
+            return default if fastapi_r is None else fastapi_r.query_params.get(key, default)
+        return default
+
+    @staticmethod
+    def form():
+        if server_type.get() == "flask":
+            return flask_request.form
+        elif server_type.get() == "fastapi":
+            fastapi_r = fastapi_request.get()
+            return {} if fastapi_r is None else dict(asyncio.run(fastapi_r._get_form()))
+        return {}
+
+    @staticmethod
+    def files():
+        if server_type.get() == "flask":
+            return flask_request.files
+        elif server_type.get() == "fastapi":
+            raise NotImplementedError("FastAPI does not support files handling yet")
+        return {}
+
+    @staticmethod
+    def cookies():
+        if server_type.get() == "flask":
+            return flask_request.cookies
+        elif server_type.get() == "fastapi":
+            fastapi_r = fastapi_request.get()
+            return {} if fastapi_r is None else fastapi_r.cookies
+        return {}
+
+    @staticmethod
+    def sid():
+        if server_type.get() == "flask" and has_request_context() and flask_request:
+            return getattr(flask_request, "sid", None)
+        elif server_type.get() == "fastapi":
+            return fastapi_sid.get()
+        return None
+
+    @staticmethod
+    def set_sid(sid: t.Optional[str]):
+        if server_type.get() == "flask":
+            flask_request.sid = sid  # type: ignore[attr-defined]
+        elif server_type.get() == "fastapi":
+            fastapi_sid.set(sid)

+ 18 - 0
taipy/gui/servers/server.py

@@ -95,6 +95,24 @@ class _Server(ABC):
             }
         )
 
+    @abstractmethod
+    def _get_default_blueprint(
+        self,
+        static_folder: str,
+        template_folder: str,
+        title: str,
+        favicon: str,
+        root_margin: str,
+        scripts: t.List[str],
+        styles: t.List[str],
+        version: str,
+        client_config: t.Dict[str, t.Any],
+        watermark: t.Optional[str],
+        css_vars: str,
+        base_url: str,
+    ):
+        raise NotImplementedError
+
     @abstractmethod
     def run(
         self,

+ 2 - 1
taipy/gui/servers/utils.py

@@ -9,6 +9,7 @@
 # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 # specific language governing permissions and limitations under the License.
 
+import os
 import typing as t
 
 from flask import g as flask_meta
@@ -98,5 +99,5 @@ def is_running_from_reloader():
     if server_type.get() == "flask":
         flask_is_running_from_reloader()
     elif server_type.get() == "fastapi":
-        return server.get().get_server_instance().state.is_reloader
+        return os.getpid() == os.getppid()
     return False

+ 1 - 3
taipy/gui/state.py

@@ -254,9 +254,7 @@ class _GuiState(State):
     def _notebook_context(self, gui: "Gui"):
         from .servers import has_server_context
 
-        return (
-            gui.get_server_instance().app_context() if not has_server_context() and _is_in_notebook() else nullcontext()
-        )
+        return gui.get_app_context() if not has_server_context() and _is_in_notebook() else nullcontext()
 
     def _get_placeholder(self, name: str):
         if name in _GuiState.__placeholder_attrs:

+ 2 - 2
tests/gui/actions/test_get_module_context.py

@@ -26,7 +26,7 @@ def test_get_module_context(gui: Gui, helpers):
     flask_client = gui._server.test_client()
     cid = helpers.create_scope_and_get_sid(gui)
     flask_client.get(f"/taipy-jsx/test?client_id={cid}")
-    with gui.get_server_instance().app_context():
+    with gui.get_app_context():
         g.client_id = cid
         module = get_module_context(gui._Gui__state)  # type: ignore[attr-defined]
         assert module == "test_get_module_context"
@@ -47,7 +47,7 @@ def test_get_module_name_from_state(gui: Gui, helpers):
     flask_client = gui._server.test_client()
     cid = helpers.create_scope_and_get_sid(gui)
     flask_client.get(f"/taipy-jsx/test?client_id={cid}")
-    with gui.get_server_instance().app_context():
+    with gui.get_app_context():
         g.client_id = cid
         module = get_module_name_from_state(gui._Gui__state)  # type: ignore[attr-defined]
         assert module == "test_get_module_context"

+ 1 - 1
tests/gui/actions/test_get_state_id.py

@@ -26,7 +26,7 @@ def test_get_state_id(gui: Gui, helpers):
     flask_client = gui._server.test_client()
     cid = helpers.create_scope_and_get_sid(gui)
     flask_client.get(f"/taipy-jsx/test?client_id={cid}")
-    with gui.get_server_instance().app_context():
+    with gui.get_app_context():
         g.client_id = cid
         assert cid == get_state_id(gui._Gui__state)  # type: ignore[attr-defined]
 

+ 2 - 2
tests/gui/actions/test_invoke_callback.py

@@ -19,7 +19,7 @@ from taipy.gui import Gui, Markdown, State
 
 @contextlib.contextmanager
 def get_state(gui: Gui, state_id: str):
-    with gui.get_server_instance().app_context():
+    with gui.get_app_context():
         client_id = gui._bindings()._get_or_create_scope(state_id)[0]
         gui._Gui__set_client_id_in_context(client_id)  # type: ignore[attr-defined]
         yield gui._Gui__state  # type: ignore[attr-defined]
@@ -65,7 +65,7 @@ def test_invoke_callback_sid(gui: Gui, helpers):
 
     # Get the jsx once so that the page will be evaluated -> variable will be registered
     flask_client.get(f"/taipy-jsx/test?client_id={cid}")
-    with gui.get_server_instance().app_context():
+    with gui.get_app_context():
         g.client_id = base_sid
         gui.invoke_callback(cid, user_callback, [])
         assert g.client_id == base_sid

+ 1 - 1
tests/gui/gui_specific/test_broadcast.py

@@ -19,7 +19,7 @@ from taipy.gui import Gui
 
 @contextlib.contextmanager
 def get_state(gui: Gui, state_id: str):
-    with gui.get_server_instance().app_context():
+    with gui.get_app_context():
         client_id = gui._bindings()._get_or_create_scope(state_id)[0]
         gui._Gui__set_client_id_in_context(client_id)  # type: ignore[attr-defined]
         yield gui._Gui__state  # type: ignore[attr-defined]

+ 7 - 7
tests/gui/gui_specific/test_gui.py

@@ -29,7 +29,7 @@ def test__get_real_var_name(gui: Gui):
     assert res[1] == ""
 
     gui.run(run_server=False)
-    with gui.get_server_instance().app_context():
+    with gui.get_app_context():
         with gui._set_locals_context(_get_module_name_from_frame(frame)) if frame else nullcontext():
             with pytest.raises(NameError):
                 res = gui._get_real_var_name(f"{_TaipyContent.get_hash()}_var")
@@ -37,13 +37,13 @@ def test__get_real_var_name(gui: Gui):
 
 def test__get_user_instance(gui: Gui):
     gui.run(run_server=False)
-    with gui.get_server_instance().app_context():
+    with gui.get_app_context():
         gui._get_user_instance("", type(None))
 
 
 def test__refresh_expr(gui: Gui):
     gui.run(run_server=False)
-    with gui.get_server_instance().app_context():
+    with gui.get_app_context():
         res = gui._refresh_expr("var", None)
         assert res is None
 
@@ -51,7 +51,7 @@ def test__refresh_expr(gui: Gui):
 def test__tbl_cols(gui: Gui):
     data = pd.DataFrame({"col1": [0, 1, 2], "col2": [True, True, False]})
     gui.run(run_server=False)
-    with gui.get_server_instance().app_context():
+    with gui.get_app_context():
         res = gui._tbl_cols(True, None, json.dumps({}), json.dumps({"data": "data"}), data=data)
         assert isinstance(res, str)
 
@@ -66,7 +66,7 @@ def test__tbl_cols(gui: Gui):
 def test__chart_conf(gui: Gui):
     data = pd.DataFrame({"col1": [0, 1, 2], "col2": [True, True, False]})
     gui.run(run_server=False)
-    with gui.get_server_instance().app_context():
+    with gui.get_app_context():
         res = gui._chart_conf(True, None, json.dumps({}), json.dumps({"data": "data"}), data=data)
         assert isinstance(res, str)
 
@@ -84,7 +84,7 @@ def test__chart_conf(gui: Gui):
 
 def test__get_valid_adapter_result(gui: Gui):
     gui.run(run_server=False)
-    with gui.get_server_instance().app_context():
+    with gui.get_app_context():
         res = gui._get_valid_adapter_result(("id", "label"))
         assert isinstance(res, tuple)
         assert res[0] == "id"
@@ -109,6 +109,6 @@ def test_on_action_call(gui: Gui):
     gui._set_frame(inspect.currentframe())
 
     gui.run(run_server=False)
-    with gui.get_server_instance().app_context():
+    with gui.get_app_context():
         gui._Gui__on_action(an_id, a_non_action_payload)  # type: ignore[attr-defined]
         gui._Gui__on_action(an_id, an_action_payload)  # type: ignore[attr-defined]

+ 1 - 1
tests/gui/gui_specific/test_locals_context.py

@@ -19,7 +19,7 @@ from taipy.gui.utils._locals_context import _LocalsContext
 def test_locals_context(gui: Gui):
     lc = _LocalsContext()
     gui.run(run_server=False)
-    with gui.get_server_instance().app_context():
+    with gui.get_app_context():
         with pytest.raises(KeyError):
             lc.get_default()
         current_locals = locals()

+ 1 - 1
tests/gui/gui_specific/test_state.py

@@ -24,7 +24,7 @@ def test_state(gui: Gui):
     gui.add_page("page1", md_page1)
     gui.run(run_server=False, single_client=True)
     state = gui._Gui__state  # type: ignore[attr-defined]
-    with gui.get_server_instance().app_context():
+    with gui.get_app_context():
         assert state.a == 10
         assert state["page1"].a == 20
         assert state["tests.gui.gui_specific.state_asset.page1"].a == 20

+ 1 - 1
tests/gui/gui_specific/test_variable_binding.py

@@ -34,7 +34,7 @@ def test_variable_binding(helpers):
     assert gui._bindings().x == x
     assert gui._bindings().y == y
     assert gui._bindings().z == z
-    with gui.get_server_instance().app_context():
+    with gui.get_app_context():
         assert callable(gui._get_user_function("another_function"))
     helpers.test_cleanup()
 

+ 1 - 1
tests/gui/gui_specific/test_variable_directory.py

@@ -19,7 +19,7 @@ from .state_asset.page2 import md_page2
 
 def test_variable_directory_dyanmic_process(gui: Gui):
     gui.run(run_server=False)
-    with gui.get_server_instance().app_context():
+    with gui.get_app_context():
         locals_context = _LocalsContext()
         variable_directory = _VariableDirectory(locals_context)
         page1_module = str(md_page1._get_module_name())

+ 1 - 1
tests/gui/long_runnig/test_long_running.py

@@ -35,7 +35,7 @@ def test_long_callback(gui: Gui):
     gui.run(run_server=False, single_client=True)
     state = gui._Gui__state  # type: ignore[attr-defined]
 
-    with gui.get_server_instance().app_context():
+    with gui.get_app_context():
         assert state.status is None
         invoke_long_callback(state, heavy_function)
         invoke_long_callback(state, heavy_function_with_exception)

+ 1 - 1
tests/gui/server/http/test_file_upload.py

@@ -103,7 +103,7 @@ def test_file_upload_multiple(gui: Gui, helpers):
     gui._set_frame(inspect.currentframe())
     gui.run(run_server=False, single_client=True)
     flask_client = gui._server.test_client()
-    with gui.get_server_instance().app_context():
+    with gui.get_app_context():
         gui._bind_var_val(var_name, None)
     # Get the jsx once so that the page will be evaluated -> variable will be registered
     sid = _DataScopes._GLOBAL_ID

+ 6 - 6
tests/gui/utils/test_evaluator.py

@@ -21,7 +21,7 @@ from taipy.gui.utils.types import _TaipyNumber
 def test_unbind_variable_in_expression(gui: Gui, helpers):
     gui.run(run_server=False, single_client=True)
     with warnings.catch_warnings(record=True) as records:
-        with gui.get_server_instance().app_context():
+        with gui.get_app_context():
             gui._evaluate_expr("{x}")
             warns = helpers.get_taipy_warnings(records)
             assert len(warns) == 3
@@ -35,7 +35,7 @@ def test_evaluate_same_expression_multiple_times(gui: Gui):
     x = 10  # noqa: F841
     gui._set_frame(inspect.currentframe())
     gui.run(run_server=False, single_client=True)
-    with gui.get_server_instance().app_context():
+    with gui.get_app_context():
         s1 = gui._evaluate_expr("x + 10 = {x + 10}")
         s2 = gui._evaluate_expr("x + 10 = {x + 10}")
         assert s1 == s2
@@ -45,7 +45,7 @@ def test_evaluate_expressions_same_variable(gui: Gui):
     x = 10  # noqa: F841
     gui._set_frame(inspect.currentframe())
     gui.run(run_server=False, single_client=True)
-    with gui.get_server_instance().app_context():
+    with gui.get_app_context():
         s1 = gui._evaluate_expr("x + 10 = {x + 10}")
         s2 = gui._evaluate_expr("x = {x}")
         assert "tp_TpExPr_x" in s1 and "tp_TpExPr_x" in s2
@@ -56,7 +56,7 @@ def test_evaluate_holder(gui: Gui):
     gui._set_frame(inspect.currentframe())
     gui.run(run_server=False, single_client=True)
     with warnings.catch_warnings(record=True):
-        with gui.get_server_instance().app_context():
+        with gui.get_app_context():
             gui._evaluate_expr("{x + 10}")
             hash = gui._evaluate_bind_holder(_TaipyNumber, "TpExPr_x + 10_TPMDL_0")
             assert "_TpN_tp_TpExPr_x_10_TPMDL_0_0" in hash
@@ -70,7 +70,7 @@ def test_evaluate_holder(gui: Gui):
 
 def test_evaluate_not_expression_type(gui: Gui):
     gui.run(run_server=False)
-    with gui.get_server_instance().app_context():
+    with gui.get_app_context():
         assert "x + 10" == gui._evaluate_expr("x + 10")
 
 
@@ -79,7 +79,7 @@ def test_evaluate_expression_2_clients(gui: Gui):
     y = 20  # noqa: F841
     gui._set_frame(inspect.currentframe())
     gui.run(run_server=False)
-    with gui.get_server_instance().app_context():
+    with gui.get_app_context():
         gui._bindings()._get_or_create_scope("A")
         gui._bindings()._get_or_create_scope("B")
         g.client_id = "A"

+ 1 - 1
tests/gui/utils/test_map_dict.py

@@ -112,7 +112,7 @@ def test_map_dict_set(gui: Gui, test_client):
     gui._set_frame(inspect.currentframe())
 
     gui.run(run_server=False, single_client=True)
-    with gui.get_server_instance().app_context():
+    with gui.get_app_context():
         assert isinstance(gui._Gui__state.d, _MapDict)  # type: ignore[attr-defined]
         gui._Gui__state.d = {"b": 2}  # type: ignore[attr-defined]
         assert isinstance(gui._Gui__state.d, _MapDict)  # type: ignore[attr-defined]