gui.py 116 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653
  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. from __future__ import annotations
  12. import contextlib
  13. import importlib
  14. import inspect
  15. import json
  16. import math
  17. import os
  18. import re
  19. import sys
  20. import tempfile
  21. import time
  22. import typing as t
  23. import warnings
  24. from importlib import metadata, util
  25. from importlib.util import find_spec
  26. from pathlib import Path
  27. from tempfile import mkstemp
  28. from types import FrameType, FunctionType, LambdaType, ModuleType, SimpleNamespace
  29. from urllib.parse import unquote, urlencode, urlparse
  30. import markdown as md_lib
  31. import tzlocal
  32. from flask import (
  33. Blueprint,
  34. Flask,
  35. g,
  36. has_app_context,
  37. has_request_context,
  38. jsonify,
  39. request,
  40. send_file,
  41. send_from_directory,
  42. )
  43. from werkzeug.utils import secure_filename
  44. import __main__ # noqa: F401
  45. from taipy.logger._taipy_logger import _TaipyLogger
  46. if util.find_spec("pyngrok"):
  47. from pyngrok import ngrok
  48. from ._default_config import _default_stylekit, default_config
  49. from ._page import _Page
  50. from ._renderers import _EmptyPage
  51. from ._renderers._markdown import _TaipyMarkdownExtension
  52. from ._renderers.factory import _Factory
  53. from ._renderers.json import _TaipyJsonEncoder
  54. from ._renderers.utils import _get_columns_dict
  55. from ._warnings import TaipyGuiWarning, _warn
  56. from .builder import _ElementApiGenerator
  57. from .config import Config, ConfigParameter, _Config
  58. from .custom import Page as CustomPage
  59. from .data.content_accessor import _ContentAccessor
  60. from .data.data_accessor import _DataAccessor, _DataAccessors
  61. from .data.data_format import _DataFormat
  62. from .data.data_scope import _DataScopes
  63. from .extension.library import Element, ElementLibrary
  64. from .page import Page
  65. from .partial import Partial
  66. from .server import _Server
  67. from .state import State
  68. from .types import _WsType
  69. from .utils import (
  70. _delscopeattr,
  71. _DoNotUpdate,
  72. _filter_locals,
  73. _get_broadcast_var_name,
  74. _get_client_var_name,
  75. _get_css_var_value,
  76. _get_expr_var_name,
  77. _get_module_name_from_frame,
  78. _get_non_existent_file_path,
  79. _get_page_from_module,
  80. _getscopeattr,
  81. _getscopeattr_drill,
  82. _hasscopeattr,
  83. _is_in_notebook,
  84. _LocalsContext,
  85. _MapDict,
  86. _setscopeattr,
  87. _setscopeattr_drill,
  88. _TaipyBase,
  89. _TaipyContent,
  90. _TaipyContentHtml,
  91. _TaipyContentImage,
  92. _TaipyData,
  93. _TaipyLov,
  94. _TaipyLovValue,
  95. _TaipyToJson,
  96. _to_camel_case,
  97. _variable_decode,
  98. is_debugging,
  99. )
  100. from .utils._adapter import _Adapter
  101. from .utils._bindings import _Bindings
  102. from .utils._evaluator import _Evaluator
  103. from .utils._variable_directory import _MODULE_ID, _VariableDirectory
  104. from .utils.chart_config_builder import _build_chart_config
  105. from .utils.table_col_builder import _enhance_columns
  106. class Gui:
  107. """Entry point for the Graphical User Interface generation.
  108. Attributes:
  109. on_action (Callable): The function that is called when a control
  110. triggers an action, as the result of an interaction with the end-user.<br/>
  111. It defaults to the `on_action()` global function defined in the Python
  112. application. If there is no such function, actions will not trigger anything.<br/>
  113. The signature of the *on_action* callback function must be:
  114. - *state*: the `State^` instance of the caller.
  115. - *id* (optional): a string representing the identifier of the caller.
  116. - *payload* (optional): an optional payload from the caller.
  117. on_change (Callable): The function that is called when a control
  118. modifies variables it is bound to, as the result of an interaction with the
  119. end-user.<br/>
  120. It defaults to the `on_change()` global function defined in the Python
  121. application. If there is no such function, user interactions will not trigger
  122. anything.<br/>
  123. The signature of the *on_change* callback function must be:
  124. - *state*: the `State^` instance of the caller.
  125. - *var_name* (str): The name of the variable that triggered this callback.
  126. - *var_value* (any): The new value for this variable.
  127. on_init (Callable): The function that is called on the first connection of a new client.<br/>
  128. It defaults to the `on_init()` global function defined in the Python
  129. application. If there is no such function, the first connection will not trigger
  130. anything.<br/>
  131. The signature of the *on_init* callback function must be:
  132. - *state*: the `State^` instance of the caller.
  133. on_navigate (Callable): The function that is called when a page is requested.<br/>
  134. It defaults to the `on_navigate()` global function defined in the Python
  135. application. If there is no such function, page requests will not trigger
  136. anything.<br/>
  137. The signature of the *on_navigate* callback function must be:
  138. - *state*: the `State^` instance of the caller.
  139. - *page_name*: the name of the page the user is navigating to.
  140. - *params* (Optional): the query parameters provided in the URL.
  141. The *on_navigate* callback function must return the name of the page the user should be
  142. directed to.
  143. on_exception (Callable): The function that is called an exception occurs on user code.<br/>
  144. It defaults to the `on_exception()` global function defined in the Python
  145. application. If there is no such function, exceptions will not trigger
  146. anything.<br/>
  147. The signature of the *on_exception* callback function must be:
  148. - *state*: the `State^` instance of the caller.
  149. - *function_name*: the name of the function that raised the exception.
  150. - *exception*: the exception object that was raised.
  151. on_status (Callable): The function that is called when the status page is shown.<br/>
  152. It defaults to the `on_status()` global function defined in the Python
  153. application. If there is no such function, status page content shows only the status of the
  154. server.<br/>
  155. The signature of the *on_status* callback function must be:
  156. - *state*: the `State^` instance of the caller.
  157. It must return raw and valid HTML content as a string.
  158. on_user_content (Callable): The function that is called when a specific URL (generated by
  159. `get_user_content_url()^`) is requested.<br/>
  160. This callback function must return the raw HTML content of the page to be displayed on
  161. the browser.
  162. This attribute defaults to the `on_user_content()` global function defined in the Python
  163. application. If there is no such function, those specific URLs will not trigger
  164. anything.<br/>
  165. The signature of the *on_user_content* callback function must be:
  166. - *state*: the `State^` instance of the caller.
  167. - *path*: the path provided to the `get_user_content_url()^` to build the URL.
  168. - *parameters*: An optional dictionary as defined in the `get_user_content_url()^` call.
  169. The returned HTML content can therefore use both the variables stored in the *state*
  170. and the parameters provided in the call to `get_user_content_url()^`.
  171. state (State^): **Only defined when running in an IPython notebook context.**<br/>
  172. The unique instance of `State^` that you can use to change bound variables
  173. directly, potentially impacting the user interface in real-time.
  174. !!! note
  175. This class belongs to and is documented in the `taipy.gui` package but it is
  176. accessible from the top `taipy` package to simplify its access, allowing to
  177. use:
  178. ```py
  179. from taipy import Gui
  180. ```
  181. """
  182. __root_page_name = "TaiPy_root_page"
  183. __env_filename = "taipy.gui.env"
  184. __UI_BLOCK_NAME = "TaipyUiBlockVar"
  185. __MESSAGE_GROUPING_NAME = "TaipyMessageGrouping"
  186. __ON_INIT_NAME = "TaipyOnInit"
  187. __ARG_CLIENT_ID = "client_id"
  188. __INIT_URL = "taipy-init"
  189. __JSX_URL = "taipy-jsx"
  190. __CONTENT_ROOT = "taipy-content"
  191. __UPLOAD_URL = "taipy-uploads"
  192. _EXTENSION_ROOT = "taipy-extension"
  193. __USER_CONTENT_URL = "taipy-user-content"
  194. __BROADCAST_G_ID = "taipy_broadcasting"
  195. __BRDCST_CALLBACK_G_ID = "taipy_brdcst_callback"
  196. __SELF_VAR = "__gui"
  197. __DO_NOT_UPDATE_VALUE = _DoNotUpdate()
  198. _HTML_CONTENT_KEY = "__taipy_html_content"
  199. __USER_CONTENT_CB = "custom_user_content_cb"
  200. __ROBOTO_FONT = "https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
  201. __DOWNLOAD_ACTION = "__Taipy__download_csv"
  202. __DOWNLOAD_DELETE_ACTION = "__Taipy__download_delete_csv"
  203. __DEFAULT_FAVICON_URL = "https://raw.githubusercontent.com/Avaiga/taipy-assets/develop/favicon.png"
  204. __RE_HTML = re.compile(r"(.*?)\.html$")
  205. __RE_MD = re.compile(r"(.*?)\.md$")
  206. __RE_PY = re.compile(r"(.*?)\.py$")
  207. __RE_PAGE_NAME = re.compile(r"^[\w\-\/]+$")
  208. __reserved_routes: t.List[str] = [
  209. __INIT_URL,
  210. __JSX_URL,
  211. __CONTENT_ROOT,
  212. __UPLOAD_URL,
  213. _EXTENSION_ROOT,
  214. __USER_CONTENT_URL,
  215. ]
  216. __LOCAL_TZ = str(tzlocal.get_localzone())
  217. __extensions: t.Dict[str, t.List[ElementLibrary]] = {}
  218. __shared_variables: t.List[str] = []
  219. __content_providers: t.Dict[type, t.Callable[..., str]] = {}
  220. def __init__(
  221. self,
  222. page: t.Optional[t.Union[str, Page]] = None,
  223. pages: t.Optional[dict] = None,
  224. css_file: t.Optional[str] = None,
  225. path_mapping: t.Optional[dict] = None,
  226. env_filename: t.Optional[str] = None,
  227. libraries: t.Optional[t.List[ElementLibrary]] = None,
  228. flask: t.Optional[Flask] = None,
  229. ):
  230. """Initialize a new Gui instance.
  231. Arguments:
  232. page (Optional[Union[str, Page^]]): An optional `Page^` instance that is used
  233. when there is a single page in this interface, referenced as the *root*
  234. page (located at `/`).<br/>
  235. If *page* is a raw string and if it holds a path to a readable file then
  236. a `Markdown^` page is built from the content of that file.<br/>
  237. If *page* is a string that does not indicate a path to readable file then
  238. a `Markdown^` page is built from that string.<br/>
  239. Note that if *pages* is provided, those pages are added as well.
  240. pages (Optional[dict]): Used if you want to initialize this instance with a set
  241. of pages.<br/>
  242. The method `(Gui.)add_pages(pages)^` is called if *pages* is not None.
  243. You can find details on the possible values of this argument in the
  244. documentation for this method.
  245. css_file (Optional[str]): A pathname to a CSS file that gets used as a style sheet in
  246. all the pages.<br/>
  247. The default value is a file that has the same base name as the Python
  248. file defining the `main` function, sitting next to this Python file,
  249. with the `.css` extension.
  250. path_mapping (Optional[dict]): A dictionary that associates a URL prefix to
  251. a path in the server file system.<br/>
  252. If the assets of your application are located in */home/me/app_assets* and
  253. you want to access them using only '*assets*' in your application, you can
  254. set *path_mapping={"assets": "/home/me/app_assets"}*. If your application
  255. then requests the file *"/assets/images/logo.png"*, the server searches
  256. for the file *"/home/me/app_assets/images/logo.png"*.<br/>
  257. If empty or not defined, access through the browser to all resources under the directory
  258. of the main Python file is allowed.
  259. env_filename (Optional[str]): An optional file from which to load application
  260. configuration variables (see the
  261. [Configuration](../gui/configuration.md#configuring-the-gui-instance) section
  262. of the User Manual for details.)<br/>
  263. The default value is "taipy.gui.env"
  264. libraries (Optional[List[ElementLibrary]]): An optional list of extension library
  265. instances that pages can reference.<br/>
  266. Using this argument is equivalent to calling `(Gui.)add_library()^` for each
  267. list's elements.
  268. flask (Optional[Flask]): An optional instance of a Flask application object.<br/>
  269. If this argument is set, this `Gui` instance will use the value of this argument
  270. as the underlying server. If omitted or set to None, this `Gui` will create its
  271. own Flask application instance and use it to serve the pages.
  272. """
  273. # store suspected local containing frame
  274. self.__frame = t.cast(FrameType, t.cast(FrameType, inspect.currentframe()).f_back)
  275. self.__default_module_name = _get_module_name_from_frame(self.__frame)
  276. self._set_css_file(css_file)
  277. # Preserve server config for server initialization
  278. if path_mapping is None:
  279. path_mapping = {}
  280. self._path_mapping = path_mapping
  281. self._flask = flask
  282. self._config = _Config()
  283. self.__content_accessor = None
  284. self._accessors = _DataAccessors()
  285. self.__state: t.Optional[State] = None
  286. self.__bindings = _Bindings(self)
  287. self.__locals_context = _LocalsContext()
  288. self.__var_dir = _VariableDirectory(self.__locals_context)
  289. self.__evaluator: _Evaluator = None # type: ignore
  290. self.__adapter = _Adapter()
  291. self.__directory_name_of_pages: t.List[str] = []
  292. self.__favicon: t.Optional[t.Union[str, Path]] = None
  293. # default actions
  294. self.on_action: t.Optional[t.Callable] = None
  295. self.on_change: t.Optional[t.Callable] = None
  296. self.on_init: t.Optional[t.Callable] = None
  297. self.on_navigate: t.Optional[t.Callable] = None
  298. self.on_exception: t.Optional[t.Callable] = None
  299. self.on_status: t.Optional[t.Callable] = None
  300. self.on_user_content: t.Optional[t.Callable] = None
  301. # sid from client_id
  302. self.__client_id_2_sid: t.Dict[str, t.Set[str]] = {}
  303. # Load default config
  304. self._flask_blueprint: t.List[Blueprint] = []
  305. self._config._load(default_config)
  306. # get taipy version
  307. try:
  308. gui_file = Path(__file__ or ".").resolve()
  309. with open(gui_file.parent / "version.json") as version_file:
  310. self.__version = json.load(version_file)
  311. except Exception as e: # pragma: no cover
  312. _warn("Cannot retrieve version.json file", e)
  313. self.__version = {}
  314. # Load Markdown extension
  315. # NOTE: Make sure, if you change this extension list, that the User Manual gets updated.
  316. # There's a section that explicitly lists these extensions in
  317. # docs/gui/pages.md#markdown-specifics
  318. self._markdown = md_lib.Markdown(
  319. extensions=[
  320. "fenced_code",
  321. "meta",
  322. "admonition",
  323. "sane_lists",
  324. "tables",
  325. "attr_list",
  326. "md_in_html",
  327. _TaipyMarkdownExtension(gui=self),
  328. ]
  329. )
  330. if page:
  331. self.add_page(name=Gui.__root_page_name, page=page)
  332. if pages is not None:
  333. self.add_pages(pages)
  334. if env_filename is not None:
  335. self.__env_filename = env_filename
  336. if libraries is not None:
  337. for library in libraries:
  338. Gui.add_library(library)
  339. @staticmethod
  340. def add_library(library: ElementLibrary) -> None:
  341. """Add a custom visual element library.
  342. This application will be able to use custom visual elements defined in this library.
  343. Arguments:
  344. library: The custom visual element library to add to this application.
  345. Multiple libraries with the same name can be added. This allows to split multiple custom visual
  346. elements in several `ElementLibrary^` instances, but still refer to these elements with the same
  347. prefix in the page definitions.
  348. """
  349. if isinstance(library, ElementLibrary):
  350. _Factory.set_library(library)
  351. library_name = library.get_name()
  352. if library_name.isidentifier():
  353. libs = Gui.__extensions.get(library_name)
  354. if libs is None:
  355. Gui.__extensions[library_name] = [library]
  356. else:
  357. libs.append(library)
  358. _ElementApiGenerator().add_library(library)
  359. else:
  360. raise NameError(f"ElementLibrary passed to add_library() has an invalid name: '{library_name}'")
  361. else: # pragma: no cover
  362. raise RuntimeError(
  363. f"add_library() argument should be a subclass of ElementLibrary instead of '{type(library)}'"
  364. )
  365. @staticmethod
  366. def register_content_provider(content_type: type, content_provider: t.Callable[..., str]) -> None:
  367. """Add a custom content provider.
  368. The application can use custom content for the `part` block when its *content* property is set to an object with type *type*.
  369. Arguments:
  370. content_type: The type of the content that triggers the content provider.
  371. content_provider: The function that converts content of type *type* into an HTML string.
  372. """ # noqa: E501
  373. if Gui.__content_providers.get(content_type):
  374. _warn(f"The type {content_type} is already associated with a provider.")
  375. return
  376. if not callable(content_provider):
  377. _warn(f"The provider for {content_type} must be a function.")
  378. return
  379. Gui.__content_providers[content_type] = content_provider
  380. def __process_content_provider(self, state: State, path: str, query: t.Dict[str, str]):
  381. variable_name = query.get("variable_name")
  382. content = None
  383. if variable_name:
  384. content = _getscopeattr(self, variable_name)
  385. if isinstance(content, _TaipyContentHtml):
  386. content = content.get()
  387. provider_fn = Gui.__content_providers.get(type(content))
  388. if provider_fn is None:
  389. # try plotly
  390. if find_spec("plotly") and find_spec("plotly.graph_objs"):
  391. from plotly.graph_objs import Figure as PlotlyFigure
  392. if isinstance(content, PlotlyFigure):
  393. def get_plotly_content(figure: PlotlyFigure):
  394. return figure.to_html()
  395. Gui.register_content_provider(PlotlyFigure, get_plotly_content)
  396. provider_fn = get_plotly_content
  397. if provider_fn is None:
  398. # try matplotlib
  399. if find_spec("matplotlib") and find_spec("matplotlib.figure"):
  400. from matplotlib.figure import Figure as MatplotlibFigure
  401. if isinstance(content, MatplotlibFigure):
  402. def get_matplotlib_content(figure: MatplotlibFigure):
  403. import base64
  404. from io import BytesIO
  405. buf = BytesIO()
  406. figure.savefig(buf, format="png")
  407. data = base64.b64encode(buf.getbuffer()).decode("ascii")
  408. return f'<img src="data:image/png;base64,{data}"/>'
  409. Gui.register_content_provider(MatplotlibFigure, get_matplotlib_content)
  410. provider_fn = get_matplotlib_content
  411. if callable(provider_fn):
  412. try:
  413. return provider_fn(content)
  414. except Exception as e:
  415. _warn(f"Error in content provider for type {str(type(content))}", e)
  416. return (
  417. '<div style="background:white;color:red;">'
  418. + (f"No valid provider for type {type(content).__name__}" if content else "Wrong context.")
  419. + "</div>"
  420. )
  421. @staticmethod
  422. def add_shared_variable(*names: str) -> None:
  423. """Add shared variables.
  424. The variables will be synchronized between all clients when updated.
  425. Note that only variables from the main module will be registered.
  426. This is a synonym for `(Gui.)add_shared_variables()^`.
  427. Arguments:
  428. names: The names of the variables that become shared, as a list argument.
  429. """
  430. for name in names:
  431. if name not in Gui.__shared_variables:
  432. Gui.__shared_variables.append(name)
  433. @staticmethod
  434. def add_shared_variables(*names: str) -> None:
  435. """Add shared variables.
  436. The variables will be synchronized between all clients when updated.
  437. Note that only variables from the main module will be registered.
  438. This is a synonym for `(Gui.)add_shared_variable()^`.
  439. Arguments:
  440. names: The names of the variables that become shared, as a list argument.
  441. """
  442. Gui.add_shared_variable(*names)
  443. def _get_shared_variables(self) -> t.List[str]:
  444. return self.__evaluator.get_shared_variables()
  445. def __get_content_accessor(self):
  446. if self.__content_accessor is None:
  447. self.__content_accessor = _ContentAccessor(self._get_config("data_url_max_size", 50 * 1024))
  448. return self.__content_accessor
  449. def _bindings(self):
  450. return self.__bindings
  451. def _get_data_scope(self) -> SimpleNamespace:
  452. return self.__bindings._get_data_scope()
  453. def _get_data_scope_metadata(self) -> t.Dict[str, t.Any]:
  454. return self.__bindings._get_data_scope_metadata()
  455. def _get_all_data_scopes(self) -> t.Dict[str, SimpleNamespace]:
  456. return self.__bindings._get_all_scopes()
  457. def _get_config(self, name: ConfigParameter, default_value: t.Any) -> t.Any:
  458. return self._config._get_config(name, default_value)
  459. def _get_themes(self) -> t.Optional[t.Dict[str, t.Any]]:
  460. theme = self._get_config("theme", None)
  461. dark_theme = self._get_config("dark_theme", None)
  462. light_theme = self._get_config("light_theme", None)
  463. res = {}
  464. if theme:
  465. res["base"] = theme
  466. if dark_theme:
  467. res["dark"] = dark_theme
  468. if light_theme:
  469. res["light"] = light_theme
  470. return res if theme or dark_theme or light_theme else None
  471. def _bind(self, name: str, value: t.Any) -> None:
  472. self._bindings()._bind(name, value)
  473. def __get_state(self):
  474. return self.__state
  475. def _get_client_id(self) -> str:
  476. return (
  477. _DataScopes._GLOBAL_ID
  478. if self._bindings()._is_single_client()
  479. else getattr(g, Gui.__ARG_CLIENT_ID, "unknown id")
  480. )
  481. def __set_client_id_in_context(self, client_id: t.Optional[str] = None, force=False):
  482. if not client_id and request:
  483. client_id = request.args.get(Gui.__ARG_CLIENT_ID, "")
  484. if not client_id and (ws_client_id := getattr(g, "ws_client_id", None)):
  485. client_id = ws_client_id
  486. if not client_id and force:
  487. res = self._bindings()._get_or_create_scope("")
  488. client_id = res[0] if res[1] else None
  489. if client_id and request:
  490. if sid := getattr(request, "sid", None):
  491. sids = self.__client_id_2_sid.get(client_id, None)
  492. if sids is None:
  493. sids = set()
  494. self.__client_id_2_sid[client_id] = sids
  495. sids.add(sid)
  496. g.client_id = client_id
  497. def __is_var_modified_in_context(self, var_name: str, derived_vars: t.Set[str]) -> bool:
  498. modified_vars: t.Optional[t.Set[str]] = getattr(g, "modified_vars", None)
  499. der_vars: t.Optional[t.Set[str]] = getattr(g, "derived_vars", None)
  500. setattr(g, "update_count", getattr(g, "update_count", 0) + 1) # noqa: B010
  501. if modified_vars is None:
  502. modified_vars = set()
  503. g.modified_vars = modified_vars
  504. if der_vars is None:
  505. g.derived_vars = derived_vars
  506. else:
  507. der_vars.update(derived_vars)
  508. if var_name in modified_vars:
  509. return True
  510. modified_vars.add(var_name)
  511. return False
  512. def __clean_vars_on_exit(self) -> t.Optional[t.Set[str]]:
  513. update_count = getattr(g, "update_count", 0) - 1
  514. if update_count < 1:
  515. derived_vars: t.Set[str] = getattr(g, "derived_vars", set())
  516. delattr(g, "update_count")
  517. delattr(g, "modified_vars")
  518. delattr(g, "derived_vars")
  519. return derived_vars
  520. else:
  521. setattr(g, "update_count", update_count) # noqa: B010
  522. return None
  523. def _handle_connect(self):
  524. pass
  525. def _handle_disconnect(self):
  526. pass
  527. def _manage_message(self, msg_type: _WsType, message: dict) -> None:
  528. try:
  529. client_id = None
  530. if msg_type == _WsType.CLIENT_ID.value:
  531. res = self._bindings()._get_or_create_scope(message.get("payload", ""))
  532. client_id = res[0] if res[1] else None
  533. expected_client_id = client_id or message.get(Gui.__ARG_CLIENT_ID)
  534. self.__set_client_id_in_context(expected_client_id)
  535. g.ws_client_id = expected_client_id
  536. with self._set_locals_context(message.get("module_context") or None):
  537. with self._get_autorization():
  538. payload = message.get("payload", {})
  539. if msg_type == _WsType.UPDATE.value:
  540. self.__front_end_update(
  541. str(message.get("name")),
  542. payload.get("value"),
  543. message.get("propagate", True),
  544. payload.get("relvar"),
  545. payload.get("on_change"),
  546. )
  547. elif msg_type == _WsType.ACTION.value:
  548. self.__on_action(message.get("name"), message.get("payload"))
  549. elif msg_type == _WsType.DATA_UPDATE.value:
  550. self.__request_data_update(str(message.get("name")), message.get("payload"))
  551. elif msg_type == _WsType.REQUEST_UPDATE.value:
  552. self.__request_var_update(message.get("payload"))
  553. elif msg_type == _WsType.GET_MODULE_CONTEXT.value:
  554. self.__handle_ws_get_module_context(payload)
  555. elif msg_type == _WsType.GET_DATA_TREE.value:
  556. self.__handle_ws_get_data_tree()
  557. elif msg_type == _WsType.APP_ID.value:
  558. self.__handle_ws_app_id(message)
  559. elif msg_type == _WsType.GET_ROUTES.value:
  560. self.__handle_ws_get_routes()
  561. else:
  562. self._manage_external_message(msg_type, message)
  563. self.__send_ack(message.get("ack_id"))
  564. except Exception as e: # pragma: no cover
  565. if isinstance(e, AttributeError) and (name := message.get("name")):
  566. try:
  567. names = self._get_real_var_name(name)
  568. var_name = names[0] if isinstance(names, tuple) else names
  569. var_context = names[1] if isinstance(names, tuple) else None
  570. if var_name.startswith("tpec_"):
  571. var_name = var_name[5:]
  572. if var_name.startswith("TpExPr_"):
  573. var_name = var_name[7:]
  574. _warn(
  575. f"A problem occurred while resolving variable '{var_name}'"
  576. + (f" in module '{var_context}'." if var_context else ".")
  577. )
  578. except Exception as e1:
  579. _warn(f"Resolving name '{name}' failed", e1)
  580. else:
  581. _warn(f"Decoding Message has failed: {message}", e)
  582. # To be expanded by inheriting classes
  583. # this will be used to handle ws messages that is not handled by the base Gui class
  584. def _manage_external_message(self, msg_type: _WsType, message: dict) -> None:
  585. pass
  586. def __front_end_update(
  587. self,
  588. var_name: str,
  589. value: t.Any,
  590. propagate=True,
  591. rel_var: t.Optional[str] = None,
  592. on_change: t.Optional[str] = None,
  593. ) -> None:
  594. if not var_name:
  595. return
  596. # Check if Variable is a managed type
  597. current_value = _getscopeattr_drill(self, self.__evaluator.get_hash_from_expr(var_name))
  598. if isinstance(current_value, _TaipyData):
  599. return
  600. elif rel_var and isinstance(current_value, _TaipyLovValue): # pragma: no cover
  601. lov_holder = _getscopeattr_drill(self, self.__evaluator.get_hash_from_expr(rel_var))
  602. if isinstance(lov_holder, _TaipyLov):
  603. val = value if isinstance(value, list) else [value]
  604. elt_4_ids = self.__adapter._get_elt_per_ids(lov_holder.get_name(), lov_holder.get())
  605. ret_val = [elt_4_ids.get(x, x) for x in val]
  606. if isinstance(value, list):
  607. value = ret_val
  608. elif ret_val:
  609. value = ret_val[0]
  610. elif isinstance(current_value, _TaipyBase):
  611. value = current_value.cast_value(value)
  612. self._update_var(
  613. var_name, value, propagate, current_value if isinstance(current_value, _TaipyBase) else None, on_change
  614. )
  615. def _update_var(
  616. self,
  617. var_name: str,
  618. value: t.Any,
  619. propagate=True,
  620. holder: t.Optional[_TaipyBase] = None,
  621. on_change: t.Optional[str] = None,
  622. forward: t.Optional[bool] = True,
  623. ) -> None:
  624. if holder:
  625. var_name = holder.get_name()
  626. hash_expr = self.__evaluator.get_hash_from_expr(var_name)
  627. derived_vars = {hash_expr}
  628. # set to broadcast mode if hash_expr is in shared_variable
  629. if hash_expr in self._get_shared_variables():
  630. self._set_broadcast()
  631. # Use custom attrsetter function to allow value binding for _MapDict
  632. if propagate:
  633. _setscopeattr_drill(self, hash_expr, value)
  634. # In case expression == hash (which is when there is only a single variable in expression)
  635. if var_name == hash_expr or hash_expr.startswith("tpec_"):
  636. derived_vars.update(self._re_evaluate_expr(var_name))
  637. elif holder:
  638. derived_vars.update(self._evaluate_holders(hash_expr))
  639. if forward:
  640. # if the variable has been evaluated then skip updating to prevent infinite loop
  641. var_modified = self.__is_var_modified_in_context(hash_expr, derived_vars)
  642. if not var_modified:
  643. self._call_on_change(
  644. var_name,
  645. value.get()
  646. if isinstance(value, _TaipyBase)
  647. else value._dict
  648. if isinstance(value, _MapDict)
  649. else value,
  650. on_change,
  651. )
  652. derived_modified = self.__clean_vars_on_exit()
  653. if derived_modified is not None:
  654. self.__send_var_list_update(list(derived_modified), var_name)
  655. def _get_real_var_name(self, var_name: str) -> t.Tuple[str, str]:
  656. if not var_name:
  657. return (var_name, var_name)
  658. # Handle holder prefix if needed
  659. if var_name.startswith(_TaipyBase._HOLDER_PREFIX):
  660. for hp in _TaipyBase._get_holder_prefixes():
  661. if var_name.startswith(hp):
  662. var_name = var_name[len(hp) :]
  663. break
  664. suffix_var_name = ""
  665. if "." in var_name:
  666. first_dot_index = var_name.index(".")
  667. suffix_var_name = var_name[first_dot_index + 1 :]
  668. var_name = var_name[:first_dot_index]
  669. var_name_decode, module_name = _variable_decode(self._get_expr_from_hash(var_name))
  670. current_context = self._get_locals_context()
  671. # #583: allow module resolution for var_name in current_context root_page context
  672. if (
  673. module_name
  674. and self._config.root_page
  675. and self._config.root_page._renderer
  676. and self._config.root_page._renderer._get_module_name() == module_name
  677. ):
  678. return f"{var_name_decode}.{suffix_var_name}" if suffix_var_name else var_name_decode, module_name
  679. if module_name == current_context:
  680. var_name = var_name_decode
  681. # only strict checking for cross-context linked variable when the context has been properly set
  682. elif self._has_set_context():
  683. if var_name not in self.__var_dir._var_head:
  684. raise NameError(f"Can't find matching variable for {var_name} on context: {current_context}")
  685. _found = False
  686. for k, v in self.__var_dir._var_head[var_name]:
  687. if v == current_context:
  688. var_name = k
  689. _found = True
  690. break
  691. if not _found: # pragma: no cover
  692. raise NameError(f"Can't find matching variable for {var_name} on context: {current_context}")
  693. return f"{var_name}.{suffix_var_name}" if suffix_var_name else var_name, current_context
  694. def _call_on_change(self, var_name: str, value: t.Any, on_change: t.Optional[str] = None):
  695. try:
  696. var_name, current_context = self._get_real_var_name(var_name)
  697. except Exception as e: # pragma: no cover
  698. _warn("", e)
  699. return
  700. on_change_fn = self._get_user_function(on_change) if on_change else None
  701. if not callable(on_change_fn):
  702. on_change_fn = self._get_user_function("on_change")
  703. if callable(on_change_fn):
  704. try:
  705. argcount = on_change_fn.__code__.co_argcount
  706. if argcount > 0 and inspect.ismethod(on_change_fn):
  707. argcount -= 1
  708. args: t.List[t.Any] = [None for _ in range(argcount)]
  709. if argcount > 0:
  710. args[0] = self.__get_state()
  711. if argcount > 1:
  712. args[1] = var_name
  713. if argcount > 2:
  714. args[2] = value
  715. if argcount > 3:
  716. args[3] = current_context
  717. on_change_fn(*args)
  718. except Exception as e: # pragma: no cover
  719. if not self._call_on_exception(on_change or "on_change", e):
  720. _warn(f"{on_change or 'on_change'}(): callback function raised an exception", e)
  721. def _get_content(self, var_name: str, value: t.Any, image: bool) -> t.Any:
  722. ret_value = self.__get_content_accessor().get_info(var_name, value, image)
  723. return f"/{Gui.__CONTENT_ROOT}/{ret_value[0]}" if isinstance(ret_value, tuple) else ret_value
  724. def __serve_content(self, path: str) -> t.Any:
  725. self.__set_client_id_in_context()
  726. parts = path.split("/")
  727. if len(parts) > 1:
  728. file_name = parts[-1]
  729. (dir_path, as_attachment) = self.__get_content_accessor().get_content_path(
  730. path[: -len(file_name) - 1], file_name, request.args.get("bypass")
  731. )
  732. if dir_path:
  733. return send_from_directory(str(dir_path), file_name, as_attachment=as_attachment)
  734. return ("", 404)
  735. def _get_user_content_url(
  736. self, path: t.Optional[str] = None, query_args: t.Optional[t.Dict[str, str]] = None
  737. ) -> t.Optional[str]:
  738. qargs = query_args or {}
  739. qargs.update({Gui.__ARG_CLIENT_ID: self._get_client_id()})
  740. return f"/{Gui.__USER_CONTENT_URL}/{path or 'TaIpY'}?{urlencode(qargs)}"
  741. def __serve_user_content(self, path: str) -> t.Any:
  742. self.__set_client_id_in_context()
  743. qargs: t.Dict[str, str] = {}
  744. qargs.update(request.args)
  745. qargs.pop(Gui.__ARG_CLIENT_ID, None)
  746. cb_function: t.Optional[t.Union[t.Callable, str]] = None
  747. cb_function_name = None
  748. if qargs.get(Gui._HTML_CONTENT_KEY):
  749. cb_function = self.__process_content_provider
  750. cb_function_name = cb_function.__name__
  751. else:
  752. cb_function_name = qargs.get(Gui.__USER_CONTENT_CB)
  753. if cb_function_name:
  754. cb_function = self._get_user_function(cb_function_name)
  755. if not callable(cb_function):
  756. parts = cb_function_name.split(".", 1)
  757. if len(parts) > 1:
  758. base = _getscopeattr(self, parts[0], None)
  759. if base and (meth := getattr(base, parts[1], None)):
  760. cb_function = meth
  761. else:
  762. base = self.__evaluator._get_instance_in_context(parts[0])
  763. if base and (meth := getattr(base, parts[1], None)):
  764. cb_function = meth
  765. if not callable(cb_function):
  766. _warn(f"{cb_function_name}() callback function has not been defined.")
  767. cb_function = None
  768. if cb_function is None:
  769. cb_function_name = "on_user_content"
  770. if hasattr(self, cb_function_name) and callable(self.on_user_content):
  771. cb_function = self.on_user_content
  772. else:
  773. _warn("on_user_content() callback function has not been defined.")
  774. if callable(cb_function):
  775. try:
  776. args: t.List[t.Any] = []
  777. if path:
  778. args.append(path)
  779. if len(qargs):
  780. args.append(qargs)
  781. ret = self._call_function_with_state(cb_function, args)
  782. if ret is None:
  783. _warn(f"{cb_function_name}() callback function must return a value.")
  784. else:
  785. return (ret, 200)
  786. except Exception as e: # pragma: no cover
  787. if not self._call_on_exception(str(cb_function_name), e):
  788. _warn(f"{cb_function_name}() callback function raised an exception", e)
  789. return ("", 404)
  790. def __serve_extension(self, path: str) -> t.Any:
  791. parts = path.split("/")
  792. last_error = ""
  793. resource_name = None
  794. if len(parts) > 1:
  795. libs = Gui.__extensions.get(parts[0], [])
  796. for library in libs:
  797. try:
  798. resource_name = library.get_resource("/".join(parts[1:]))
  799. if resource_name:
  800. return send_file(resource_name)
  801. except Exception as e:
  802. last_error = f"\n{e}" # Check if the resource is served by another library with the same name
  803. _warn(f"Resource '{resource_name or path}' not accessible for library '{parts[0]}'{last_error}")
  804. return ("", 404)
  805. def __get_version(self) -> str:
  806. return f'{self.__version.get("major", 0)}.{self.__version.get("minor", 0)}.{self.__version.get("patch", 0)}'
  807. def __append_libraries_to_status(self, status: t.Dict[str, t.Any]):
  808. libraries: t.Dict[str, t.Any] = {}
  809. for libs_list in self.__extensions.values():
  810. for lib in libs_list:
  811. if not isinstance(lib, ElementLibrary):
  812. continue
  813. libs = libraries.get(lib.get_name())
  814. if libs is None:
  815. libs = []
  816. libraries[lib.get_name()] = libs
  817. elts: t.List[t.Dict[str, str]] = []
  818. libs.append({"js module": lib.get_js_module_name(), "elements": elts})
  819. for element_name, elt in lib.get_elements().items():
  820. if not isinstance(elt, Element):
  821. continue
  822. elt_dict = {"name": element_name}
  823. if hasattr(elt, "_render_xhtml"):
  824. elt_dict["render function"] = elt._render_xhtml.__code__.co_name
  825. else:
  826. elt_dict["react name"] = elt._get_js_name(element_name)
  827. elts.append(elt_dict)
  828. status.update({"libraries": libraries})
  829. def _serve_status(self, template: Path) -> t.Dict[str, t.Dict[str, str]]:
  830. base_json: t.Dict[str, t.Any] = {"user_status": str(self.__call_on_status() or "")}
  831. if self._get_config("extended_status", False):
  832. base_json.update(
  833. {
  834. "flask_version": str(metadata.version("flask") or ""),
  835. "backend_version": self.__get_version(),
  836. "host": f'{self._get_config("host", "localhost")}:{self._get_config("port", "default")}',
  837. "python_version": sys.version,
  838. }
  839. )
  840. self.__append_libraries_to_status(base_json)
  841. try:
  842. base_json.update(json.loads(template.read_text()))
  843. except Exception as e: # pragma: no cover
  844. _warn(f"Exception raised reading JSON in '{template}'", e)
  845. return {"gui": base_json}
  846. def __upload_files(self):
  847. self.__set_client_id_in_context()
  848. if "var_name" not in request.form:
  849. _warn("No var name")
  850. return ("No var name", 400)
  851. var_name = request.form["var_name"]
  852. multiple = "multiple" in request.form and request.form["multiple"] == "True"
  853. if "blob" not in request.files:
  854. _warn("No file part")
  855. return ("No file part", 400)
  856. file = request.files["blob"]
  857. # If the user does not select a file, the browser submits an
  858. # empty file without a filename.
  859. if file.filename == "":
  860. _warn("No selected file")
  861. return ("No selected file", 400)
  862. suffix = ""
  863. complete = True
  864. part = 0
  865. if "total" in request.form:
  866. total = int(request.form["total"])
  867. if total > 1 and "part" in request.form:
  868. part = int(request.form["part"])
  869. suffix = f".part.{part}"
  870. complete = part == total - 1
  871. if file: # and allowed_file(file.filename)
  872. upload_path = Path(self._get_config("upload_folder", tempfile.gettempdir())).resolve()
  873. file_path = _get_non_existent_file_path(upload_path, secure_filename(file.filename))
  874. file.save(str(upload_path / (file_path.name + suffix)))
  875. if complete:
  876. if part > 0:
  877. try:
  878. with open(file_path, "wb") as grouped_file:
  879. for nb in range(part + 1):
  880. part_file_path = upload_path / f"{file_path.name}.part.{nb}"
  881. with open(part_file_path, "rb") as part_file:
  882. grouped_file.write(part_file.read())
  883. # remove file_path after it is merged
  884. part_file_path.unlink()
  885. except EnvironmentError as ee: # pragma: no cover
  886. _warn(f"Cannot group file after chunk upload for {file.filename}", ee)
  887. return (f"Cannot group file after chunk upload for {file.filename}", 500)
  888. # notify the file is uploaded
  889. newvalue = str(file_path)
  890. if multiple:
  891. value = _getscopeattr(self, var_name)
  892. if not isinstance(value, t.List):
  893. value = [] if value is None else [value]
  894. value.append(newvalue)
  895. newvalue = value
  896. setattr(self._bindings(), var_name, newvalue)
  897. return ("", 200)
  898. _data_request_counter = 1
  899. def __send_var_list_update( # noqa C901
  900. self,
  901. modified_vars: t.List[str],
  902. front_var: t.Optional[str] = None,
  903. ):
  904. ws_dict = {}
  905. values = {v: _getscopeattr_drill(self, v) for v in modified_vars}
  906. for k, v in values.items():
  907. if isinstance(v, (_TaipyData, _TaipyContentHtml)) and v.get_name() in modified_vars:
  908. modified_vars.remove(v.get_name())
  909. elif isinstance(v, _DoNotUpdate):
  910. modified_vars.remove(k)
  911. for _var in modified_vars:
  912. newvalue = values.get(_var)
  913. if isinstance(newvalue, _TaipyData):
  914. # A changing integer that triggers a data request
  915. newvalue = Gui._data_request_counter
  916. Gui._data_request_counter = (Gui._data_request_counter % 100) + 1
  917. else:
  918. if isinstance(newvalue, (_TaipyContent, _TaipyContentImage)):
  919. ret_value = self.__get_content_accessor().get_info(
  920. front_var, newvalue.get(), isinstance(newvalue, _TaipyContentImage)
  921. )
  922. if isinstance(ret_value, tuple):
  923. newvalue = f"/{Gui.__CONTENT_ROOT}/{ret_value[0]}"
  924. else:
  925. newvalue = ret_value
  926. elif isinstance(newvalue, _TaipyContentHtml):
  927. newvalue = self._get_user_content_url(
  928. None, {"variable_name": str(_var), Gui._HTML_CONTENT_KEY: str(time.time())}
  929. )
  930. elif isinstance(newvalue, (_TaipyLov, _TaipyLovValue)):
  931. newvalue = self.__adapter.run(
  932. newvalue.get_name(), newvalue.get(), id_only=isinstance(newvalue, _TaipyLovValue)
  933. )
  934. elif isinstance(newvalue, _TaipyToJson):
  935. newvalue = newvalue.get()
  936. if isinstance(newvalue, (dict, _MapDict)):
  937. # Skip in taipy-gui, available in custom frontend
  938. resource_handler_id = None
  939. with contextlib.suppress(Exception):
  940. if has_request_context():
  941. resource_handler_id = request.cookies.get(_Server._RESOURCE_HANDLER_ARG, None)
  942. if resource_handler_id is None:
  943. continue # this var has no transformer
  944. if isinstance(newvalue, float) and math.isnan(newvalue):
  945. # do not let NaN go through json, it is not handle well (dies silently through websocket)
  946. newvalue = None
  947. debug_warnings: t.List[warnings.WarningMessage] = []
  948. with warnings.catch_warnings(record=True) as warns:
  949. warnings.resetwarnings()
  950. json.dumps(newvalue, cls=_TaipyJsonEncoder)
  951. if len(warns):
  952. keep_value = True
  953. for w in warns:
  954. if is_debugging():
  955. debug_warnings.append(w)
  956. if w.category is not DeprecationWarning and w.category is not PendingDeprecationWarning:
  957. keep_value = False
  958. break
  959. if not keep_value:
  960. # do not send data that is not serializable
  961. continue
  962. for w in debug_warnings:
  963. warnings.warn(w.message, w.category) # noqa: B028
  964. ws_dict[_var] = newvalue
  965. # TODO: What if value == newvalue?
  966. self.__send_ws_update_with_dict(ws_dict)
  967. def __update_state_context(self, payload: dict):
  968. # apply state context if any
  969. state_context = payload.get("state_context")
  970. if isinstance(state_context, dict):
  971. for var, val in state_context.items():
  972. self._update_var(var, val, True, forward=False)
  973. def __request_data_update(self, var_name: str, payload: t.Any) -> None:
  974. # Use custom attrgetter function to allow value binding for _MapDict
  975. newvalue = _getscopeattr_drill(self, var_name)
  976. if isinstance(newvalue, _TaipyData):
  977. ret_payload = None
  978. if isinstance(payload, dict):
  979. self.__update_state_context(payload)
  980. lib_name = payload.get("library")
  981. if isinstance(lib_name, str):
  982. libs = self.__extensions.get(lib_name, [])
  983. for lib in libs:
  984. user_var_name = var_name
  985. try:
  986. with contextlib.suppress(NameError):
  987. # ignore name error and keep var_name
  988. user_var_name = self._get_real_var_name(var_name)[0]
  989. ret_payload = lib.get_data(lib_name, payload, user_var_name, newvalue)
  990. if ret_payload:
  991. break
  992. except Exception as e: # pragma: no cover
  993. _warn(
  994. f"Exception raised in '{lib_name}.get_data({lib_name}, payload, {user_var_name}, value)'", # noqa: E501
  995. e,
  996. )
  997. if not isinstance(ret_payload, dict):
  998. ret_payload = self._accessors._get_data(self, var_name, newvalue, payload)
  999. self.__send_ws_update_with_dict({var_name: ret_payload})
  1000. def __request_var_update(self, payload: t.Any):
  1001. if isinstance(payload, dict) and isinstance(payload.get("names"), list):
  1002. self.__update_state_context(payload)
  1003. if payload.get("refresh", False):
  1004. # refresh vars
  1005. for _var in t.cast(list, payload.get("names")):
  1006. val = _getscopeattr_drill(self, _var)
  1007. self._refresh_expr(
  1008. val.get_name() if isinstance(val, _TaipyBase) else _var,
  1009. val if isinstance(val, _TaipyBase) else None,
  1010. )
  1011. self.__send_var_list_update(payload["names"])
  1012. def __handle_ws_get_module_context(self, payload: t.Any):
  1013. if isinstance(payload, dict):
  1014. page_path = str(payload.get("path"))
  1015. if page_path in {"/", ""}:
  1016. page_path = Gui.__root_page_name
  1017. # Get Module Context
  1018. if mc := self._get_page_context(page_path):
  1019. page_renderer = self._get_page(page_path)._renderer
  1020. self._bind_custom_page_variables(page_renderer, self._get_client_id())
  1021. # get metadata if there is one
  1022. metadata: t.Dict[str, t.Any] = {}
  1023. if hasattr(page_renderer, "_metadata"):
  1024. metadata = getattr(page_renderer, "_metadata", {})
  1025. meta_return = json.dumps(metadata, cls=_TaipyJsonEncoder) if metadata else None
  1026. self.__send_ws(
  1027. {
  1028. "type": _WsType.GET_MODULE_CONTEXT.value,
  1029. "payload": {"context": mc, "metadata": meta_return},
  1030. }
  1031. )
  1032. def __get_variable_tree(self, data: t.Dict[str, t.Any]):
  1033. # Module Context -> Variable -> Variable data (name, type, initial_value)
  1034. variable_tree: t.Dict[str, t.Dict[str, t.Dict[str, t.Any]]] = {}
  1035. for k, v in data.items():
  1036. if isinstance(v, _TaipyBase):
  1037. data[k] = v.get()
  1038. var_name, var_module_name = _variable_decode(k)
  1039. if var_module_name == "" or var_module_name is None:
  1040. var_module_name = "__main__"
  1041. if var_module_name not in variable_tree:
  1042. variable_tree[var_module_name] = {}
  1043. variable_tree[var_module_name][var_name] = {
  1044. "type": type(v).__name__,
  1045. "value": data[k],
  1046. "encoded_name": k,
  1047. }
  1048. return variable_tree
  1049. def __handle_ws_get_data_tree(self):
  1050. # Get Variables
  1051. self.__pre_render_pages()
  1052. data = {
  1053. k: v
  1054. for k, v in vars(self._get_data_scope()).items()
  1055. if not k.startswith("_")
  1056. and not callable(v)
  1057. and "TpExPr" not in k
  1058. and not isinstance(v, (ModuleType, FunctionType, LambdaType, type, Page))
  1059. }
  1060. function_data = {
  1061. k: v
  1062. for k, v in vars(self._get_data_scope()).items()
  1063. if not k.startswith("_") and "TpExPr" not in k and isinstance(v, (FunctionType, LambdaType))
  1064. }
  1065. self.__send_ws(
  1066. {
  1067. "type": _WsType.GET_DATA_TREE.value,
  1068. "payload": {
  1069. "variable": self.__get_variable_tree(data),
  1070. "function": self.__get_variable_tree(function_data),
  1071. },
  1072. }
  1073. )
  1074. def __handle_ws_app_id(self, message: t.Any):
  1075. if not isinstance(message, dict):
  1076. return
  1077. name = message.get("name", "")
  1078. payload = message.get("payload", "")
  1079. app_id = id(self)
  1080. if payload == app_id:
  1081. return
  1082. self.__send_ws(
  1083. {
  1084. "type": _WsType.APP_ID.value,
  1085. "payload": {"name": name, "id": app_id},
  1086. }
  1087. )
  1088. def __handle_ws_get_routes(self):
  1089. routes = (
  1090. [[self._config.root_page._route, self._config.root_page._renderer.page_type]]
  1091. if self._config.root_page
  1092. else []
  1093. )
  1094. routes += [
  1095. [page._route, page._renderer.page_type]
  1096. for page in self._config.pages
  1097. if page._route != Gui.__root_page_name
  1098. ]
  1099. self.__send_ws(
  1100. {
  1101. "type": _WsType.GET_ROUTES.value,
  1102. "payload": routes,
  1103. }
  1104. )
  1105. def __send_ws(self, payload: dict, allow_grouping=True, send_back_only=False) -> None:
  1106. grouping_message = self.__get_message_grouping() if allow_grouping else None
  1107. if grouping_message is None:
  1108. try:
  1109. self._server._ws.emit(
  1110. "message",
  1111. payload,
  1112. to=self.__get_ws_receiver(send_back_only),
  1113. )
  1114. time.sleep(0.001)
  1115. except Exception as e: # pragma: no cover
  1116. _warn(f"Exception raised in WebSocket communication in '{self.__frame.f_code.co_name}'", e)
  1117. else:
  1118. grouping_message.append(payload)
  1119. def __broadcast_ws(self, payload: dict, client_id: t.Optional[str] = None):
  1120. try:
  1121. to = list(self.__get_sids(client_id)) if client_id else []
  1122. self._server._ws.emit("message", payload, to=to if to else None, include_self=True)
  1123. time.sleep(0.001)
  1124. except Exception as e: # pragma: no cover
  1125. _warn(f"Exception raised in WebSocket communication in '{self.__frame.f_code.co_name}'", e)
  1126. def __send_ack(self, ack_id: t.Optional[str]) -> None:
  1127. if ack_id:
  1128. try:
  1129. self._server._ws.emit(
  1130. "message",
  1131. {"type": _WsType.ACKNOWLEDGEMENT.value, "id": ack_id},
  1132. to=self.__get_ws_receiver(True),
  1133. )
  1134. time.sleep(0.001)
  1135. except Exception as e: # pragma: no cover
  1136. _warn(f"Exception raised in WebSocket communication (send ack) in '{self.__frame.f_code.co_name}'", e)
  1137. def _send_ws_id(self, id: str) -> None:
  1138. self.__send_ws(
  1139. {
  1140. "type": _WsType.CLIENT_ID.value,
  1141. "id": id,
  1142. },
  1143. allow_grouping=False,
  1144. )
  1145. def __send_ws_download(self, content: str, name: str, on_action: str) -> None:
  1146. self.__send_ws(
  1147. {"type": _WsType.DOWNLOAD_FILE.value, "content": content, "name": name, "onAction": on_action},
  1148. send_back_only=True,
  1149. )
  1150. def __send_ws_alert(self, type: str, message: str, system_notification: bool, duration: int) -> None:
  1151. self.__send_ws(
  1152. {
  1153. "type": _WsType.ALERT.value,
  1154. "atype": type,
  1155. "message": message,
  1156. "system": system_notification,
  1157. "duration": duration,
  1158. }
  1159. )
  1160. def __send_ws_partial(self, partial: str):
  1161. self.__send_ws(
  1162. {
  1163. "type": _WsType.PARTIAL.value,
  1164. "name": partial,
  1165. }
  1166. )
  1167. def __send_ws_block(
  1168. self,
  1169. action: t.Optional[str] = None,
  1170. message: t.Optional[str] = None,
  1171. close: t.Optional[bool] = False,
  1172. cancel: t.Optional[bool] = False,
  1173. ):
  1174. self.__send_ws(
  1175. {
  1176. "type": _WsType.BLOCK.value,
  1177. "action": action,
  1178. "close": close,
  1179. "message": message,
  1180. "noCancel": not cancel,
  1181. }
  1182. )
  1183. def __send_ws_navigate(
  1184. self,
  1185. to: str,
  1186. params: t.Optional[t.Dict[str, str]],
  1187. tab: t.Optional[str],
  1188. force: bool,
  1189. ):
  1190. self.__send_ws({"type": _WsType.NAVIGATE.value, "to": to, "params": params, "tab": tab, "force": force})
  1191. def __send_ws_update_with_dict(self, modified_values: dict) -> None:
  1192. payload = [
  1193. {"name": _get_client_var_name(k), "payload": v if isinstance(v, dict) and "value" in v else {"value": v}}
  1194. for k, v in modified_values.items()
  1195. ]
  1196. if self._is_broadcasting():
  1197. self.__broadcast_ws({"type": _WsType.MULTIPLE_UPDATE.value, "payload": payload})
  1198. self._set_broadcast(False)
  1199. else:
  1200. self.__send_ws({"type": _WsType.MULTIPLE_UPDATE.value, "payload": payload})
  1201. def __send_ws_broadcast(
  1202. self,
  1203. var_name: str,
  1204. var_value: t.Any,
  1205. client_id: t.Optional[str] = None,
  1206. message_type: t.Optional[_WsType] = None,
  1207. ):
  1208. self.__broadcast_ws(
  1209. {
  1210. "type": _WsType.UPDATE.value if message_type is None else message_type.value,
  1211. "name": _get_broadcast_var_name(var_name),
  1212. "payload": {"value": var_value},
  1213. },
  1214. client_id,
  1215. )
  1216. def __get_ws_receiver(self, send_back_only=False) -> t.Union[t.List[str], t.Any, None]:
  1217. if self._bindings()._is_single_client():
  1218. return None
  1219. sid = getattr(request, "sid", None) if request else None
  1220. sids = self.__get_sids(self._get_client_id())
  1221. if sid:
  1222. sids.add(sid)
  1223. if send_back_only:
  1224. return sid
  1225. return list(sids)
  1226. def __get_sids(self, client_id: str) -> t.Set[str]:
  1227. return self.__client_id_2_sid.get(client_id, set())
  1228. def __get_message_grouping(self):
  1229. return (
  1230. _getscopeattr(self, Gui.__MESSAGE_GROUPING_NAME)
  1231. if _hasscopeattr(self, Gui.__MESSAGE_GROUPING_NAME)
  1232. else None
  1233. )
  1234. def __enter__(self):
  1235. self.__hold_messages()
  1236. return self
  1237. def __exit__(self, exc_type, exc_value, traceback):
  1238. try:
  1239. self.__send_messages()
  1240. except Exception as e: # pragma: no cover
  1241. _warn("Exception raised while sending messages", e)
  1242. if exc_value: # pragma: no cover
  1243. _warn(f"An {exc_type or 'Exception'} was raised", exc_value)
  1244. return True
  1245. def __hold_messages(self):
  1246. grouping_message = self.__get_message_grouping()
  1247. if grouping_message is None:
  1248. self._bind_var_val(Gui.__MESSAGE_GROUPING_NAME, [])
  1249. def __send_messages(self):
  1250. grouping_message = self.__get_message_grouping()
  1251. if grouping_message is not None:
  1252. _delscopeattr(self, Gui.__MESSAGE_GROUPING_NAME)
  1253. if len(grouping_message):
  1254. self.__send_ws({"type": _WsType.MULTIPLE_MESSAGE.value, "payload": grouping_message})
  1255. def _get_user_function(self, func_name: str) -> t.Union[t.Callable, str]:
  1256. func = _getscopeattr(self, func_name, None)
  1257. if not callable(func):
  1258. func = self._get_locals_bind().get(func_name)
  1259. if not callable(func):
  1260. func = self.__locals_context.get_default().get(func_name)
  1261. return func if callable(func) else func_name
  1262. def _get_user_instance(self, class_name: str, class_type: type) -> t.Union[object, str]:
  1263. cls = _getscopeattr(self, class_name, None)
  1264. if not isinstance(cls, class_type):
  1265. cls = self._get_locals_bind().get(class_name)
  1266. if not isinstance(cls, class_type):
  1267. cls = self.__locals_context.get_default().get(class_name)
  1268. return cls if isinstance(cls, class_type) else class_name
  1269. def __download_csv(self, state: State, var_name: str, payload: dict):
  1270. holder_name = t.cast(str, payload.get("var_name"))
  1271. ret = self._accessors._get_data(
  1272. self,
  1273. holder_name,
  1274. _getscopeattr(self, holder_name, None),
  1275. {"alldata": True, "csv": True},
  1276. )
  1277. if isinstance(ret, dict):
  1278. df = ret.get("df")
  1279. try:
  1280. fd, temp_path = mkstemp(".csv", var_name, text=True)
  1281. with os.fdopen(fd, "wt", newline="") as csv_file:
  1282. df.to_csv(csv_file, index=False) # type:ignore
  1283. self._download(temp_path, "data.csv", Gui.__DOWNLOAD_DELETE_ACTION)
  1284. except Exception as e: # pragma: no cover
  1285. if not self._call_on_exception("download_csv", e):
  1286. _warn("download_csv(): Exception raised", e)
  1287. def __delete_csv(self, state: State, var_name: str, payload: dict):
  1288. try:
  1289. (Path(tempfile.gettempdir()) / t.cast(str, payload.get("args", [])[-1]).split("/")[-1]).unlink(True)
  1290. except Exception:
  1291. pass
  1292. def __on_action(self, id: t.Optional[str], payload: t.Any) -> None:
  1293. if isinstance(payload, dict):
  1294. action = payload.get("action")
  1295. else:
  1296. action = str(payload)
  1297. payload = {"action": action}
  1298. if action:
  1299. action_fn: t.Union[t.Callable, str]
  1300. if Gui.__DOWNLOAD_ACTION == action:
  1301. action_fn = self.__download_csv
  1302. payload["var_name"] = id
  1303. elif Gui.__DOWNLOAD_DELETE_ACTION == action:
  1304. action_fn = self.__delete_csv
  1305. else:
  1306. action_fn = self._get_user_function(action)
  1307. if self.__call_function_with_args(action_function=action_fn, id=id, payload=payload):
  1308. return
  1309. else: # pragma: no cover
  1310. _warn(f"on_action(): '{action}' is not a valid function.")
  1311. if hasattr(self, "on_action"):
  1312. self.__call_function_with_args(action_function=self.on_action, id=id, payload=payload)
  1313. def __call_function_with_args(self, **kwargs):
  1314. action_function = kwargs.get("action_function")
  1315. id = kwargs.get("id")
  1316. payload = kwargs.get("payload")
  1317. if callable(action_function):
  1318. try:
  1319. argcount = action_function.__code__.co_argcount
  1320. if argcount > 0 and inspect.ismethod(action_function):
  1321. argcount -= 1
  1322. args = [None for _ in range(argcount)]
  1323. if argcount > 0:
  1324. args[0] = self.__get_state()
  1325. if argcount > 1:
  1326. try:
  1327. args[1] = self._get_real_var_name(id)[0]
  1328. except Exception:
  1329. args[1] = id
  1330. if argcount > 2:
  1331. args[2] = payload
  1332. action_function(*args)
  1333. return True
  1334. except Exception as e: # pragma: no cover
  1335. if not self._call_on_exception(action_function.__name__, e):
  1336. _warn(f"on_action(): Exception raised in '{action_function.__name__}()'", e)
  1337. return False
  1338. def _call_function_with_state(self, user_function: t.Callable, args: t.List[t.Any]) -> t.Any:
  1339. args.insert(0, self.__get_state())
  1340. argcount = user_function.__code__.co_argcount
  1341. if argcount > 0 and inspect.ismethod(user_function):
  1342. argcount -= 1
  1343. if argcount > len(args):
  1344. args += (argcount - len(args)) * [None]
  1345. else:
  1346. args = args[:argcount]
  1347. return user_function(*args)
  1348. def _set_module_context(self, module_context: t.Optional[str]) -> t.ContextManager[None]:
  1349. return self._set_locals_context(module_context) if module_context is not None else contextlib.nullcontext()
  1350. def _call_user_callback(
  1351. self,
  1352. state_id: t.Optional[str],
  1353. user_callback: t.Union[t.Callable, str],
  1354. args: t.List[t.Any],
  1355. module_context: t.Optional[str],
  1356. ) -> t.Any:
  1357. try:
  1358. with self.get_flask_app().app_context():
  1359. self.__set_client_id_in_context(state_id)
  1360. with self._set_module_context(module_context):
  1361. if not callable(user_callback):
  1362. user_callback = self._get_user_function(user_callback)
  1363. if not callable(user_callback):
  1364. _warn(f"invoke_callback(): {user_callback} is not callable.")
  1365. return None
  1366. return self._call_function_with_state(user_callback, args)
  1367. except Exception as e: # pragma: no cover
  1368. if not self._call_on_exception(user_callback.__name__ if callable(user_callback) else user_callback, e):
  1369. _warn(
  1370. "invoke_callback(): Exception raised in "
  1371. + f"'{user_callback.__name__ if callable(user_callback) else user_callback}()'",
  1372. e,
  1373. )
  1374. return None
  1375. def _call_broadcast_callback(
  1376. self, user_callback: t.Callable, args: t.List[t.Any], module_context: t.Optional[str]
  1377. ) -> t.Any:
  1378. @contextlib.contextmanager
  1379. def _broadcast_callback() -> t.Iterator[None]:
  1380. try:
  1381. setattr(g, Gui.__BRDCST_CALLBACK_G_ID, True)
  1382. yield
  1383. finally:
  1384. setattr(g, Gui.__BRDCST_CALLBACK_G_ID, False)
  1385. with _broadcast_callback():
  1386. # Use global scopes for broadcast callbacks
  1387. return self._call_user_callback(_DataScopes._GLOBAL_ID, user_callback, args, module_context)
  1388. def _is_in_brdcst_callback(self):
  1389. try:
  1390. return getattr(g, Gui.__BRDCST_CALLBACK_G_ID, False)
  1391. except RuntimeError:
  1392. return False
  1393. # Proxy methods for Evaluator
  1394. def _evaluate_expr(self, expr: str, lazy_declare: t.Optional[bool] = False) -> t.Any:
  1395. return self.__evaluator.evaluate_expr(self, expr, lazy_declare)
  1396. def _re_evaluate_expr(self, var_name: str) -> t.Set[str]:
  1397. return self.__evaluator.re_evaluate_expr(self, var_name)
  1398. def _refresh_expr(self, var_name: str, holder: t.Optional[_TaipyBase]):
  1399. return self.__evaluator.refresh_expr(self, var_name, holder)
  1400. def _get_expr_from_hash(self, hash_val: str) -> str:
  1401. return self.__evaluator.get_expr_from_hash(hash_val)
  1402. def _evaluate_bind_holder(self, holder: t.Type[_TaipyBase], expr: str) -> str:
  1403. return self.__evaluator.evaluate_bind_holder(self, holder, expr)
  1404. def _evaluate_holders(self, expr: str) -> t.List[str]:
  1405. return self.__evaluator.evaluate_holders(self, expr)
  1406. def _is_expression(self, expr: str) -> bool:
  1407. if self.__evaluator is None:
  1408. return False
  1409. return self.__evaluator._is_expression(expr)
  1410. # make components resettable
  1411. def _set_building(self, building: bool):
  1412. self._building = building
  1413. def __is_building(self):
  1414. return hasattr(self, "_building") and self._building
  1415. def _get_call_method_name(self, name: str):
  1416. return f"{Gui.__SELF_VAR}.{name}"
  1417. def __get_attributes(self, attr_json: str, hash_json: str, args_dict: t.Dict[str, t.Any]):
  1418. attributes: t.Dict[str, t.Any] = json.loads(unquote(attr_json))
  1419. hashes: t.Dict[str, str] = json.loads(unquote(hash_json))
  1420. attributes.update({k: args_dict.get(v) for k, v in hashes.items()})
  1421. return attributes, hashes
  1422. def _compare_data(self, *data):
  1423. return data[0]
  1424. def _get_adapted_lov(self, lov: list, var_type: str):
  1425. return self.__adapter._get_adapted_lov(lov, var_type)
  1426. def _tbl_cols(
  1427. self, rebuild: bool, rebuild_val: t.Optional[bool], attr_json: str, hash_json: str, **kwargs
  1428. ) -> t.Union[str, _DoNotUpdate]:
  1429. if not self.__is_building():
  1430. try:
  1431. rebuild = rebuild_val if rebuild_val is not None else rebuild
  1432. if rebuild:
  1433. attributes, hashes = self.__get_attributes(attr_json, hash_json, kwargs)
  1434. data_hash = hashes.get("data", "")
  1435. data = kwargs.get(data_hash)
  1436. col_dict = _get_columns_dict(
  1437. data,
  1438. attributes.get("columns", {}),
  1439. self._accessors._get_col_types(data_hash, _TaipyData(data, data_hash)),
  1440. attributes.get("date_format"),
  1441. attributes.get("number_format"),
  1442. )
  1443. _enhance_columns(attributes, hashes, col_dict, "table(cols)")
  1444. return json.dumps(col_dict, cls=_TaipyJsonEncoder)
  1445. except Exception as e: # pragma: no cover
  1446. _warn("Exception while rebuilding table columns", e)
  1447. return Gui.__DO_NOT_UPDATE_VALUE
  1448. def _chart_conf(
  1449. self, rebuild: bool, rebuild_val: t.Optional[bool], attr_json: str, hash_json: str, **kwargs
  1450. ) -> t.Union[str, _DoNotUpdate]:
  1451. if not self.__is_building():
  1452. try:
  1453. rebuild = rebuild_val if rebuild_val is not None else rebuild
  1454. if rebuild:
  1455. attributes, hashes = self.__get_attributes(attr_json, hash_json, kwargs)
  1456. data_hash = hashes.get("data", "")
  1457. config = _build_chart_config(
  1458. self,
  1459. attributes,
  1460. self._accessors._get_col_types(data_hash, _TaipyData(kwargs.get(data_hash), data_hash)),
  1461. )
  1462. return json.dumps(config, cls=_TaipyJsonEncoder)
  1463. except Exception as e: # pragma: no cover
  1464. _warn("Exception while rebuilding chart config", e)
  1465. return Gui.__DO_NOT_UPDATE_VALUE
  1466. # Proxy methods for Adapter
  1467. def _add_adapter_for_type(self, type_name: str, adapter: t.Callable) -> None:
  1468. self.__adapter._add_for_type(type_name, adapter)
  1469. def _add_type_for_var(self, var_name: str, type_name: str) -> None:
  1470. self.__adapter._add_type_for_var(var_name, type_name)
  1471. def _get_adapter_for_type(self, type_name: str) -> t.Optional[t.Callable]:
  1472. return self.__adapter._get_for_type(type_name)
  1473. def _get_unique_type_adapter(self, type_name: str) -> str:
  1474. return self.__adapter._get_unique_type(type_name)
  1475. def _run_adapter(
  1476. self, adapter: t.Optional[t.Callable], value: t.Any, var_name: str, id_only=False
  1477. ) -> t.Union[t.Tuple[str, ...], str, None]:
  1478. return self.__adapter._run(adapter, value, var_name, id_only)
  1479. def _get_valid_adapter_result(self, value: t.Any, id_only=False) -> t.Union[t.Tuple[str, ...], str, None]:
  1480. return self.__adapter._get_valid_result(value, id_only)
  1481. def _is_ui_blocked(self):
  1482. return _getscopeattr(self, Gui.__UI_BLOCK_NAME, False)
  1483. def __get_on_cancel_block_ui(self, callback: t.Optional[str]):
  1484. def _taipy_on_cancel_block_ui(guiApp, id: t.Optional[str], payload: t.Any):
  1485. if _hasscopeattr(guiApp, Gui.__UI_BLOCK_NAME):
  1486. _setscopeattr(guiApp, Gui.__UI_BLOCK_NAME, False)
  1487. guiApp.__on_action(id, {"action": callback})
  1488. return _taipy_on_cancel_block_ui
  1489. def __add_pages_in_folder(self, folder_name: str, folder_path: str):
  1490. from ._renderers import Html, Markdown
  1491. list_of_files = os.listdir(folder_path)
  1492. for file_name in list_of_files:
  1493. if file_name.startswith("__"):
  1494. continue
  1495. if (re_match := Gui.__RE_HTML.match(file_name)) and f"{re_match.group(1)}.py" not in list_of_files:
  1496. _renderers = Html(os.path.join(folder_path, file_name), frame=None)
  1497. _renderers.modify_taipy_base_url(folder_name)
  1498. self.add_page(name=f"{folder_name}/{re_match.group(1)}", page=_renderers)
  1499. elif (re_match := Gui.__RE_MD.match(file_name)) and f"{re_match.group(1)}.py" not in list_of_files:
  1500. _renderers_md = Markdown(os.path.join(folder_path, file_name), frame=None)
  1501. self.add_page(name=f"{folder_name}/{re_match.group(1)}", page=_renderers_md)
  1502. elif re_match := Gui.__RE_PY.match(file_name):
  1503. module_name = re_match.group(1)
  1504. module_path = os.path.join(folder_name, module_name).replace(os.path.sep, ".")
  1505. try:
  1506. module = importlib.import_module(module_path)
  1507. page_instance = _get_page_from_module(module)
  1508. if page_instance is not None:
  1509. self.add_page(name=f"{folder_name}/{module_name}", page=page_instance)
  1510. except Exception as e:
  1511. _warn(f"Error while importing module '{module_path}'", e)
  1512. elif os.path.isdir(child_dir_path := os.path.join(folder_path, file_name)):
  1513. child_dir_name = f"{folder_name}/{file_name}"
  1514. self.__add_pages_in_folder(child_dir_name, child_dir_path)
  1515. # Proxy methods for LocalsContext
  1516. def _get_locals_bind(self) -> t.Dict[str, t.Any]:
  1517. return self.__locals_context.get_locals()
  1518. def _get_default_locals_bind(self) -> t.Dict[str, t.Any]:
  1519. return self.__locals_context.get_default()
  1520. def _get_locals_bind_from_context(self, context: t.Optional[str]) -> t.Dict[str, t.Any]:
  1521. return self.__locals_context._get_locals_bind_from_context(context)
  1522. def _get_locals_context(self) -> str:
  1523. current_context = self.__locals_context.get_context()
  1524. return current_context if current_context is not None else self.__default_module_name
  1525. def _set_locals_context(self, context: t.Optional[str]) -> t.ContextManager[None]:
  1526. return self.__locals_context.set_locals_context(context)
  1527. def _has_set_context(self):
  1528. return self.__locals_context.get_context() is not None
  1529. def _get_page_context(self, page_name: str) -> str | None:
  1530. if page_name not in self._config.routes:
  1531. return None
  1532. page = None
  1533. for p in self._config.pages:
  1534. if p._route == page_name:
  1535. page = p
  1536. if page is None:
  1537. return None
  1538. return (
  1539. (page._renderer._get_module_name() or self.__default_module_name)
  1540. if page._renderer is not None
  1541. else self.__default_module_name
  1542. )
  1543. @staticmethod
  1544. def _get_root_page_name():
  1545. return Gui.__root_page_name
  1546. def _set_flask(self, flask: Flask):
  1547. self._flask = flask
  1548. def _get_default_module_name(self):
  1549. return self.__default_module_name
  1550. @staticmethod
  1551. def _get_timezone() -> str:
  1552. return Gui.__LOCAL_TZ
  1553. @staticmethod
  1554. def _set_timezone(tz: str):
  1555. Gui.__LOCAL_TZ = tz
  1556. # Public methods
  1557. def add_page(
  1558. self,
  1559. name: str,
  1560. page: t.Union[str, Page],
  1561. style: t.Optional[str] = "",
  1562. ) -> None:
  1563. """Add a page to the Graphical User Interface.
  1564. Arguments:
  1565. name: The name of the page.
  1566. page (Union[str, Page^]): The content of the page.<br/>
  1567. It can be an instance of `Markdown^` or `Html^`.<br/>
  1568. If *page* is a string, then:
  1569. - If *page* is set to the pathname of a readable file, the page
  1570. content is read as Markdown input text.
  1571. - If it is not, the page content is read from this string as
  1572. Markdown text.
  1573. style (Optional[str]): Additional CSS style to apply to this page.
  1574. - if there is style associated with a page, it is used at a global level
  1575. - if there is no style associated with the page, the style is cleared at a global level
  1576. - if the page is embedded in a block control, the style is ignored
  1577. Note that page names cannot start with the slash ('/') character and that each
  1578. page must have a unique name.
  1579. """
  1580. # Validate name
  1581. if name is None: # pragma: no cover
  1582. raise Exception("name is required for add_page() function.")
  1583. if not Gui.__RE_PAGE_NAME.match(name): # pragma: no cover
  1584. raise SyntaxError(
  1585. f'Page name "{name}" is invalid. It must only contain letters, digits, dash (-), underscore (_), and forward slash (/) characters.' # noqa: E501
  1586. )
  1587. if name.startswith("/"): # pragma: no cover
  1588. raise SyntaxError(f'Page name "{name}" cannot start with forward slash (/) character.')
  1589. if name in self._config.routes: # pragma: no cover
  1590. raise Exception(f'Page name "{name if name != Gui.__root_page_name else "/"}" is already defined.')
  1591. if isinstance(page, str):
  1592. from ._renderers import Markdown
  1593. page = Markdown(page, frame=None)
  1594. elif not isinstance(page, Page): # pragma: no cover
  1595. raise Exception(
  1596. f'Parameter "page" is invalid for page name "{name if name != Gui.__root_page_name else "/"}.'
  1597. )
  1598. # Init a new page
  1599. new_page = _Page()
  1600. new_page._route = name
  1601. new_page._renderer = page
  1602. new_page._style = style
  1603. # Append page to _config
  1604. self._config.pages.append(new_page)
  1605. self._config.routes.append(name)
  1606. # set root page
  1607. if name == Gui.__root_page_name:
  1608. self._config.root_page = new_page
  1609. # Update locals context
  1610. self.__locals_context.add(page._get_module_name(), page._get_locals())
  1611. # Update variable directory
  1612. if not page._is_class_module():
  1613. self.__var_dir.add_frame(page._frame)
  1614. # Special case needed for page to access gui to trigger reload in notebook
  1615. if _is_in_notebook():
  1616. page._notebook_gui = self
  1617. page._notebook_page = new_page
  1618. def add_pages(self, pages: t.Optional[t.Union[t.Mapping[str, t.Union[str, Page]], str]] = None) -> None:
  1619. """Add several pages to the Graphical User Interface.
  1620. Arguments:
  1621. pages (Union[dict[str, Union[str, Page^]], str]): The pages to add.<br/>
  1622. If *pages* is a dictionary, a page is added to this `Gui` instance
  1623. for each of the entries in *pages*:
  1624. - The entry key is used as the page name.
  1625. - The entry value is used as the page content:
  1626. - The value can can be an instance of `Markdown^` or `Html^`, then
  1627. it is used as the page definition.
  1628. - If entry value is a string, then:
  1629. - If it is set to the pathname of a readable file, the page
  1630. content is read as Markdown input text.
  1631. - If it is not, the page content is read from this string as
  1632. Markdown text.
  1633. !!! note "Reading pages from a directory"
  1634. If *pages* is a string that holds the path to a readable directory, then
  1635. this directory is traversed, recursively, to find files that Taipy can build
  1636. pages from.
  1637. For every new directory that is traversed, a new hierarchical level
  1638. for pages is created.
  1639. For every file that is found:
  1640. - If the filename extension is *.md*, it is read as Markdown content and
  1641. a new page is created with the base name of this filename.
  1642. - If the filename extension is *.html*, it is read as HTML content and
  1643. a new page is created with the base name of this filename.
  1644. For example, say you have the following directory structure:
  1645. ```
  1646. reports
  1647. ├── home.html
  1648. ├── budget/
  1649. │ ├── expenses/
  1650. │ │ ├── marketing.md
  1651. │ │ └── production.md
  1652. │ └── revenue/
  1653. │ ├── EMAE.md
  1654. │ ├── USA.md
  1655. │ └── ASIA.md
  1656. └── cashflow/
  1657. ├── weekly.md
  1658. ├── monthly.md
  1659. └── yearly.md
  1660. ```
  1661. Calling `gui.add_pages('reports')` is equivalent to calling:
  1662. ```py
  1663. gui.add_pages({
  1664. "reports/home", Html("reports/home.html"),
  1665. "reports/budget/expenses/marketing", Markdown("reports/budget/expenses/marketing.md"),
  1666. "reports/budget/expenses/production", Markdown("reports/budget/expenses/production.md"),
  1667. "reports/budget/revenue/EMAE", Markdown("reports/budget/revenue/EMAE.md"),
  1668. "reports/budget/revenue/USA", Markdown("reports/budget/revenue/USA.md"),
  1669. "reports/budget/revenue/ASIA", Markdown("reports/budget/revenue/ASIA.md"),
  1670. "reports/cashflow/weekly", Markdown("reports/cashflow/weekly.md"),
  1671. "reports/cashflow/monthly", Markdown("reports/cashflow/monthly.md"),
  1672. "reports/cashflow/yearly", Markdown("reports/cashflow/yearly.md")
  1673. })
  1674. ```
  1675. """
  1676. if isinstance(pages, dict):
  1677. for k, v in pages.items():
  1678. if k == "/":
  1679. k = Gui.__root_page_name
  1680. self.add_page(name=k, page=v)
  1681. elif isinstance(folder_name := pages, str):
  1682. if not hasattr(self, "_root_dir"):
  1683. self._root_dir = os.path.dirname(inspect.getabsfile(self.__frame))
  1684. folder_path = folder_name if os.path.isabs(folder_name) else os.path.join(self._root_dir, folder_name)
  1685. folder_name = os.path.basename(folder_path)
  1686. if not os.path.isdir(folder_path): # pragma: no cover
  1687. raise RuntimeError(f"Path {folder_path} is not a valid directory")
  1688. if folder_name in self.__directory_name_of_pages: # pragma: no cover
  1689. raise Exception(f"Base directory name {folder_name} of path {folder_path} is not unique")
  1690. if folder_name in Gui.__reserved_routes: # pragma: no cover
  1691. raise Exception(f"Invalid directory. Directory {folder_name} is a reserved route")
  1692. self.__directory_name_of_pages.append(folder_name)
  1693. self.__add_pages_in_folder(folder_name, folder_path)
  1694. # partials
  1695. def add_partial(
  1696. self,
  1697. page: t.Union[str, Page],
  1698. ) -> Partial:
  1699. """Create a new `Partial^`.
  1700. The [User Manual section on Partials](../gui/pages/index.md#partials) gives details on
  1701. when and how to use this class.
  1702. Arguments:
  1703. page (Union[str, Page^]): The page to create a new Partial from.<br/>
  1704. It can be an instance of `Markdown^` or `Html^`.<br/>
  1705. If *page* is a string, then:
  1706. - If *page* is set to the pathname of a readable file, the content of
  1707. the new `Partial` is read as Markdown input text.
  1708. - If it is not, the content of the new `Partial` is read from this string
  1709. as Markdown text.
  1710. Returns:
  1711. The new `Partial` object defined by *page*.
  1712. """
  1713. new_partial = Partial()
  1714. # Validate name
  1715. if (
  1716. new_partial._route in self._config.partial_routes or new_partial._route in self._config.routes
  1717. ): # pragma: no cover
  1718. _warn(f'Partial name "{new_partial._route}" is already defined.')
  1719. if isinstance(page, str):
  1720. from ._renderers import Markdown
  1721. page = Markdown(page, frame=None)
  1722. elif not isinstance(page, Page): # pragma: no cover
  1723. raise Exception(f'Partial name "{new_partial._route}" has an invalid Page.')
  1724. new_partial._renderer = page
  1725. # Append partial to _config
  1726. self._config.partials.append(new_partial)
  1727. self._config.partial_routes.append(str(new_partial._route))
  1728. # Update locals context
  1729. self.__locals_context.add(page._get_module_name(), page._get_locals())
  1730. # Update variable directory
  1731. self.__var_dir.add_frame(page._frame)
  1732. return new_partial
  1733. def _update_partial(self, partial: Partial):
  1734. partials = _getscopeattr(self, Partial._PARTIALS, {})
  1735. partials[partial._route] = partial
  1736. _setscopeattr(self, Partial._PARTIALS, partials)
  1737. self.__send_ws_partial(str(partial._route))
  1738. def _get_partial(self, route: str) -> t.Optional[Partial]:
  1739. partials = _getscopeattr(self, Partial._PARTIALS, {})
  1740. partial = partials.get(route)
  1741. if partial is None:
  1742. partial = next((p for p in self._config.partials if p._route == route), None)
  1743. return partial
  1744. # Main binding method (bind in markdown declaration)
  1745. def _bind_var(self, var_name: str) -> str:
  1746. bind_context = None
  1747. if var_name in self._get_locals_bind().keys():
  1748. bind_context = self._get_locals_context()
  1749. if bind_context is None:
  1750. encoded_var_name = self.__var_dir.add_var(var_name, self._get_locals_context(), var_name)
  1751. else:
  1752. encoded_var_name = self.__var_dir.add_var(var_name, bind_context)
  1753. if not hasattr(self._bindings(), encoded_var_name):
  1754. bind_locals = self._get_locals_bind_from_context(bind_context)
  1755. if var_name in bind_locals.keys():
  1756. self._bind(encoded_var_name, bind_locals[var_name])
  1757. else:
  1758. _warn(
  1759. f"Variable '{var_name}' is not available in either the '{self._get_locals_context()}' or '__main__' modules." # noqa: E501
  1760. )
  1761. return encoded_var_name
  1762. def _bind_var_val(self, var_name: str, value: t.Any) -> bool:
  1763. if _MODULE_ID not in var_name:
  1764. var_name = self.__var_dir.add_var(var_name, self._get_locals_context())
  1765. if not hasattr(self._bindings(), var_name):
  1766. self._bind(var_name, value)
  1767. return True
  1768. return False
  1769. def __bind_local_func(self, name: str):
  1770. func = getattr(self, name, None)
  1771. if func is not None and not callable(func): # pragma: no cover
  1772. _warn(f"{self.__class__.__name__}.{name}: {func} should be a function; looking for {name} in the script.")
  1773. func = None
  1774. if func is None:
  1775. func = self._get_locals_bind().get(name)
  1776. if func is not None:
  1777. if callable(func):
  1778. setattr(self, name, func)
  1779. else: # pragma: no cover
  1780. _warn(f"{name}: {func} should be a function.")
  1781. def load_config(self, config: Config) -> None:
  1782. self._config._load(config)
  1783. def _broadcast(
  1784. self,
  1785. name: str,
  1786. value: t.Any,
  1787. client_id: t.Optional[str] = None,
  1788. message_type: t.Optional[_WsType] = None,
  1789. ):
  1790. """NOT DOCUMENTED
  1791. Send the new value of a variable to all connected clients.
  1792. Arguments:
  1793. name: The name of the variable to update or create.
  1794. value: The value (must be serializable to the JSON format).
  1795. client_id: The client id (broadcast to all client if None)
  1796. """
  1797. self.__send_ws_broadcast(name, value, client_id, message_type)
  1798. def _broadcast_all_clients(self, name: str, value: t.Any):
  1799. try:
  1800. self._set_broadcast()
  1801. self._update_var(name, value)
  1802. finally:
  1803. self._set_broadcast(False)
  1804. def _set_broadcast(self, broadcast: bool = True):
  1805. with contextlib.suppress(RuntimeError):
  1806. setattr(g, Gui.__BROADCAST_G_ID, broadcast)
  1807. def _is_broadcasting(self) -> bool:
  1808. try:
  1809. return getattr(g, Gui.__BROADCAST_G_ID, False)
  1810. except RuntimeError:
  1811. return False
  1812. def _download(
  1813. self, content: t.Any, name: t.Optional[str] = "", on_action: t.Optional[t.Union[str, t.Callable]] = ""
  1814. ):
  1815. if callable(on_action) and on_action.__name__:
  1816. on_action_name = (
  1817. _get_expr_var_name(str(on_action.__code__))
  1818. if on_action.__name__ == "<lambda>"
  1819. else _get_expr_var_name(on_action.__name__)
  1820. )
  1821. if on_action_name:
  1822. self._bind_var_val(on_action_name, on_action)
  1823. on_action = on_action_name
  1824. else:
  1825. _warn("download() on_action is invalid.")
  1826. content_str = self._get_content("Gui.download", content, False)
  1827. self.__send_ws_download(content_str, str(name), str(on_action) if on_action is not None else "")
  1828. def _notify(
  1829. self,
  1830. notification_type: str = "I",
  1831. message: str = "",
  1832. system_notification: t.Optional[bool] = None,
  1833. duration: t.Optional[int] = None,
  1834. ):
  1835. self.__send_ws_alert(
  1836. notification_type,
  1837. message,
  1838. self._get_config("system_notification", False) if system_notification is None else system_notification,
  1839. self._get_config("notification_duration", 3000) if duration is None else duration,
  1840. )
  1841. def _hold_actions(
  1842. self,
  1843. callback: t.Optional[t.Union[str, t.Callable]] = None,
  1844. message: t.Optional[str] = "Work in Progress...",
  1845. ): # pragma: no cover
  1846. action_name = callback.__name__ if callable(callback) else callback
  1847. # TODO: what if lambda? (it does work)
  1848. func = self.__get_on_cancel_block_ui(action_name)
  1849. def_action_name = func.__name__
  1850. _setscopeattr(self, def_action_name, func)
  1851. if _hasscopeattr(self, Gui.__UI_BLOCK_NAME):
  1852. _setscopeattr(self, Gui.__UI_BLOCK_NAME, True)
  1853. else:
  1854. self._bind(Gui.__UI_BLOCK_NAME, True)
  1855. self.__send_ws_block(action=def_action_name, message=message, cancel=bool(action_name))
  1856. def _resume_actions(self): # pragma: no cover
  1857. if _hasscopeattr(self, Gui.__UI_BLOCK_NAME):
  1858. _setscopeattr(self, Gui.__UI_BLOCK_NAME, False)
  1859. self.__send_ws_block(close=True)
  1860. def _navigate(
  1861. self,
  1862. to: t.Optional[str] = "",
  1863. params: t.Optional[t.Dict[str, str]] = None,
  1864. tab: t.Optional[str] = None,
  1865. force: t.Optional[bool] = False,
  1866. ):
  1867. to = to or Gui.__root_page_name
  1868. if not to.startswith("/") and to not in self._config.routes and not urlparse(to).netloc:
  1869. _warn(f'Cannot navigate to "{to if to != Gui.__root_page_name else "/"}": unknown page.')
  1870. return False
  1871. self.__send_ws_navigate(to if to != Gui.__root_page_name else "/", params, tab, force or False)
  1872. return True
  1873. def __init_libs(self):
  1874. for name, libs in self.__extensions.items():
  1875. for lib in libs:
  1876. if not isinstance(lib, ElementLibrary):
  1877. continue
  1878. try:
  1879. self._call_function_with_state(lib.on_user_init, [])
  1880. except Exception as e: # pragma: no cover
  1881. if not self._call_on_exception(f"{name}.on_user_init", e):
  1882. _warn(f"Exception raised in {name}.on_user_init()", e)
  1883. def __init_route(self):
  1884. self.__set_client_id_in_context(force=True)
  1885. if not _hasscopeattr(self, Gui.__ON_INIT_NAME):
  1886. _setscopeattr(self, Gui.__ON_INIT_NAME, True)
  1887. self.__pre_render_pages()
  1888. self.__init_libs()
  1889. if hasattr(self, "on_init") and callable(self.on_init):
  1890. try:
  1891. self._call_function_with_state(self.on_init, [])
  1892. except Exception as e: # pragma: no cover
  1893. if not self._call_on_exception("on_init", e):
  1894. _warn("Exception raised in on_init()", e)
  1895. return self._render_route()
  1896. def _call_on_exception(self, function_name: str, exception: Exception) -> bool:
  1897. if hasattr(self, "on_exception") and callable(self.on_exception):
  1898. try:
  1899. self.on_exception(self.__get_state(), function_name, exception)
  1900. except Exception as e: # pragma: no cover
  1901. _warn("Exception raised in on_exception()", e)
  1902. return True
  1903. return False
  1904. def __call_on_status(self) -> t.Optional[str]:
  1905. if hasattr(self, "on_status") and callable(self.on_status):
  1906. try:
  1907. return self.on_status(self.__get_state())
  1908. except Exception as e: # pragma: no cover
  1909. if not self._call_on_exception("on_status", e):
  1910. _warn("Exception raised in on_status", e)
  1911. return None
  1912. def __pre_render_pages(self) -> None:
  1913. """Pre-render all pages to have a proper initialization of all variables"""
  1914. self.__set_client_id_in_context()
  1915. scope_metadata = self._get_data_scope_metadata()
  1916. if scope_metadata[_DataScopes._META_PRE_RENDER]:
  1917. return
  1918. for page in self._config.pages:
  1919. if page is not None:
  1920. with contextlib.suppress(Exception):
  1921. if isinstance(page._renderer, CustomPage):
  1922. self._bind_custom_page_variables(page._renderer, self._get_client_id())
  1923. else:
  1924. page.render(self, silent=True)
  1925. scope_metadata[_DataScopes._META_PRE_RENDER] = True
  1926. def _get_navigated_page(self, page_name: str) -> t.Any:
  1927. nav_page = page_name
  1928. if hasattr(self, "on_navigate") and callable(self.on_navigate):
  1929. try:
  1930. if self.on_navigate.__code__.co_argcount == 2:
  1931. nav_page = self.on_navigate(self.__get_state(), page_name)
  1932. else:
  1933. params = request.args.to_dict() if hasattr(request, "args") else {}
  1934. params.pop("client_id", None)
  1935. params.pop("v", None)
  1936. nav_page = self.on_navigate(self.__get_state(), page_name, params)
  1937. if nav_page != page_name:
  1938. if isinstance(nav_page, str):
  1939. if self._navigate(nav_page):
  1940. return ("Root page cannot be re-routed by on_navigate().", 302)
  1941. else:
  1942. _warn(f"on_navigate() returned an invalid page name '{nav_page}'.")
  1943. nav_page = page_name
  1944. except Exception as e: # pragma: no cover
  1945. if not self._call_on_exception("on_navigate", e):
  1946. _warn("Exception raised in on_navigate()", e)
  1947. return nav_page
  1948. def _get_page(self, page_name: str):
  1949. return next((page_i for page_i in self._config.pages if page_i._route == page_name), None)
  1950. def _bind_custom_page_variables(self, page: CustomPage, client_id: t.Optional[str]):
  1951. """Handle the bindings of custom page variables"""
  1952. if not isinstance(page, CustomPage):
  1953. return
  1954. with self.get_flask_app().app_context() if has_app_context() else contextlib.nullcontext(): # type: ignore[attr-defined]
  1955. self.__set_client_id_in_context(client_id)
  1956. with self._set_locals_context(page._get_module_name()):
  1957. for k in self._get_locals_bind().keys():
  1958. if (not page._binding_variables or k in page._binding_variables) and not k.startswith("_"):
  1959. self._bind_var(k)
  1960. def __render_page(self, page_name: str) -> t.Any:
  1961. self.__set_client_id_in_context()
  1962. nav_page = self._get_navigated_page(page_name)
  1963. if not isinstance(nav_page, str):
  1964. return nav_page
  1965. page = self._get_page(nav_page)
  1966. # Try partials
  1967. if page is None:
  1968. page = self._get_partial(nav_page)
  1969. # Make sure that there is a page instance found
  1970. if page is None:
  1971. return (
  1972. jsonify({"error": f"Page '{nav_page}' doesn't exist."}),
  1973. 400,
  1974. {"Content-Type": "application/json; charset=utf-8"},
  1975. )
  1976. # Handle custom pages
  1977. if (pr := page._renderer) is not None and isinstance(pr, CustomPage):
  1978. if self._navigate(
  1979. to=page_name,
  1980. params={
  1981. _Server._RESOURCE_HANDLER_ARG: pr._resource_handler.get_id(),
  1982. },
  1983. ):
  1984. # Proactively handle the bindings of custom page variables
  1985. self._bind_custom_page_variables(pr, self._get_client_id())
  1986. return ("Successfully redirect to custom resource handler", 200)
  1987. return ("Failed to navigate to custom resource handler", 500)
  1988. # Handle page rendering
  1989. context = page.render(self)
  1990. if (
  1991. nav_page == Gui.__root_page_name
  1992. and page._rendered_jsx is not None
  1993. and "<PageContent" not in page._rendered_jsx
  1994. ):
  1995. page._rendered_jsx += "<PageContent />"
  1996. # Return jsx page
  1997. if page._rendered_jsx is not None:
  1998. return self._server._render(
  1999. page._rendered_jsx, page._style if page._style is not None else "", page._head, context
  2000. )
  2001. else:
  2002. return ("No page template", 404)
  2003. def _render_route(self) -> t.Any:
  2004. return self._server._direct_render_json(
  2005. {
  2006. "locations": {
  2007. "/" if route == Gui.__root_page_name else f"/{route}": f"/{route}" for route in self._config.routes
  2008. },
  2009. "blockUI": self._is_ui_blocked(),
  2010. }
  2011. )
  2012. def _register_data_accessor(self, data_accessor_class: t.Type[_DataAccessor]) -> None:
  2013. self._accessors._register(data_accessor_class)
  2014. def get_flask_app(self) -> Flask:
  2015. """Get the internal Flask application.
  2016. This method must be called **after** `(Gui.)run()^` was invoked.
  2017. Returns:
  2018. The Flask instance used.
  2019. """
  2020. if hasattr(self, "_server"):
  2021. return self._server.get_flask()
  2022. raise RuntimeError("get_flask_app() cannot be invoked before run() has been called.")
  2023. def _set_frame(self, frame: t.Optional[FrameType]):
  2024. if not isinstance(frame, FrameType): # pragma: no cover
  2025. raise RuntimeError("frame must be a FrameType where Gui can collect the local variables.")
  2026. self.__frame = frame
  2027. self.__default_module_name = _get_module_name_from_frame(self.__frame)
  2028. def _set_css_file(self, css_file: t.Optional[str] = None):
  2029. if css_file is None:
  2030. script_file = Path(self.__frame.f_code.co_filename or ".").resolve()
  2031. if script_file.with_suffix(".css").exists():
  2032. css_file = f"{script_file.stem}.css"
  2033. elif script_file.is_dir() and (script_file / "taipy.css").exists():
  2034. css_file = "taipy.css"
  2035. self.__css_file = css_file
  2036. def _set_state(self, state: State):
  2037. if isinstance(state, State):
  2038. self.__state = state
  2039. def _get_webapp_path(self):
  2040. _conf_webapp_path = (
  2041. Path(self._get_config("webapp_path", None)) if self._get_config("webapp_path", None) else None
  2042. )
  2043. _webapp_path = str((Path(__file__).parent / "webapp").resolve())
  2044. if _conf_webapp_path:
  2045. if _conf_webapp_path.is_dir():
  2046. _webapp_path = str(_conf_webapp_path.resolve())
  2047. _warn(f"Using webapp_path: '{_conf_webapp_path}'.")
  2048. else: # pragma: no cover
  2049. _warn(
  2050. f"webapp_path: '{_conf_webapp_path}' is not a valid directory. Falling back to '{_webapp_path}'." # noqa: E501
  2051. )
  2052. return _webapp_path
  2053. def __get_client_config(self) -> t.Dict[str, t.Any]:
  2054. config = {
  2055. "timeZone": self._config.get_time_zone(),
  2056. "darkMode": self._get_config("dark_mode", True),
  2057. "baseURL": self._config._get_config("base_url", "/"),
  2058. }
  2059. if themes := self._get_themes():
  2060. config["themes"] = themes
  2061. if len(self.__extensions):
  2062. config["extensions"] = {}
  2063. for libs in self.__extensions.values():
  2064. for lib in libs:
  2065. config["extensions"][f"./{Gui._EXTENSION_ROOT}/{lib.get_js_module_name()}"] = [ # type: ignore
  2066. e._get_js_name(n)
  2067. for n, e in lib.get_elements().items()
  2068. if isinstance(e, Element) and not e._is_server_only()
  2069. ]
  2070. if stylekit := self._get_config("stylekit", _default_stylekit):
  2071. config["stylekit"] = {_to_camel_case(k): v for k, v in stylekit.items()}
  2072. return config
  2073. def __get_css_vars(self) -> str:
  2074. css_vars = []
  2075. if stylekit := self._get_config("stylekit", _default_stylekit):
  2076. for k, v in stylekit.items():
  2077. css_vars.append(f'--{k.replace("_", "-")}:{_get_css_var_value(v)};')
  2078. return " ".join(css_vars)
  2079. def __init_server(self):
  2080. app_config = self._config.config
  2081. # Init server if there is no server
  2082. if not hasattr(self, "_server"):
  2083. self._server = _Server(
  2084. self,
  2085. path_mapping=self._path_mapping,
  2086. flask=self._flask,
  2087. async_mode=app_config["async_mode"],
  2088. allow_upgrades=not app_config["notebook_proxy"],
  2089. server_config=app_config.get("server_config"),
  2090. )
  2091. # Stop and reinitialize the server if it is still running as a thread
  2092. if (_is_in_notebook() or app_config["run_in_thread"]) and hasattr(self._server, "_thread"):
  2093. self.stop()
  2094. self._flask_blueprint = []
  2095. self._server = _Server(
  2096. self,
  2097. path_mapping=self._path_mapping,
  2098. flask=self._flask,
  2099. async_mode=app_config["async_mode"],
  2100. allow_upgrades=not app_config["notebook_proxy"],
  2101. server_config=app_config.get("server_config"),
  2102. )
  2103. self._bindings()._new_scopes()
  2104. def __init_ngrok(self):
  2105. app_config = self._config.config
  2106. if hasattr(self, "_ngrok"):
  2107. # Keep the ngrok instance if token has not changed
  2108. if app_config["ngrok_token"] == self._ngrok[1]:
  2109. _TaipyLogger._get_logger().info(f" * NGROK Public Url: {self._ngrok[0].public_url}")
  2110. return
  2111. # Close the old tunnel so new tunnel can open for new token
  2112. ngrok.disconnect(self._ngrok[0].public_url)
  2113. if app_config["run_server"] and (token := app_config["ngrok_token"]): # pragma: no cover
  2114. if not util.find_spec("pyngrok"):
  2115. raise RuntimeError("Cannot use ngrok as pyngrok package is not installed.")
  2116. ngrok.set_auth_token(token)
  2117. self._ngrok = (ngrok.connect(app_config["port"], "http"), token)
  2118. _TaipyLogger._get_logger().info(f" * NGROK Public Url: {self._ngrok[0].public_url}")
  2119. def __bind_default_function(self):
  2120. with self.get_flask_app().app_context():
  2121. self.__var_dir.process_imported_var()
  2122. # bind on_* function if available
  2123. self.__bind_local_func("on_init")
  2124. self.__bind_local_func("on_change")
  2125. self.__bind_local_func("on_action")
  2126. self.__bind_local_func("on_navigate")
  2127. self.__bind_local_func("on_exception")
  2128. self.__bind_local_func("on_status")
  2129. self.__bind_local_func("on_user_content")
  2130. def __register_blueprint(self):
  2131. # add en empty main page if it is not defined
  2132. if Gui.__root_page_name not in self._config.routes:
  2133. new_page = _Page()
  2134. new_page._route = Gui.__root_page_name
  2135. new_page._renderer = _EmptyPage()
  2136. self._config.pages.append(new_page)
  2137. self._config.routes.append(Gui.__root_page_name)
  2138. pages_bp = Blueprint("taipy_pages", __name__)
  2139. self._flask_blueprint.append(pages_bp)
  2140. # server URL Rule for taipy images
  2141. images_bp = Blueprint("taipy_images", __name__)
  2142. images_bp.add_url_rule(f"/{Gui.__CONTENT_ROOT}/<path:path>", view_func=self.__serve_content)
  2143. self._flask_blueprint.append(images_bp)
  2144. # server URL for uploaded files
  2145. upload_bp = Blueprint("taipy_upload", __name__)
  2146. upload_bp.add_url_rule(f"/{Gui.__UPLOAD_URL}", view_func=self.__upload_files, methods=["POST"])
  2147. self._flask_blueprint.append(upload_bp)
  2148. # server URL for user content
  2149. user_content_bp = Blueprint("taipy_user_content", __name__)
  2150. user_content_bp.add_url_rule(f"/{Gui.__USER_CONTENT_URL}/<path:path>", view_func=self.__serve_user_content)
  2151. self._flask_blueprint.append(user_content_bp)
  2152. # server URL for extension resources
  2153. extension_bp = Blueprint("taipy_extensions", __name__)
  2154. extension_bp.add_url_rule(f"/{Gui._EXTENSION_ROOT}/<path:path>", view_func=self.__serve_extension)
  2155. scripts = [
  2156. s if bool(urlparse(s).netloc) else f"{Gui._EXTENSION_ROOT}/{name}/{s}{lib.get_query(s)}"
  2157. for name, libs in Gui.__extensions.items()
  2158. for lib in libs
  2159. for s in (lib.get_scripts() or [])
  2160. ]
  2161. styles = [
  2162. s if bool(urlparse(s).netloc) else f"{Gui._EXTENSION_ROOT}/{name}/{s}{lib.get_query(s)}"
  2163. for name, libs in Gui.__extensions.items()
  2164. for lib in libs
  2165. for s in (lib.get_styles() or [])
  2166. ]
  2167. if self._get_config("stylekit", True):
  2168. styles.append("stylekit/stylekit.css")
  2169. else:
  2170. styles.append(Gui.__ROBOTO_FONT)
  2171. if self.__css_file:
  2172. styles.append(f"{self.__css_file}")
  2173. self._flask_blueprint.append(extension_bp)
  2174. _webapp_path = self._get_webapp_path()
  2175. self._flask_blueprint.append(
  2176. self._server._get_default_blueprint(
  2177. static_folder=_webapp_path,
  2178. template_folder=_webapp_path,
  2179. title=self._get_config("title", "Taipy App"),
  2180. favicon=self._get_config("favicon", Gui.__DEFAULT_FAVICON_URL),
  2181. root_margin=self._get_config("margin", None),
  2182. scripts=scripts,
  2183. styles=styles,
  2184. version=self.__get_version(),
  2185. client_config=self.__get_client_config(),
  2186. watermark=self._get_config("watermark", None),
  2187. css_vars=self.__get_css_vars(),
  2188. base_url=self._get_config("base_url", "/"),
  2189. )
  2190. )
  2191. # Run parse markdown to force variables binding at runtime
  2192. pages_bp.add_url_rule(f"/{Gui.__JSX_URL}/<path:page_name>", view_func=self.__render_page)
  2193. # server URL Rule for flask rendered react-router
  2194. pages_bp.add_url_rule(f"/{Gui.__INIT_URL}", view_func=self.__init_route)
  2195. # Register Flask Blueprint if available
  2196. for bp in self._flask_blueprint:
  2197. self._server.get_flask().register_blueprint(bp)
  2198. def run(
  2199. self,
  2200. run_server: bool = True,
  2201. run_in_thread: bool = False,
  2202. async_mode: str = "gevent",
  2203. **kwargs,
  2204. ) -> t.Optional[Flask]:
  2205. """Start the server that delivers pages to web clients.
  2206. Once you enter `run()`, users can run web browsers and point to the web server
  2207. URL that `Gui` serves. The default is to listen to the *localhost* address
  2208. (127.0.0.1) on the port number 5000. However, the configuration of this `Gui`
  2209. object may impact that (see the
  2210. [Configuration](../gui/configuration.md#configuring-the-gui-instance)
  2211. section of the User Manual for details).
  2212. Arguments:
  2213. run_server (bool): Whether or not to run a web server locally.
  2214. If set to *False*, a web server is *not* created and started.
  2215. run_in_thread (bool): Whether or not to run a web server in a separated thread.
  2216. If set to *True*, the web server runs is a separated thread.<br/>
  2217. Note that if you are running in an IPython notebook context, the web
  2218. server always runs in a separate thread.
  2219. async_mode (str): The asynchronous model to use for the Flask-SocketIO.
  2220. Valid values are:<br/>
  2221. - "gevent": Use a [gevent](https://www.gevent.org/servers.html) server.
  2222. - "threading": Use the Flask Development Server. This allows the application to use
  2223. the Flask reloader (the *use_reloader* option) and Debug mode (the *debug* option).
  2224. - "eventlet": Use an [*eventlet*](https://flask.palletsprojects.com/en/2.2.x/deploying/eventlet/)
  2225. event-driven WSGI server.
  2226. The default value is "gevent"<br/>
  2227. Note that only the "threading" value provides support for the development reloader
  2228. functionality (*use_reloader* option). Any other value makes the *use_reloader* configuration parameter
  2229. ignored.<br/>
  2230. Also note that setting the *debug* argument to True forces *async_mode* to "threading".
  2231. **kwargs (dict[str, any]): Additional keyword arguments that configure how this `Gui` is run.
  2232. Please refer to the
  2233. [Configuration section](../gui/configuration.md#configuring-the-gui-instance)
  2234. of the User Manual for more information.
  2235. Returns:
  2236. The Flask instance if *run_server* is False else None.
  2237. """
  2238. # --------------------------------------------------------------------------------
  2239. # The ssl_context argument was removed just after 1.1. It was defined as:
  2240. # t.Optional[t.Union[ssl.SSLContext, t.Tuple[str, t.Optional[str]], t.Literal["adhoc"]]] = None
  2241. #
  2242. # With the doc:
  2243. # ssl_context (Optional[Union[ssl.SSLContext, Tuple[str, Optional[str]], t.Literal['adhoc']]]):
  2244. # Configures TLS to serve over HTTPS. This value can be:
  2245. #
  2246. # - An `ssl.SSLContext` object
  2247. # - A `(cert_file, key_file)` tuple to create a typical context
  2248. # - The string "adhoc" to generate a temporary self-signed certificate.
  2249. #
  2250. # The default value is None.
  2251. # --------------------------------------------------------------------------------
  2252. app_config = self._config.config
  2253. run_root_dir = os.path.dirname(inspect.getabsfile(self.__frame))
  2254. # Register _root_dir for abs path
  2255. if not hasattr(self, "_root_dir"):
  2256. self._root_dir = run_root_dir
  2257. is_reloading = kwargs.pop("_reload", False)
  2258. if not is_reloading:
  2259. self.__run_kwargs = kwargs = {
  2260. **kwargs,
  2261. "run_server": run_server,
  2262. "run_in_thread": run_in_thread,
  2263. "async_mode": async_mode,
  2264. }
  2265. # Load application config from multiple sources (env files, kwargs, command line)
  2266. self._config._build_config(run_root_dir, self.__env_filename, kwargs)
  2267. self._config.resolve()
  2268. TaipyGuiWarning.set_debug_mode(self._get_config("debug", False))
  2269. self.__init_server()
  2270. self.__init_ngrok()
  2271. locals_bind = _filter_locals(self.__frame.f_locals)
  2272. self.__locals_context.set_default(locals_bind, self.__default_module_name)
  2273. self.__var_dir.set_default(self.__frame)
  2274. if self.__state is None or is_reloading:
  2275. self.__state = State(self, self.__locals_context.get_all_keys(), self.__locals_context.get_all_context())
  2276. if _is_in_notebook():
  2277. # Allow gui.state.x in notebook mode
  2278. self.state = self.__state
  2279. self.__bind_default_function()
  2280. # Base global ctx is TaipyHolder classes + script modules and callables
  2281. glob_ctx: t.Dict[str, t.Any] = {t.__name__: t for t in _TaipyBase.__subclasses__()}
  2282. glob_ctx.update({k: v for k, v in locals_bind.items() if inspect.ismodule(v) or callable(v)})
  2283. glob_ctx[Gui.__SELF_VAR] = self
  2284. # Call on_init on each library
  2285. for name, libs in self.__extensions.items():
  2286. for lib in libs:
  2287. if not isinstance(lib, ElementLibrary):
  2288. continue
  2289. try:
  2290. lib_context = lib.on_init(self)
  2291. if (
  2292. isinstance(lib_context, tuple)
  2293. and len(lib_context) > 1
  2294. and isinstance(lib_context[0], str)
  2295. and lib_context[0].isidentifier()
  2296. ):
  2297. if lib_context[0] in glob_ctx:
  2298. _warn(f"Method {name}.on_init() returned a name already defined '{lib_context[0]}'.")
  2299. else:
  2300. glob_ctx[lib_context[0]] = lib_context[1]
  2301. elif lib_context:
  2302. _warn(
  2303. f"Method {name}.on_init() should return a Tuple[str, Any] where the first element must be a valid Python identifier." # noqa: E501
  2304. )
  2305. except Exception as e: # pragma: no cover
  2306. if not self._call_on_exception(f"{name}.on_init", e):
  2307. _warn(f"Method {name}.on_init() raised an exception", e)
  2308. # Initiate the Evaluator with the right context
  2309. self.__evaluator = _Evaluator(glob_ctx, self.__shared_variables)
  2310. self.__register_blueprint()
  2311. # Register data accessor communication data format (JSON, Apache Arrow)
  2312. self._accessors._set_data_format(_DataFormat.APACHE_ARROW if app_config["use_arrow"] else _DataFormat.JSON)
  2313. # Use multi user or not
  2314. self._bindings()._set_single_client(bool(app_config["single_client"]))
  2315. # Start Flask Server
  2316. if not run_server:
  2317. return self.get_flask_app()
  2318. return self._server.run(
  2319. host=app_config["host"],
  2320. port=app_config["port"],
  2321. debug=app_config["debug"],
  2322. use_reloader=app_config["use_reloader"],
  2323. flask_log=app_config["flask_log"],
  2324. run_in_thread=app_config["run_in_thread"],
  2325. allow_unsafe_werkzeug=app_config["allow_unsafe_werkzeug"],
  2326. notebook_proxy=app_config["notebook_proxy"],
  2327. )
  2328. def reload(self): # pragma: no cover
  2329. """Reload the web server.
  2330. This function reloads the underlying web server only in the situation where
  2331. it was run in a separated thread: the *run_in_thread* parameter to the
  2332. `(Gui.)run^` method was set to True, or you are running in an IPython notebook
  2333. context.
  2334. """
  2335. if hasattr(self, "_server") and hasattr(self._server, "_thread") and self._server._is_running:
  2336. self._server.stop_thread()
  2337. self.run(**self.__run_kwargs, _reload=True)
  2338. _TaipyLogger._get_logger().info("Gui server has been reloaded.")
  2339. def stop(self):
  2340. """Stop the web server.
  2341. This function stops the underlying web server only in the situation where
  2342. it was run in a separated thread: the *run_in_thread* parameter to the
  2343. `(Gui.)run()^` method was set to True, or you are running in an IPython notebook
  2344. context.
  2345. """
  2346. if hasattr(self, "_server") and hasattr(self._server, "_thread") and self._server._is_running:
  2347. self._server.stop_thread()
  2348. _TaipyLogger._get_logger().info("Gui server has been stopped.")
  2349. def _get_autorization(self, client_id: t.Optional[str] = None, system: t.Optional[bool] = False):
  2350. return contextlib.nullcontext()
  2351. def set_favicon(self, favicon_path: t.Union[str, Path], state: t.Optional[State] = None):
  2352. """Change the favicon for all clients.
  2353. This function dynamically changes the favicon of Taipy GUI pages for all connected client.
  2354. favicon_path can be an URL (relative or not) or a file path.
  2355. TODO The *favicon* parameter to `(Gui.)run()^` can also be used to change
  2356. the favicon when the application starts.
  2357. """
  2358. if state or self.__favicon != favicon_path:
  2359. if not state:
  2360. self.__favicon = favicon_path
  2361. url = self._get_content("__taipy_favicon", favicon_path, True)
  2362. self._broadcast(
  2363. "taipy_favicon", url, self._get_client_id() if state else None, message_type=_WsType.FAVICON
  2364. )