wiederverwendbar 0.9.1__py3-none-any.whl → 0.9.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  __title__ = "wiederverwendbar"
2
2
  __description__ = "A collection of scripts, classes and tools they are \\\"wiederverwendbar\\\"."
3
- __version__ = "0.9.1"
3
+ __version__ = "0.9.2"
4
4
  __author__ = "Julius Koenig"
5
5
  __author_email__ = "info@bastelquartier.de"
6
6
  __license__ = "GPL-3.0"
@@ -23,13 +23,14 @@ class BrandingSettings(BaseModel):
23
23
  module_data = self.get_attributes(main_module.__dict__)
24
24
  if module_data is None:
25
25
  # ToDO: this code is not working for pyprojects.toml [project.scripts]
26
- init_file = Path(main_module.__file__).parent / "__init__.py"
27
- if init_file.is_file():
28
- init_file_module_spec = importlib.util.spec_from_file_location("__main__.__init__", init_file)
29
- init_file_module = importlib.util.module_from_spec(init_file_module_spec)
30
- sys.modules["__main__.__init__"] = init_file_module
31
- init_file_module_spec.loader.exec_module(init_file_module)
32
- module_data = self.get_attributes(init_file_module.__dict__)
26
+ if hasattr(main_module, "__file__"):
27
+ init_file = Path(main_module.__file__).parent / "__init__.py"
28
+ if init_file.is_file():
29
+ init_file_module_spec = importlib.util.spec_from_file_location("__main__.__init__", init_file)
30
+ init_file_module = importlib.util.module_from_spec(init_file_module_spec)
31
+ sys.modules["__main__.__init__"] = init_file_module
32
+ init_file_module_spec.loader.exec_module(init_file_module)
33
+ module_data = self.get_attributes(init_file_module.__dict__)
33
34
  if module_data is None:
34
35
  module_data = {}
35
36
 
@@ -172,13 +172,13 @@ class SqlalchemyDb:
172
172
  else:
173
173
  connection_string += self.password
174
174
  if self.host is None:
175
- raise RuntimeError(f"No host specified for {self}")
175
+ raise RuntimeError(f"No host specified for {self.__class__.__name__}")
176
176
  connection_string += f"@{self.host}"
177
177
  if self.port is None:
178
- raise RuntimeError(f"No port specified for {self}")
178
+ raise RuntimeError(f"No port specified for {self.__class__.__name__}")
179
179
  connection_string += f":{self.port}"
180
180
  if self.name is None:
181
- raise RuntimeError(f"No name specified for {self}")
181
+ raise RuntimeError(f"No name specified for {self.__class__.__name__}")
182
182
  connection_string += f"/{self.name}"
183
183
  return connection_string
184
184
 
@@ -1,4 +1,17 @@
1
- from wiederverwendbar.task_manger.task_manager import TaskManager
2
- from wiederverwendbar.task_manger.singleton import ManagerSingleton
3
- from wiederverwendbar.task_manger.task import Task
4
- from wiederverwendbar.task_manger.trigger import Interval, EverySeconds, EveryMinutes, EveryHours, EveryDays, EveryWeeks, EveryMonths, EveryYears, At, AtNow, AtCreation
1
+ from wiederverwendbar.task_manger.task_manager import (TaskManager)
2
+ from wiederverwendbar.task_manger.singleton import (ManagerSingleton)
3
+ from wiederverwendbar.task_manger.task import (Task)
4
+ from wiederverwendbar.task_manger.trigger import (Trigger,
5
+ Interval,
6
+ EverySeconds,
7
+ EveryMinutes,
8
+ EveryHours,
9
+ EveryDays,
10
+ EveryWeeks,
11
+ EveryMonths,
12
+ EveryYears,
13
+ At,
14
+ AtDatetime,
15
+ AtNow,
16
+ AtManagerCreation,
17
+ AtManagerStart)
@@ -1,104 +1,147 @@
1
- from datetime import datetime
2
- from typing import Optional
1
+ from datetime import datetime as _datetime
2
+ from enum import Enum
3
+ from typing import Any, Optional, Callable, Union, TYPE_CHECKING
3
4
 
