appier 1.34.6__py2.py3-none-any.whl → 1.34.8__py2.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.
appier/scheduler.py CHANGED
@@ -1,334 +1,342 @@
1
- #!/usr/bin/python
2
- # -*- coding: utf-8 -*-
3
-
4
- # Hive Appier Framework
5
- # Copyright (c) 2008-2024 Hive Solutions Lda.
6
- #
7
- # This file is part of Hive Appier Framework.
8
- #
9
- # Hive Appier Framework is free software: you can redistribute it and/or modify
10
- # it under the terms of the Apache License as published by the Apache
11
- # Foundation, either version 2.0 of the License, or (at your option) any
12
- # later version.
13
- #
14
- # Hive Appier Framework is distributed in the hope that it will be useful,
15
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
16
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
- # Apache License for more details.
18
- #
19
- # You should have received a copy of the Apache License along with
20
- # Hive Appier Framework. If not, see <http://www.apache.org/licenses/>.
21
-
22
- __author__ = "João Magalhães <joamag@hive.pt>"
23
- """ The author(s) of the module """
24
-
25
- __copyright__ = "Copyright (c) 2008-2024 Hive Solutions Lda."
26
- """ The copyright for the module """
27
-
28
- __license__ = "Apache License, Version 2.0"
29
- """ The license for the module """
30
-
31
- import time
32
- import uuid
33
- import heapq
34
- import datetime
35
- import logging
36
- import threading
37
- import traceback
38
-
39
- from . import config
40
- from . import legacy
41
-
42
- LOOP_TIMEOUT = 60.0
43
- """ The time value to be used to sleep the main sequence
44
- loop between ticks, this value should not be too small
45
- to spend many resources or to high to create a long set
46
- of time between external interactions """
47
-
48
-
49
- class Scheduler(threading.Thread):
50
- """
51
- Scheduler class that handles timeout based async tasks
52
- within the context of an Appier application.
53
-
54
- The architecture of the logic for the class should be
55
- modular in the sense that new task may be added to
56
- it through a queue or other external system. For that
57
- a proper preemption mechanism should exist allowing
58
- the scheduler to be stopped and started again.
59
- """
60
-
61
- def __init__(self, owner, timeout=LOOP_TIMEOUT, daemon=True):
62
- threading.Thread.__init__(self, name="Scheduler")
63
- self.owner = owner
64
- self.timeout = config.conf("SCHEDULER_TIMEOUT", timeout, cast=float)
65
- self.daemon = config.conf("SCHEDULER_DAEMON", daemon, cast=bool)
66
- self._condition = threading.Condition()
67
-
68
- def run(self):
69
- self.running = True
70
- self.load()
71
- while self.running:
72
- try:
73
- self.tick()
74
- except Exception as exception:
75
- self.logger.critical("Unhandled scheduler exception raised")
76
- self.logger.error(exception)
77
- lines = traceback.format_exc().splitlines()
78
- for line in lines:
79
- self.logger.warning(line)
80
- self._condition.acquire()
81
- self._condition.wait(self.timeout)
82
- self._condition.release()
83
-
84
- def stop(self, awake=True):
85
- self.running = False
86
- if awake:
87
- self.awake()
88
-
89
- def tick(self):
90
- pass
91
-
92
- def load(self):
93
- pass
94
-
95
- def awake(self):
96
- self._condition.acquire()
97
- self._condition.notify()
98
- self._condition.release()
99
-
100
- @property
101
- def logger(self):
102
- if self.owner:
103
- return self.owner.logger
104
- else:
105
- return logging.getLogger()
106
-
107
-
108
- class CronScheduler(Scheduler):
109
- """
110
- Specialized version of the scheduler that runs tasks
111
- based on a cron like configuration.
112
-
113
- The tasks are defined in a cron like format and are
114
- executed based on the current time.
115
- """
116
-
117
- def __init__(self, owner, timeout=LOOP_TIMEOUT, daemon=True):
118
- Scheduler.__init__(self, owner, timeout=timeout, daemon=daemon)
119
- self._tasks = []
120
-
121
- def tick(self, now_ts=None):
122
- current_ts = lambda: now_ts if now_ts else time.time()
123
- current_dt = lambda: (
124
- legacy.to_datetime(now_ts) if now_ts else legacy.utc_now()
125
- )
126
-
127
- timestamp = current_ts() + 5.0
128
-
129
- while True:
130
- if not self._tasks:
131
- break
132
-
133
- timestamp, task = self._tasks[0]
134
- if timestamp > current_ts():
135
- break
136
-
137
- heapq.heappop(self._tasks)
138
-
139
- if task.enabled:
140
- task.job()
141
- heapq.heappush(
142
- self._tasks, (task.next_timestamp(now=current_dt()), task)
143
- )
144
-
145
- self.timeout = max(0, timestamp - current_ts())
146
-
147
- def schedule(self, job, cron, id=None, description=None, now=None):
148
- """
149
- Schedules the provided job function for execution according
150
- to the provided cron string.
151
-
152
- The optional now parameter may be used to provide the current
153
- time reference for the scheduling operation, meaning that the next
154
- timestamp will be calculated using this value as reference.
155
-
156
- :type job: Function
157
- :param job: The function to be executed as the job.
158
- :type cron: String/SchedulerDate
159
- :param cron: The cron like string defining the schedule.
160
- :type id: String
161
- :param id: The unique identifier for the task to be created.
162
- :type description: String
163
- :param description: The description for the task to be created
164
- that describes the goal of the task.
165
- :type now: datetime
166
- :param now: Optional time reference for the job scheduling.
167
- :rtype: SchedulerTask
168
- :return: The task object that was created for the job.
169
- """
170
-
171
- task = SchedulerTask(job, cron, id=id, description=description)
172
- heapq.heappush(self._tasks, (task.next_timestamp(now=now), task))
173
- self.awake()
174
- return task
175
-
176
- def next_run(self):
177
- timestamp = self.next_timestamp()
178
- if not timestamp:
179
- return None
180
- return legacy.to_datetime(timestamp)
181
-
182
- def next_timestamp(self):
183
- if not self._tasks:
184
- return None
185
- return self._tasks[0][0]
186
-
187
-
188
- class SchedulerTask(object):
189
-
190
- def __init__(self, job, cron, id=None, description=None):
191
- self.job = job
192
- self.date = SchedulerDate.from_cron(cron)
193
- self.id = str(uuid.uuid4()) if id == None else id
194
- self.description = description
195
- self._enabled = True
196
-
197
- def __repr__(self):
198
- return "<SchedulerTask: [%s] %s, %s>" % (self.id[:-12], self.job, self.date)
199
-
200
- def __str__(self):
201
- return "<SchedulerTask: [%s] %s, %s>" % (self.id[:-12], self.job, self.date)
202
-
203
- def __eq__(self, other):
204
- if isinstance(other, self.__class__):
205
- return True
206
- return False
207
-
208
- def __lt__(self, other):
209
- return False
210
-
211
- def enable(self):
212
- self._enabled = True
213
-
214
- def disable(self):
215
- self._enabled = False
216
-
217
- def next_run(self, now=None):
218
- return self.date.next_run(now=now)
219
-
220
- def next_timestamp(self, now=None):
221
- return self.date.next_timestamp(now=now)
222
-
223
- @property
224
- def enabled(self):
225
- return self._enabled
226
-
227
- @property
228
- def cron(self):
229
- return self.date.into_cron()
230
-
231
-
232
- class SchedulerDate(object):
233
-
234
- def __init__(
235
- self, minutes="*", hours="*", days_of_month="*", months="*", days_of_week="*"
236
- ):
237
- self.minutes = self._parse_field(minutes, 0, 59)
238
- self.hours = self._parse_field(hours, 0, 23)
239
- self.days_of_month = self._parse_field(days_of_month, 1, 31)
240
- self.months = self._parse_field(months, 1, 12)
241
- self.days_of_week = self._parse_field(days_of_week, 0, 6)
242
-
243
- def __repr__(self):
244
- return "<SchedulerDate: %s, %s>" % (self.into_cron(), self.next_run())
245
-
246
- def __str__(self):
247
- return "<SchedulerDate: %s, %s>" % (self.into_cron(), self.next_run())
248
-
249
- @classmethod
250
- def from_cron(cls, cron):
251
- if isinstance(cron, cls):
252
- return cron
253
- values = (value.strip().split(",") for value in cron.split(" "))
254
- return cls(*values)
255
-
256
- def into_cron(self):
257
- return " ".join(
258
- [
259
- self._into_cron_field(self.minutes, 0, 59),
260
- self._into_cron_field(self.hours, 0, 23),
261
- self._into_cron_field(self.days_of_month, 1, 31),
262
- self._into_cron_field(self.months, 1, 12),
263
- self._into_cron_field(self.days_of_week, 0, 6),
264
- ]
265
- )
266
-
267
- def next_timestamp(self, now=None):
268
- date = self.next_run(now=now)
269
- return legacy.to_timestamp(date)
270
-
271
- def next_run(self, now=None):
272
- """
273
- Calculate the next run time starting from the current time.
274
- This operation is done respecting Cron rules.
275
-
276
- :type now: datetime
277
- :param now: Optional date time to be used as the current time.
278
- :rtype: datetime
279
- :return: The next run time respecting Cron rules.
280
- """
281
-
282
- now = now or legacy.utc_now()
283
- now_day = datetime.datetime(now.year, now.month, now.day)
284
- now_hour = datetime.datetime(now.year, now.month, now.day, hour=now.hour)
285
- now_minute = datetime.datetime(
286
- now.year, now.month, now.day, hour=now.hour, minute=now.minute
287
- )
288
-
289
- year = now.year
290
-
291
- while True:
292
- for month in sorted(self.months):
293
- if month < now.month and year < now.year:
294
- continue
295
-
296
- for day in sorted(self.days_of_month):
297
- try:
298
- date = datetime.datetime(year, month, day)
299
- except ValueError:
300
- continue
301
- if self.days_of_week and not date.weekday() in self.days_of_week:
302
- continue
303
- if date < now_day:
304
- continue
305
-
306
- for hour in sorted(self.hours):
307
- if date.replace(hour=hour) < now_hour:
308
- continue
309
-
310
- for minute in sorted(self.minutes):
311
- _date = date.replace(
312
- hour=hour, minute=minute, second=0, microsecond=0
313
- )
314
- if _date > now_minute:
315
- return _date
316
-
317
- year += 1
318
-
319
- def _parse_field(self, field, min_value, max_value):
320
- if field in ("*", ["*"], ("*",)):
321
- return set(range(min_value, max_value + 1))
322
- elif isinstance(field, (list, tuple)):
323
- return set(int(v) for v in field)
324
- else:
325
- return set((int(field),))
326
-
327
- def _into_cron_field(self, field, min_value, max_value):
328
- if field == set(range(min_value, max_value + 1)):
329
- return "*"
330
- return ",".join(str(v) for v in sorted(field))
331
-
332
-
333
- class Cron(object):
334
- pass
1
+ #!/usr/bin/python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ # Hive Appier Framework
5
+ # Copyright (c) 2008-2024 Hive Solutions Lda.
6
+ #
7
+ # This file is part of Hive Appier Framework.
8
+ #
9
+ # Hive Appier Framework is free software: you can redistribute it and/or modify
10
+ # it under the terms of the Apache License as published by the Apache
11
+ # Foundation, either version 2.0 of the License, or (at your option) any
12
+ # later version.
13
+ #
14
+ # Hive Appier Framework is distributed in the hope that it will be useful,
15
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ # Apache License for more details.
18
+ #
19
+ # You should have received a copy of the Apache License along with
20
+ # Hive Appier Framework. If not, see <http://www.apache.org/licenses/>.
21
+
22
+ """appier.scheduler
23
+
24
+ Lightweight job scheduler for periodic tasks inside an Appier app.
25
+ Wraps Python's `threading.Timer` / `sched` to run functions at given
26
+ intervals with minimal overhead. Powers cache refresh routines and
27
+ background clean-up jobs without external dependencies.
28
+ """
29
+
30
+ __author__ = "João Magalhães <joamag@hive.pt>"
31
+ """ The author(s) of the module """
32
+
33
+ __copyright__ = "Copyright (c) 2008-2024 Hive Solutions Lda."
34
+ """ The copyright for the module """
35
+
36
+ __license__ = "Apache License, Version 2.0"
37
+ """ The license for the module """
38
+
39
+ import time
40
+ import uuid
41
+ import heapq
42
+ import datetime
43
+ import logging
44
+ import threading
45
+ import traceback
46
+
47
+ from . import config
48
+ from . import legacy
49
+
50
+ LOOP_TIMEOUT = 60.0
51
+ """ The time value to be used to sleep the main sequence
52
+ loop between ticks, this value should not be too small
53
+ to spend many resources or to high to create a long set
54
+ of time between external interactions """
55
+
56
+
57
+ class Scheduler(threading.Thread):
58
+ """
59
+ Scheduler class that handles timeout based async tasks
60
+ within the context of an Appier application.
61
+
62
+ The architecture of the logic for the class should be
63
+ modular in the sense that new task may be added to
64
+ it through a queue or other external system. For that
65
+ a proper preemption mechanism should exist allowing
66
+ the scheduler to be stopped and started again.
67
+ """
68
+
69
+ def __init__(self, owner, timeout=LOOP_TIMEOUT, daemon=True):
70
+ threading.Thread.__init__(self, name="Scheduler")
71
+ self.owner = owner
72
+ self.timeout = config.conf("SCHEDULER_TIMEOUT", timeout, cast=float)
73
+ self.daemon = config.conf("SCHEDULER_DAEMON", daemon, cast=bool)
74
+ self._condition = threading.Condition()
75
+
76
+ def run(self):
77
+ self.running = True
78
+ self.load()
79
+ while self.running:
80
+ try:
81
+ self.tick()
82
+ except Exception as exception:
83
+ self.logger.critical("Unhandled scheduler exception raised")
84
+ self.logger.error(exception)
85
+ lines = traceback.format_exc().splitlines()
86
+ for line in lines:
87
+ self.logger.warning(line)
88
+ self._condition.acquire()
89
+ self._condition.wait(self.timeout)
90
+ self._condition.release()
91
+
92
+ def stop(self, awake=True):
93
+ self.running = False
94
+ if awake:
95
+ self.awake()
96
+
97
+ def tick(self):
98
+ pass
99
+
100
+ def load(self):
101
+ pass
102
+
103
+ def awake(self):
104
+ self._condition.acquire()
105
+ self._condition.notify()
106
+ self._condition.release()
107
+
108
+ @property
109
+ def logger(self):
110
+ if self.owner:
111
+ return self.owner.logger
112
+ else:
113
+ return logging.getLogger()
114
+
115
+
116
+ class CronScheduler(Scheduler):
117
+ """
118
+ Specialized version of the scheduler that runs tasks
119
+ based on a cron like configuration.
120
+
121
+ The tasks are defined in a cron like format and are
122
+ executed based on the current time.
123
+ """
124
+
125
+ def __init__(self, owner, timeout=LOOP_TIMEOUT, daemon=True):
126
+ Scheduler.__init__(self, owner, timeout=timeout, daemon=daemon)
127
+ self._tasks = []
128
+
129
+ def tick(self, now_ts=None):
130
+ current_ts = lambda: now_ts if now_ts else time.time()
131
+ current_dt = lambda: (
132
+ legacy.to_datetime(now_ts) if now_ts else legacy.utc_now()
133
+ )
134
+
135
+ timestamp = current_ts() + 5.0
136
+
137
+ while True:
138
+ if not self._tasks:
139
+ break
140
+
141
+ timestamp, task = self._tasks[0]
142
+ if timestamp > current_ts():
143
+ break
144
+
145
+ heapq.heappop(self._tasks)
146
+
147
+ if task.enabled:
148
+ task.job()
149
+ heapq.heappush(
150
+ self._tasks, (task.next_timestamp(now=current_dt()), task)
151
+ )
152
+
153
+ self.timeout = max(0, timestamp - current_ts())
154
+
155
+ def schedule(self, job, cron, id=None, description=None, now=None):
156
+ """
157
+ Schedules the provided job function for execution according
158
+ to the provided cron string.
159
+
160
+ The optional now parameter may be used to provide the current
161
+ time reference for the scheduling operation, meaning that the next
162
+ timestamp will be calculated using this value as reference.
163
+
164
+ :type job: Function
165
+ :param job: The function to be executed as the job.
166
+ :type cron: String/SchedulerDate
167
+ :param cron: The cron like string defining the schedule.
168
+ :type id: String
169
+ :param id: The unique identifier for the task to be created.
170
+ :type description: String
171
+ :param description: The description for the task to be created
172
+ that describes the goal of the task.
173
+ :type now: datetime
174
+ :param now: Optional time reference for the job scheduling.
175
+ :rtype: SchedulerTask
176
+ :return: The task object that was created for the job.
177
+ """
178
+
179
+ task = SchedulerTask(job, cron, id=id, description=description)
180
+ heapq.heappush(self._tasks, (task.next_timestamp(now=now), task))
181
+ self.awake()
182
+ return task
183
+
184
+ def next_run(self):
185
+ timestamp = self.next_timestamp()
186
+ if not timestamp:
187
+ return None
188
+ return legacy.to_datetime(timestamp)
189
+
190
+ def next_timestamp(self):
191
+ if not self._tasks:
192
+ return None
193
+ return self._tasks[0][0]
194
+
195
+
196
+ class SchedulerTask(object):
197
+
198
+ def __init__(self, job, cron, id=None, description=None):
199
+ self.job = job
200
+ self.date = SchedulerDate.from_cron(cron)
201
+ self.id = str(uuid.uuid4()) if id == None else id
202
+ self.description = description
203
+ self._enabled = True
204
+
205
+ def __repr__(self):
206
+ return "<SchedulerTask: [%s] %s, %s>" % (self.id[:-12], self.job, self.date)
207
+
208
+ def __str__(self):
209
+ return "<SchedulerTask: [%s] %s, %s>" % (self.id[:-12], self.job, self.date)
210
+
211
+ def __eq__(self, other):
212
+ if isinstance(other, self.__class__):
213
+ return True
214
+ return False
215
+
216
+ def __lt__(self, other):
217
+ return False
218
+
219
+ def enable(self):
220
+ self._enabled = True
221
+
222
+ def disable(self):
223
+ self._enabled = False
224
+
225
+ def next_run(self, now=None):
226
+ return self.date.next_run(now=now)
227
+
228
+ def next_timestamp(self, now=None):
229
+ return self.date.next_timestamp(now=now)
230
+
231
+ @property
232
+ def enabled(self):
233
+ return self._enabled
234
+
235
+ @property
236
+ def cron(self):
237
+ return self.date.into_cron()
238
+
239
+
240
+ class SchedulerDate(object):
241
+
242
+ def __init__(
243
+ self, minutes="*", hours="*", days_of_month="*", months="*", days_of_week="*"
244
+ ):
245
+ self.minutes = self._parse_field(minutes, 0, 59)
246
+ self.hours = self._parse_field(hours, 0, 23)
247
+ self.days_of_month = self._parse_field(days_of_month, 1, 31)
248
+ self.months = self._parse_field(months, 1, 12)
249
+ self.days_of_week = self._parse_field(days_of_week, 0, 6)
250
+
251
+ def __repr__(self):
252
+ return "<SchedulerDate: %s, %s>" % (self.into_cron(), self.next_run())
253
+
254
+ def __str__(self):
255
+ return "<SchedulerDate: %s, %s>" % (self.into_cron(), self.next_run())
256
+
257
+ @classmethod
258
+ def from_cron(cls, cron):
259
+ if isinstance(cron, cls):
260
+ return cron
261
+ values = (value.strip().split(",") for value in cron.split(" "))
262
+ return cls(*values)
263
+
264
+ def into_cron(self):
265
+ return " ".join(
266
+ [
267
+ self._into_cron_field(self.minutes, 0, 59),
268
+ self._into_cron_field(self.hours, 0, 23),
269
+ self._into_cron_field(self.days_of_month, 1, 31),
270
+ self._into_cron_field(self.months, 1, 12),
271
+ self._into_cron_field(self.days_of_week, 0, 6),
272
+ ]
273
+ )
274
+
275
+ def next_timestamp(self, now=None):
276
+ date = self.next_run(now=now)
277
+ return legacy.to_timestamp(date)
278
+
279
+ def next_run(self, now=None):
280
+ """
281
+ Calculate the next run time starting from the current time.
282
+ This operation is done respecting Cron rules.
283
+
284
+ :type now: datetime
285
+ :param now: Optional date time to be used as the current time.
286
+ :rtype: datetime
287
+ :return: The next run time respecting Cron rules.
288
+ """
289
+
290
+ now = now or legacy.utc_now()
291
+ now_day = datetime.datetime(now.year, now.month, now.day)
292
+ now_hour = datetime.datetime(now.year, now.month, now.day, hour=now.hour)
293
+ now_minute = datetime.datetime(
294
+ now.year, now.month, now.day, hour=now.hour, minute=now.minute
295
+ )
296
+
297
+ year = now.year
298
+
299
+ while True:
300
+ for month in sorted(self.months):
301
+ if month < now.month and year < now.year:
302
+ continue
303
+
304
+ for day in sorted(self.days_of_month):
305
+ try:
306
+ date = datetime.datetime(year, month, day)
307
+ except ValueError:
308
+ continue
309
+ if self.days_of_week and not date.weekday() in self.days_of_week:
310
+ continue
311
+ if date < now_day:
312
+ continue
313
+
314
+ for hour in sorted(self.hours):
315
+ if date.replace(hour=hour) < now_hour:
316
+ continue
317
+
318
+ for minute in sorted(self.minutes):
319
+ _date = date.replace(
320
+ hour=hour, minute=minute, second=0, microsecond=0
321
+ )
322
+ if _date > now_minute:
323
+ return _date
324
+
325
+ year += 1
326
+
327
+ def _parse_field(self, field, min_value, max_value):
328
+ if field in ("*", ["*"], ("*",)):
329
+ return set(range(min_value, max_value + 1))
330
+ elif isinstance(field, (list, tuple)):
331
+ return set(int(v) for v in field)
332
+ else:
333
+ return set((int(field),))
334
+
335
+ def _into_cron_field(self, field, min_value, max_value):
336
+ if field == set(range(min_value, max_value + 1)):
337
+ return "*"
338
+ return ",".join(str(v) for v in sorted(field))
339
+
340
+
341
+ class Cron(object):
342
+ pass