gui.py 143 KB

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