From c4c073f2446af0418da5a97b059047fcd6a1e9cc Mon Sep 17 00:00:00 2001 From: Yevhenii Kachanov Date: Tue, 29 Jul 2025 10:47:27 +0200 Subject: [PATCH 1/7] working on optimization of notebook --- taipy/gui/config.py | 13 +++-- taipy/gui/servers/flask/server.py | 84 +++++++++++++++++++++++-------- taipy/gui/utils/proxy.py | 76 ++++++++++++++++++++++++---- 3 files changed, 137 insertions(+), 36 deletions(-) diff --git a/taipy/gui/config.py b/taipy/gui/config.py index 6e52012df6..40c4c3196c 100644 --- a/taipy/gui/config.py +++ b/taipy/gui/config.py @@ -18,7 +18,6 @@ from dotenv import dotenv_values from taipy.common.logger._taipy_logger import _TaipyLogger - from ._gui_cli import _GuiCLI from ._hook import _Hooks from ._page import _Page @@ -255,10 +254,12 @@ def _build_config(self, root_dir, env_filename, kwargs): # pragma: no cover elif key == "port" and str(value).strip() == "auto": config["port"] = "auto" else: - config[key] = value if config.get(key) is None else type(config.get(key))(value) # type: ignore[reportCallIssue] + config[key] = value if config.get(key) is None else type(config.get(key))( + value) # type: ignore[reportCallIssue] except Exception as e: _warn( - f"Invalid keyword arguments value in Gui.run(): {key} - {value}. Unable to parse value to the correct type", # noqa: E501 + f"Invalid keyword arguments value in Gui.run(): {key} - {value}. Unable to parse value to the correct type", + # noqa: E501 e, ) # Load config from env file @@ -273,10 +274,12 @@ def _build_config(self, root_dir, env_filename, kwargs): # pragma: no cover if isinstance(config[key], bool): config[key] = _is_true(value) else: - config[key] = value if config[key] is None else type(config[key])(value) # type: ignore[reportCallIssue] + config[key] = value if config[key] is None else type(config[key])( + value) # type: ignore[reportCallIssue] except Exception as e: _warn( - f"Invalid env value in Gui.run(): {key} - {value}. Unable to parse value to the correct type", # noqa: E501 + f"Invalid env value in Gui.run(): {key} - {value}. Unable to parse value to the correct type", + # noqa: E501 e, ) diff --git a/taipy/gui/servers/flask/server.py b/taipy/gui/servers/flask/server.py index 8b953d9bd8..8ab0dd3847 100644 --- a/taipy/gui/servers/flask/server.py +++ b/taipy/gui/servers/flask/server.py @@ -11,7 +11,7 @@ from __future__ import annotations -import contextlib +import __main__ import logging import os import pathlib @@ -37,15 +37,13 @@ from kthread import KThread from werkzeug.serving import is_running_from_reloader -import __main__ from taipy.common.logger._taipy_logger import _TaipyLogger - +from .request import _RequestAccessorFlask +from ..server import _Server from ..._hook import _Hooks from ..._renderers.json import _TaipyJsonProvider from ...config import ServerConfig from ...utils import _is_in_notebook, _is_port_open, _RuntimeManager -from ..server import _Server -from .request import _RequestAccessorFlask if t.TYPE_CHECKING: from ...gui import Gui @@ -142,6 +140,7 @@ def _get_default_handler( base_url: str, ) -> Blueprint: taipy_bp = Blueprint("Taipy", __name__, static_folder=static_folder, template_folder=template_folder) + # Serve static react build @taipy_bp.route("/", defaults={"path": ""}) @@ -171,11 +170,13 @@ def my_index(path): ) except Exception: raise RuntimeError( - "Something is wrong with the taipy-gui front-end installation. Check that the js bundle has been properly built (is Node.js installed?)." # noqa: E501 + "Something is wrong with the taipy-gui front-end installation. Check that the js bundle has been properly built (is Node.js installed?)." + # noqa: E501 ) from None if path == "taipy.status.json": - return self.direct_render_json(self._gui._serve_status(pathlib.Path(template_folder) / path)) # type: ignore[attr-defined] + return self.direct_render_json( + self._gui._serve_status(pathlib.Path(template_folder) / path)) # type: ignore[attr-defined] if (file_path := str(os.path.normpath((base_path := static_folder + os.path.sep) + path))).startswith( base_path ) and os.path.isfile(file_path): @@ -185,25 +186,26 @@ def my_index(path): 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) + 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 :]) + 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) + 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] + 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) @@ -237,7 +239,15 @@ def test_request_context(self, path, data=None): def _run_notebook(self): self._is_running = True - self._ws.run(self._server, host=self._host, port=self._port, debug=False, use_reloader=False) + self._ws.run( + self._server, + host=self._host, + port=self._port, + debug=False, + use_reloader=False, + allow_unsafe_werkzeug=True, + log_output=True + ) def _get_async_mode(self) -> str: return self._ws.async_mode # type: ignore[attr-defined] @@ -380,9 +390,14 @@ def run( if not self.is_running_from_reloader() and self._gui._get_config("run_browser", False): # type: ignore[attr-defined] webbrowser.open(client_url or server_url, new=2) if _is_in_notebook() or run_in_thread: - self._thread = KThread(target=self._run_notebook) + self._thread = KThread( + target=self._run_notebook, + daemon=True, + name=f"TaipyGUI-{port}" + ) self._thread.start() return + self._is_running = True run_config = { "app": self._server, @@ -407,17 +422,44 @@ def is_running(self): def stop_thread(self): if hasattr(self, "_thread") and self._thread.is_alive() and self._is_running: self._is_running = False - with contextlib.suppress(Exception): + + try: if self._get_async_mode() == "gevent": - if self._ws.wsgi_server is not None: # type: ignore[attr-defined] - self._ws.wsgi_server.stop() # type: ignore[attr-defined] + if hasattr(self._ws, 'wsgi_server') and self._ws.wsgi_server is not None: + self._ws.wsgi_server.stop() else: self._thread.kill() else: self._thread.kill() + except Exception as e: + _TaipyLogger._get_logger().warning(f"Error stopping thread: {e}") + + timeout_start = time.time() + timeout_duration = 5.0 # 5 seconds timeout + while _is_port_open(self._host, self._port): + if time.time() - timeout_start > timeout_duration: + _TaipyLogger._get_logger().warning( + f"Port {self._port} still occupied after {timeout_duration}s timeout" + ) + break time.sleep(0.1) + def __del__(self): + try: + if hasattr(self, '_thread') and self._thread and self._thread.is_alive(): + self.stop_thread() + if hasattr(self, '_proxy'): + self.stop_proxy() + except Exception: + pass + def stop_proxy(self): if hasattr(self, "_proxy"): - self._proxy.stop() + try: + self._proxy.stop() + except Exception as e: + _TaipyLogger._get_logger().warning(f"Error stopping proxy: {e}") + finally: + if hasattr(self, "_proxy"): + delattr(self, "_proxy") diff --git a/taipy/gui/utils/proxy.py b/taipy/gui/utils/proxy.py index b9edf2271f..f194d245f6 100644 --- a/taipy/gui/utils/proxy.py +++ b/taipy/gui/utils/proxy.py @@ -10,9 +10,10 @@ # specific language governing permissions and limitations under the License. import contextlib +import threading import typing as t import warnings -from threading import Thread +from threading import Thread, Event from urllib.parse import quote as urlquote from urllib.parse import urlparse @@ -22,17 +23,16 @@ from twisted.web.server import NOT_DONE_YET, Site from .is_port_open import _is_port_open - # flake8: noqa: E402 from .singleton import _Singleton warnings.filterwarnings( "ignore", category=UserWarning, - message="You do not have a working installation of the service_identity module: 'No module named 'service_identity''.*", # noqa: E501 + message="You do not have a working installation of the service_identity module: 'No module named 'service_identity''.*", + # noqa: E501 ) - if t.TYPE_CHECKING: from ..gui import Gui @@ -93,23 +93,79 @@ def __init__(self, gui: "Gui", listening_port: int) -> None: self._listening_port = listening_port self._gui = gui self._is_running = False + self._thread: t.Optional[Thread] = None + self._stop_event = Event() + self._reactor_thread_id = None def run(self): - if self._is_running: + if self._is_running and self._thread and self._thread.is_alive(): return + host = self._gui._get_config("host", "127.0.0.1") port = self._listening_port + if _is_port_open(host, port): raise ConnectionError( - f"Port {port} is already opened on {host}. You have another server application running on the same port." # noqa: E501 + f"Port {port} is already opened on {host}. " + f"You have another server application running on the same port." ) - site = Site(_TaipyReverseProxyResource(host, b"", self._gui)) - reactor.listenTCP(port, site) - Thread(target=reactor.run, args=(False,)).start() + + self._thread = Thread( + target=self._run_reactor, + args=(host, port), + daemon=True, + name=f"TaipyNotebookProxy-{port}" + ) + + self._stop_event.clear() + self._thread.start() self._is_running = True + import time + time.sleep(0.1) + + def _run_reactor(self, host: str, port: int): + try: + self._reactor_thread_id = threading.current_thread().ident + site = Site(_TaipyReverseProxyResource(host, b"", self._gui)) + reactor.listenTCP(port, site) + + reactor.run(installSignalHandlers=False) + + except Exception as e: + print(f"Reactor error: {e}") + finally: + self._is_running = False + self._reactor_thread_id = None + def stop(self): if not self._is_running: return + + self._stop_event.set() self._is_running = False - reactor.stop() + + if (self._reactor_thread_id and + threading.current_thread().ident == self._reactor_thread_id): + reactor.stop() + else: + + reactor.callFromThread(reactor.stop) + + if self._thread and self._thread.is_alive(): + self._thread.join(timeout=2.0) + + if self._thread.is_alive(): + print(f"Warning: Proxy thread {self._thread.name} did not terminate cleanly") + + self._thread = None + self._reactor_thread_id = None + + def is_alive(self) -> bool: + return (self._is_running and + self._thread is not None and + self._thread.is_alive()) + + def __del__(self): + with contextlib.suppress(Exception): + self.stop() From ed9be879a82bed604d2f8f7b24015d99914f1126 Mon Sep 17 00:00:00 2001 From: Yevhenii Kachanov Date: Thu, 31 Jul 2025 09:06:31 +0200 Subject: [PATCH 2/7] fix linter issues --- taipy/gui/config.py | 7 +++++-- taipy/gui/servers/flask/server.py | 10 ++++++---- taipy/gui/utils/proxy.py | 11 +++++++---- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/taipy/gui/config.py b/taipy/gui/config.py index 40c4c3196c..0e605a6cfb 100644 --- a/taipy/gui/config.py +++ b/taipy/gui/config.py @@ -18,6 +18,7 @@ from dotenv import dotenv_values from taipy.common.logger._taipy_logger import _TaipyLogger + from ._gui_cli import _GuiCLI from ._hook import _Hooks from ._page import _Page @@ -258,7 +259,8 @@ def _build_config(self, root_dir, env_filename, kwargs): # pragma: no cover value) # type: ignore[reportCallIssue] except Exception as e: _warn( - f"Invalid keyword arguments value in Gui.run(): {key} - {value}. Unable to parse value to the correct type", + f"Invalid keyword arguments value in Gui.run(): {key} - {value}. " + f"Unable to parse value to the correct type", # noqa: E501 e, ) @@ -278,7 +280,8 @@ def _build_config(self, root_dir, env_filename, kwargs): # pragma: no cover value) # type: ignore[reportCallIssue] except Exception as e: _warn( - f"Invalid env value in Gui.run(): {key} - {value}. Unable to parse value to the correct type", + f"Invalid env value in Gui.run(): {key} - {value}. " + f"Unable to parse value to the correct type", # noqa: E501 e, ) diff --git a/taipy/gui/servers/flask/server.py b/taipy/gui/servers/flask/server.py index 8ab0dd3847..f8e369118d 100644 --- a/taipy/gui/servers/flask/server.py +++ b/taipy/gui/servers/flask/server.py @@ -11,7 +11,6 @@ from __future__ import annotations -import __main__ import logging import os import pathlib @@ -37,13 +36,15 @@ from kthread import KThread from werkzeug.serving import is_running_from_reloader +import __main__ from taipy.common.logger._taipy_logger import _TaipyLogger -from .request import _RequestAccessorFlask -from ..server import _Server + from ..._hook import _Hooks from ..._renderers.json import _TaipyJsonProvider from ...config import ServerConfig from ...utils import _is_in_notebook, _is_port_open, _RuntimeManager +from ..server import _Server +from .request import _RequestAccessorFlask if t.TYPE_CHECKING: from ...gui import Gui @@ -170,7 +171,8 @@ def my_index(path): ) except Exception: raise RuntimeError( - "Something is wrong with the taipy-gui front-end installation. Check that the js bundle has been properly built (is Node.js installed?)." + "Something is wrong with the taipy-gui front-end installation. " + "Check that the js bundle has been properly built (is Node.js installed?)." # noqa: E501 ) from None diff --git a/taipy/gui/utils/proxy.py b/taipy/gui/utils/proxy.py index f194d245f6..7a5fb32e89 100644 --- a/taipy/gui/utils/proxy.py +++ b/taipy/gui/utils/proxy.py @@ -13,7 +13,7 @@ import threading import typing as t import warnings -from threading import Thread, Event +from threading import Event, Thread from urllib.parse import quote as urlquote from urllib.parse import urlparse @@ -22,14 +22,17 @@ from twisted.web.resource import Resource from twisted.web.server import NOT_DONE_YET, Site +from .._warnings import _warn from .is_port_open import _is_port_open + # flake8: noqa: E402 from .singleton import _Singleton warnings.filterwarnings( "ignore", category=UserWarning, - message="You do not have a working installation of the service_identity module: 'No module named 'service_identity''.*", + message="You don't have a working installation of the service_identity module: " + "'No module named 'service_identity''.*", # noqa: E501 ) @@ -133,7 +136,7 @@ def _run_reactor(self, host: str, port: int): reactor.run(installSignalHandlers=False) except Exception as e: - print(f"Reactor error: {e}") + _warn(f"Reactor error: {e}") finally: self._is_running = False self._reactor_thread_id = None @@ -156,7 +159,7 @@ def stop(self): self._thread.join(timeout=2.0) if self._thread.is_alive(): - print(f"Warning: Proxy thread {self._thread.name} did not terminate cleanly") + _warn(f"Warning: Proxy thread {self._thread.name} did not terminate cleanly") self._thread = None self._reactor_thread_id = None From 647bfad8de53d2e44d79f46e3495f5fd17f20a30 Mon Sep 17 00:00:00 2001 From: Yevhenii Kachanov Date: Thu, 31 Jul 2025 09:29:53 +0200 Subject: [PATCH 3/7] fix linter type issue --- taipy/gui/utils/proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/taipy/gui/utils/proxy.py b/taipy/gui/utils/proxy.py index 7a5fb32e89..3a2c6a0d49 100644 --- a/taipy/gui/utils/proxy.py +++ b/taipy/gui/utils/proxy.py @@ -129,7 +129,7 @@ def run(self): def _run_reactor(self, host: str, port: int): try: - self._reactor_thread_id = threading.current_thread().ident + self._reactor_thread_id = threading.current_thread().ident or 0 site = Site(_TaipyReverseProxyResource(host, b"", self._gui)) reactor.listenTCP(port, site) From a372ab0c7b9b41e33102c03aa8381b8240deb2f0 Mon Sep 17 00:00:00 2001 From: Yevhenii Kachanov Date: Thu, 31 Jul 2025 09:34:41 +0200 Subject: [PATCH 4/7] put ident to another row add type checker from None to Int --- taipy/gui/utils/proxy.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/taipy/gui/utils/proxy.py b/taipy/gui/utils/proxy.py index 3a2c6a0d49..c4a9dad610 100644 --- a/taipy/gui/utils/proxy.py +++ b/taipy/gui/utils/proxy.py @@ -129,7 +129,8 @@ def run(self): def _run_reactor(self, host: str, port: int): try: - self._reactor_thread_id = threading.current_thread().ident or 0 + ident = threading.current_thread().ident + self._reactor_thread_id = ident if ident is not None else 0 site = Site(_TaipyReverseProxyResource(host, b"", self._gui)) reactor.listenTCP(port, site) From e1746b4827d7b273eb16457c39cbd21f9c1eee38 Mon Sep 17 00:00:00 2001 From: Yevhenii Kachanov Date: Thu, 31 Jul 2025 09:56:19 +0200 Subject: [PATCH 5/7] Optional[int] --- taipy/gui/utils/proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/taipy/gui/utils/proxy.py b/taipy/gui/utils/proxy.py index c4a9dad610..2244341721 100644 --- a/taipy/gui/utils/proxy.py +++ b/taipy/gui/utils/proxy.py @@ -98,7 +98,7 @@ def __init__(self, gui: "Gui", listening_port: int) -> None: self._is_running = False self._thread: t.Optional[Thread] = None self._stop_event = Event() - self._reactor_thread_id = None + self._reactor_thread_id: t.Optional[int] = None def run(self): if self._is_running and self._thread and self._thread.is_alive(): From 4f3fccee81e4a8c862708046fdd82b9d8b2bc4a4 Mon Sep 17 00:00:00 2001 From: Yevhenii Kachanov Date: Thu, 14 Aug 2025 14:25:34 +0200 Subject: [PATCH 6/7] fixed _warn messages --- taipy/gui/config.py | 8 ++------ taipy/gui/utils/proxy.py | 1 - 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/taipy/gui/config.py b/taipy/gui/config.py index 0e605a6cfb..869a36ffd5 100644 --- a/taipy/gui/config.py +++ b/taipy/gui/config.py @@ -260,9 +260,7 @@ def _build_config(self, root_dir, env_filename, kwargs): # pragma: no cover except Exception as e: _warn( f"Invalid keyword arguments value in Gui.run(): {key} - {value}. " - f"Unable to parse value to the correct type", - # noqa: E501 - e, + f"Unable to parse value to the correct type: {e}", ) # Load config from env file if os.path.isfile(env_file_abs_path): @@ -281,9 +279,7 @@ def _build_config(self, root_dir, env_filename, kwargs): # pragma: no cover except Exception as e: _warn( f"Invalid env value in Gui.run(): {key} - {value}. " - f"Unable to parse value to the correct type", - # noqa: E501 - e, + f"Unable to parse value to the correct type: {e}", ) # Taipy-config diff --git a/taipy/gui/utils/proxy.py b/taipy/gui/utils/proxy.py index 2244341721..2a3ad5c237 100644 --- a/taipy/gui/utils/proxy.py +++ b/taipy/gui/utils/proxy.py @@ -33,7 +33,6 @@ category=UserWarning, message="You don't have a working installation of the service_identity module: " "'No module named 'service_identity''.*", - # noqa: E501 ) if t.TYPE_CHECKING: From 2b95d41f04b512e5a57a1f571441d33e2d04bb0a Mon Sep 17 00:00:00 2001 From: Yevhenii Kachanov Date: Thu, 14 Aug 2025 14:27:51 +0200 Subject: [PATCH 7/7] fix string --- taipy/gui/servers/flask/server.py | 1 - 1 file changed, 1 deletion(-) diff --git a/taipy/gui/servers/flask/server.py b/taipy/gui/servers/flask/server.py index f8e369118d..663dc89b68 100644 --- a/taipy/gui/servers/flask/server.py +++ b/taipy/gui/servers/flask/server.py @@ -173,7 +173,6 @@ def my_index(path): raise RuntimeError( "Something is wrong with the taipy-gui front-end installation. " "Check that the js bundle has been properly built (is Node.js installed?)." - # noqa: E501 ) from None if path == "taipy.status.json":