_context.py 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853
  1. # Copyright 2021-2024 Avaiga Private Limited
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
  4. # the License. You may obtain a copy of the License at
  5. #
  6. # http://www.apache.org/licenses/LICENSE-2.0
  7. #
  8. # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
  9. # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
  10. # specific language governing permissions and limitations under the License.
  11. import json
  12. import typing as t
  13. from collections import defaultdict
  14. from numbers import Number
  15. from threading import Lock
  16. try:
  17. import zoneinfo
  18. except ImportError:
  19. from backports import zoneinfo # type: ignore[no-redef]
  20. import pandas as pd
  21. from dateutil import parser
  22. from taipy.config import Config
  23. from taipy.core import (
  24. Cycle,
  25. DataNode,
  26. DataNodeId,
  27. Job,
  28. Scenario,
  29. ScenarioId,
  30. Sequence,
  31. SequenceId,
  32. Submission,
  33. SubmissionId,
  34. cancel_job,
  35. create_scenario,
  36. delete_job,
  37. get_cycles_scenarios,
  38. get_data_nodes,
  39. get_jobs,
  40. is_deletable,
  41. is_editable,
  42. is_promotable,
  43. is_readable,
  44. is_submittable,
  45. set_primary,
  46. )
  47. from taipy.core import delete as core_delete
  48. from taipy.core import get as core_get
  49. from taipy.core import submit as core_submit
  50. from taipy.core.data._abstract_tabular import _AbstractTabularDataNode
  51. from taipy.core.notification import CoreEventConsumerBase, EventEntityType
  52. from taipy.core.notification.event import Event, EventOperation
  53. from taipy.core.notification.notifier import Notifier
  54. from taipy.core.submission.submission_status import SubmissionStatus
  55. from taipy.gui import Gui, State
  56. from taipy.gui._warnings import _warn
  57. from taipy.gui.gui import _DoNotUpdate
  58. from ._adapters import _EntityType
  59. class _GuiCoreContext(CoreEventConsumerBase):
  60. __PROP_ENTITY_ID = "id"
  61. __PROP_ENTITY_COMMENT = "comment"
  62. __PROP_CONFIG_ID = "config"
  63. __PROP_DATE = "date"
  64. __PROP_ENTITY_NAME = "name"
  65. __PROP_SCENARIO_PRIMARY = "primary"
  66. __PROP_SCENARIO_TAGS = "tags"
  67. __ENTITY_PROPS = (__PROP_CONFIG_ID, __PROP_DATE, __PROP_ENTITY_NAME)
  68. __ACTION = "action"
  69. _CORE_CHANGED_NAME = "core_changed"
  70. _SCENARIO_SELECTOR_ERROR_VAR = "gui_core_sc_error"
  71. _SCENARIO_SELECTOR_ID_VAR = "gui_core_sc_id"
  72. _SCENARIO_VIZ_ERROR_VAR = "gui_core_sv_error"
  73. _JOB_SELECTOR_ERROR_VAR = "gui_core_js_error"
  74. _DATANODE_VIZ_ERROR_VAR = "gui_core_dv_error"
  75. _DATANODE_VIZ_OWNER_ID_VAR = "gui_core_dv_owner_id"
  76. _DATANODE_VIZ_HISTORY_ID_VAR = "gui_core_dv_history_id"
  77. _DATANODE_VIZ_DATA_ID_VAR = "gui_core_dv_data_id"
  78. _DATANODE_VIZ_DATA_CHART_ID_VAR = "gui_core_dv_data_chart_id"
  79. _DATANODE_VIZ_DATA_NODE_PROP = "data_node"
  80. _DATANODE_SEL_SCENARIO_PROP = "scenario"
  81. def __init__(self, gui: Gui) -> None:
  82. self.gui = gui
  83. self.scenario_by_cycle: t.Optional[t.Dict[t.Optional[Cycle], t.List[Scenario]]] = None
  84. self.data_nodes_by_owner: t.Optional[t.Dict[t.Optional[str], DataNode]] = None
  85. self.scenario_configs: t.Optional[t.List[t.Tuple[str, str]]] = None
  86. self.jobs_list: t.Optional[t.List[Job]] = None
  87. self.client_submission: t.Dict[str, SubmissionStatus] = dict()
  88. # register to taipy core notification
  89. reg_id, reg_queue = Notifier.register()
  90. # locks
  91. self.lock = Lock()
  92. self.submissions_lock = Lock()
  93. # super
  94. super().__init__(reg_id, reg_queue)
  95. self.start()
  96. def process_event(self, event: Event):
  97. if event.entity_type == EventEntityType.SCENARIO:
  98. self.scenario_refresh(
  99. event.entity_id
  100. if event.operation != EventOperation.DELETION and is_readable(t.cast(ScenarioId, event.entity_id))
  101. else None
  102. )
  103. elif event.entity_type == EventEntityType.SEQUENCE and event.entity_id:
  104. sequence = None
  105. try:
  106. sequence = (
  107. core_get(event.entity_id)
  108. if event.operation != EventOperation.DELETION and is_readable(t.cast(SequenceId, event.entity_id))
  109. else None
  110. )
  111. if sequence and hasattr(sequence, "parent_ids") and sequence.parent_ids: # type: ignore
  112. self.gui._broadcast(
  113. _GuiCoreContext._CORE_CHANGED_NAME,
  114. {"scenario": [x for x in sequence.parent_ids]}, # type: ignore
  115. )
  116. except Exception as e:
  117. _warn(f"Access to sequence {event.entity_id} failed", e)
  118. elif event.entity_type == EventEntityType.JOB:
  119. with self.lock:
  120. self.jobs_list = None
  121. if event.operation == EventOperation.UPDATE:
  122. try:
  123. job_entity = t.cast(Job, core_get(str(event.entity_id)))
  124. self.gui._broadcast(
  125. _GuiCoreContext._CORE_CHANGED_NAME,
  126. {"task": {"id": job_entity.task.id, "status": job_entity.status.name}},
  127. )
  128. except Exception as e:
  129. _warn(f"Access to sequence {event.entity_id} failed", e)
  130. elif event.entity_type == EventEntityType.SUBMISSION:
  131. self.scenario_status_callback(event.entity_id)
  132. elif event.entity_type == EventEntityType.DATA_NODE:
  133. with self.lock:
  134. self.data_nodes_by_owner = None
  135. self.gui._broadcast(
  136. _GuiCoreContext._CORE_CHANGED_NAME,
  137. {"datanode": event.entity_id if event.operation != EventOperation.DELETION else True},
  138. )
  139. def scenario_refresh(self, scenario_id: t.Optional[str]):
  140. with self.lock:
  141. self.scenario_by_cycle = None
  142. self.data_nodes_by_owner = None
  143. self.gui._broadcast(
  144. _GuiCoreContext._CORE_CHANGED_NAME,
  145. {"scenario": scenario_id or True},
  146. )
  147. def scenario_status_callback(self, submission_id: t.Optional[str]):
  148. if not submission_id or not is_readable(t.cast(SubmissionId, submission_id)):
  149. return
  150. try:
  151. last_status = self.client_submission.get(submission_id)
  152. if not last_status:
  153. return
  154. submission = t.cast(Submission, core_get(submission_id))
  155. if not submission or not submission.entity_id:
  156. return
  157. entity = core_get(submission.entity_id)
  158. if not entity:
  159. return
  160. new_status = submission.submission_status
  161. if last_status != new_status:
  162. # callback
  163. submission_name = submission.properties.get("on_submission")
  164. if not submission_name:
  165. return
  166. submission_fn = self.gui._get_user_function(submission_name)
  167. if not callable(submission_fn):
  168. return
  169. self.gui._call_user_callback(
  170. submission.properties.get("client_id"),
  171. submission_fn,
  172. [entity, {"submission_status": new_status.name}],
  173. submission.properties.get("module_context"),
  174. )
  175. with self.submissions_lock:
  176. if new_status in (
  177. SubmissionStatus.COMPLETED,
  178. SubmissionStatus.FAILED,
  179. SubmissionStatus.CANCELED,
  180. ):
  181. self.client_submission.pop(submission_id, None)
  182. else:
  183. self.client_submission[submission_id] = new_status
  184. except Exception as e:
  185. _warn(f"Submission ({submission_id}) is not available", e)
  186. finally:
  187. self.gui._broadcast(_GuiCoreContext._CORE_CHANGED_NAME, {"jobs": True})
  188. def scenario_adapter(self, scenario_or_cycle):
  189. try:
  190. if (
  191. hasattr(scenario_or_cycle, "id")
  192. and is_readable(scenario_or_cycle.id)
  193. and core_get(scenario_or_cycle.id) is not None
  194. ):
  195. if self.scenario_by_cycle and isinstance(scenario_or_cycle, Cycle):
  196. return (
  197. scenario_or_cycle.id,
  198. scenario_or_cycle.get_simple_label(),
  199. self.scenario_by_cycle.get(scenario_or_cycle),
  200. _EntityType.CYCLE.value,
  201. False,
  202. )
  203. elif isinstance(scenario_or_cycle, Scenario):
  204. return (
  205. scenario_or_cycle.id,
  206. scenario_or_cycle.get_simple_label(),
  207. None,
  208. _EntityType.SCENARIO.value,
  209. scenario_or_cycle.is_primary,
  210. )
  211. except Exception as e:
  212. _warn(
  213. f"Access to {type(scenario_or_cycle)} "
  214. + f"({scenario_or_cycle.id if hasattr(scenario_or_cycle, 'id') else 'No_id'})"
  215. + " failed",
  216. e,
  217. )
  218. return None
  219. def get_scenarios(self):
  220. cycles_scenarios = []
  221. with self.lock:
  222. if self.scenario_by_cycle is None:
  223. self.scenario_by_cycle = get_cycles_scenarios()
  224. for cycle, scenarios in self.scenario_by_cycle.items():
  225. if cycle is None:
  226. cycles_scenarios.extend(scenarios)
  227. else:
  228. cycles_scenarios.append(cycle)
  229. return cycles_scenarios
  230. def select_scenario(self, state: State, id: str, payload: t.Dict[str, str]):
  231. args = payload.get("args")
  232. if args is None or not isinstance(args, list) or len(args) == 0:
  233. return
  234. state.assign(_GuiCoreContext._SCENARIO_SELECTOR_ID_VAR, args[0])
  235. def get_scenario_by_id(self, id: str) -> t.Optional[Scenario]:
  236. if not id or not is_readable(t.cast(ScenarioId, id)):
  237. return None
  238. try:
  239. return core_get(t.cast(ScenarioId, id))
  240. except Exception:
  241. return None
  242. def get_scenario_configs(self):
  243. with self.lock:
  244. if self.scenario_configs is None:
  245. configs = Config.scenarios
  246. if isinstance(configs, dict):
  247. self.scenario_configs = [(id, f"{c.id}") for id, c in configs.items() if id != "default"]
  248. return self.scenario_configs
  249. def crud_scenario(self, state: State, id: str, payload: t.Dict[str, str]): # noqa: C901
  250. args = payload.get("args")
  251. if (
  252. args is None
  253. or not isinstance(args, list)
  254. or len(args) < 4
  255. or not isinstance(args[1], bool)
  256. or not isinstance(args[2], bool)
  257. or not isinstance(args[3], dict)
  258. ):
  259. return
  260. update = args[1]
  261. delete = args[2]
  262. data = args[3]
  263. with_dialog = True if len(args) < 5 else bool(args[4])
  264. scenario = None
  265. name = data.get(_GuiCoreContext.__PROP_ENTITY_NAME)
  266. if update:
  267. scenario_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
  268. if delete:
  269. if not is_deletable(scenario_id):
  270. state.assign(
  271. _GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, f"Scenario. {scenario_id} is not deletable."
  272. )
  273. return
  274. try:
  275. core_delete(scenario_id)
  276. except Exception as e:
  277. state.assign(_GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, f"Error deleting Scenario. {e}")
  278. else:
  279. if not self.__check_readable_editable(
  280. state, scenario_id, "Scenario", _GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR
  281. ):
  282. return
  283. scenario = core_get(scenario_id)
  284. else:
  285. if with_dialog:
  286. config_id = data.get(_GuiCoreContext.__PROP_CONFIG_ID)
  287. scenario_config = Config.scenarios.get(config_id)
  288. if with_dialog and scenario_config is None:
  289. state.assign(
  290. _GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, f"Invalid configuration id ({config_id})"
  291. )
  292. return
  293. date_str = data.get(_GuiCoreContext.__PROP_DATE)
  294. try:
  295. date = parser.parse(date_str) if isinstance(date_str, str) else None
  296. except Exception as e:
  297. state.assign(_GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, f"Invalid date ({date_str}).{e}")
  298. return
  299. else:
  300. scenario_config = None
  301. date = None
  302. scenario_id = None
  303. try:
  304. gui: Gui = state._gui
  305. on_creation = args[0] if isinstance(args[0], str) else None
  306. on_creation_function = gui._get_user_function(on_creation) if on_creation else None
  307. if callable(on_creation_function):
  308. try:
  309. res = gui._call_function_with_state(
  310. on_creation_function,
  311. [
  312. id,
  313. {
  314. "action": on_creation,
  315. "config": scenario_config,
  316. "date": date,
  317. "label": name,
  318. "properties": {
  319. v.get("key"): v.get("value") for v in data.get("properties", dict())
  320. },
  321. },
  322. ],
  323. )
  324. if isinstance(res, Scenario):
  325. # everything's fine
  326. scenario_id = res.id
  327. state.assign(_GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, "")
  328. return
  329. if res:
  330. # do not create
  331. state.assign(_GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, f"{res}")
  332. return
  333. except Exception as e: # pragma: no cover
  334. if not gui._call_on_exception(on_creation, e):
  335. _warn(f"on_creation(): Exception raised in '{on_creation}()'", e)
  336. state.assign(
  337. _GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR,
  338. f"Error creating Scenario with '{on_creation}()'. {e}",
  339. )
  340. return
  341. elif on_creation is not None:
  342. _warn(f"on_creation(): '{on_creation}' is not a function.")
  343. elif not with_dialog:
  344. if len(Config.scenarios) == 2:
  345. scenario_config = [sc for k, sc in Config.scenarios.items() if k != "default"][0]
  346. else:
  347. state.assign(
  348. _GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR,
  349. "Error creating Scenario: only one scenario config needed "
  350. + f"({len(Config.scenarios) - 1}) found.",
  351. )
  352. return
  353. scenario = create_scenario(scenario_config, date, name)
  354. scenario_id = scenario.id
  355. except Exception as e:
  356. state.assign(_GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, f"Error creating Scenario. {e}")
  357. finally:
  358. self.scenario_refresh(scenario_id)
  359. if scenario:
  360. if not is_editable(scenario):
  361. state.assign(
  362. _GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, f"Scenario {scenario_id or name} is not editable."
  363. )
  364. return
  365. with scenario as sc:
  366. sc.properties[_GuiCoreContext.__PROP_ENTITY_NAME] = name
  367. if props := data.get("properties"):
  368. try:
  369. new_keys = [prop.get("key") for prop in props]
  370. for key in t.cast(dict, sc.properties).keys():
  371. if key and key not in _GuiCoreContext.__ENTITY_PROPS and key not in new_keys:
  372. t.cast(dict, sc.properties).pop(key, None)
  373. for prop in props:
  374. key = prop.get("key")
  375. if key and key not in _GuiCoreContext.__ENTITY_PROPS:
  376. sc._properties[key] = prop.get("value")
  377. state.assign(_GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, "")
  378. except Exception as e:
  379. state.assign(_GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, f"Error creating Scenario. {e}")
  380. def edit_entity(self, state: State, id: str, payload: t.Dict[str, str]):
  381. args = payload.get("args")
  382. if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict):
  383. return
  384. data = args[0]
  385. entity_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
  386. if not self.__check_readable_editable(
  387. state, entity_id, data.get("type", "Scenario"), _GuiCoreContext._SCENARIO_VIZ_ERROR_VAR
  388. ):
  389. return
  390. entity: t.Union[Scenario, Sequence] = core_get(entity_id)
  391. if entity:
  392. try:
  393. if isinstance(entity, Scenario):
  394. primary = data.get(_GuiCoreContext.__PROP_SCENARIO_PRIMARY)
  395. if primary is True:
  396. if not is_promotable(entity):
  397. state.assign(
  398. _GuiCoreContext._SCENARIO_VIZ_ERROR_VAR, f"Scenario {entity_id} is not promotable."
  399. )
  400. return
  401. set_primary(entity)
  402. self.__edit_properties(entity, data)
  403. state.assign(_GuiCoreContext._SCENARIO_VIZ_ERROR_VAR, "")
  404. except Exception as e:
  405. state.assign(_GuiCoreContext._SCENARIO_VIZ_ERROR_VAR, f"Error updating Scenario. {e}")
  406. def submit_entity(self, state: State, id: str, payload: t.Dict[str, str]):
  407. args = payload.get("args")
  408. if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict):
  409. return
  410. data = args[0]
  411. entity_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
  412. if not is_submittable(entity_id):
  413. state.assign(
  414. _GuiCoreContext._SCENARIO_VIZ_ERROR_VAR,
  415. f"{data.get('type', 'Scenario')} {entity_id} is not submittable.",
  416. )
  417. return
  418. entity = core_get(entity_id)
  419. if entity:
  420. try:
  421. on_submission = data.get("on_submission_change")
  422. submission_entity = core_submit(
  423. entity,
  424. on_submission=on_submission,
  425. client_id=self.gui._get_client_id(),
  426. module_context=self.gui._get_locals_context(),
  427. )
  428. if on_submission:
  429. with self.submissions_lock:
  430. self.client_submission[submission_entity.id] = submission_entity.submission_status
  431. if Config.core.mode == "development":
  432. self.client_submission[submission_entity.id] = SubmissionStatus.SUBMITTED
  433. self.scenario_status_callback(submission_entity.id)
  434. state.assign(_GuiCoreContext._SCENARIO_VIZ_ERROR_VAR, "")
  435. except Exception as e:
  436. state.assign(_GuiCoreContext._SCENARIO_VIZ_ERROR_VAR, f"Error submitting entity. {e}")
  437. def __do_datanodes_tree(self):
  438. if self.data_nodes_by_owner is None:
  439. self.data_nodes_by_owner = defaultdict(list)
  440. for dn in get_data_nodes():
  441. self.data_nodes_by_owner[dn.owner_id].append(dn)
  442. def get_datanodes_tree(self, scenario: t.Optional[Scenario]):
  443. with self.lock:
  444. self.__do_datanodes_tree()
  445. return (
  446. self.data_nodes_by_owner.get(scenario.id if scenario else None, []) if self.data_nodes_by_owner else []
  447. ) + (self.get_scenarios() if not scenario else [])
  448. def data_node_adapter(self, data):
  449. try:
  450. if hasattr(data, "id") and is_readable(data.id) and core_get(data.id) is not None:
  451. if isinstance(data, DataNode):
  452. return (data.id, data.get_simple_label(), None, _EntityType.DATANODE.value, False)
  453. else:
  454. with self.lock:
  455. self.__do_datanodes_tree()
  456. if self.data_nodes_by_owner:
  457. if isinstance(data, Cycle):
  458. return (
  459. data.id,
  460. data.get_simple_label(),
  461. self.data_nodes_by_owner[data.id] + self.scenario_by_cycle.get(data, []),
  462. _EntityType.CYCLE.value,
  463. False,
  464. )
  465. elif isinstance(data, Scenario):
  466. return (
  467. data.id,
  468. data.get_simple_label(),
  469. self.data_nodes_by_owner[data.id] + list(data.sequences.values()),
  470. _EntityType.SCENARIO.value,
  471. data.is_primary,
  472. )
  473. elif isinstance(data, Sequence):
  474. if datanodes := self.data_nodes_by_owner.get(data.id):
  475. return (
  476. data.id,
  477. data.get_simple_label(),
  478. datanodes,
  479. _EntityType.SEQUENCE.value,
  480. False,
  481. )
  482. except Exception as e:
  483. _warn(
  484. f"Access to {type(data)} ({data.id if hasattr(data, 'id') else 'No_id'}) failed",
  485. e,
  486. )
  487. return None
  488. def get_jobs_list(self):
  489. with self.lock:
  490. if self.jobs_list is None:
  491. self.jobs_list = get_jobs()
  492. return self.jobs_list
  493. def job_adapter(self, job):
  494. try:
  495. if hasattr(job, "id") and is_readable(job.id) and core_get(job.id) is not None:
  496. if isinstance(job, Job):
  497. entity = core_get(job.owner_id)
  498. return (
  499. job.id,
  500. job.get_simple_label(),
  501. [],
  502. entity.get_simple_label() if entity else "",
  503. entity.id if entity else "",
  504. job.submit_id,
  505. job.creation_date,
  506. job.status.value,
  507. is_deletable(job),
  508. is_readable(job),
  509. is_editable(job),
  510. )
  511. except Exception as e:
  512. _warn(f"Access to job ({job.id if hasattr(job, 'id') else 'No_id'}) failed", e)
  513. return None
  514. def act_on_jobs(self, state: State, id: str, payload: t.Dict[str, str]):
  515. args = payload.get("args")
  516. if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict):
  517. return
  518. data = args[0]
  519. job_ids = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
  520. job_action = data.get(_GuiCoreContext.__ACTION)
  521. if job_action and isinstance(job_ids, list):
  522. errs = []
  523. if job_action == "delete":
  524. for job_id in job_ids:
  525. if not is_readable(job_id):
  526. errs.append(f"Job {job_id} is not readable.")
  527. continue
  528. if not is_deletable(job_id):
  529. errs.append(f"Job {job_id} is not deletable.")
  530. continue
  531. try:
  532. delete_job(core_get(job_id))
  533. except Exception as e:
  534. errs.append(f"Error deleting job. {e}")
  535. elif job_action == "cancel":
  536. for job_id in job_ids:
  537. if not is_readable(job_id):
  538. errs.append(f"Job {job_id} is not readable.")
  539. continue
  540. if not is_editable(job_id):
  541. errs.append(f"Job {job_id} is not cancelable.")
  542. continue
  543. try:
  544. cancel_job(job_id)
  545. except Exception as e:
  546. errs.append(f"Error canceling job. {e}")
  547. state.assign(_GuiCoreContext._JOB_SELECTOR_ERROR_VAR, "<br/>".join(errs) if errs else "")
  548. def edit_data_node(self, state: State, id: str, payload: t.Dict[str, str]):
  549. args = payload.get("args")
  550. if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict):
  551. return
  552. data = args[0]
  553. entity_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
  554. if not self.__check_readable_editable(state, entity_id, "DataNode", _GuiCoreContext._DATANODE_VIZ_ERROR_VAR):
  555. return
  556. entity: DataNode = core_get(entity_id)
  557. if isinstance(entity, DataNode):
  558. try:
  559. self.__edit_properties(entity, data)
  560. state.assign(_GuiCoreContext._DATANODE_VIZ_ERROR_VAR, "")
  561. except Exception as e:
  562. state.assign(_GuiCoreContext._DATANODE_VIZ_ERROR_VAR, f"Error updating Datanode. {e}")
  563. def lock_datanode_for_edit(self, state: State, id: str, payload: t.Dict[str, str]):
  564. args = payload.get("args")
  565. if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict):
  566. return
  567. data = args[0]
  568. entity_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
  569. if not self.__check_readable_editable(state, entity_id, "Datanode", _GuiCoreContext._DATANODE_VIZ_ERROR_VAR):
  570. return
  571. lock = data.get("lock", True)
  572. entity: DataNode = core_get(entity_id)
  573. if isinstance(entity, DataNode):
  574. try:
  575. if lock:
  576. entity.lock_edit(self.gui._get_client_id())
  577. else:
  578. entity.unlock_edit(self.gui._get_client_id())
  579. state.assign(_GuiCoreContext._DATANODE_VIZ_ERROR_VAR, "")
  580. except Exception as e:
  581. state.assign(_GuiCoreContext._DATANODE_VIZ_ERROR_VAR, f"Error locking Datanode. {e}")
  582. def __edit_properties(self, entity: t.Union[Scenario, Sequence, DataNode], data: t.Dict[str, str]):
  583. with entity as ent:
  584. if isinstance(ent, Scenario):
  585. tags = data.get(_GuiCoreContext.__PROP_SCENARIO_TAGS)
  586. if isinstance(tags, (list, tuple)):
  587. ent.tags = {t for t in tags}
  588. name = data.get(_GuiCoreContext.__PROP_ENTITY_NAME)
  589. if isinstance(name, str):
  590. ent.properties[_GuiCoreContext.__PROP_ENTITY_NAME] = name
  591. props = data.get("properties")
  592. if isinstance(props, (list, tuple)):
  593. for prop in props:
  594. key = prop.get("key")
  595. if key and key not in _GuiCoreContext.__ENTITY_PROPS:
  596. ent.properties[key] = prop.get("value")
  597. deleted_props = data.get("deleted_properties")
  598. if isinstance(deleted_props, (list, tuple)):
  599. for prop in deleted_props:
  600. key = prop.get("key")
  601. if key and key not in _GuiCoreContext.__ENTITY_PROPS:
  602. ent.properties.pop(key, None)
  603. def get_scenarios_for_owner(self, owner_id: str):
  604. cycles_scenarios: t.List[t.Union[Scenario, Cycle]] = []
  605. with self.lock:
  606. if self.scenario_by_cycle is None:
  607. self.scenario_by_cycle = get_cycles_scenarios()
  608. if owner_id:
  609. if owner_id == "GLOBAL":
  610. for cycle, scenarios in self.scenario_by_cycle.items():
  611. if cycle is None:
  612. cycles_scenarios.extend(scenarios)
  613. else:
  614. cycles_scenarios.append(cycle)
  615. elif is_readable(t.cast(ScenarioId, owner_id)):
  616. entity = core_get(owner_id)
  617. if entity and (scenarios_cycle := self.scenario_by_cycle.get(t.cast(Cycle, entity))):
  618. cycles_scenarios.extend(scenarios_cycle)
  619. elif isinstance(entity, Scenario):
  620. cycles_scenarios.append(entity)
  621. return cycles_scenarios
  622. def get_data_node_history(self, datanode: DataNode, id: str):
  623. if (
  624. id
  625. and isinstance(datanode, DataNode)
  626. and id == datanode.id
  627. and (dn := core_get(id))
  628. and isinstance(dn, DataNode)
  629. ):
  630. res = []
  631. for e in dn.edits:
  632. job_id = e.get("job_id")
  633. job: t.Optional[Job] = None
  634. if job_id:
  635. if not is_readable(job_id):
  636. job_id += " not readable"
  637. else:
  638. job = core_get(job_id)
  639. res.append(
  640. (
  641. e.get("timestamp"),
  642. job_id if job_id else e.get("writer_identifier", ""),
  643. f"Execution of task {job.task.get_simple_label()}."
  644. if job and job.task
  645. else e.get("comment", ""),
  646. )
  647. )
  648. return sorted(res, key=lambda r: r[0], reverse=True)
  649. return _DoNotUpdate()
  650. def get_data_node_data(self, datanode: DataNode, id: str):
  651. if (
  652. id
  653. and isinstance(datanode, DataNode)
  654. and id == datanode.id
  655. and (dn := core_get(id))
  656. and isinstance(dn, DataNode)
  657. ):
  658. if dn._last_edit_date:
  659. if isinstance(dn, _AbstractTabularDataNode):
  660. return (None, None, True, None)
  661. try:
  662. value = dn.read()
  663. if isinstance(value, (pd.DataFrame, pd.Series)):
  664. return (None, None, True, None)
  665. return (
  666. value,
  667. "date"
  668. if "date" in type(value).__name__
  669. else type(value).__name__
  670. if isinstance(value, Number)
  671. else None,
  672. None,
  673. None,
  674. )
  675. except Exception as e:
  676. return (None, None, None, f"read data_node: {e}")
  677. return (None, None, None, f"Data unavailable for {dn.get_simple_label()}")
  678. return _DoNotUpdate()
  679. def __check_readable_editable(self, state: State, id: str, type: str, var: str):
  680. if not is_readable(t.cast(DataNodeId, id)):
  681. state.assign(var, f"{type} {id} is not readable.")
  682. return False
  683. if not is_editable(t.cast(DataNodeId, id)):
  684. state.assign(var, f"{type} {id} is not editable.")
  685. return False
  686. return True
  687. def update_data(self, state: State, id: str, payload: t.Dict[str, str]):
  688. args = payload.get("args")
  689. if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict):
  690. return
  691. data = args[0]
  692. entity_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
  693. if not self.__check_readable_editable(state, entity_id, "DataNode", _GuiCoreContext._DATANODE_VIZ_ERROR_VAR):
  694. return
  695. entity: DataNode = core_get(entity_id)
  696. if isinstance(entity, DataNode):
  697. try:
  698. entity.write(
  699. parser.parse(data.get("value"))
  700. if data.get("type") == "date"
  701. else int(data.get("value"))
  702. if data.get("type") == "int"
  703. else float(data.get("value"))
  704. if data.get("type") == "float"
  705. else data.get("value"),
  706. comment=data.get(_GuiCoreContext.__PROP_ENTITY_COMMENT),
  707. )
  708. entity.unlock_edit(self.gui._get_client_id())
  709. state.assign(_GuiCoreContext._DATANODE_VIZ_ERROR_VAR, "")
  710. except Exception as e:
  711. state.assign(_GuiCoreContext._DATANODE_VIZ_ERROR_VAR, f"Error updating Datanode value. {e}")
  712. state.assign(_GuiCoreContext._DATANODE_VIZ_DATA_ID_VAR, entity_id) # this will update the data value
  713. def tabular_data_edit(self, state: State, var_name: str, payload: dict):
  714. user_data = payload.get("user_data", {})
  715. dn_id = user_data.get("dn_id")
  716. if not self.__check_readable_editable(state, dn_id, "DataNode", _GuiCoreContext._DATANODE_VIZ_ERROR_VAR):
  717. return
  718. datanode = core_get(dn_id) if dn_id else None
  719. if isinstance(datanode, DataNode):
  720. try:
  721. idx = payload.get("index")
  722. col = payload.get("col")
  723. tz = payload.get("tz")
  724. val = (
  725. parser.parse(str(payload.get("value"))).astimezone(zoneinfo.ZoneInfo(tz)).replace(tzinfo=None)
  726. if tz is not None
  727. else payload.get("value")
  728. )
  729. # user_value = payload.get("user_value")
  730. data = self.__read_tabular_data(datanode)
  731. if hasattr(data, "at"):
  732. data.at[idx, col] = val
  733. datanode.write(data, comment=user_data.get(_GuiCoreContext.__PROP_ENTITY_COMMENT))
  734. state.assign(_GuiCoreContext._DATANODE_VIZ_ERROR_VAR, "")
  735. else:
  736. state.assign(
  737. _GuiCoreContext._DATANODE_VIZ_ERROR_VAR,
  738. "Error updating Datanode tabular value: type does not support at[] indexer.",
  739. )
  740. except Exception as e:
  741. state.assign(_GuiCoreContext._DATANODE_VIZ_ERROR_VAR, f"Error updating Datanode tabular value. {e}")
  742. setattr(state, _GuiCoreContext._DATANODE_VIZ_DATA_ID_VAR, dn_id)
  743. def __read_tabular_data(self, datanode: DataNode):
  744. return datanode.read()
  745. def get_data_node_tabular_data(self, datanode: DataNode, id: str):
  746. if (
  747. id
  748. and isinstance(datanode, DataNode)
  749. and id == datanode.id
  750. and is_readable(t.cast(DataNodeId, id))
  751. and (dn := core_get(id))
  752. and isinstance(dn, DataNode)
  753. and dn.is_ready_for_reading
  754. ):
  755. try:
  756. return self.__read_tabular_data(dn)
  757. except Exception:
  758. return None
  759. return None
  760. def get_data_node_tabular_columns(self, datanode: DataNode, id: str):
  761. if (
  762. id
  763. and isinstance(datanode, DataNode)
  764. and id == datanode.id
  765. and is_readable(t.cast(DataNodeId, id))
  766. and (dn := core_get(id))
  767. and isinstance(dn, DataNode)
  768. and dn.is_ready_for_reading
  769. ):
  770. try:
  771. return self.gui._tbl_cols(
  772. True, True, "{}", json.dumps({"data": "tabular_data"}), tabular_data=self.__read_tabular_data(dn)
  773. )
  774. except Exception:
  775. return None
  776. return None
  777. def get_data_node_chart_config(self, datanode: DataNode, id: str):
  778. if (
  779. id
  780. and isinstance(datanode, DataNode)
  781. and id == datanode.id
  782. and is_readable(t.cast(DataNodeId, id))
  783. and (dn := core_get(id))
  784. and isinstance(dn, DataNode)
  785. and dn.is_ready_for_reading
  786. ):
  787. try:
  788. return self.gui._chart_conf(
  789. True, True, "{}", json.dumps({"data": "tabular_data"}), tabular_data=self.__read_tabular_data(dn)
  790. )
  791. except Exception:
  792. return None
  793. return None
  794. def select_id(self, state: State, id: str, payload: t.Dict[str, str]):
  795. args = payload.get("args")
  796. if args is None or not isinstance(args, list) or len(args) == 0 and isinstance(args[0], dict):
  797. return
  798. data = args[0]
  799. if owner_id := data.get("owner_id"):
  800. state.assign(_GuiCoreContext._DATANODE_VIZ_OWNER_ID_VAR, owner_id)
  801. elif history_id := data.get("history_id"):
  802. state.assign(_GuiCoreContext._DATANODE_VIZ_HISTORY_ID_VAR, history_id)
  803. elif data_id := data.get("data_id"):
  804. state.assign(_GuiCoreContext._DATANODE_VIZ_DATA_ID_VAR, data_id)
  805. elif chart_id := data.get("chart_id"):
  806. state.assign(_GuiCoreContext._DATANODE_VIZ_DATA_CHART_ID_VAR, chart_id)