4
- from wiederverwendbar.task_manger.trigger import Trigger, Interval, At
5
+ from wiederverwendbar.functions.is_coroutine_function import is_coroutine_function
6
+ from wiederverwendbar.task_manger.trigger import Trigger
7
+
8
+ if TYPE_CHECKING:
9
+ from wiederverwendbar.task_manger.task_manager import TaskManager
5
10
 
6
11
 
7
12
  class Task:
13
+ class TimeMeasurement(str, Enum):
14
+ START = "START"
15
+ END = "END"
16
+
8
17
  def __init__(self,
9
- payload,
10
- manager=None,
18
+ payload: Callable[..., None],
19
+ *triggers: Trigger,
11
20
  name: Optional[str] = None,
12
- trigger: Optional[Trigger] = None,
13
- time_measurement_before_run: bool = True,
14
- auto_add: Optional[bool] = None,
15
- *args,
16
- **kwargs):
17
- # set task name
21
+ time_measurement: Optional[TimeMeasurement] = None,
22
+ task_args: Optional[Union[list, tuple]] = None,
23
+ task_kwargs: Optional[dict] = None):
24
+ self._manager = None
25
+
26
+ # set the task name
18
27
  if name is None:
19
28
  name = payload.__name__
20
- self.name = name
21
-
22
- self.manager = None
23
-
24
- # set task trigger
25
- if not isinstance(trigger, (Interval, At)):
26
- raise ValueError("Invalid trigger object.")
27
- self.trigger = trigger
29
+ self._name = name
30
+
31
+ # set task triggers
32
+ self._triggers = []
33
+ for trigger in triggers:
34
+ if not isinstance(trigger, Trigger):
35
+ raise ValueError("Trigger must be an instance of Trigger.")
36
+ if trigger.task is not None:
37
+ raise ValueError("Trigger already assigned to a task.")
38
+ trigger._task = self
39
+ self._triggers.append(trigger)
40
+ self._triggers = tuple(self._triggers)
28
41
 
29
42
  # set task payload
30
43
  if not callable(payload):
31
44
  raise ValueError("Payload must be callable.")
45
+ if is_coroutine_function(payload):
46
+ raise ValueError("Coroutine functions are not supported.")
32
47
  self._payload = payload
33
48
 
49
+ # indicates when the last run time should be measured
50
+ if time_measurement is None:
51
+ time_measurement = self.TimeMeasurement.START
52
+ if not isinstance(time_measurement, self.TimeMeasurement):
53
+ raise ValueError("Time measurement must be an instance of TaskTimeMeasurement.")
54
+ self._time_measurement = time_measurement
55
+
34
56
  # set payload args and kwargs
35
- self.args = []
36
- self.kwargs = {}
37
- if not iter(args):
38
- raise ValueError("Args must be iterable.")
39
- for arg in args:
40
- self.args.append(arg)
41
- if not isinstance(kwargs, dict):
42
- raise ValueError("Kwargs must be dict.")
43
- self.kwargs = kwargs
44
-
45
- self.time_measurement_before_run = time_measurement_before_run
46
-
47
- self._last_run: Optional[datetime] = None
48
- self._next_run: Optional[datetime] = None
49
-
50
- # indicate if task is done
57
+ if task_args is None:
58
+ task_args = []
59
+ if not iter(task_args):
60
+ raise ValueError("Task args must be iterable.")
61
+ self._task_args = tuple(task_args)
62
+ if task_kwargs is None:
63
+ task_kwargs = {}
64
+ if not isinstance(task_kwargs, dict):
65
+ raise ValueError("Task kwargs must be dict.")
66
+ self._task_kwargs = task_kwargs
67
+
68
+ # indicate last run
69
+ self._last_run = None
70
+
71
+ # indicate if the task is done
51
72
  self._done = False
52
73
 
53
- # auto add task to manager
54
- if auto_add is None:
55
- auto_add = True if manager else False
74
+ def __str__(self):
75
+ return (f"{self.__class__.__name__}("
76
+ f"name={self.name}, "
77
+ f"trigger=[{', '.join([str(trigger) for trigger in self._triggers])}], "
78
+ f"last_run={self.last_run})")
56
79
 
