_adapters.py 23 KB

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