config.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. # Copyright 2021-2024 Avaiga Private Limited
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
  4. # the License. You may obtain a copy of the License at
  5. #
  6. # http://www.apache.org/licenses/LICENSE-2.0
  7. #
  8. # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
  9. # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
  10. # specific language governing permissions and limitations under the License.
  11. import os
  12. import re
  13. import typing as t
  14. from importlib.util import find_spec
  15. import pytz
  16. import tzlocal
  17. from dotenv import dotenv_values
  18. from werkzeug.serving import is_running_from_reloader
  19. from taipy.common.logger._taipy_logger import _TaipyLogger
  20. from ._gui_cli import _GuiCLI
  21. from ._hook import _Hooks
  22. from ._page import _Page
  23. from ._warnings import _warn
  24. from .partial import Partial
  25. from .utils import _is_in_notebook
  26. ConfigParameter = t.Literal[
  27. "allow_unsafe_werkzeug",
  28. "async_mode",
  29. "change_delay",
  30. "chart_dark_template",
  31. "base_url",
  32. "client_url",
  33. "dark_mode",
  34. "dark_theme",
  35. "data_url_max_size",
  36. "debug",
  37. "extended_status",
  38. "favicon",
  39. "flask_log",
  40. "host",
  41. "light_theme",
  42. "margin",
  43. "ngrok_token",
  44. "notebook_proxy",
  45. "notification_duration",
  46. "port",
  47. "port_auto_ranges",
  48. "propagate",
  49. "run_browser",
  50. "run_in_thread",
  51. "run_server",
  52. "server_config",
  53. "single_client",
  54. "system_notification",
  55. "theme",
  56. "time_zone",
  57. "title",
  58. "state_retention_period",
  59. "stylekit",
  60. "upload_folder",
  61. "use_arrow",
  62. "use_reloader",
  63. "watermark",
  64. "webapp_path",
  65. ]
  66. Stylekit = t.TypedDict(
  67. "Stylekit",
  68. {
  69. "color_primary": str,
  70. "color_secondary": str,
  71. "color_error": str,
  72. "color_warning": str,
  73. "color_success": str,
  74. "color_background_light": str,
  75. "color_paper_light": str,
  76. "color_background_dark": str,
  77. "color_paper_dark": str,
  78. "font_family": str,
  79. "root_margin": str,
  80. "border_radius": int,
  81. "input_button_height": str,
  82. },
  83. total=False,
  84. )
  85. ServerConfig = t.TypedDict(
  86. "ServerConfig",
  87. {
  88. "cors": t.Optional[t.Union[bool, t.Dict[str, t.Any]]],
  89. "socketio": t.Optional[t.Dict[str, t.Any]],
  90. "ssl_context": t.Optional[t.Union[str, t.Tuple[str, str]]],
  91. "flask": t.Optional[t.Dict[str, t.Any]],
  92. },
  93. total=False,
  94. )
  95. Config = t.TypedDict(
  96. "Config",
  97. {
  98. "allow_unsafe_werkzeug": bool,
  99. "async_mode": str,
  100. "change_delay": t.Optional[int],
  101. "chart_dark_template": t.Optional[t.Dict[str, t.Any]],
  102. "base_url": t.Optional[str],
  103. "client_url": t.Optional[str],
  104. "dark_mode": bool,
  105. "dark_theme": t.Optional[t.Dict[str, t.Any]],
  106. "data_url_max_size": t.Optional[int],
  107. "debug": bool,
  108. "extended_status": bool,
  109. "favicon": t.Optional[str],
  110. "flask_log": bool,
  111. "host": str,
  112. "light_theme": t.Optional[t.Dict[str, t.Any]],
  113. "margin": t.Optional[str],
  114. "ngrok_token": str,
  115. "notebook_proxy": bool,
  116. "notification_duration": int,
  117. "port": t.Union[t.Literal["auto"], int],
  118. "port_auto_ranges": t.List[t.Union[int, t.Tuple[int, int]]],
  119. "propagate": bool,
  120. "run_browser": bool,
  121. "run_in_thread": bool,
  122. "run_server": bool,
  123. "server_config": t.Optional[ServerConfig],
  124. "single_client": bool,
  125. "state_retention_period": int,
  126. "stylekit": t.Union[bool, Stylekit],
  127. "system_notification": bool,
  128. "theme": t.Optional[t.Dict[str, t.Any]],
  129. "time_zone": t.Optional[str],
  130. "title": t.Optional[str],
  131. "upload_folder": t.Optional[str],
  132. "use_arrow": bool,
  133. "use_reloader": bool,
  134. "watermark": t.Optional[str],
  135. "webapp_path": t.Optional[str],
  136. },
  137. total=False,
  138. )
  139. class _Config(object):
  140. __RE_PORT_NUMBER = re.compile(
  141. r"^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$"
  142. )
  143. def __init__(self) -> None:
  144. self.pages: t.List[_Page] = []
  145. self.root_page: t.Optional[_Page] = None
  146. self.routes: t.List[str] = []
  147. self.partials: t.List[Partial] = []
  148. self.partial_routes: t.List[str] = []
  149. self.config: Config = {}
  150. def _load(self, config: Config) -> None:
  151. self.config.update(config)
  152. # Check that the user timezone configuration setting is valid
  153. self.get_time_zone()
  154. def _get_config(self, name: ConfigParameter, default_value: t.Any) -> t.Any: # pragma: no cover
  155. if name in self.config and self.config.get(name) is not None:
  156. if default_value is not None and not isinstance(self.config.get(name), type(default_value)):
  157. try:
  158. return type(default_value)(self.config.get(name))
  159. except Exception as e:
  160. _warn(
  161. f'app_config "{name}" value "{self.config.get(name)}" is not of type {type(default_value)}', e
  162. )
  163. return default_value
  164. return self.config.get(name)
  165. return default_value
  166. def get_time_zone(self) -> t.Optional[str]:
  167. tz = self.config.get("time_zone")
  168. if tz is None or tz == "client":
  169. return tz
  170. if tz == "server":
  171. # return python tzlocal IANA Time Zone
  172. return str(tzlocal.get_localzone())
  173. # Verify user defined IANA Time Zone is valid
  174. if tz not in pytz.all_timezones_set:
  175. raise Exception(
  176. "Time Zone configuration is not valid. Mistyped 'server', 'client' options or invalid IANA Time Zone"
  177. )
  178. # return user defined IANA Time Zone (https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)
  179. return tz
  180. def _handle_argparse(self):
  181. _GuiCLI.create_parser()
  182. args = _GuiCLI.handle_command()
  183. config = self.config
  184. if args.taipy_port:
  185. if str(args.taipy_port).strip() == "auto":
  186. config["port"] = "auto"
  187. elif not _Config.__RE_PORT_NUMBER.match(args.taipy_port):
  188. _warn("Port value for --port option is not valid.")
  189. else:
  190. config["port"] = int(args.taipy_port)
  191. if args.taipy_host:
  192. config["host"] = args.taipy_host
  193. if args.taipy_debug:
  194. config["debug"] = True
  195. if args.taipy_no_debug:
  196. config["debug"] = False
  197. if args.taipy_use_reloader:
  198. config["use_reloader"] = True
  199. if args.taipy_no_reloader:
  200. config["use_reloader"] = False
  201. if args.taipy_run_browser:
  202. config["run_browser"] = True
  203. if args.taipy_no_run_browser:
  204. config["run_browser"] = False
  205. if args.taipy_dark_mode or args.taipy_light_mode:
  206. config["dark_mode"] = not args.taipy_light_mode
  207. if args.taipy_ngrok_token:
  208. config["ngrok_token"] = args.taipy_ngrok_token
  209. if args.taipy_webapp_path:
  210. config["webapp_path"] = args.taipy_webapp_path
  211. elif os.environ.get("TAIPY_GUI_WEBAPP_PATH"):
  212. config["webapp_path"] = os.environ.get("TAIPY_GUI_WEBAPP_PATH")
  213. if args.taipy_upload_folder:
  214. config["upload_folder"] = args.taipy_upload_folder
  215. elif os.environ.get("TAIPY_GUI_UPLOAD_FOLDER"):
  216. config["webapp_path"] = os.environ.get("TAIPY_GUI_UPLOAD_FOLDER")
  217. if args.taipy_client_url:
  218. config["client_url"] = args.taipy_client_url
  219. _Hooks()._handle_argparse(args, config)
  220. def _build_config(self, root_dir, env_filename, kwargs): # pragma: no cover
  221. config = self.config
  222. env_file_abs_path = env_filename if os.path.isabs(env_filename) else os.path.join(root_dir, env_filename)
  223. # Load keyword arguments
  224. for key, value in kwargs.items():
  225. key = key.lower()
  226. if value is not None and key in config:
  227. # Special case for "stylekit" that can be a Boolean or a dict
  228. if key == "stylekit" and isinstance(value, bool):
  229. from ._default_config import _default_stylekit
  230. config[key] = _default_stylekit if value else {}
  231. continue
  232. try:
  233. if isinstance(value, dict) and isinstance(config.get(key), dict):
  234. t.cast(dict, config.get(key)).update(value)
  235. elif key == "port" and str(value).strip() == "auto":
  236. config["port"] = "auto"
  237. else:
  238. config[key] = value if config.get(key) is None else type(config.get(key))(value) # type: ignore[reportCallIssue]
  239. except Exception as e:
  240. _warn(
  241. f"Invalid keyword arguments value in Gui.run(): {key} - {value}. Unable to parse value to the correct type", # noqa: E501
  242. e,
  243. )
  244. # Load config from env file
  245. if os.path.isfile(env_file_abs_path):
  246. for key, value in dotenv_values(env_file_abs_path).items():
  247. key = key.lower()
  248. if value is not None and key in config:
  249. try:
  250. if key == "port" and str(value).strip() == "auto":
  251. config["port"] = "auto"
  252. else:
  253. config[key] = value if config[key] is None else type(config[key])(value) # type: ignore
  254. except Exception as e:
  255. _warn(
  256. f"Invalid env value in Gui.run(): {key} - {value}. Unable to parse value to the correct type", # noqa: E501
  257. e,
  258. )
  259. # Taipy-config
  260. if find_spec("taipy") and find_spec("taipy.common.config"):
  261. from taipy.common.config import Config as TaipyConfig
  262. try:
  263. section = TaipyConfig.unique_sections["gui"]
  264. self.config.update(section._to_dict())
  265. except KeyError:
  266. _warn("taipy-common section for taipy-gui is not initialized.")
  267. # Load from system arguments
  268. self._handle_argparse()
  269. def __log_outside_reloader(self, logger, msg):
  270. if not is_running_from_reloader():
  271. logger.info(msg)
  272. def resolve(self):
  273. app_config = self.config
  274. logger = _TaipyLogger._get_logger()
  275. # Special config for notebook runtime
  276. if _is_in_notebook() or app_config.get("run_in_thread") and not app_config.get("single_client"):
  277. app_config["single_client"] = True
  278. self.__log_outside_reloader(logger, "Running in 'single_client' mode in notebook environment")
  279. if app_config.get("run_server") and app_config.get("ngrok_token") and app_config.get("use_reloader"):
  280. app_config["use_reloader"] = False
  281. self.__log_outside_reloader(
  282. logger, "'use_reloader' parameter will not be used when 'ngrok_token' parameter is available"
  283. )
  284. if app_config.get("use_reloader") and _is_in_notebook():
  285. app_config["use_reloader"] = False
  286. self.__log_outside_reloader(logger, "'use_reloader' parameter is not available in notebook environment")
  287. if app_config.get("use_reloader") and not app_config.get("debug"):
  288. app_config["debug"] = True
  289. self.__log_outside_reloader(logger, "Application is running in 'debug' mode")
  290. if app_config.get("debug") and not app_config.get("allow_unsafe_werkzeug"):
  291. app_config["allow_unsafe_werkzeug"] = True
  292. self.__log_outside_reloader(logger, "'allow_unsafe_werkzeug' has been set to True")
  293. if app_config.get("debug") and app_config.get("async_mode") != "threading":
  294. app_config["async_mode"] = "threading"
  295. self.__log_outside_reloader(
  296. logger,
  297. "'async_mode' parameter has been overridden to 'threading'. Using Flask built-in development server with debug mode", # noqa: E501
  298. )
  299. self._resolve_notebook_proxy()
  300. self._resolve_stylekit()
  301. self._resolve_url_prefix()
  302. def _resolve_stylekit(self):
  303. app_config = self.config
  304. # support legacy margin variable
  305. stylekit_config = app_config.get("stylekit")
  306. if isinstance(stylekit_config, dict) and "root_margin" in stylekit_config:
  307. from ._default_config import _default_stylekit, default_config
  308. if stylekit_config.get("root_margin") == _default_stylekit.get("root_margin") and app_config.get(
  309. "margin"
  310. ) != default_config.get("margin"):
  311. stylekit_config["root_margin"] = str(app_config.get("margin"))
  312. app_config["margin"] = None
  313. def _resolve_url_prefix(self):
  314. app_config = self.config
  315. base_url = app_config.get("base_url")
  316. if base_url is None:
  317. app_config["base_url"] = "/"
  318. else:
  319. base_url = f"{'' if base_url.startswith('/') else '/'}{base_url}"
  320. base_url = f"{base_url}{'' if base_url.endswith('/') else '/'}"
  321. app_config["base_url"] = base_url
  322. def _resolve_notebook_proxy(self):
  323. app_config = self.config
  324. app_config["notebook_proxy"] = app_config.get("notebook_proxy", False) if _is_in_notebook() else False