json.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  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 dataclasses
  12. import json
  13. from datetime import date, datetime, timedelta
  14. from enum import Enum
  15. from pydoc import locate
  16. from typing import Any, Dict, List, Optional, Set
  17. from taipy.config.common.scope import Scope
  18. from .._entity._reload import _Reloader, _self_reload
  19. from .._version._version_manager_factory import _VersionManagerFactory
  20. from ._file_datanode_mixin import _FileDataNodeMixin
  21. from .data_node import DataNode
  22. from .data_node_id import DataNodeId, Edit
  23. class JSONDataNode(DataNode, _FileDataNodeMixin):
  24. """Data Node stored as a JSON file.
  25. Attributes:
  26. config_id (str): Identifier of the data node configuration. This string must be a valid
  27. Python identifier.
  28. scope (Scope^): The scope of this data node.
  29. id (str): The unique identifier of this data node.
  30. owner_id (str): The identifier of the owner (sequence_id, scenario_id, cycle_id) or `None`.
  31. parent_ids (Optional[Set[str]]): The identifiers of the parent tasks or `None`.
  32. last_edit_date (datetime): The date and time of the last modification.
  33. edits (List[Edit^]): The ordered list of edits for that job.
  34. version (str): The string indicates the application version of the data node to instantiate. If not provided,
  35. the current version is used.
  36. validity_period (Optional[timedelta]): The duration implemented as a timedelta since the last edit date for
  37. which the data node can be considered up-to-date. Once the validity period has passed, the data node is
  38. considered stale and relevant tasks will run even if they are skippable (see the
  39. [Task management page](../core/entities/task-mgt.md) for more details).
  40. If _validity_period_ is set to `None`, the data node is always up-to-date.
  41. edit_in_progress (bool): True if a task computing the data node has been submitted
  42. and not completed yet. False otherwise.
  43. editor_id (Optional[str]): The identifier of the user who is currently editing the data node.
  44. editor_expiration_date (Optional[datetime]): The expiration date of the editor lock.
  45. path (str): The path to the JSON file.
  46. encoder (json.JSONEncoder): The JSON encoder that is used to write into the JSON file.
  47. decoder (json.JSONDecoder): The JSON decoder that is used to read from the JSON file.
  48. properties (dict[str, Any]): A dictionary of additional properties. The _properties_
  49. must have a _"default_path"_ or _"path"_ entry with the path of the JSON file:
  50. - _"default_path"_ `(str)`: The default path of the CSV file.\n
  51. - _"encoding"_ `(str)`: The encoding of the CSV file. The default value is `utf-8`.\n
  52. - _"default_data"_: The default data of the data nodes instantiated from this json data node.\n
  53. """
  54. __STORAGE_TYPE = "json"
  55. __ENCODING_KEY = "encoding"
  56. _ENCODER_KEY = "encoder"
  57. _DECODER_KEY = "decoder"
  58. _REQUIRED_PROPERTIES: List[str] = []
  59. def __init__(
  60. self,
  61. config_id: str,
  62. scope: Scope,
  63. id: Optional[DataNodeId] = None,
  64. owner_id: Optional[str] = None,
  65. parent_ids: Optional[Set[str]] = None,
  66. last_edit_date: Optional[datetime] = None,
  67. edits: Optional[List[Edit]] = None,
  68. version: Optional[str] = None,
  69. validity_period: Optional[timedelta] = None,
  70. edit_in_progress: bool = False,
  71. editor_id: Optional[str] = None,
  72. editor_expiration_date: Optional[datetime] = None,
  73. properties: Optional[Dict] = None,
  74. ) -> None:
  75. self.id = id or self._new_id(config_id)
  76. if properties is None:
  77. properties = {}
  78. if self.__ENCODING_KEY not in properties.keys():
  79. properties[self.__ENCODING_KEY] = "utf-8"
  80. default_value = properties.pop(self._DEFAULT_DATA_KEY, None)
  81. _FileDataNodeMixin.__init__(self, properties)
  82. DataNode.__init__(
  83. self,
  84. config_id,
  85. scope,
  86. self.id,
  87. owner_id,
  88. parent_ids,
  89. last_edit_date,
  90. edits,
  91. version or _VersionManagerFactory._build_manager()._get_latest_version(),
  92. validity_period,
  93. edit_in_progress,
  94. editor_id,
  95. editor_expiration_date,
  96. **properties,
  97. )
  98. self._decoder = self._properties.get(self._DECODER_KEY, _DefaultJSONDecoder)
  99. self._encoder = self._properties.get(self._ENCODER_KEY, _DefaultJSONEncoder)
  100. with _Reloader():
  101. self._write_default_data(default_value)
  102. self._TAIPY_PROPERTIES.update(
  103. {
  104. self._PATH_KEY,
  105. self._DEFAULT_PATH_KEY,
  106. self._DEFAULT_DATA_KEY,
  107. self._IS_GENERATED_KEY,
  108. self.__ENCODING_KEY,
  109. self._ENCODER_KEY,
  110. self._DECODER_KEY,
  111. }
  112. )
  113. @classmethod
  114. def storage_type(cls) -> str:
  115. return cls.__STORAGE_TYPE
  116. @property # type: ignore
  117. @_self_reload(DataNode._MANAGER_NAME)
  118. def encoder(self):
  119. return self._encoder
  120. @encoder.setter
  121. def encoder(self, encoder: json.JSONEncoder):
  122. self.properties[self._ENCODER_KEY] = encoder
  123. @property # type: ignore
  124. @_self_reload(DataNode._MANAGER_NAME)
  125. def decoder(self):
  126. return self._decoder
  127. @decoder.setter
  128. def decoder(self, decoder: json.JSONDecoder):
  129. self.properties[self._DECODER_KEY] = decoder
  130. def _read(self):
  131. return self._read_from_path()
  132. def _read_from_path(self, path: Optional[str] = None, **read_kwargs) -> Any:
  133. if path is None:
  134. path = self._path
  135. with open(path, "r", encoding=self.properties[self.__ENCODING_KEY]) as f:
  136. return json.load(f, cls=self._decoder)
  137. def _append(self, data: Any):
  138. with open(self._path, "r+", encoding=self.properties[self.__ENCODING_KEY]) as f:
  139. file_data = json.load(f, cls=self._decoder)
  140. if isinstance(file_data, List):
  141. if isinstance(data, List):
  142. file_data.extend(data)
  143. else:
  144. file_data.append(data)
  145. elif isinstance(data, Dict):
  146. file_data.update(data)
  147. f.seek(0)
  148. json.dump(file_data, f, indent=4, cls=self._encoder)
  149. def _write(self, data: Any):
  150. with open(self._path, "w", encoding=self.properties[self.__ENCODING_KEY]) as f: # type: ignore
  151. json.dump(data, f, indent=4, cls=self._encoder)
  152. class _DefaultJSONEncoder(json.JSONEncoder):
  153. def default(self, o):
  154. if isinstance(o, Enum):
  155. return {
  156. "__type__": f"Enum-{o.__class__.__module__}-{o.__class__.__qualname__}-{o.name}",
  157. "__value__": o.value,
  158. }
  159. if isinstance(o, (datetime, date)):
  160. return {"__type__": "Datetime", "__value__": o.isoformat()}
  161. if dataclasses.is_dataclass(o):
  162. return {
  163. "__type__": f"dataclass-{o.__class__.__module__}-{o.__class__.__qualname__}",
  164. "__value__": dataclasses.asdict(o),
  165. }
  166. return super().default(o)
  167. class _DefaultJSONDecoder(json.JSONDecoder):
  168. def __init__(self, *args, **kwargs):
  169. json.JSONDecoder.__init__(self, *args, **kwargs, object_hook=self.object_hook)
  170. def object_hook(self, source):
  171. if _type := source.get("__type__"):
  172. if _type.startswith("Enum"):
  173. _, module, classname, name = _type.split("-")
  174. _enum_class = locate(f"{module}.{classname}")
  175. return _enum_class[name]
  176. if _type == "Datetime":
  177. return datetime.fromisoformat(source.get("__value__"))
  178. if _type.startswith("dataclass"):
  179. _, module, classname = _type.split("-")
  180. _data_class = locate(f"{module}.{classname}")
  181. return _data_class(**source.get("__value__"))
  182. return source