57
- if auto_add:
58
- if not manager:
59
- raise ValueError("Manager object is required.")
60
- manager.add_task(self)
80
+ def __call__(self, *args, **kwargs) -> bool:
81
+ if self.manager is None:
82
+ raise ValueError(f"Task {self} is not assigned to a manager.")
83
+ for trigger in self.triggers:
84
+ if trigger():
85
+ return True
86
+ return False
61
87
 
62
- def init(self, manager):
63
- self.manager = manager
64
- self.trigger.init(manager)
65
- self.set_next_run()
88
+ @property
89
+ def manager(self) -> Optional["TaskManager"]:
90
+ return self._manager
91
+
92
+ @manager.setter
93
+ def manager(self, manager: "TaskManager") -> None:
94
+ if manager is None:
95
+ if self._manager is None:
96
+ return
97
+ manager = self._manager
98
+ with manager.lock:
99
+ # noinspection PyProtectedMember
100
+ manager._tasks.remove(self)
101
+ self.manager_removed()
102
+ else:
103
+ if self._manager is not None:
104
+ raise ValueError(f"Task {self} is already assigned to manager {self._manager}.")
105
+ self._manager = manager
66
106
 
67
- # log task creation
68
- self.manager.logger.debug(f"Task created.")
107
+ with manager.lock:
108
+ # noinspection PyProtectedMember
109
+ manager._tasks.append(self)
110
+ self.manager_added()
69
111
 
70
112
  @property
71
- def last_run(self) -> Optional[datetime]:
72
- if self._last_run is None:
73
- return datetime.fromtimestamp(0)
74
- return self._last_run
113
+ def name(self) -> str:
114
+ return self._name
75
115
 
76
116
  @property
77
- def next_run(self) -> Optional[datetime]:
78
- if self.is_done:
79
- return None
80
- return self._next_run
117
+ def triggers(self) -> tuple[Trigger, ...]:
118
+ return self._triggers
81
119
 
82
120
  @property
83
- def is_done(self) -> bool:
84
- return self._done
85
-
86
- def set_next_run(self):
87
- if isinstance(self.trigger, Interval):
88
- next_run = self.trigger.next(self.last_run)
89
- elif isinstance(self.trigger, At):
90
- next_run = self.trigger.next()
91
- if next_run == self._next_run:
92
- self.done()
93
- else:
94
- raise ValueError("Invalid trigger object.")
95
- self._next_run = next_run
121
+ def time_measurement(self) -> TimeMeasurement:
122
+ return self._time_measurement
123
+
124
+ @property
125
+ def task_args(self) -> tuple[Any]:
126
+ return self._task_args
127
+
128
+ @property
129
+ def task_kwargs(self) -> dict[str, Any]:
130
+ return self._task_kwargs
131
+
132
+ @property
133
+ def last_run(self) -> Optional[_datetime]:
134
+ return self._last_run
96
135
 
97
- def set_last_run(self):
98
- self._last_run = datetime.now()
136
+ def manager_added(self) -> None:
137
+ for trigger in self.triggers:
138
+ trigger.manager_added()
139
+ self.manager.logger.debug(f"{self.manager} -> Task {self} added.")
99
140
 
100
- def done(self):
101
- self._done = True
141
+ def manager_removed(self) -> None:
142
+ for trigger in self.triggers:
143
+ trigger.manager_removed()
144
+ self.manager.logger.debug(f"{self.manager} -> Task {self} removed.")
102
145
 
103
- def payload(self):
104
- self._payload(*self.args, **self.kwargs)
146
+ def payload(self) -> None:
147
+ self._payload(*self.task_args, **self.task_kwargs)
@@ -1,57 +1,73 @@
1
1
  import logging
2
2
  import multiprocessing
3
3
  import threading
4
- import time
5
- from datetime import datetime
6
- from typing import Any, Optional
4
+ from datetime import datetime as _datetime
5
+ from enum import Enum
6
+ from typing import Optional, Union, TYPE_CHECKING
7
7
 
8
8
  from wiederverwendbar.task_manger.task import Task
9
- from wiederverwendbar.task_manger.trigger import Trigger
9
+ from wiederverwendbar.timer import timer_loop
10
+
11
+ if TYPE_CHECKING:
12
+ from wiederverwendbar.task_manger.trigger import Trigger
13
+
14
+
15
+ class TaskManagerStates(str, Enum):
16
+ INITIAL = "INITIAL"
17
+ RUNNING = "RUNNING"
18
+ STOPPED = "STOPPED"
19
+
10
20
 
