cycle.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  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 re
  12. import uuid
  13. from datetime import datetime
  14. from typing import Any, Dict, Optional
  15. from taipy.config.common._validate_id import _validate_id
  16. from taipy.config.common.frequency import Frequency
  17. from .._entity._entity import _Entity
  18. from .._entity._labeled import _Labeled
  19. from .._entity._properties import _Properties
  20. from .._entity._reload import _Reloader, _self_reload, _self_setter
  21. from ..exceptions.exceptions import AttributeKeyAlreadyExisted, _SuspiciousFileOperation
  22. from ..notification.event import Event, EventEntityType, EventOperation, _make_event
  23. from .cycle_id import CycleId
  24. class Cycle(_Entity, _Labeled):
  25. """An iteration of a recurrent work pattern.
  26. Many business operations are periodic, such as weekly predictions of sales data, monthly
  27. master planning of supply chains, quarterly financial reports, yearly budgeting, etc.
  28. The data applications to solve these business problems often require modeling the
  29. corresponding periods (i.e., cycles).
  30. For this purpose, a `Cycle^` represents a single iteration of such a time pattern.
  31. Each _cycle_ has a start date and a duration. Examples of cycles are:
  32. - Monday, 2. January 2023 as a daily cycle
  33. - Week 01 2023, from 2. January as a weekly cycle
  34. - January 2023 as a monthly cycle
  35. - etc.
  36. `Cycle^`s are created along with the `Scenario^`s that are attached to them.
  37. At its creation, a new scenario is attached to a single cycle, the one that
  38. matches its optional _frequency_ and its _creation_date_.
  39. The possible frequencies are:
  40. - `Frequency.DAILY`
  41. - `Frequency.WEEKLY`
  42. - `Frequency.MONTHLY`
  43. - `Frequency.QUARTERLY`
  44. - `Frequency.YEARLY`
  45. Attributes:
  46. id (str): The unique identifier of the cycle.
  47. frequency (Frequency^): The frequency of this cycle.
  48. creation_date (datetime): The date and time of the creation of this cycle.
  49. start_date (datetime): The date and time of the start of this cycle.
  50. end_date (datetime): The date and time of the end of this cycle.
  51. name (str): The name of this cycle.
  52. properties (dict[str, Any]): A dictionary of additional properties.
  53. !!! example "Example for January cycle"
  54. ![cycles](../img/cycles_january_colored.svg){ align=left width="250" }
  55. Let's assume an end-user publishes production orders (i.e., a production plan) every
  56. month. During each month (the cycle), he/she will be interested in experimenting with
  57. different scenarios until only one of those scenarios is selected as the official
  58. production plan to be published. Each month is modeled as a cycle, and each cycle
  59. can contain one or more scenarios.
  60. The picture on the left shows the tree of entities: Cycles, Scenarios, and their
  61. associated Sequence(s). There is an existing past cycle for December and a current
  62. cycle for January containing a single scenario.
  63. When comes the end of a _cycle_ (start date + duration), only one of the scenarios is
  64. applied in production. This "official" scenario is called the _**primary scenario**_.
  65. Only one _**primary scenario**_ per cycle is allowed.
  66. !!! example "Example for February cycle"
  67. ![cycles](../img/cycles_colored.svg){ align=left width="250" }
  68. Now the user starts working on the February work cycle. He or she creates two
  69. scenarios for the February cycle (one with a low capacity assumption and one with
  70. a high capacity assumption). The user can then decide to elect the low capacity
  71. scenario as the "official" scenario for February. To accomplish that, he just
  72. needs to promote the low capacity scenario as _**primary**_ for the February cycle.
  73. The tree of entities resulting from the various scenarios created is represented
  74. in the picture on the left. The underlined scenarios are _**primary**_.
  75. !!! note
  76. For a scenario, cycles are optional. If a scenario has no Frequency, it will not be
  77. attached to any cycle.
  78. """
  79. _ID_PREFIX = "CYCLE"
  80. __SEPARATOR = "_"
  81. _MANAGER_NAME = "cycle"
  82. __CHECK_INIT_DONE_ATTR_NAME = "_init_done"
  83. def __init__(
  84. self,
  85. frequency: Frequency,
  86. properties: Dict[str, Any],
  87. creation_date: datetime,
  88. start_date: datetime,
  89. end_date: datetime,
  90. name: Optional[str] = None,
  91. id: Optional[CycleId] = None,
  92. ) -> None:
  93. self._frequency = frequency
  94. self._creation_date = creation_date
  95. self._start_date = start_date
  96. self._end_date = end_date
  97. self._name = self._new_name(name)
  98. self.id = id or self._new_id(self._name)
  99. self._properties = _Properties(self, **properties)
  100. self._init_done = True
  101. def _new_name(self, name: Optional[str] = None) -> str:
  102. if name:
  103. return name
  104. if self._frequency == Frequency.DAILY:
  105. # Example "Monday, 2. January 2023"
  106. return self._start_date.strftime("%A, %d. %B %Y")
  107. if self._frequency == Frequency.WEEKLY:
  108. # Example "Week 01 2023, from 2. January"
  109. return self._start_date.strftime("Week %W %Y, from %d. %B")
  110. if self._frequency == Frequency.MONTHLY:
  111. # Example "January 2023"
  112. return self._start_date.strftime("%B %Y")
  113. if self._frequency == Frequency.QUARTERLY:
  114. # Example "2023 Q1"
  115. return f"{self._start_date.strftime('%Y')} Q{(self._start_date.month-1)//3+1}"
  116. if self._frequency == Frequency.YEARLY:
  117. # Example "2023"
  118. return self._start_date.strftime("%Y")
  119. return Cycle.__SEPARATOR.join([str(self._frequency.value), self._start_date.ctime()])
  120. @property # type: ignore
  121. @_self_reload(_MANAGER_NAME)
  122. def frequency(self):
  123. return self._frequency
  124. @frequency.setter # type: ignore
  125. @_self_setter(_MANAGER_NAME)
  126. def frequency(self, val):
  127. self._frequency = val
  128. @property # type: ignore
  129. @_self_reload(_MANAGER_NAME)
  130. def creation_date(self):
  131. return self._creation_date
  132. @creation_date.setter # type: ignore
  133. @_self_setter(_MANAGER_NAME)
  134. def creation_date(self, val):
  135. self._creation_date = val
  136. @property # type: ignore
  137. @_self_reload(_MANAGER_NAME)
  138. def start_date(self):
  139. return self._start_date
  140. @start_date.setter # type: ignore
  141. @_self_setter(_MANAGER_NAME)
  142. def start_date(self, val):
  143. self._start_date = val
  144. @property # type: ignore
  145. @_self_reload(_MANAGER_NAME)
  146. def end_date(self):
  147. return self._end_date
  148. @end_date.setter # type: ignore
  149. @_self_setter(_MANAGER_NAME)
  150. def end_date(self, val):
  151. self._end_date = val
  152. @property # type: ignore
  153. @_self_reload(_MANAGER_NAME)
  154. def name(self):
  155. return self._name
  156. @name.setter # type: ignore
  157. @_self_setter(_MANAGER_NAME)
  158. def name(self, val):
  159. self._name = val
  160. @property
  161. def properties(self):
  162. self._properties = _Reloader()._reload(self._MANAGER_NAME, self)._properties
  163. return self._properties
  164. @staticmethod
  165. def _new_id(name: str) -> CycleId:
  166. def _get_valid_filename(name: str) -> str:
  167. """
  168. Source: https://github.com/django/django/blob/main/django/utils/text.py
  169. """
  170. s = name.strip().replace(" ", "_")
  171. s = re.sub(r"(?u)[^-\w.]", "", s)
  172. if s in {"", ".", ".."}:
  173. raise _SuspiciousFileOperation(f"Could not derive file name from '{name}'")
  174. s = s.strip().replace(" ", "_")
  175. return re.sub(r"(?u)[^-\w.]", "", s)
  176. return CycleId(_get_valid_filename(Cycle.__SEPARATOR.join([Cycle._ID_PREFIX, name, str(uuid.uuid4())])))
  177. def __setattr__(self, name: str, value: Any) -> None:
  178. if self.__CHECK_INIT_DONE_ATTR_NAME not in dir(self) or name in dir(self):
  179. return super().__setattr__(name, value)
  180. else:
  181. protected_attribute_name = _validate_id(name)
  182. try:
  183. if protected_attribute_name not in self._properties:
  184. raise AttributeError
  185. raise AttributeKeyAlreadyExisted(name)
  186. except AttributeError:
  187. return super().__setattr__(name, value)
  188. def _get_attributes(self, protected_attribute_name, attribute_name):
  189. raise AttributeError
  190. def __getattr__(self, attribute_name):
  191. protected_attribute_name = attribute_name
  192. if protected_attribute_name in self._properties:
  193. return self._properties[protected_attribute_name]
  194. raise AttributeError(f"{attribute_name} is not an attribute of cycle {self.id}")
  195. def __eq__(self, other):
  196. return isinstance(other, Cycle) and self.id == other.id
  197. def __hash__(self):
  198. return hash(self.id)
  199. def get_label(self) -> str:
  200. """Returns the cycle label.
  201. Returns:
  202. The label of the cycle as a string.
  203. """
  204. return self._get_label()
  205. def get_simple_label(self) -> str:
  206. """Returns the cycle simple label.
  207. Returns:
  208. The simple label of the cycle as a string.
  209. """
  210. return self._get_simple_label()
  211. @_make_event.register(Cycle)
  212. def _make_event_for_cycle(
  213. cycle: Cycle,
  214. operation: EventOperation,
  215. /,
  216. attribute_name: Optional[str] = None,
  217. attribute_value: Optional[Any] = None,
  218. **kwargs,
  219. ) -> Event:
  220. metadata = {**kwargs}
  221. return Event(
  222. entity_type=EventEntityType.CYCLE,
  223. entity_id=cycle.id,
  224. operation=operation,
  225. attribute_name=attribute_name,
  226. attribute_value=attribute_value,
  227. metadata=metadata,
  228. )