_adapters.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  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 inspect
  12. import json
  13. import math
  14. import sys
  15. import typing as t
  16. from abc import ABC, abstractmethod
  17. from dataclasses import dataclass
  18. from datetime import date, datetime
  19. from enum import Enum
  20. from numbers import Number
  21. from operator import attrgetter, contains, eq, ge, gt, le, lt, ne
  22. import pandas as pd
  23. from taipy.core import (
  24. Cycle,
  25. DataNode,
  26. Scenario,
  27. Sequence,
  28. is_deletable,
  29. is_editable,
  30. is_promotable,
  31. is_readable,
  32. is_submittable,
  33. )
  34. from taipy.core import get as core_get
  35. from taipy.core.config import Config
  36. from taipy.core.data._tabular_datanode_mixin import _TabularDataNodeMixin
  37. from taipy.gui._warnings import _warn
  38. from taipy.gui.gui import _DoNotUpdate
  39. from taipy.gui.utils import _is_boolean, _is_true, _TaipyBase
  40. # prevent gui from trying to push scenario instances to the front-end
  41. class _GuiCoreDoNotUpdate(_DoNotUpdate):
  42. def __repr__(self):
  43. return self.get_label() if hasattr(self, "get_label") else super().__repr__()
  44. class _EntityType(Enum):
  45. CYCLE = 0
  46. SCENARIO = 1
  47. SEQUENCE = 2
  48. DATANODE = 3
  49. class _GuiCoreScenarioAdapter(_TaipyBase):
  50. __INNER_PROPS = ["name"]
  51. def get(self):
  52. data = super().get()
  53. if isinstance(data, (list, tuple)) and len(data) == 1:
  54. data = data[0]
  55. if isinstance(data, Scenario):
  56. try:
  57. if scenario := core_get(data.id):
  58. return [
  59. scenario.id,
  60. scenario.is_primary,
  61. scenario.config_id,
  62. scenario.creation_date.isoformat(),
  63. scenario.cycle.get_simple_label() if scenario.cycle else "",
  64. scenario.get_simple_label(),
  65. list(scenario.tags) if scenario.tags else [],
  66. [
  67. (k, v)
  68. for k, v in scenario.properties.items()
  69. if k not in _GuiCoreScenarioAdapter.__INNER_PROPS
  70. ]
  71. if scenario.properties
  72. else [],
  73. [
  74. (
  75. s.get_simple_label(),
  76. [t.id for t in s.tasks.values()] if hasattr(s, "tasks") else [],
  77. "" if (reason := is_submittable(s)) else f"Sequence not submittable: {reason.reasons}",
  78. is_editable(s),
  79. )
  80. for s in scenario.sequences.values()
  81. ]
  82. if hasattr(scenario, "sequences") and scenario.sequences
  83. else [],
  84. {t.id: t.get_simple_label() for t in scenario.tasks.values()}
  85. if hasattr(scenario, "tasks")
  86. else {},
  87. list(scenario.properties.get("authorized_tags", [])) if scenario.properties else [],
  88. is_deletable(scenario),
  89. is_promotable(scenario),
  90. "" if (reason := is_submittable(scenario)) else f"Scenario not submittable: {reason.reasons}",
  91. is_readable(scenario),
  92. is_editable(scenario),
  93. ]
  94. except Exception as e:
  95. _warn(f"Access to scenario ({data.id if hasattr(data, 'id') else 'No_id'}) failed", e)
  96. return None
  97. @staticmethod
  98. def get_hash():
  99. return _TaipyBase._HOLDER_PREFIX + "Sc"
  100. class _GuiCoreScenarioDagAdapter(_TaipyBase):
  101. @staticmethod
  102. def get_entity_type(node: t.Any):
  103. return DataNode.__name__ if isinstance(node.entity, DataNode) else node.type
  104. def get(self):
  105. data = super().get()
  106. if isinstance(data, (list, tuple)) and len(data) == 1:
  107. data = data[0]
  108. if isinstance(data, Scenario):
  109. try:
  110. if scenario := core_get(data.id):
  111. dag = scenario._get_dag()
  112. nodes = {}
  113. for id, node in dag.nodes.items():
  114. entityType = _GuiCoreScenarioDagAdapter.get_entity_type(node)
  115. cat = nodes.get(entityType)
  116. if cat is None:
  117. cat = {}
  118. nodes[entityType] = cat
  119. cat[id] = {
  120. "name": node.entity.get_simple_label(),
  121. "type": node.entity.storage_type() if hasattr(node.entity, "storage_type") else None,
  122. }
  123. return [
  124. data.id,
  125. nodes,
  126. [
  127. (
  128. _GuiCoreScenarioDagAdapter.get_entity_type(e.src),
  129. e.src.entity.id,
  130. _GuiCoreScenarioDagAdapter.get_entity_type(e.dest),
  131. e.dest.entity.id,
  132. )
  133. for e in dag.edges
  134. ],
  135. ]
  136. except Exception as e:
  137. _warn(f"Access to scenario ({data.id if hasattr(data, 'id') else 'No_id'}) failed", e)
  138. return None
  139. @staticmethod
  140. def get_hash():
  141. return _TaipyBase._HOLDER_PREFIX + "ScG"
  142. class _GuiCoreScenarioNoUpdate(_TaipyBase, _DoNotUpdate):
  143. @staticmethod
  144. def get_hash():
  145. return _TaipyBase._HOLDER_PREFIX + "ScN"
  146. class _GuiCoreDatanodeAdapter(_TaipyBase):
  147. @staticmethod
  148. def _is_tabular_data(datanode: DataNode, value: t.Any):
  149. return isinstance(datanode, _TabularDataNodeMixin) or isinstance(
  150. value, (pd.DataFrame, pd.Series, list, tuple, dict)
  151. )
  152. def __get_data(self, dn: DataNode):
  153. if dn._last_edit_date:
  154. if isinstance(dn, _TabularDataNodeMixin):
  155. return (None, None, True, None)
  156. try:
  157. value = dn.read()
  158. if _GuiCoreDatanodeAdapter._is_tabular_data(dn, value):
  159. return (None, None, True, None)
  160. val_type = (
  161. "date"
  162. if "date" in type(value).__name__
  163. else type(value).__name__
  164. if isinstance(value, Number)
  165. else None
  166. )
  167. if isinstance(value, float) and math.isnan(value):
  168. value = None
  169. return (
  170. value,
  171. val_type,
  172. None,
  173. None,
  174. )
  175. except Exception as e:
  176. return (None, None, None, f"read data_node: {e}")
  177. return (None, None, None, f"Data unavailable for {dn.get_simple_label()}")
  178. def get(self):
  179. data = super().get()
  180. if isinstance(data, (list, tuple)) and len(data) == 1:
  181. data = data[0]
  182. if isinstance(data, DataNode):
  183. try:
  184. if datanode := core_get(data.id):
  185. owner = core_get(datanode.owner_id) if datanode.owner_id else None
  186. return [
  187. datanode.id,
  188. datanode.storage_type() if hasattr(datanode, "storage_type") else "",
  189. datanode.config_id,
  190. f"{datanode.last_edit_date}" if datanode.last_edit_date else "",
  191. f"{datanode.expiration_date}" if datanode.last_edit_date else "",
  192. datanode.get_simple_label(),
  193. datanode.owner_id or "",
  194. owner.get_simple_label() if owner else "GLOBAL",
  195. _EntityType.CYCLE.value
  196. if isinstance(owner, Cycle)
  197. else _EntityType.SCENARIO.value
  198. if isinstance(owner, Scenario)
  199. else -1,
  200. self.__get_data(datanode),
  201. datanode._edit_in_progress,
  202. datanode._editor_id,
  203. is_readable(datanode),
  204. is_editable(datanode),
  205. ]
  206. except Exception as e:
  207. _warn(f"Access to datanode ({data.id if hasattr(data, 'id') else 'No_id'}) failed", e)
  208. return None
  209. @staticmethod
  210. def get_hash():
  211. return _TaipyBase._HOLDER_PREFIX + "Dn"
  212. _operators: t.Dict[str, t.Callable] = {
  213. "==": eq,
  214. "!=": ne,
  215. "<": lt,
  216. "<=": le,
  217. ">": gt,
  218. ">=": ge,
  219. "contains": contains,
  220. }
  221. def _invoke_action(
  222. ent: t.Any, col: str, col_type: str, is_dn: bool, action: str, val: t.Any, col_fn: t.Optional[str]
  223. ) -> bool:
  224. if ent is None:
  225. return False
  226. try:
  227. if col_type == "any":
  228. # when a property is not found, return True only if action is not equals
  229. if not is_dn and not hasattr(ent, "properties") or not ent.properties.get(col_fn or col):
  230. return action == "!="
  231. if op := _operators.get(action):
  232. cur_val = attrgetter(col_fn or col)(ent)
  233. cur_val = cur_val() if col_fn else cur_val
  234. return op(cur_val.isoformat() if isinstance(cur_val, (datetime, date)) else cur_val, val)
  235. except Exception as e:
  236. if _is_debugging():
  237. _warn(f"Error filtering with {col} {action} {val} on {ent}.", e)
  238. return col_type == "any" and action == "!="
  239. return True
  240. def _get_entity_property(col: str, a_type: t.Type):
  241. col_parts = col.split("(") # handle the case where the col is a method (ie get_simple_label())
  242. col_fn = (
  243. next(
  244. (col_parts[0] for i in inspect.getmembers(a_type, predicate=inspect.isfunction) if i[0] == col_parts[0]),
  245. None,
  246. )
  247. if len(col_parts) > 1
  248. else None
  249. )
  250. def sort_key(entity: t.Union[Scenario, Cycle, Sequence, DataNode]):
  251. # we compare only strings
  252. if isinstance(entity, a_type):
  253. try:
  254. val = attrgetter(col_fn or col)(entity)
  255. if col_fn:
  256. val = val()
  257. except AttributeError as e:
  258. if _is_debugging():
  259. _warn("Attribute", e)
  260. val = ""
  261. else:
  262. val = ""
  263. return val.isoformat() if isinstance(val, (datetime, date)) else str(val)
  264. return sort_key
  265. def _get_datanode_property(attr: str):
  266. if (parts := attr.split(".")) and len(parts) > 1:
  267. return parts[1]
  268. return None
  269. class _GuiCoreProperties(ABC):
  270. @staticmethod
  271. @abstractmethod
  272. def get_type(attr: str):
  273. raise NotImplementedError
  274. @staticmethod
  275. @abstractmethod
  276. def get_col_name(attr: str):
  277. raise NotImplementedError
  278. @staticmethod
  279. @abstractmethod
  280. def get_default_list():
  281. raise NotImplementedError
  282. @staticmethod
  283. @abstractmethod
  284. def full_desc():
  285. raise NotImplementedError
  286. def get_enums(self):
  287. return {}
  288. def get(self):
  289. data = super().get()
  290. if _is_boolean(data):
  291. if _is_true(data):
  292. data = self.get_default_list()
  293. else:
  294. return None
  295. if isinstance(data, str):
  296. data = data.split(";")
  297. if isinstance(data, (list, tuple)):
  298. flist = []
  299. for f in data:
  300. if f == "*":
  301. flist.extend(self.get_default_list())
  302. else:
  303. flist.append(f)
  304. return json.dumps(
  305. [
  306. (attr, self.get_type(attr), self.get_enums().get(attr)) if self.full_desc() else (attr,)
  307. for attr in flist
  308. if attr and isinstance(attr, str)
  309. ]
  310. )
  311. return None
  312. @dataclass(frozen=True)
  313. class _GuiCorePropDesc:
  314. attr: str
  315. type: str
  316. extended: bool = False
  317. for_sort: bool = False
  318. _EMPTY_PROP_DESC = _GuiCorePropDesc("", "any")
  319. class _GuiCoreScenarioProperties(_GuiCoreProperties):
  320. _SC_PROPS: t.Dict[str, _GuiCorePropDesc] = {
  321. "Config id": _GuiCorePropDesc("config_id", "string", for_sort=True),
  322. "Label": _GuiCorePropDesc("get_simple_label()", "string", for_sort=True),
  323. "Creation date": _GuiCorePropDesc("creation_date", "date", for_sort=True),
  324. "Cycle label": _GuiCorePropDesc("cycle.name", "string", extended=True),
  325. "Cycle start": _GuiCorePropDesc("cycle.start_date", "date", extended=True),
  326. "Cycle end": _GuiCorePropDesc("cycle.end_date", "date", extended=True),
  327. "Primary": _GuiCorePropDesc("is_primary", "boolean", extended=True),
  328. "Tags": _GuiCorePropDesc("tags", "string"),
  329. }
  330. __DN_PROPS = {
  331. "Up to date": _GuiCorePropDesc("is_up_to_date", "boolean"),
  332. "Valid": _GuiCorePropDesc("is_valid", "boolean"),
  333. "Last edit date": _GuiCorePropDesc("last_edit_date", "date"),
  334. }
  335. __ENUMS = None
  336. __SC_CYCLE = None
  337. @staticmethod
  338. def get_type(attr: str):
  339. if prop := _get_datanode_property(attr):
  340. return _GuiCoreScenarioProperties.__DN_PROPS.get(prop, _EMPTY_PROP_DESC).type
  341. return _GuiCoreScenarioProperties._SC_PROPS.get(attr, _EMPTY_PROP_DESC).type
  342. @staticmethod
  343. def get_col_name(attr: str):
  344. if prop := _get_datanode_property(attr):
  345. return (
  346. attr.split(".")[0]
  347. + f".{_GuiCoreScenarioProperties.__DN_PROPS.get(prop, _EMPTY_PROP_DESC).attr or prop}"
  348. )
  349. return _GuiCoreScenarioProperties._SC_PROPS.get(attr, _EMPTY_PROP_DESC).attr or attr
  350. def get_enums(self):
  351. if _GuiCoreScenarioProperties.__ENUMS is None:
  352. _GuiCoreScenarioProperties.__ENUMS = {
  353. k: v
  354. for k, v in {
  355. "Config id": [c for c in Config.scenarios.keys() if c != "default"],
  356. "Tags": list(
  357. {t for s in Config.scenarios.values() for t in s.properties.get("authorized_tags", [])}
  358. ),
  359. }.items()
  360. if len(v)
  361. }
  362. return _GuiCoreScenarioProperties.__ENUMS if self.full_desc() else {}
  363. @staticmethod
  364. def has_cycle():
  365. if _GuiCoreScenarioProperties.__SC_CYCLE is None:
  366. _GuiCoreScenarioProperties.__SC_CYCLE = (
  367. next(filter(lambda sc: sc.frequency is not None, Config.scenarios.values()), None) is not None
  368. )
  369. return _GuiCoreScenarioProperties.__SC_CYCLE
  370. class _GuiCoreScenarioFilter(_GuiCoreScenarioProperties, _TaipyBase):
  371. DEFAULT = list(_GuiCoreScenarioProperties._SC_PROPS.keys())
  372. DEFAULT_NO_CYCLE = [
  373. p[0] for p in filter(lambda prop: not prop[1].extended, _GuiCoreScenarioProperties._SC_PROPS.items())
  374. ]
  375. @staticmethod
  376. def full_desc():
  377. return True
  378. @staticmethod
  379. def get_hash():
  380. return _TaipyBase._HOLDER_PREFIX + "ScF"
  381. @staticmethod
  382. def get_default_list():
  383. return (
  384. _GuiCoreScenarioFilter.DEFAULT
  385. if _GuiCoreScenarioProperties.has_cycle()
  386. else _GuiCoreScenarioFilter.DEFAULT_NO_CYCLE
  387. )
  388. class _GuiCoreScenarioSort(_GuiCoreScenarioProperties, _TaipyBase):
  389. DEFAULT = [p[0] for p in filter(lambda prop: prop[1].for_sort, _GuiCoreScenarioProperties._SC_PROPS.items())]
  390. DEFAULT_NO_CYCLE = [
  391. p[0]
  392. for p in filter(
  393. lambda prop: prop[1].for_sort and not prop[1].extended, _GuiCoreScenarioProperties._SC_PROPS.items()
  394. )
  395. ]
  396. @staticmethod
  397. def full_desc():
  398. return False
  399. @staticmethod
  400. def get_hash():
  401. return _TaipyBase._HOLDER_PREFIX + "ScS"
  402. @staticmethod
  403. def get_default_list():
  404. return (
  405. _GuiCoreScenarioSort.DEFAULT
  406. if _GuiCoreScenarioProperties.has_cycle()
  407. else _GuiCoreScenarioSort.DEFAULT_NO_CYCLE
  408. )
  409. class _GuiCoreDatanodeProperties(_GuiCoreProperties):
  410. _DN_PROPS: t.Dict[str, _GuiCorePropDesc] = {
  411. "Config id": _GuiCorePropDesc("config_id", "string", for_sort=True),
  412. "Label": _GuiCorePropDesc("get_simple_label()", "string", for_sort=True),
  413. "Up to date": _GuiCorePropDesc("is_up_to_date", "boolean"),
  414. "Last edit date": _GuiCorePropDesc("last_edit_date", "date", for_sort=True),
  415. "Input": _GuiCorePropDesc("is_input", "boolean"),
  416. "Output": _GuiCorePropDesc("is_output", "boolean"),
  417. "Intermediate": _GuiCorePropDesc("is_intermediate", "boolean"),
  418. "Expiration date": _GuiCorePropDesc("expiration_date", "date", extended=True, for_sort=True),
  419. "Expired": _GuiCorePropDesc("is_expired", "boolean", extended=True),
  420. }
  421. __DN_VALIDITY = None
  422. @staticmethod
  423. def get_type(attr: str):
  424. return _GuiCoreDatanodeProperties._DN_PROPS.get(attr, _EMPTY_PROP_DESC).type
  425. @staticmethod
  426. def get_col_name(attr: str):
  427. return _GuiCoreDatanodeProperties._DN_PROPS.get(attr, _EMPTY_PROP_DESC).attr or attr
  428. @staticmethod
  429. def has_validity():
  430. if _GuiCoreDatanodeProperties.__DN_VALIDITY is None:
  431. _GuiCoreDatanodeProperties.__DN_VALIDITY = (
  432. next(filter(lambda dn: dn.validity_period is not None, Config.data_nodes.values()), None) is not None
  433. )
  434. return _GuiCoreDatanodeProperties.__DN_VALIDITY
  435. class _GuiCoreDatanodeFilter(_GuiCoreDatanodeProperties, _TaipyBase):
  436. DEFAULT = list(_GuiCoreDatanodeProperties._DN_PROPS.keys())
  437. DEFAULT_NO_VALIDITY = [
  438. p[0] for p in filter(lambda prop: not prop[1].extended, _GuiCoreDatanodeProperties._DN_PROPS.items())
  439. ]
  440. @staticmethod
  441. def full_desc():
  442. return True
  443. @staticmethod
  444. def get_hash():
  445. return _TaipyBase._HOLDER_PREFIX + "DnF"
  446. @staticmethod
  447. def get_default_list():
  448. return (
  449. _GuiCoreDatanodeFilter.DEFAULT
  450. if _GuiCoreDatanodeProperties.has_validity()
  451. else _GuiCoreDatanodeFilter.DEFAULT_NO_VALIDITY
  452. )
  453. class _GuiCoreDatanodeSort(_GuiCoreDatanodeProperties, _TaipyBase):
  454. DEFAULT = [p[0] for p in filter(lambda prop: prop[1].for_sort, _GuiCoreDatanodeProperties._DN_PROPS.items())]
  455. DEFAULT_NO_VALIDITY = [
  456. p[0]
  457. for p in filter(
  458. lambda prop: prop[1].for_sort and not prop[1].extended, _GuiCoreDatanodeProperties._DN_PROPS.items()
  459. )
  460. ]
  461. @staticmethod
  462. def full_desc():
  463. return False
  464. @staticmethod
  465. def get_hash():
  466. return _TaipyBase._HOLDER_PREFIX + "DnS"
  467. @staticmethod
  468. def get_default_list():
  469. return (
  470. _GuiCoreDatanodeSort.DEFAULT
  471. if _GuiCoreDatanodeProperties.has_validity()
  472. else _GuiCoreDatanodeSort.DEFAULT_NO_VALIDITY
  473. )
  474. def _is_debugging() -> bool:
  475. return hasattr(sys, "gettrace") and sys.gettrace() is not None