11
21
  class TaskManager:
22
+ class States(str, Enum):
23
+ INITIAL = "INITIAL"
24
+ RUNNING = "RUNNING"
25
+ STOPPED = "STOPPED"
26
+
12
27
  lock = threading.Lock()
13
28
 
14
29
  def __init__(self,
15
30
  name: Optional[str] = None,
16
31
  worker_count: Optional[int] = None,
17
32
  daemon: bool = False,
18
- keep_done_tasks: bool = False,
19
33
  loop_delay: Optional[float] = None,
20
- logger: Optional[logging.Logger] = None,
21
- log_self: bool = True):
34
+ logger: Optional[logging.Logger] = None):
35
+ self._id = id(self)
22
36
  if name is None:
23
37
  name = self.__class__.__name__
24
- self.name = name
38
+ self._name = name
25
39
  self._workers: list[threading.Thread] = []
26
40
  self._tasks: list[Task] = []
27
- self._stopped: bool = False
28
- self._creation_time: datetime = datetime.now()
29
- self._keep_done_tasks = keep_done_tasks
30
- self.logger = logger or logging.getLogger(self.name)
41
+ self._state: TaskManagerStates = TaskManagerStates.INITIAL
42
+ self._creation_time: _datetime = _datetime.now()
43
+ self._start_time: Optional[_datetime] = None
44
+ self.logger = logger or logging.getLogger(self._name)
31
45
 
32
46
  # create workers
33
47
  if worker_count is None:
34
48
  worker_count = multiprocessing.cpu_count()
35
- if worker_count - 2 < 1:
49
+ worker_count = round(worker_count / 2)
50
+ if worker_count < 1:
36
51
  worker_count = 1
37
- if worker_count > 4:
38
- worker_count = 4
39
52
  for i in range(worker_count):
40
- worker = threading.Thread(name=f"{self.name}.Worker{i}", target=self.loop, daemon=daemon)
53
+ worker = threading.Thread(name=f"{self._name}.Worker{i}", target=self.loop, daemon=daemon)
41
54
  self._workers.append(worker)
42
55
 
43
56
  # set loop delay
44
57
  if loop_delay is None:
45
- if self.worker_count > 1:
58
+ if self.worker_count >= 1:
46
59
  loop_delay = 0.001
47
60
  self._loop_delay = loop_delay
48
61
 
62
+ def __str__(self):
63
+ return f"{self.__class__.__name__}(name={self._name}, id={self._id}, state={self._state.value})"
64
+
49
65
  def __del__(self):
50
- if not self.stopped:
66
+ if self.state == TaskManagerStates.RUNNING:
51
67
  self.stop()
52
68
 
53
69
  @property
54
- def worker_count(self):
70
+ def worker_count(self) -> int:
55
71
  """
56
72
  Number of workers.
57
73
 
@@ -61,19 +77,18 @@ class TaskManager:
61
77
  return len(self._workers)
62
78
 
63
79
  @property
64
- def stopped(self):
80
+ def state(self) -> TaskManagerStates:
65
81
  """
66
- Manager stopped flag.
82
+ Manager state
67
83
 
68
- :return: bool
84
+ :return: TaskManagerStates
69
85
  """
70
86
 
71
87
  with self.lock:
72
- stopped = self._stopped
73
- return stopped
88
+ return self._state
74
89
 
75
90
  @property
76
- def creation_time(self) -> datetime:
91
+ def creation_time(self) -> _datetime:
77
92
  """
78
93
  Manager creation time.
79
94
 
@@ -81,8 +96,18 @@ class TaskManager:
81
96
  """
82
97
 
83
98
  with self.lock:
84
- creation_time = self._creation_time
85
- return creation_time
99
+ return self._creation_time
100
+
101
+ @property
102
+ def start_time(self) -> Optional[_datetime]:
103
+ """
104
+ Manager start time.
105
+
106
+ :return: datetime or None
107
+ """
108
+
109
+ with self.lock:
110
+ return self._start_time
86
111
 
87
112
  def start(self) -> None:
88
113
  """
@@ -91,14 +116,24 @@ class TaskManager:
91
116
  :return: None
