_context.py 40 KB

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