123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279 |
- # Copyright 2021-2024 Avaiga Private Limited
- #
- # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
- # the License. You may obtain a copy of the License at
- #
- # http://www.apache.org/licenses/LICENSE-2.0
- #
- # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
- # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
- # specific language governing permissions and limitations under the License.
- import re
- import uuid
- from datetime import datetime
- from typing import Any, Dict, Optional
- from taipy.config.common._validate_id import _validate_id
- from taipy.config.common.frequency import Frequency
- from .._entity._entity import _Entity
- from .._entity._labeled import _Labeled
- from .._entity._properties import _Properties
- from .._entity._reload import _Reloader, _self_reload, _self_setter
- from ..exceptions.exceptions import AttributeKeyAlreadyExisted, _SuspiciousFileOperation
- from ..notification.event import Event, EventEntityType, EventOperation, _make_event
- from .cycle_id import CycleId
- class Cycle(_Entity, _Labeled):
- """An iteration of a recurrent work pattern.
- Many business operations are periodic, such as weekly predictions of sales data, monthly
- master planning of supply chains, quarterly financial reports, yearly budgeting, etc.
- The data applications to solve these business problems often require modeling the
- corresponding periods (i.e., cycles).
- For this purpose, a `Cycle^` represents a single iteration of such a time pattern.
- Each _cycle_ has a start date and a duration. Examples of cycles are:
- - Monday, 2. January 2023 as a daily cycle
- - Week 01 2023, from 2. January as a weekly cycle
- - January 2023 as a monthly cycle
- - etc.
- `Cycle^`s are created along with the `Scenario^`s that are attached to them.
- At its creation, a new scenario is attached to a single cycle, the one that
- matches its optional _frequency_ and its _creation_date_.
- The possible frequencies are:
- - `Frequency.DAILY`
- - `Frequency.WEEKLY`
- - `Frequency.MONTHLY`
- - `Frequency.QUARTERLY`
- - `Frequency.YEARLY`
- Attributes:
- id (str): The unique identifier of the cycle.
- frequency (Frequency^): The frequency of this cycle.
- creation_date (datetime): The date and time of the creation of this cycle.
- start_date (datetime): The date and time of the start of this cycle.
- end_date (datetime): The date and time of the end of this cycle.
- name (str): The name of this cycle.
- properties (dict[str, Any]): A dictionary of additional properties.
- !!! example "Example for January cycle"
- { align=left width="250" }
- Let's assume an end-user publishes production orders (i.e., a production plan) every
- month. During each month (the cycle), he/she will be interested in experimenting with
- different scenarios until only one of those scenarios is selected as the official
- production plan to be published. Each month is modeled as a cycle, and each cycle
- can contain one or more scenarios.
- The picture on the left shows the tree of entities: Cycles, Scenarios, and their
- associated Sequence(s). There is an existing past cycle for December and a current
- cycle for January containing a single scenario.
- When comes the end of a _cycle_ (start date + duration), only one of the scenarios is
- applied in production. This "official" scenario is called the _**primary scenario**_.
- Only one _**primary scenario**_ per cycle is allowed.
- !!! example "Example for February cycle"
- { align=left width="250" }
- Now the user starts working on the February work cycle. He or she creates two
- scenarios for the February cycle (one with a low capacity assumption and one with
- a high capacity assumption). The user can then decide to elect the low capacity
- scenario as the "official" scenario for February. To accomplish that, he just
- needs to promote the low capacity scenario as _**primary**_ for the February cycle.
- The tree of entities resulting from the various scenarios created is represented
- in the picture on the left. The underlined scenarios are _**primary**_.
- !!! note
- For a scenario, cycles are optional. If a scenario has no Frequency, it will not be
- attached to any cycle.
- """
- _ID_PREFIX = "CYCLE"
- __SEPARATOR = "_"
- _MANAGER_NAME = "cycle"
- __CHECK_INIT_DONE_ATTR_NAME = "_init_done"
- def __init__(
- self,
- frequency: Frequency,
- properties: Dict[str, Any],
- creation_date: datetime,
- start_date: datetime,
- end_date: datetime,
- name: Optional[str] = None,
- id: Optional[CycleId] = None,
- ) -> None:
- self._frequency = frequency
- self._creation_date = creation_date
- self._start_date = start_date
- self._end_date = end_date
- self._name = self._new_name(name)
- self.id = id or self._new_id(self._name)
- self._properties = _Properties(self, **properties)
- self._init_done = True
- def _new_name(self, name: Optional[str] = None) -> str:
- if name:
- return name
- if self._frequency == Frequency.DAILY:
- # Example "Monday, 2. January 2023"
- return self._start_date.strftime("%A, %d. %B %Y")
- if self._frequency == Frequency.WEEKLY:
- # Example "Week 01 2023, from 2. January"
- return self._start_date.strftime("Week %W %Y, from %d. %B")
- if self._frequency == Frequency.MONTHLY:
- # Example "January 2023"
- return self._start_date.strftime("%B %Y")
- if self._frequency == Frequency.QUARTERLY:
- # Example "2023 Q1"
- return f"{self._start_date.strftime('%Y')} Q{(self._start_date.month-1)//3+1}"
- if self._frequency == Frequency.YEARLY:
- # Example "2023"
- return self._start_date.strftime("%Y")
- return Cycle.__SEPARATOR.join([str(self._frequency.value), self._start_date.ctime()])
- @property # type: ignore
- @_self_reload(_MANAGER_NAME)
- def frequency(self):
- return self._frequency
- @frequency.setter # type: ignore
- @_self_setter(_MANAGER_NAME)
- def frequency(self, val):
- self._frequency = val
- @property # type: ignore
- @_self_reload(_MANAGER_NAME)
- def creation_date(self):
- return self._creation_date
- @creation_date.setter # type: ignore
- @_self_setter(_MANAGER_NAME)
- def creation_date(self, val):
- self._creation_date = val
- @property # type: ignore
- @_self_reload(_MANAGER_NAME)
- def start_date(self):
- return self._start_date
- @start_date.setter # type: ignore
- @_self_setter(_MANAGER_NAME)
- def start_date(self, val):
- self._start_date = val
- @property # type: ignore
- @_self_reload(_MANAGER_NAME)
- def end_date(self):
- return self._end_date
- @end_date.setter # type: ignore
- @_self_setter(_MANAGER_NAME)
- def end_date(self, val):
- self._end_date = val
- @property # type: ignore
- @_self_reload(_MANAGER_NAME)
- def name(self):
- return self._name
- @name.setter # type: ignore
- @_self_setter(_MANAGER_NAME)
- def name(self, val):
- self._name = val
- @property
- def properties(self):
- self._properties = _Reloader()._reload(self._MANAGER_NAME, self)._properties
- return self._properties
- @staticmethod
- def _new_id(name: str) -> CycleId:
- def _get_valid_filename(name: str) -> str:
- """
- Source: https://github.com/django/django/blob/main/django/utils/text.py
- """
- s = name.strip().replace(" ", "_")
- s = re.sub(r"(?u)[^-\w.]", "", s)
- if s in {"", ".", ".."}:
- raise _SuspiciousFileOperation(f"Could not derive file name from '{name}'")
- s = s.strip().replace(" ", "_")
- return re.sub(r"(?u)[^-\w.]", "", s)
- return CycleId(_get_valid_filename(Cycle.__SEPARATOR.join([Cycle._ID_PREFIX, name, str(uuid.uuid4())])))
- def __setattr__(self, name: str, value: Any) -> None:
- if self.__CHECK_INIT_DONE_ATTR_NAME not in dir(self) or name in dir(self):
- return super().__setattr__(name, value)
- else:
- protected_attribute_name = _validate_id(name)
- try:
- if protected_attribute_name not in self._properties:
- raise AttributeError
- raise AttributeKeyAlreadyExisted(name)
- except AttributeError:
- return super().__setattr__(name, value)
- def _get_attributes(self, protected_attribute_name, attribute_name):
- raise AttributeError
- def __getattr__(self, attribute_name):
- protected_attribute_name = attribute_name
- if protected_attribute_name in self._properties:
- return self._properties[protected_attribute_name]
- raise AttributeError(f"{attribute_name} is not an attribute of cycle {self.id}")
- def __eq__(self, other):
- return isinstance(other, Cycle) and self.id == other.id
- def __hash__(self):
- return hash(self.id)
- def get_label(self) -> str:
- """Returns the cycle label.
- Returns:
- The label of the cycle as a string.
- """
- return self._get_label()
- def get_simple_label(self) -> str:
- """Returns the cycle simple label.
- Returns:
- The simple label of the cycle as a string.
- """
- return self._get_simple_label()
- @_make_event.register(Cycle)
- def _make_event_for_cycle(
- cycle: Cycle,
- operation: EventOperation,
- /,
- attribute_name: Optional[str] = None,
- attribute_value: Optional[Any] = None,
- **kwargs,
- ) -> Event:
- metadata = {**kwargs}
- return Event(
- entity_type=EventEntityType.CYCLE,
- entity_id=cycle.id,
- operation=operation,
- attribute_name=attribute_name,
- attribute_value=attribute_value,
- metadata=metadata,
- )
|