92
117
  """
93
118
 
94
- self.logger.debug(f"Starting manager ...")
119
+ if self.state != TaskManagerStates.INITIAL:
120
+ raise ValueError(f"Manager '{self._name}' is not in state '{TaskManagerStates.INITIAL.value}'.")
121
+
122
+ self.logger.debug(f"Starting manager {self} ...")
123
+
124
+ with self.lock:
125
+ self._state = TaskManagerStates.RUNNING
95
126
 
96
127
  # start workers
97
128
  for worker in self._workers:
98
- self.logger.debug(f"Starting worker '{worker.name}' ...")
129
+ self.logger.debug(f"{self} -> Starting worker '{worker.name}' ...")
99
130
  worker.start()
100
131
 
101
- self.logger.debug(f"Manager started.")
132
+ # set the start time
133
+ with self.lock:
134
+ self._start_time = _datetime.now()
135
+
136
+ self.logger.debug(f"Manager {self} started.")
102
137
 
103
138
  def stop(self) -> None:
104
139
  """
@@ -107,21 +142,24 @@ class TaskManager:
107
142
  :return: None
108
143
  """
109
144
 
110
- self.logger.debug(f"Stopping manager ...")
145
+ if self.state != TaskManagerStates.RUNNING:
146
+ raise ValueError(f"Manager {self} is not in state '{TaskManagerStates.RUNNING.value}'.")
147
+
148
+ self.logger.debug(f"Stopping manager {self} ...")
111
149
 
112
150
  # set stopped flag
113
151
  with self.lock:
114
- self._stopped = True
152
+ self._state = TaskManagerStates.STOPPED
115
153
 
116
154
  # wait for workers to finish
117
155
  for worker in self._workers:
118
156
  if worker.is_alive():
119
- self.logger.debug(f"Waiting for worker '{worker.name}' to finish ...")
157
+ self.logger.debug(f"{self} -> Waiting for worker '{worker.name}' to finish ...")
120
158
  worker.join()
121
159
 
122
- self.logger.debug(f"Manager stopped.")
160
+ self.logger.debug(f"Manager {self} stopped.")
123
161
 
124
- def loop(self, stay_in_loop: Optional[bool] = None) -> None:
162
+ def loop(self, stay_in_loop: bool = True) -> None:
125
163
  """
126
164
  Manager loop. All workers run this loop. If worker_count is 0, you can run this loop manually.
127
165
 
@@ -129,54 +167,48 @@ class TaskManager:
129
167
  :return: None
130
168
  """
131
169
 
132
- if stay_in_loop is None:
133
- with self.lock:
134
- stay_in_loop = bool(self._loop_delay)
135
- while not self.stopped:
136
- now = datetime.now()
137
- current_task = None
138
- with self.lock:
139
- for i, task in enumerate(self._tasks):
140
- if task.next_run is None:
141
- continue
142
- if task.next_run > now:
143
- continue
144
- current_task = self._tasks.pop(i)
145
- break
146
- if current_task is None:
147
- with self.lock:
148
- loop_delay = self._loop_delay
149
- if loop_delay:
150
- time.sleep(loop_delay)
151
- continue
170
+ if self.state != TaskManagerStates.RUNNING:
171
+ raise ValueError(f"Manager {self} is not in state '{TaskManagerStates.RUNNING.value}'.")
152
172
 
153
- self.logger.debug(f"Running task '{current_task.name}' ...")
173
+ # check if running in a worker thread
174
+ with self.lock:
175
+ if threading.current_thread() not in self._workers:
176
+ if len(self._workers) > 0:
177
+ raise ValueError(f"{self} -> Running manager loop outside of worker thread is not allowed, if worker_count > 0.")
154
178
 
179
+ while self.state == TaskManagerStates.RUNNING:
180
+ # get the next task from the list
181
+ task = None
155
182
  with self.lock:
156
- if current_task.time_measurement_before_run:
157
- current_task.set_last_run()
158
- current_task.set_next_run()
159
-
160
- # run task
161
- current_task.payload()
162
-
163
- self.logger.debug(f"Task '{current_task.name}' successfully run.")
183
+ for i, _task in enumerate(self._tasks):
184
+ if not _task():
185
+ continue
186
+ task = self._tasks.pop(i)
187
+ break
164
188
 
165
- with self.lock:
166
- if not current_task.time_measurement_before_run:
167
- current_task.set_last_run()
168
- current_task.set_next_run()
169
- if not current_task.is_done:
170
- self._tasks.append(current_task)
171
- else:
172
- self.logger.debug(f"Task '{current_task.name}' is done.")
173
- if self._keep_done_tasks:
174
- self._tasks.append(current_task)
189
+ # if a task to run available, run the task
190
+ if task is not None:
191
+ self.logger.debug(f"{self} -> Running task {task} ...")
192
+ if task.time_measurement == Task.TimeMeasurement.START:
193
+ task._last_run = _datetime.now()
194
+ try:
195
+ task.payload()
196
+ self.logger.debug(f"{self} -> Task {task} successfully run.")
197
+ except Exception as e:
198
+ self.logger.error(f"{self} -> Task {task} failed: {e}")
199
+ if task.time_measurement == Task.TimeMeasurement.END:
200
+ task._last_run = _datetime.now()
201
+
202
+ # put the task back to list
203
+ with self.lock:
204
+ self._tasks.append(task)
175
205
 
176
206
  if not stay_in_loop:
177
207
  break
208
+ if self._loop_delay:
209
+ timer_loop(name=f"{self._name}_{self._id}_LOOP", seconds=self._loop_delay, loop_delay=self._loop_delay)
178
210
 
179
- def add_task(self, task: Task):
211
+ def add_task(self, task: Task) -> None:
180
212
  """
