clockwork 0.1.0__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.
- clockwork/__init__.py +4 -0
- clockwork/chronicle.py +97 -0
- clockwork/clockwork.py +780 -0
- clockwork/taskmaster.py +537 -0
- clockwork-0.1.0.dist-info/LICENSE +21 -0
- clockwork-0.1.0.dist-info/METADATA +26 -0
- clockwork-0.1.0.dist-info/RECORD +8 -0
- clockwork-0.1.0.dist-info/WHEEL +4 -0
clockwork/taskmaster.py
ADDED
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
from clockwork import Date, elapsed_time
|
|
2
|
+
from iterlab import to_iter
|
|
3
|
+
from pathpilot import Folder
|
|
4
|
+
from clockwork.chronicle import Logger
|
|
5
|
+
from contextlib import redirect_stdout
|
|
6
|
+
from schedule import Scheduler, CancelJob
|
|
7
|
+
import datetime
|
|
8
|
+
import time
|
|
9
|
+
import uuid
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
#+---------------------------------------------------------------------------+
|
|
16
|
+
# Classes
|
|
17
|
+
#+---------------------------------------------------------------------------+
|
|
18
|
+
|
|
19
|
+
class PrerequisiteError(Exception):
|
|
20
|
+
''' exception used to indicate a prerequisite condition has not been met '''
|
|
21
|
+
def __init__(self, message):
|
|
22
|
+
super().__init__(message)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ContinueFailedJob(object):
|
|
27
|
+
''' can be returned to continue running a failed job '''
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SmartScheduler(Scheduler):
|
|
33
|
+
'''
|
|
34
|
+
Description
|
|
35
|
+
--------------------
|
|
36
|
+
Custom implementation of schedule.Scheduler designed to interact with SmartJob objects.
|
|
37
|
+
This gives the user complete control over the behavior of individual jobs.
|
|
38
|
+
'''
|
|
39
|
+
|
|
40
|
+
def __init__(self):
|
|
41
|
+
super().__init__()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _run_job(self, job):
|
|
45
|
+
ret = job.run()
|
|
46
|
+
if ret is CancelJob:
|
|
47
|
+
self.cancel_job(job)
|
|
48
|
+
elif ret is ContinueFailedJob:
|
|
49
|
+
job.last_run = datetime.datetime.now()
|
|
50
|
+
job._schedule_next_run()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class SmartJob(object):
|
|
55
|
+
'''
|
|
56
|
+
Description
|
|
57
|
+
--------------------
|
|
58
|
+
job object designed to be used in conjunction with SmartScheduler as the 'job' argument
|
|
59
|
+
in self._run_job()
|
|
60
|
+
|
|
61
|
+
Class Attributes
|
|
62
|
+
--------------------
|
|
63
|
+
verbose : bool
|
|
64
|
+
If True, class content is printed to console.
|
|
65
|
+
disable_print : bool
|
|
66
|
+
If True, printing is suppressed while func runs. Must be True if verbose is True.
|
|
67
|
+
cascade_status : str
|
|
68
|
+
cascade status
|
|
69
|
+
|
|
70
|
+
Instance Attributes
|
|
71
|
+
--------------------
|
|
72
|
+
name : str
|
|
73
|
+
name of job
|
|
74
|
+
at : str
|
|
75
|
+
at time string
|
|
76
|
+
expiry : Date
|
|
77
|
+
If not None, job will be set inactive and stop running after this datetime
|
|
78
|
+
func : func
|
|
79
|
+
function to run
|
|
80
|
+
args : tuple
|
|
81
|
+
func arguments
|
|
82
|
+
kwargs : dict
|
|
83
|
+
func key word arguments
|
|
84
|
+
cancel_on_failure : bool
|
|
85
|
+
If True, the job will be cancelled if it raises an exception.
|
|
86
|
+
cancel_on_completion : bool
|
|
87
|
+
If True, the job will be cancelled if it completed successfully (i.e. job will run only once).
|
|
88
|
+
notify_on_failure : bool
|
|
89
|
+
If True, an email notification is sent to my inbox that includes the traceback. cancel_on_failure
|
|
90
|
+
and cascade are automatically set to True if this argument is True (prevents endless spam).
|
|
91
|
+
restrict_to_business_hours : bool
|
|
92
|
+
If True, the job will only execute during business hours. This is a more restrictive version of restrict_to_business_days.
|
|
93
|
+
restrict_to_business_days : bool
|
|
94
|
+
If True, the job will only execute during weekdays (i.e. not weekends).
|
|
95
|
+
cascade : bool
|
|
96
|
+
If True, when the job (denoted by the 'name' argument) is reflected multiple times in the jobs table
|
|
97
|
+
due to having multiple 'at' values, changes in activiation in one will cascade to all others. For example,
|
|
98
|
+
consider the job named 'my job' which is scheduled at 8:00 AM and 5:00 PM that is cancelled on completion.
|
|
99
|
+
If the 8:00 AM completes successfully then the job with that 'at' time will be set to inactive and have its
|
|
100
|
+
status updated in the table accordingly. Under default behavior, the 5:00 PM run will be unaffected by the
|
|
101
|
+
completion of the 8:00 AM run, however, if cascade is set to True then the 5:00 PM run will also receive
|
|
102
|
+
the same updates.
|
|
103
|
+
attempts : int
|
|
104
|
+
If greater than 1, the job will be attempted this number of times before being cancelled.
|
|
105
|
+
status : str | None
|
|
106
|
+
current status of the job
|
|
107
|
+
'''
|
|
108
|
+
|
|
109
|
+
verbose = True
|
|
110
|
+
disable_print = True
|
|
111
|
+
cascade_status = 'cancelled on cascade'
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def __init__(self, name, at, expiry, func, args=(), kwargs={}, cancel_on_failure=False, cancel_on_completion=False,
|
|
115
|
+
notify_on_failure=False, restrict_to_business_hours=False, restrict_to_business_days=False, cascade=True,
|
|
116
|
+
attempts=1):
|
|
117
|
+
|
|
118
|
+
if self.verbose and not self.disable_print:
|
|
119
|
+
raise NotImplementedError('if SmartJob.verbose is True then disable_print must be True to suppress intra-function prints.')
|
|
120
|
+
|
|
121
|
+
self.name = name
|
|
122
|
+
self.at = at
|
|
123
|
+
self.expiry = expiry
|
|
124
|
+
self.func = func
|
|
125
|
+
self.args = args
|
|
126
|
+
self.kwargs = kwargs
|
|
127
|
+
self.cancel_on_failure = cancel_on_failure
|
|
128
|
+
self.cancel_on_completion = cancel_on_completion
|
|
129
|
+
self.notify_on_failure = notify_on_failure
|
|
130
|
+
self.cascade = cascade
|
|
131
|
+
self.restrict_to_business_hours = restrict_to_business_hours
|
|
132
|
+
self.restrict_to_business_days = restrict_to_business_days
|
|
133
|
+
self.attempts = attempts
|
|
134
|
+
self.status = None
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def update_table(self, active):
|
|
139
|
+
|
|
140
|
+
def exe(sql, status):
|
|
141
|
+
TaskMaster.db.execute(sql, (active, status, self.name, self.at))
|
|
142
|
+
|
|
143
|
+
sql = self.db.update_query(
|
|
144
|
+
tbl_name='jobs',
|
|
145
|
+
update_cols=['active','status'],
|
|
146
|
+
where_cols=['name','at']
|
|
147
|
+
)
|
|
148
|
+
exe(sql, self.status)
|
|
149
|
+
|
|
150
|
+
if self.cascade:
|
|
151
|
+
exe(sql.replace('[at] = ?', '[at] <> ?'), self.cascade_status)
|
|
152
|
+
for name, at in TaskMaster.jobs.keys():
|
|
153
|
+
if name == self.name:
|
|
154
|
+
TaskMaster.jobs[name, at].status = self.cascade_status
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def send_email_notification(self):
|
|
159
|
+
raise NotImplementedError
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def __call__(self):
|
|
164
|
+
|
|
165
|
+
logger = getattr(TaskMaster.logger, 'logger', None)
|
|
166
|
+
|
|
167
|
+
def update_status(status, set_inactive=False):
|
|
168
|
+
self.status = status
|
|
169
|
+
if logger: logger.info(f"{self.name} status update: '{self.status}'")
|
|
170
|
+
self.update_table(active=0 if set_inactive else 1)
|
|
171
|
+
|
|
172
|
+
if self.expiry and self.expiry < Date():
|
|
173
|
+
update_status('cancelled on expiration', set_inactive=True)
|
|
174
|
+
if self.verbose:
|
|
175
|
+
print('Killing', end=' ')
|
|
176
|
+
print(self.name, end='')
|
|
177
|
+
print(f' @ {Date()} ->', end=' ')
|
|
178
|
+
print('Job Cancelled')
|
|
179
|
+
|
|
180
|
+
# cancel job if it was cancelled on cascasde or if it was set inactive after being scheduled
|
|
181
|
+
if self.status == self.cascade_status or not TaskMaster.is_active(self.name, self.at):
|
|
182
|
+
return CancelJob
|
|
183
|
+
|
|
184
|
+
while True:
|
|
185
|
+
try:
|
|
186
|
+
start = time.time()
|
|
187
|
+
if logger: logger.info(f'{self.name} start')
|
|
188
|
+
|
|
189
|
+
if self.verbose:
|
|
190
|
+
print('Running', end=' ')
|
|
191
|
+
print(self.name, end='')
|
|
192
|
+
print(f' @ {Date()} ->', end=' ')
|
|
193
|
+
|
|
194
|
+
if self.restrict_to_business_hours and not Date().is_business_hours:
|
|
195
|
+
raise PrerequisiteError('cannot execute outside of business hours')
|
|
196
|
+
|
|
197
|
+
if self.restrict_to_business_days and not Date().is_business_day:
|
|
198
|
+
raise PrerequisiteError('cannot execute during the weekend')
|
|
199
|
+
|
|
200
|
+
if self.disable_print:
|
|
201
|
+
with redirect_stdout(open(os.devnull, 'w')):
|
|
202
|
+
out = self.func(*self.args, **self.kwargs)
|
|
203
|
+
else:
|
|
204
|
+
out = self.func(*self.args, **self.kwargs)
|
|
205
|
+
|
|
206
|
+
if self.verbose:
|
|
207
|
+
print(f'Complete {elapsed_time(time.time() - start)}', end='')
|
|
208
|
+
print(' (Job Cancelled)') if self.cancel_on_completion else print('')
|
|
209
|
+
|
|
210
|
+
if logger: logger.info(f'{self.name} complete')
|
|
211
|
+
|
|
212
|
+
if self.cancel_on_completion:
|
|
213
|
+
update_status('cancelled on completion', set_inactive=True)
|
|
214
|
+
|
|
215
|
+
return CancelJob if self.cancel_on_completion else out
|
|
216
|
+
|
|
217
|
+
except Exception as e:
|
|
218
|
+
|
|
219
|
+
if isinstance(e, PrerequisiteError):
|
|
220
|
+
if self.verbose: print(f'PrerequisiteError: {e}')
|
|
221
|
+
if logger: logger.exception('see traceback below')
|
|
222
|
+
return ContinueFailedJob
|
|
223
|
+
|
|
224
|
+
if self.attempts > 0: self.attempts -= 1
|
|
225
|
+
self.cancel_on_failure = True if self.attempts == 0 else False
|
|
226
|
+
|
|
227
|
+
# wait 5 seconds then try again
|
|
228
|
+
if self.attempts > 0:
|
|
229
|
+
time.sleep(5)
|
|
230
|
+
continue
|
|
231
|
+
|
|
232
|
+
if self.verbose:
|
|
233
|
+
if self.cancel_on_failure:
|
|
234
|
+
print(f'Job Cancelled: {e}')
|
|
235
|
+
else:
|
|
236
|
+
print(f'Failed: {e}')
|
|
237
|
+
|
|
238
|
+
if logger: logger.exception('see traceback below')
|
|
239
|
+
|
|
240
|
+
if self.cancel_on_failure:
|
|
241
|
+
update_status('cancelled on failure', set_inactive=False)
|
|
242
|
+
|
|
243
|
+
if self.notify_on_failure:
|
|
244
|
+
self.send_email_notification()
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
return CancelJob if self.cancel_on_failure else ContinueFailedJob
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class TaskMaster(object):
|
|
252
|
+
'''
|
|
253
|
+
Description
|
|
254
|
+
--------------------
|
|
255
|
+
Class provides a user-friendly means of using the SmartScheduler implementation of
|
|
256
|
+
schedule.Scheduler to schedule SmartJobs
|
|
257
|
+
|
|
258
|
+
Class Attributes
|
|
259
|
+
--------------------
|
|
260
|
+
db : SQLiteFile
|
|
261
|
+
database
|
|
262
|
+
smart_scheduler : SmartScheduler
|
|
263
|
+
SmartScheduler instance
|
|
264
|
+
jobs : dict
|
|
265
|
+
...
|
|
266
|
+
logger : None | Logger
|
|
267
|
+
logger
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
Instance Attributes
|
|
271
|
+
--------------------
|
|
272
|
+
None
|
|
273
|
+
'''
|
|
274
|
+
|
|
275
|
+
jobs = {}
|
|
276
|
+
logger = None
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
@classmethod
|
|
280
|
+
def set_inactive(cls, name, status=None):
|
|
281
|
+
cls.db.execute(
|
|
282
|
+
'UPDATE jobs SET active = ?, status = ? WHERE name = ?',
|
|
283
|
+
(0, status or 'manual intervention', name)
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
@classmethod
|
|
288
|
+
def clear_all(cls):
|
|
289
|
+
cls.clear_table()
|
|
290
|
+
if cls.logger is not None:
|
|
291
|
+
cls.logger.clear()
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@classmethod
|
|
295
|
+
def build_table(cls):
|
|
296
|
+
'''
|
|
297
|
+
Columns
|
|
298
|
+
----------
|
|
299
|
+
name : str
|
|
300
|
+
SmartJob.name
|
|
301
|
+
at : str
|
|
302
|
+
SmartJob.at
|
|
303
|
+
active : binary
|
|
304
|
+
If 1, the job is active and able to be scheduled.
|
|
305
|
+
If 0, the job is inactive and unable to be scheduled. This value is obtained via one of the following avenues
|
|
306
|
+
1) job completed succesfully and was cancelled
|
|
307
|
+
2) job obained this status via cascade from a job that satisfied criteria in 1)
|
|
308
|
+
3) job was manually set inactive via TaskMaster.set_inactive method
|
|
309
|
+
expiry : str
|
|
310
|
+
SmartJob.expiry
|
|
311
|
+
status : str
|
|
312
|
+
SmartJob.status
|
|
313
|
+
'''
|
|
314
|
+
|
|
315
|
+
sql = """
|
|
316
|
+
CREATE TABLE IF NOT EXISTS
|
|
317
|
+
jobs (
|
|
318
|
+
name TEXT,
|
|
319
|
+
at TEXT,
|
|
320
|
+
active INTEGER,
|
|
321
|
+
status TEXT,
|
|
322
|
+
expiry TEXT,
|
|
323
|
+
PRIMARY KEY(name, at)
|
|
324
|
+
)"""
|
|
325
|
+
|
|
326
|
+
cls.db.execute(sql)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
@classmethod
|
|
331
|
+
def clear_table(cls, warn=False):
|
|
332
|
+
cls.db.clear_tables(warn=warn)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
@classmethod
|
|
337
|
+
def add(cls, func, every=None, at=None, interval=1, start=None, expiry=None, **kwargs):
|
|
338
|
+
'''
|
|
339
|
+
Description
|
|
340
|
+
------------
|
|
341
|
+
schedule a job
|
|
342
|
+
|
|
343
|
+
https://schedule.readthedocs.io/en/stable/
|
|
344
|
+
schedule.every(10).minutes.do(job)
|
|
345
|
+
schedule.every().hour.do(job)
|
|
346
|
+
schedule.every().day.at('10:30').do(job)
|
|
347
|
+
schedule.every().monday.do(job)
|
|
348
|
+
schedule.every().wednesday.at('13:15').do(job)
|
|
349
|
+
schedule.every().minute.at(':17').do(job)
|
|
350
|
+
|
|
351
|
+
Parameters
|
|
352
|
+
------------
|
|
353
|
+
func : func
|
|
354
|
+
SmartJob func argument
|
|
355
|
+
every : str
|
|
356
|
+
string representation of schedule.Job property (e.g. 'minutes', 'hour', 'day' etc.).
|
|
357
|
+
If None and cancel_on_completion kwarg is True, every and interval will be set to
|
|
358
|
+
'second' and 1, respectively, so that the job will run ASAP once the 'start' criteria
|
|
359
|
+
has been met (if applicable).
|
|
360
|
+
at : str | iter
|
|
361
|
+
time_str argument passed to schedule.Job.at(time_str). Times may be passed in
|
|
362
|
+
'%I:%M%p' format (e.g ['06:15 AM', '12:15 PM', '06:15 PM']). If argument is an
|
|
363
|
+
iterable then the job will be scheduled at each constituent time.
|
|
364
|
+
interval : int
|
|
365
|
+
schedule.Scheduler.every interval argument
|
|
366
|
+
start : Date
|
|
367
|
+
If not None, job will be not be added until this datetime
|
|
368
|
+
expiry : Date
|
|
369
|
+
If not None, job will be set inactive and stop running after this datetime
|
|
370
|
+
kwargs : keyword arguments
|
|
371
|
+
keyword arguments passed to SmartJob.__init__
|
|
372
|
+
|
|
373
|
+
Returns
|
|
374
|
+
------------
|
|
375
|
+
None
|
|
376
|
+
'''
|
|
377
|
+
|
|
378
|
+
if not hasattr(cls, 'db'):
|
|
379
|
+
cls.db = Folder().parent.join('Data', 'SQLite', read_only=False).join('taskmaster.sqlite')
|
|
380
|
+
cls.db.connect()
|
|
381
|
+
cls.db.enable_foreign_keys()
|
|
382
|
+
|
|
383
|
+
if not hasattr(cls, 'smart_scheduler'):
|
|
384
|
+
cls.smart_scheduler = SmartScheduler()
|
|
385
|
+
|
|
386
|
+
now = Date()
|
|
387
|
+
if (start and start > now) or (expiry and expiry < now): return
|
|
388
|
+
expiry_str = expiry.dt.strftime('%Y-%m-%d %I:%M:%S.{} %p')\
|
|
389
|
+
.format('%03d' % (expiry.dt.microsecond / 1000))\
|
|
390
|
+
if expiry else None
|
|
391
|
+
|
|
392
|
+
cls.build_table()
|
|
393
|
+
name = kwargs.pop('name', f'Unnamed {uuid.uuid4().hex}')
|
|
394
|
+
|
|
395
|
+
if every is None:
|
|
396
|
+
if kwargs.get('cancel_on_completion'):
|
|
397
|
+
every, interval = 'second', 1
|
|
398
|
+
else:
|
|
399
|
+
raise Exception("'every' argument cannot be None")
|
|
400
|
+
|
|
401
|
+
for at in to_iter(at):
|
|
402
|
+
job = getattr(cls.smart_scheduler.every(interval), every)
|
|
403
|
+
if at is not None:
|
|
404
|
+
try:
|
|
405
|
+
time_str = datetime.datetime.strptime(
|
|
406
|
+
at.upper().replace(' ',''), '%I:%M%p'
|
|
407
|
+
).strftime('%H:%M')
|
|
408
|
+
except:
|
|
409
|
+
time_str = at
|
|
410
|
+
job = job.at(time_str)
|
|
411
|
+
else:
|
|
412
|
+
at = 'N/A'
|
|
413
|
+
|
|
414
|
+
# if already scheduled or inactive then do not schedule
|
|
415
|
+
if (name, at) in cls.jobs or not cls.is_active(name, at): continue
|
|
416
|
+
cls.db.insert('jobs', (name, at, 1, None, expiry_str))
|
|
417
|
+
cls.jobs[name, at] = SmartJob(name, at, expiry, func, **kwargs)
|
|
418
|
+
job.do(cls.jobs[name, at])
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
@classmethod
|
|
422
|
+
def run(cls, wait=0):
|
|
423
|
+
while True:
|
|
424
|
+
cls.smart_scheduler.run_pending()
|
|
425
|
+
time.sleep(wait)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
@classmethod
|
|
429
|
+
def is_active(cls, name, at=None):
|
|
430
|
+
where_cols, params = ['name'], [name]
|
|
431
|
+
if at is not None:
|
|
432
|
+
where_cols.append('at')
|
|
433
|
+
params.append(at)
|
|
434
|
+
sql = cls.db.select_query(
|
|
435
|
+
tbl_name='jobs',
|
|
436
|
+
select_cols='active',
|
|
437
|
+
where_cols=where_cols
|
|
438
|
+
)
|
|
439
|
+
try:
|
|
440
|
+
active = int(cls.db.c.execute(sql, tuple(params)).fetchone()[0])
|
|
441
|
+
if active == 0: return False
|
|
442
|
+
except:
|
|
443
|
+
pass
|
|
444
|
+
return True
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
class FileMonitor(object):
|
|
449
|
+
'''
|
|
450
|
+
Description
|
|
451
|
+
--------------------
|
|
452
|
+
Monitors a folder for new files and passes latest and 2nd latest file names to user-defined function.
|
|
453
|
+
Class is intended to be used in conjunction with TaskMaster as a func argument which allows for file
|
|
454
|
+
monitoring at regular intervals.
|
|
455
|
+
|
|
456
|
+
Class Attributes
|
|
457
|
+
--------------------
|
|
458
|
+
None
|
|
459
|
+
|
|
460
|
+
Instance Attributes
|
|
461
|
+
--------------------
|
|
462
|
+
func : func
|
|
463
|
+
Custom function that takes the latest and second-latest file names in a folder as the first and second arguments, respectively.
|
|
464
|
+
folder : Folder object
|
|
465
|
+
folder to monitor for new files.
|
|
466
|
+
pick_file_kwargs : dict
|
|
467
|
+
Key word arguments for file_tools.folder.pick_file
|
|
468
|
+
verbose : bool
|
|
469
|
+
If True, information is printed to the console.
|
|
470
|
+
latest_file : File
|
|
471
|
+
the latest file
|
|
472
|
+
'''
|
|
473
|
+
|
|
474
|
+
def __init__(self, func, folder, pick_file_kwargs={}, verbose=False):
|
|
475
|
+
self.func = func
|
|
476
|
+
self.folder = folder
|
|
477
|
+
self.pick_file_kwargs = pick_file_kwargs
|
|
478
|
+
self.verbose = verbose
|
|
479
|
+
self.latest_file = self.pick_file()
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def pick_file(self):
|
|
483
|
+
return self.folder.pick_file(**self.pick_file_kwargs)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def __call__(self):
|
|
487
|
+
latest_file = self.pick_file()
|
|
488
|
+
|
|
489
|
+
if latest_file != self.latest_file:
|
|
490
|
+
if self.verbose:
|
|
491
|
+
print('new file detected:')
|
|
492
|
+
print('\t*', latest_file)
|
|
493
|
+
print('\t*', self.latest_file)
|
|
494
|
+
print()
|
|
495
|
+
|
|
496
|
+
self.func(latest_file, self.latest_file)
|
|
497
|
+
self.latest_file = latest_file
|
|
498
|
+
else:
|
|
499
|
+
raise PrerequisiteError('no new files have been detected')
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def ad_hoc(self):
|
|
503
|
+
''' runs self.func using latest and 2nd latest file '''
|
|
504
|
+
new_file = self.pick_file()
|
|
505
|
+
|
|
506
|
+
kwargs = self.pick_file_kwargs.copy()
|
|
507
|
+
kwargs['func'] = 1
|
|
508
|
+
prior_file = self.folder.pick_file(**kwargs)
|
|
509
|
+
|
|
510
|
+
if self.verbose:
|
|
511
|
+
print('Ad hoc files:')
|
|
512
|
+
print('\t*', new_file)
|
|
513
|
+
print('\t*', prior_file)
|
|
514
|
+
print()
|
|
515
|
+
|
|
516
|
+
self.func(new_file, prior_file)
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
if __name__ == '__main__':
|
|
521
|
+
|
|
522
|
+
def test_func():
|
|
523
|
+
time.sleep(3)
|
|
524
|
+
|
|
525
|
+
TaskMaster.logger = Logger('TaskMaster')
|
|
526
|
+
|
|
527
|
+
TaskMaster.add(
|
|
528
|
+
func=test_func,
|
|
529
|
+
every='seconds',
|
|
530
|
+
at=None,
|
|
531
|
+
interval=10,
|
|
532
|
+
start=None,
|
|
533
|
+
expiry=None,
|
|
534
|
+
# **kwargs
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
TaskMaster.run()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Zachary Einck
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: clockwork
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Toolkit for time-related operations including scheduling, logging, date manipulation, and more.
|
|
5
|
+
Home-page: https://github.com/zteinck/clockwork
|
|
6
|
+
License: MIT
|
|
7
|
+
Author: Zachary Einck
|
|
8
|
+
Author-email: zacharyeinck@gmail.com
|
|
9
|
+
Requires-Python: >=3.8,<4.0
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Requires-Dist: holidays
|
|
18
|
+
Requires-Dist: iterlab
|
|
19
|
+
Requires-Dist: pathpilot
|
|
20
|
+
Requires-Dist: schedule
|
|
21
|
+
Requires-Dist: textwrap3
|
|
22
|
+
Project-URL: Repository, https://github.com/zteinck/clockwork
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# clockwork
|
|
26
|
+
`clockwork` is a library that provides a multitude of time-related functionalities, facilitating tasks such as scheduling, logging, date manipulation, and more.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
clockwork/__init__.py,sha256=CRl9a6xE4YxLsdnLTKHDIGmsyaysRFFYyNVJLGsvfnE,104
|
|
2
|
+
clockwork/chronicle.py,sha256=-BRa62T-a_DQN12hSzDcVlpb0-y-vGLa_XljzReEq28,3019
|
|
3
|
+
clockwork/clockwork.py,sha256=MEfWPGA1oEINJGT7HBDNPm8tVGGrpJFCC1KeBjrkYG4,23755
|
|
4
|
+
clockwork/taskmaster.py,sha256=g33otNzVtLLi4Dadyit3A7QRabPFxcJjOgN5n7tnU8I,18025
|
|
5
|
+
clockwork-0.1.0.dist-info/LICENSE,sha256=e2vk_CZyvQY50wY_UPKw03gYPnT8BV9nG2lgmPfUlnU,1091
|
|
6
|
+
clockwork-0.1.0.dist-info/METADATA,sha256=XPr1J1nyXCXX3LT7z7pUwBGJwaaGW-7AbJHgl7B-SPM,1055
|
|
7
|
+
clockwork-0.1.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
8
|
+
clockwork-0.1.0.dist-info/RECORD,,
|