181
213
  Add task to manager.
182
214
 
@@ -184,12 +216,9 @@ class TaskManager:
184
216
  :return:
185
217
  """
186
218
 
187
- task.init(self)
188
- with self.lock:
189
- self._tasks.append(task)
190
- self.logger.debug(f"Task '{task.name}' added.")
219
+ task.manager = self
191
220
 
192
- def remove_task(self, task: Task):
221
+ def remove_task(self, task: Task) -> None:
193
222
  """
194
223
  Remove task from manager.
195
224
 
@@ -197,40 +226,34 @@ class TaskManager:
197
226
  :return:
198
227
  """
199
228
 
200
- with self.lock:
201
- self._tasks.remove(task)
202
- self.logger.debug(f"Task '{task.name}' removed.")
229
+ if task.manager is not self:
230
+ raise ValueError(f"Task {task} is not assigned to manager {self}.")
231
+ task.manager = None
203
232
 
204
233
  def task(self,
234
+ *triggers: "Trigger",
205
235
  name: Optional[str] = None,
206
- trigger: Optional[Trigger] = None,
207
- time_measurement_before_run: bool = True,
208
- return_func: bool = True,
209
- *args,
210
- **kwargs) -> Any:
236
+ time_measurement: Optional[Task.TimeMeasurement] = None,
237
+ task_args: Optional[Union[list, tuple]] = None,
238
+ task_kwargs: Optional[dict] = None):
211
239
  """
212
240
  Task decorator.
213
241
 
242
+ :param triggers: The trigger of the task.
214
243
  :param name: The name of the task.
215
- :param trigger: The trigger of the task.
216
- :param time_measurement_before_run: Time measurement before run flag.
217
- :param return_func: Return function flag. If True, the function will be returned instead of the task.
218
- :param args: Args for the task payload.
219
- :param kwargs: Kwargs for the task payload.
244
+ :param time_measurement: Time measurement for the task.
245
+ :param task_args: Args for the task payload.
246
+ :param task_kwargs: Kwargs for the task payload.
220
247
  :return: Task or function
221
248
  """
222
249
 
223
250
  def decorator(func):
224
- task = Task(name=name,
225
- manager=self,
226
- trigger=trigger,
227
- time_measurement_before_run=time_measurement_before_run,
228
- payload=func,
229
- auto_add=True,
230
- *args,
231
- **kwargs)
232
- return task if not return_func else func
251
+ self.add_task(task=Task(func,
252
+ *triggers,
253
+ name=name,
254
+ time_measurement=time_measurement,
255
+ task_args=task_args,
256
+ task_kwargs=task_kwargs))
257
+ return func
233
258
 
234
259
  return decorator
235
-
236
-