clockwork 0.2.0__tar.gz → 0.2.2__tar.gz
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-0.2.0 → clockwork-0.2.2}/PKG-INFO +2 -2
- clockwork-0.2.2/clockwork/__init__.py +4 -0
- {clockwork-0.2.0 → clockwork-0.2.2}/clockwork/_base.py +55 -10
- {clockwork-0.2.0 → clockwork-0.2.2}/clockwork/_month_end.py +29 -4
- {clockwork-0.2.0 → clockwork-0.2.2}/clockwork/_quarter_end.py +18 -5
- {clockwork-0.2.0 → clockwork-0.2.2}/clockwork/core.py +57 -35
- clockwork-0.2.2/clockwork/decorators.py +18 -0
- clockwork-0.2.2/clockwork/taskmaster/_scheduler.py +27 -0
- {clockwork-0.2.0 → clockwork-0.2.2}/clockwork/taskmaster/_task.py +26 -18
- {clockwork-0.2.0 → clockwork-0.2.2}/clockwork/taskmaster/file_monitor.py +6 -6
- {clockwork-0.2.0 → clockwork-0.2.2}/clockwork/taskmaster/logger.py +10 -6
- {clockwork-0.2.0 → clockwork-0.2.2}/clockwork/taskmaster/taskmaster.py +30 -17
- {clockwork-0.2.0 → clockwork-0.2.2}/clockwork/utils.py +1 -21
- {clockwork-0.2.0 → clockwork-0.2.2}/pyproject.toml +2 -2
- clockwork-0.2.0/clockwork/__init__.py +0 -4
- clockwork-0.2.0/clockwork/taskmaster/_scheduler.py +0 -20
- {clockwork-0.2.0 → clockwork-0.2.2}/LICENSE +0 -0
- {clockwork-0.2.0 → clockwork-0.2.2}/README.md +0 -0
- {clockwork-0.2.0 → clockwork-0.2.2}/clockwork/taskmaster/__init__.py +0 -0
- {clockwork-0.2.0 → clockwork-0.2.2}/clockwork/taskmaster/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: clockwork
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Toolkit for time-related operations including scheduling, logging, date manipulation, and more.
|
|
5
5
|
Home-page: https://github.com/zteinck/clockwork
|
|
6
6
|
License: MIT
|
|
@@ -15,8 +15,8 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
17
17
|
Requires-Dist: holidays
|
|
18
|
-
Requires-Dist: iterlab
|
|
19
18
|
Requires-Dist: numpy
|
|
19
|
+
Requires-Dist: oddments
|
|
20
20
|
Requires-Dist: pandas
|
|
21
21
|
Requires-Dist: pathpilot
|
|
22
22
|
Requires-Dist: schedule
|
|
@@ -7,7 +7,6 @@ import pandas as pd
|
|
|
7
7
|
import numpy as np
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
|
|
11
10
|
class DateBase(object):
|
|
12
11
|
'''
|
|
13
12
|
Description
|
|
@@ -71,7 +70,8 @@ class DateBase(object):
|
|
|
71
70
|
Description
|
|
72
71
|
----------
|
|
73
72
|
Returns the # of days in a month for a given year.
|
|
74
|
-
For example, if year=2024 and month=2, 29 days is returned since
|
|
73
|
+
For example, if year=2024 and month=2, 29 days is returned since
|
|
74
|
+
it's a leap year.
|
|
75
75
|
|
|
76
76
|
Parameters
|
|
77
77
|
----------
|
|
@@ -172,15 +172,18 @@ class DateBase(object):
|
|
|
172
172
|
|
|
173
173
|
def wrapper(self, weekday, delta=0):
|
|
174
174
|
'''
|
|
175
|
-
returns DateBase object representing the next or last day of
|
|
175
|
+
returns DateBase object representing the next or last day of
|
|
176
|
+
the week relative to self. For example self.next('Mon')
|
|
176
177
|
would return the date of the following monday.
|
|
177
178
|
|
|
178
179
|
Attributes
|
|
179
180
|
-----------------------
|
|
180
181
|
weekday : str
|
|
181
|
-
Day of the week either fully spelled out or the first three
|
|
182
|
+
Day of the week either fully spelled out or the first three
|
|
183
|
+
characters (e.g. 'Friday' or 'Fri') not case-sensitive.
|
|
182
184
|
delta : int
|
|
183
|
-
Offset value +/- from the current week (e.g. 0 is the current
|
|
185
|
+
Offset value +/- from the current week (e.g. 0 is the current
|
|
186
|
+
week and -1 is last week).
|
|
184
187
|
'''
|
|
185
188
|
day, desired_day = self.dt.weekday(), self.weekdays[weekday.title()]
|
|
186
189
|
delta += func(self, day - desired_day)
|
|
@@ -198,104 +201,132 @@ class DateBase(object):
|
|
|
198
201
|
''' returns normalized instance '''
|
|
199
202
|
return self.normalize()
|
|
200
203
|
|
|
204
|
+
|
|
201
205
|
@property
|
|
202
206
|
def d(self):
|
|
203
207
|
''' converts datetime.datetime to datetime.date '''
|
|
204
208
|
return self.datetime.date()
|
|
205
209
|
|
|
210
|
+
|
|
206
211
|
@property
|
|
207
212
|
def dt(self):
|
|
208
213
|
''' datetime alias '''
|
|
209
214
|
return self.datetime
|
|
210
215
|
|
|
216
|
+
|
|
211
217
|
@property
|
|
212
218
|
def pandas(self):
|
|
213
219
|
''' pandas format '''
|
|
214
220
|
return pd.to_datetime(self.dt)
|
|
215
221
|
|
|
222
|
+
|
|
216
223
|
@property
|
|
217
224
|
def ymd(self):
|
|
218
225
|
''' string in YYYY-MM-DD format '''
|
|
219
226
|
return self.str('%Y-%m-%d')
|
|
220
227
|
|
|
228
|
+
|
|
221
229
|
@property
|
|
222
230
|
def sql_server(self):
|
|
223
231
|
''' ymd alias '''
|
|
224
232
|
return self.ymd
|
|
225
233
|
|
|
234
|
+
|
|
226
235
|
@property
|
|
227
236
|
def oracle(self):
|
|
228
237
|
''' string in DD-%b-YY (e.g. 30-Sep-19) format '''
|
|
229
238
|
return self.str('%d-%b-%y')
|
|
230
239
|
|
|
240
|
+
|
|
231
241
|
@property
|
|
232
242
|
def timestamp(self):
|
|
233
243
|
''' integer '''
|
|
234
244
|
return self.to_timestamp(self.dt)
|
|
235
245
|
|
|
246
|
+
|
|
236
247
|
@property
|
|
237
248
|
def year(self):
|
|
238
249
|
return self.dt.year
|
|
239
250
|
|
|
251
|
+
|
|
240
252
|
@property
|
|
241
253
|
def month(self):
|
|
242
254
|
return self.dt.month
|
|
243
255
|
|
|
256
|
+
|
|
244
257
|
@property
|
|
245
258
|
def month_name(self):
|
|
246
259
|
return self.dt.strftime('%B')
|
|
247
260
|
|
|
261
|
+
|
|
248
262
|
@property
|
|
249
263
|
def day(self):
|
|
250
264
|
return self.dt.day
|
|
251
265
|
|
|
266
|
+
|
|
252
267
|
@property
|
|
253
268
|
def yesterday(self):
|
|
254
269
|
return self - 1
|
|
255
270
|
|
|
271
|
+
|
|
256
272
|
@property
|
|
257
273
|
def tomorrow(self):
|
|
258
274
|
return self + 1
|
|
259
275
|
|
|
276
|
+
|
|
260
277
|
@property
|
|
261
278
|
def month_start(self):
|
|
262
279
|
''' return date object representing the first day of the month '''
|
|
263
280
|
return self.spawn(datetime.datetime(self.year, self.month, 1))
|
|
264
281
|
|
|
282
|
+
|
|
265
283
|
@property
|
|
266
284
|
def month_end(self):
|
|
267
285
|
''' last day of the month as a MonthEnd instance '''
|
|
268
286
|
return self.last_day_of_month(self.year, self.month)
|
|
269
287
|
|
|
288
|
+
|
|
289
|
+
@property
|
|
290
|
+
def is_month_end(self):
|
|
291
|
+
''' returns True if date aligns with a month-end date '''
|
|
292
|
+
return self.ymd == self.month_end.ymd
|
|
293
|
+
|
|
294
|
+
|
|
270
295
|
@property
|
|
271
296
|
def last_business_day_of_month(self):
|
|
272
297
|
dt = self.month_end
|
|
273
298
|
dt -= {'Saturday': 1, 'Sunday': 2}.get(dt.weekday, 0)
|
|
274
299
|
return dt
|
|
275
300
|
|
|
301
|
+
|
|
276
302
|
@property
|
|
277
303
|
def weekday(self):
|
|
278
304
|
return self.str('%A')
|
|
279
305
|
|
|
306
|
+
|
|
280
307
|
@property
|
|
281
308
|
def weekday_short(self):
|
|
282
309
|
return self.str('%a')
|
|
283
310
|
|
|
311
|
+
|
|
284
312
|
@property
|
|
285
313
|
def is_weekend(self):
|
|
286
314
|
''' returns True if date does not fall on a weekend '''
|
|
287
315
|
return self.weekday in ('Saturday','Sunday')
|
|
288
316
|
|
|
317
|
+
|
|
289
318
|
@property
|
|
290
319
|
def is_holiday(self):
|
|
291
320
|
''' returns True if date is a U.S. holiday '''
|
|
292
321
|
return self.ymd in self.holidays
|
|
293
322
|
|
|
323
|
+
|
|
294
324
|
@property
|
|
295
325
|
def is_business_day(self):
|
|
296
326
|
''' returns True if date does not fall on a weekend '''
|
|
297
327
|
return not (self.is_weekend or self.is_holiday)
|
|
298
328
|
|
|
329
|
+
|
|
299
330
|
@property
|
|
300
331
|
def is_business_hours(self):
|
|
301
332
|
''' returns True if date is within business hours (8am - 9pm) '''
|
|
@@ -303,11 +334,13 @@ class DateBase(object):
|
|
|
303
334
|
out = self.is_business_day and self.dt >= set_hour(8) and self.dt <= set_hour(21)
|
|
304
335
|
return out
|
|
305
336
|
|
|
337
|
+
|
|
306
338
|
@property
|
|
307
339
|
def is_today(self):
|
|
308
340
|
''' returns True if date is the current day '''
|
|
309
341
|
return self.ymd == self.spawn().ymd
|
|
310
342
|
|
|
343
|
+
|
|
311
344
|
@property
|
|
312
345
|
def holiday(self):
|
|
313
346
|
''' returns the current holiday, if applicable '''
|
|
@@ -321,51 +354,62 @@ class DateBase(object):
|
|
|
321
354
|
def __repr__(self):
|
|
322
355
|
return str(self)
|
|
323
356
|
|
|
357
|
+
|
|
324
358
|
def __str__(self):
|
|
325
359
|
components = ['%Y-%m-%d']
|
|
326
360
|
if self.dt.hour + self.dt.second + self.dt.microsecond > 0:
|
|
327
361
|
components.append('%I:%M:%S.%f %p')
|
|
328
362
|
return self.__class__.__name__ + '(%s)' % self.str(' '.join(components))
|
|
329
363
|
|
|
364
|
+
|
|
330
365
|
def __int__(self):
|
|
331
366
|
return self.timestamp
|
|
332
367
|
|
|
368
|
+
|
|
333
369
|
@Decorators.other_to_dt
|
|
334
370
|
def __eq__(self, other):
|
|
335
371
|
return self.dt == other
|
|
336
372
|
|
|
373
|
+
|
|
337
374
|
@Decorators.other_to_dt
|
|
338
375
|
def __ne__(self, other):
|
|
339
376
|
return self.dt != other
|
|
340
377
|
|
|
378
|
+
|
|
341
379
|
@Decorators.other_to_dt
|
|
342
380
|
def __lt__(self, other):
|
|
343
381
|
return self.dt < other
|
|
344
382
|
|
|
383
|
+
|
|
345
384
|
@Decorators.other_to_dt
|
|
346
385
|
def __gt__(self, other):
|
|
347
386
|
return self.dt > other
|
|
348
387
|
|
|
388
|
+
|
|
349
389
|
@Decorators.other_to_dt
|
|
350
390
|
def __le__(self, other):
|
|
351
391
|
return self.dt <= other
|
|
352
392
|
|
|
393
|
+
|
|
353
394
|
@Decorators.other_to_dt
|
|
354
395
|
def __ge__(self, other):
|
|
355
396
|
return self.dt >= other
|
|
356
397
|
|
|
398
|
+
|
|
357
399
|
@Decorators.arithmetic_other
|
|
358
400
|
def __add__(self, other):
|
|
359
|
-
''' if other is date-like then implements default behavior for adding
|
|
360
|
-
|
|
401
|
+
''' if other is date-like then implements default behavior for adding
|
|
402
|
+
datetimes otherwise other is treated as timedelta '''
|
|
361
403
|
return self.dt + other
|
|
362
404
|
|
|
405
|
+
|
|
363
406
|
@Decorators.arithmetic_other
|
|
364
407
|
def __sub__(self, other):
|
|
365
|
-
''' if other is date-like then implements default behavior for subtracting
|
|
366
|
-
|
|
408
|
+
''' if other is date-like then implements default behavior for subtracting
|
|
409
|
+
datetimes otherwise other is treated as timedelta '''
|
|
367
410
|
return self.dt - other
|
|
368
411
|
|
|
412
|
+
|
|
369
413
|
@Decorators.other_to_dt
|
|
370
414
|
def __contains__(self, item):
|
|
371
415
|
return self.normalize(item).dt <= self.dt < (self.normalize(item) + 1).dt
|
|
@@ -447,5 +491,6 @@ class DateBase(object):
|
|
|
447
491
|
|
|
448
492
|
@Decorators.spawn
|
|
449
493
|
def normalize(self):
|
|
450
|
-
''' the time component (hours, minutes, seconds, microseconds) is set to
|
|
494
|
+
''' the time component (hours, minutes, seconds, microseconds) is set to
|
|
495
|
+
zero (midnight) '''
|
|
451
496
|
return datetime.datetime(self.year, self.month, self.day)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from ._base import DateBase
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
|
|
5
4
|
class MonthEnd(DateBase):
|
|
6
5
|
|
|
7
6
|
#╭-------------------------------------------------------------------------╮
|
|
@@ -9,7 +8,8 @@ class MonthEnd(DateBase):
|
|
|
9
8
|
#╰-------------------------------------------------------------------------╯
|
|
10
9
|
|
|
11
10
|
def __init__(self, dt):
|
|
12
|
-
|
|
11
|
+
day = self.n_days_in_month(dt.year, dt.month)
|
|
12
|
+
super().__init__(dt.replace(day=day))
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
#╭-------------------------------------------------------------------------╮
|
|
@@ -20,21 +20,35 @@ class MonthEnd(DateBase):
|
|
|
20
20
|
def long(self):
|
|
21
21
|
return self.str('%Y-%m-%d')
|
|
22
22
|
|
|
23
|
+
|
|
23
24
|
@property
|
|
24
25
|
def compact(self):
|
|
25
26
|
return self.str('%Y-%m')
|
|
26
27
|
|
|
28
|
+
|
|
27
29
|
@property
|
|
28
30
|
def short(self):
|
|
29
31
|
return self.str('%b')
|
|
30
32
|
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def is_year_end(self):
|
|
36
|
+
return self.month == 12
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def is_quarter_end(self):
|
|
41
|
+
return self.month in {3, 6, 9, 12}
|
|
42
|
+
|
|
43
|
+
|
|
31
44
|
@property
|
|
32
45
|
def last_quarter_end(self):
|
|
33
46
|
''' returns most recent quarter end '''
|
|
34
47
|
delta = 0
|
|
35
48
|
while True:
|
|
36
|
-
|
|
37
|
-
if
|
|
49
|
+
me = self.offset(delta=delta)
|
|
50
|
+
if me.is_quarter_end:
|
|
51
|
+
return me.to_quarter_end()
|
|
38
52
|
delta -= 1
|
|
39
53
|
|
|
40
54
|
|
|
@@ -42,6 +56,17 @@ class MonthEnd(DateBase):
|
|
|
42
56
|
#| Instance Methods |
|
|
43
57
|
#╰-------------------------------------------------------------------------╯
|
|
44
58
|
|
|
59
|
+
def to_quarter_end(self):
|
|
60
|
+
from ._quarter_end import QuarterEnd
|
|
61
|
+
if self.is_quarter_end:
|
|
62
|
+
return QuarterEnd(self.dt)
|
|
63
|
+
else:
|
|
64
|
+
raise ValueError(
|
|
65
|
+
"Month-end date does not align with a quarter-end "
|
|
66
|
+
f"date: '{self.ymd}'"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
45
70
|
def offset(self, delta):
|
|
46
71
|
''' returns the month end 'delta' months away from the instance '''
|
|
47
72
|
if not isinstance(delta, int):
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
from ._month_end import MonthEnd
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
|
|
5
4
|
class QuarterEnd(MonthEnd):
|
|
6
5
|
|
|
6
|
+
#╭-------------------------------------------------------------------------╮
|
|
7
|
+
#| Class Attributes |
|
|
8
|
+
#╰-------------------------------------------------------------------------╯
|
|
9
|
+
scheme = (3, 6, 9, 12)
|
|
10
|
+
|
|
11
|
+
|
|
7
12
|
#╭-------------------------------------------------------------------------╮
|
|
8
13
|
#| Initialize Instance |
|
|
9
14
|
#╰-------------------------------------------------------------------------╯
|
|
10
15
|
|
|
11
|
-
def __init__(self, dt
|
|
16
|
+
def __init__(self, dt):
|
|
12
17
|
super().__init__(dt)
|
|
13
|
-
self.qtr = qtr
|
|
14
18
|
|
|
15
19
|
|
|
16
20
|
#╭-------------------------------------------------------------------------╮
|
|
@@ -21,17 +25,26 @@ class QuarterEnd(MonthEnd):
|
|
|
21
25
|
def long(self):
|
|
22
26
|
return f'{self.year}Q{self.qtr}'
|
|
23
27
|
|
|
28
|
+
|
|
24
29
|
@property
|
|
25
30
|
def compact(self):
|
|
26
31
|
return f'{self.qtr}Q' + self.str('%y')
|
|
27
32
|
|
|
33
|
+
|
|
28
34
|
@property
|
|
29
35
|
def short(self):
|
|
30
36
|
return f'Q{self.qtr}'
|
|
31
37
|
|
|
38
|
+
|
|
32
39
|
@property
|
|
33
40
|
def quarter(self):
|
|
34
|
-
return self.
|
|
41
|
+
return int(self.scheme.index(self.month) + 1)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def qtr(self):
|
|
46
|
+
''' quarter alias '''
|
|
47
|
+
return self.quarter
|
|
35
48
|
|
|
36
49
|
|
|
37
50
|
#╭-------------------------------------------------------------------------╮
|
|
@@ -49,4 +62,4 @@ class QuarterEnd(MonthEnd):
|
|
|
49
62
|
if delta == 0: return self
|
|
50
63
|
dt = self.shift(months=delta * 3).dt
|
|
51
64
|
qtr = ((self.qtr - 1 + delta) % 4) + 1
|
|
52
|
-
return self.__class__(dt
|
|
65
|
+
return self.__class__(dt)
|
|
@@ -7,11 +7,6 @@ from ._quarter_end import QuarterEnd
|
|
|
7
7
|
from ._month_end import MonthEnd
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
#╭-------------------------------------------------------------------------╮
|
|
12
|
-
#| Functions |
|
|
13
|
-
#╰-------------------------------------------------------------------------╯
|
|
14
|
-
|
|
15
10
|
def Date(arg=None, normalize=False, format=None, week_offset=0):
|
|
16
11
|
'''
|
|
17
12
|
Description
|
|
@@ -21,26 +16,31 @@ def Date(arg=None, normalize=False, format=None, week_offset=0):
|
|
|
21
16
|
Parameters
|
|
22
17
|
------------
|
|
23
18
|
arg : str | object
|
|
24
|
-
object to convert to DateBase object. Currently supported formats
|
|
19
|
+
object to convert to DateBase object. Currently supported formats
|
|
20
|
+
include:
|
|
25
21
|
• None (the current date and time will be used)
|
|
26
22
|
• pandas._libs.tslibs.timestamps.Timestamp
|
|
27
23
|
• datetime.datetime
|
|
28
24
|
• datetime.date
|
|
29
25
|
• integer or float (in seconds)
|
|
30
26
|
• string
|
|
31
|
-
► day of the week fully spelled out or first 3 letters
|
|
32
|
-
(e.g. 'Monday', 'monday', 'mon')
|
|
27
|
+
► day of the week fully spelled out or first 3 letters
|
|
28
|
+
(not case sensitive) (e.g. 'Monday', 'monday', 'mon')
|
|
33
29
|
► quarter in #QYY, YYYYQ#, or Q# format
|
|
34
30
|
► any string format supported by pd.to_datetime
|
|
35
31
|
• DateBase object or DateBase polymorphism
|
|
36
32
|
normalize : bool
|
|
37
|
-
if True, only the year, month, and day are retained (hours, minutes,
|
|
33
|
+
if True, only the year, month, and day are retained (hours, minutes,
|
|
34
|
+
seconds, microseconds are set to zero)
|
|
38
35
|
format : str
|
|
39
|
-
if 'arg' is a string, this format is used to parse it
|
|
36
|
+
if 'arg' is a string, this format is used to parse it
|
|
37
|
+
(e.g. '%Y%m%d %H%S').
|
|
40
38
|
week_offset : int
|
|
41
|
-
by default, if a day of the week is supplied (e.g. 'Monday') then
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
by default, if a day of the week is supplied (e.g. 'Monday') then
|
|
40
|
+
the date returned will be that day of the week for the current week.
|
|
41
|
+
This argument is used to override this behavior by shifting the week
|
|
42
|
+
backwards or forwards (e.g. if arg='Monday' and week_offset=-1 then
|
|
43
|
+
the Monday of last week will be returned).
|
|
44
44
|
|
|
45
45
|
Returns
|
|
46
46
|
------------
|
|
@@ -48,7 +48,8 @@ def Date(arg=None, normalize=False, format=None, week_offset=0):
|
|
|
48
48
|
DateBase object or polymorphism
|
|
49
49
|
'''
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
qtr_scheme = [(3, 31), (6, 30), (9, 30), (12, 31)]
|
|
52
|
+
qtr_map = {k: i for i, k in enumerate(qtr_scheme, 1)}
|
|
52
53
|
|
|
53
54
|
def qtr_label_to_dt(x):
|
|
54
55
|
''' attempts to convert quarter expressed as string to datetime.
|
|
@@ -74,7 +75,10 @@ def Date(arg=None, normalize=False, format=None, week_offset=0):
|
|
|
74
75
|
|
|
75
76
|
if format is not None:
|
|
76
77
|
if not isinstance(arg, str):
|
|
77
|
-
raise TypeError(
|
|
78
|
+
raise TypeError(
|
|
79
|
+
"When 'format' argument is not None, 'arg' must be a "
|
|
80
|
+
f"string, not {type(arg).__name__}."
|
|
81
|
+
)
|
|
78
82
|
arg = DateBase.to_datetime(arg, format=format)
|
|
79
83
|
return Date(arg, normalize=normalize)
|
|
80
84
|
|
|
@@ -102,22 +106,21 @@ def Date(arg=None, normalize=False, format=None, week_offset=0):
|
|
|
102
106
|
else:
|
|
103
107
|
raise ValueError(f'date argument {arg} of type {type(arg)} is not supported.')
|
|
104
108
|
|
|
105
|
-
if normalize:
|
|
109
|
+
if normalize:
|
|
110
|
+
dt = datetime.datetime(dt.year, dt.month, dt.day)
|
|
106
111
|
|
|
107
112
|
if dt.day == DateBase.n_days_in_month(dt.year, dt.month):
|
|
108
113
|
qtr = qtr_map.get((dt.month, dt.day))
|
|
109
|
-
return QuarterEnd(dt
|
|
114
|
+
return QuarterEnd(dt) if qtr else MonthEnd(dt)
|
|
110
115
|
else:
|
|
111
116
|
return DateBase(dt)
|
|
112
117
|
|
|
113
118
|
|
|
114
|
-
|
|
115
119
|
def day_of_week(day, delta=0):
|
|
116
120
|
''' see DateBase.first_last decorator for documentation '''
|
|
117
121
|
return Date(normalize=True).last(day, delta)
|
|
118
122
|
|
|
119
123
|
|
|
120
|
-
|
|
121
124
|
def month_end(delta=0):
|
|
122
125
|
'''
|
|
123
126
|
Description
|
|
@@ -127,21 +130,29 @@ def month_end(delta=0):
|
|
|
127
130
|
Parameters
|
|
128
131
|
------------
|
|
129
132
|
delta : int | str | DateBase
|
|
130
|
-
Offset value +/- from the most recent month end (e.g. 0 is the most
|
|
133
|
+
Offset value +/- from the most recent month end (e.g. 0 is the most
|
|
134
|
+
recent month end and -1 is the 2nd most recent month end).
|
|
131
135
|
|
|
132
136
|
Returns
|
|
133
137
|
------------
|
|
134
138
|
clockwork.MonthEnd object
|
|
135
139
|
'''
|
|
136
|
-
if isinstance(delta, (DateBase, str)):
|
|
137
|
-
|
|
140
|
+
if isinstance(delta, (DateBase, str)):
|
|
141
|
+
out = Date(delta)
|
|
142
|
+
if not isinstance(out, MonthEnd):
|
|
143
|
+
raise ValueError(
|
|
144
|
+
f"delta '{delta}' did not yield a month-end object: {out}"
|
|
145
|
+
)
|
|
146
|
+
return out
|
|
147
|
+
|
|
148
|
+
today = datetime.date.today()
|
|
149
|
+
y, m = divmod(today.year * 12 + today.month + delta - 1, 12)
|
|
138
150
|
if m == 0:
|
|
139
151
|
y -= 1
|
|
140
152
|
m = 12
|
|
141
153
|
return MonthEnd(datetime.datetime(y, m, 1))
|
|
142
154
|
|
|
143
155
|
|
|
144
|
-
|
|
145
156
|
def quarter_end(delta=0, scheme=None):
|
|
146
157
|
'''
|
|
147
158
|
Description
|
|
@@ -151,10 +162,11 @@ def quarter_end(delta=0, scheme=None):
|
|
|
151
162
|
Parameters
|
|
152
163
|
------------
|
|
153
164
|
delta : int | str | DateBase
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
String values represent specific quarters. Acceptable formats include:
|
|
165
|
+
Integer values are treated as offsets (+/-) from the most recent quarter
|
|
166
|
+
end (e.g. 0 is the most recent quarter end and -1 is the 2nd most recent
|
|
167
|
+
quarter end).
|
|
168
|
+
String values represent specific quarters. Acceptable formats include:
|
|
169
|
+
'#QYY' or 'YYYYQ#'
|
|
158
170
|
scheme : tuple
|
|
159
171
|
Tuple listing the quarter end months. Defaults to calendar year-end.
|
|
160
172
|
|
|
@@ -162,18 +174,29 @@ def quarter_end(delta=0, scheme=None):
|
|
|
162
174
|
------------
|
|
163
175
|
clockwork.QuarterEnd object
|
|
164
176
|
'''
|
|
165
|
-
if
|
|
166
|
-
|
|
167
|
-
|
|
177
|
+
if scheme is not None:
|
|
178
|
+
raise NotImplementedError(
|
|
179
|
+
'Only default quarter-end months are currently supported'
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
if isinstance(delta, (DateBase, str)):
|
|
183
|
+
out = Date(delta)
|
|
184
|
+
if not isinstance(out, QuarterEnd):
|
|
185
|
+
raise ValueError(
|
|
186
|
+
f"delta '{delta}' did not yield a quarter-end object: {out}"
|
|
187
|
+
)
|
|
188
|
+
return out
|
|
189
|
+
|
|
190
|
+
scheme = QuarterEnd.scheme
|
|
168
191
|
today = datetime.datetime.now()
|
|
169
192
|
cy, cm, cd = today.year, today.month, today.day
|
|
170
193
|
if cd <= DateBase.n_days_in_month(cy, cm): cm -= 1
|
|
171
194
|
candidates = [((cy * 12) + m) + (delta * 3) - 1 for m in scheme]
|
|
172
|
-
candidates.insert(0,((cy - 1) * 12) + scheme[-1] + (delta * 3) - 1)
|
|
173
|
-
|
|
195
|
+
candidates.insert(0, ((cy - 1) * 12) + scheme[-1] + (delta * 3) - 1)
|
|
196
|
+
index = np.digitize(((cy * 12) + cm + (delta * 3)), candidates, right=True) - 1
|
|
197
|
+
y, m = divmod(candidates[index], 12)
|
|
174
198
|
m += 1
|
|
175
|
-
return QuarterEnd(
|
|
176
|
-
|
|
199
|
+
return QuarterEnd(datetime.datetime(y, m, 1))
|
|
177
200
|
|
|
178
201
|
|
|
179
202
|
def year_end(delta=0, **kwargs):
|
|
@@ -183,7 +206,6 @@ def year_end(delta=0, **kwargs):
|
|
|
183
206
|
return quarter_end(delta, **kwargs)
|
|
184
207
|
|
|
185
208
|
|
|
186
|
-
|
|
187
209
|
#╭-------------------------------------------------------------------------╮
|
|
188
210
|
#| Assign Class Attribute |
|
|
189
211
|
#╰-------------------------------------------------------------------------╯
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
from .utils import elapsed_time, add_border
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def action_timer(func):
|
|
7
|
+
|
|
8
|
+
def wrapper(*args, **kwargs):
|
|
9
|
+
start_time = time.time()
|
|
10
|
+
print(add_border(func.__name__, width=75))
|
|
11
|
+
print()
|
|
12
|
+
out = func(*args, **kwargs)
|
|
13
|
+
et = elapsed_time(time.time() - start_time)
|
|
14
|
+
print(add_border(f'{func.__name__} complete in {et}', width=75))
|
|
15
|
+
print()
|
|
16
|
+
return out
|
|
17
|
+
|
|
18
|
+
return wrapper
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from schedule import Scheduler, CancelJob
|
|
3
|
+
|
|
4
|
+
from .utils import ContinueFailedJob
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TaskScheduler(Scheduler):
|
|
8
|
+
|
|
9
|
+
#╭-------------------------------------------------------------------------╮
|
|
10
|
+
#| Initialize Instance |
|
|
11
|
+
#╰-------------------------------------------------------------------------╯
|
|
12
|
+
|
|
13
|
+
def __init__(self, *args, **kwargs):
|
|
14
|
+
super().__init__(*args, **kwargs)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
#╭-------------------------------------------------------------------------╮
|
|
18
|
+
#| Instance Methods |
|
|
19
|
+
#╰-------------------------------------------------------------------------╯
|
|
20
|
+
|
|
21
|
+
def _run_job(self, job):
|
|
22
|
+
ret = job.run()
|
|
23
|
+
if ret is CancelJob:
|
|
24
|
+
self.cancel_job(job)
|
|
25
|
+
elif ret is ContinueFailedJob:
|
|
26
|
+
job.last_run = datetime.datetime.now()
|
|
27
|
+
job._schedule_next_run()
|
|
@@ -8,13 +8,12 @@ from ..utils import elapsed_time
|
|
|
8
8
|
from .utils import PrerequisiteError, ContinueFailedJob
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
|
|
12
11
|
class Task(object):
|
|
13
12
|
'''
|
|
14
13
|
Description
|
|
15
14
|
--------------------
|
|
16
|
-
job object designed to be used in conjunction with TaskScheduler as the
|
|
17
|
-
in self._run_job()
|
|
15
|
+
job object designed to be used in conjunction with TaskScheduler as the
|
|
16
|
+
'job' argument in self._run_job()
|
|
18
17
|
|
|
19
18
|
Class Attributes
|
|
20
19
|
--------------------
|
|
@@ -23,7 +22,8 @@ class Task(object):
|
|
|
23
22
|
verbose : bool
|
|
24
23
|
If True, class content is printed to console.
|
|
25
24
|
disable_print : bool
|
|
26
|
-
If True, printing is suppressed while func runs. Must be True if verbose
|
|
25
|
+
If True, printing is suppressed while func runs. Must be True if verbose
|
|
26
|
+
is True.
|
|
27
27
|
cascade_status : str
|
|
28
28
|
cascade status
|
|
29
29
|
|
|
@@ -34,7 +34,8 @@ class Task(object):
|
|
|
34
34
|
at : str
|
|
35
35
|
at time string
|
|
36
36
|
expiry : Date
|
|
37
|
-
If not None, job will be set inactive and stop running after this
|
|
37
|
+
If not None, job will be set inactive and stop running after this
|
|
38
|
+
datetime
|
|
38
39
|
func : func
|
|
39
40
|
function to run
|
|
40
41
|
args : tuple
|
|
@@ -44,24 +45,30 @@ class Task(object):
|
|
|
44
45
|
cancel_on_failure : bool
|
|
45
46
|
If True, the job will be cancelled if it raises an exception.
|
|
46
47
|
cancel_on_completion : bool
|
|
47
|
-
If True, the job will be cancelled if it completed successfully
|
|
48
|
+
If True, the job will be cancelled if it completed successfully
|
|
49
|
+
(i.e. job will run only once).
|
|
48
50
|
notify_on_failure : bool
|
|
49
|
-
If True, an email notification is sent to my inbox that includes the
|
|
50
|
-
and cascade are automatically set to True
|
|
51
|
+
If True, an email notification is sent to my inbox that includes the
|
|
52
|
+
traceback. cancel_on_failure and cascade are automatically set to True
|
|
53
|
+
if this argument is True (prevents endless spam).
|
|
51
54
|
restrict_to_business_hours : bool
|
|
52
|
-
If True, the job will only execute during business hours. This is a more
|
|
55
|
+
If True, the job will only execute during business hours. This is a more
|
|
56
|
+
restrictive version of restrict_to_business_days.
|
|
53
57
|
restrict_to_business_days : bool
|
|
54
58
|
If True, the job will only execute during weekdays (i.e. not weekends).
|
|
55
59
|
cascade : bool
|
|
56
|
-
If True, when the job (denoted by the 'name' argument) is reflected
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
the
|
|
60
|
+
If True, when the job (denoted by the 'name' argument) is reflected
|
|
61
|
+
multiple times in the jobs table due to having multiple 'at' values,
|
|
62
|
+
changes in activiation in one will cascade to all others. For example,
|
|
63
|
+
consider the job named 'my job' which is scheduled at 8:00 AM and 5:00
|
|
64
|
+
PM that is cancelled on completion. If the 8:00 AM completes successfully
|
|
65
|
+
then the job with that 'at' time will be set to inactive and have its status
|
|
66
|
+
updated in the table accordingly. Under default behavior, the 5:00 PM run
|
|
67
|
+
will be unaffected by the completion of the 8:00 AM run, however, if cascade
|
|
68
|
+
is set to True then the 5:00 PM run will also receive the same updates.
|
|
63
69
|
attempts : int
|
|
64
|
-
If greater than 1, the job will be attempted this number of times before
|
|
70
|
+
If greater than 1, the job will be attempted this number of times before
|
|
71
|
+
being cancelled.
|
|
65
72
|
status : str | None
|
|
66
73
|
current status of the job
|
|
67
74
|
'''
|
|
@@ -165,7 +172,8 @@ class Task(object):
|
|
|
165
172
|
print(f' @ {Date()} ->', end=' ')
|
|
166
173
|
print('Job Cancelled')
|
|
167
174
|
|
|
168
|
-
# cancel job if it was cancelled on cascasde or
|
|
175
|
+
# cancel job if it was cancelled on cascasde or
|
|
176
|
+
# if it was set inactive after being scheduled
|
|
169
177
|
if self.status == self.cascade_status or \
|
|
170
178
|
not self.master.is_active(self.name, self.at):
|
|
171
179
|
return CancelJob
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
from .utils import PrerequisiteError
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
|
|
5
4
|
class FileMonitor(object):
|
|
6
5
|
'''
|
|
7
6
|
Description
|
|
8
7
|
--------------------
|
|
9
|
-
Monitors a folder for new files and passes latest and 2nd latest file
|
|
10
|
-
user-defined function. Class is intended to be used in
|
|
11
|
-
as a func argument which allows for file
|
|
8
|
+
Monitors a folder for new files and passes latest and 2nd latest file
|
|
9
|
+
names to user-defined function. Class is intended to be used in
|
|
10
|
+
conjunction with TaskMaster as a func argument which allows for file
|
|
11
|
+
monitoring at regular intervals.
|
|
12
12
|
|
|
13
13
|
Class Attributes
|
|
14
14
|
--------------------
|
|
@@ -17,8 +17,8 @@ class FileMonitor(object):
|
|
|
17
17
|
Instance Attributes
|
|
18
18
|
--------------------
|
|
19
19
|
func : func
|
|
20
|
-
Custom function that takes the latest and second-latest file names
|
|
21
|
-
the first and second arguments, respectively.
|
|
20
|
+
Custom function that takes the latest and second-latest file names
|
|
21
|
+
in a folder as the first and second arguments, respectively.
|
|
22
22
|
folder : Folder object
|
|
23
23
|
folder to monitor for new files.
|
|
24
24
|
filter_kwargs : dict
|
|
@@ -8,14 +8,17 @@ from pathpilot import Folder
|
|
|
8
8
|
#╰-------------------------------------------------------------------------╯
|
|
9
9
|
|
|
10
10
|
class CustomLogFormatter(logging.Formatter):
|
|
11
|
-
''' the default implementation of logging.Formatter does not allow
|
|
11
|
+
''' the default implementation of logging.Formatter does not allow
|
|
12
|
+
timestamps to be formatted how I want '''
|
|
12
13
|
|
|
13
14
|
converter = datetime.datetime.fromtimestamp
|
|
14
15
|
|
|
15
16
|
def formatTime(self, record, datefmt=None):
|
|
16
|
-
if datefmt is not None:
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
if datefmt is not None:
|
|
18
|
+
raise TypeError('datefmt argument must be None')
|
|
19
|
+
return self.converter(record.created)\
|
|
20
|
+
.strftime('%Y-%m-%d %I:%M:%S.{} %p')\
|
|
21
|
+
.format('%03d' % record.msecs)
|
|
19
22
|
|
|
20
23
|
|
|
21
24
|
class Logger(object):
|
|
@@ -55,7 +58,9 @@ class Logger(object):
|
|
|
55
58
|
#formatter = CustomLogFormatter(fmt='%(asctime)s %(name)s %(levelname)s %(message)s')
|
|
56
59
|
|
|
57
60
|
# create file handler which logs even debug messages
|
|
58
|
-
self.file = Folder().parent
|
|
61
|
+
self.file = Folder().parent\
|
|
62
|
+
.join('Data', 'Logger', read_only=False)\
|
|
63
|
+
.join(f'{name}.log').path
|
|
59
64
|
if clear: self.clear()
|
|
60
65
|
fh = logging.FileHandler(self.file)
|
|
61
66
|
fh.setLevel(logging.DEBUG)
|
|
@@ -92,7 +97,6 @@ class Logger(object):
|
|
|
92
97
|
open(self.file, 'w').close()
|
|
93
98
|
|
|
94
99
|
|
|
95
|
-
|
|
96
100
|
#╭-------------------------------------------------------------------------╮
|
|
97
101
|
#| Functions |
|
|
98
102
|
#╰-------------------------------------------------------------------------╯
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
import time
|
|
3
3
|
import uuid
|
|
4
|
-
from
|
|
4
|
+
from oddments import to_iter
|
|
5
5
|
from pathpilot import Folder
|
|
6
6
|
|
|
7
7
|
from ..core import Date
|
|
@@ -9,7 +9,6 @@ from ._scheduler import TaskScheduler
|
|
|
9
9
|
from ._task import Task
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
|
|
13
12
|
#╭-------------------------------------------------------------------------╮
|
|
14
13
|
#| Classes |
|
|
15
14
|
#╰-------------------------------------------------------------------------╯
|
|
@@ -18,8 +17,8 @@ class TaskMaster(object):
|
|
|
18
17
|
'''
|
|
19
18
|
Description
|
|
20
19
|
--------------------
|
|
21
|
-
Class provides a user-friendly means of using the TaskScheduler
|
|
22
|
-
schedule.Scheduler to schedule SmartJobs
|
|
20
|
+
Class provides a user-friendly means of using the TaskScheduler
|
|
21
|
+
implementation of schedule.Scheduler to schedule SmartJobs
|
|
23
22
|
|
|
24
23
|
Class Attributes
|
|
25
24
|
--------------------
|
|
@@ -75,9 +74,11 @@ class TaskMaster(object):
|
|
|
75
74
|
Task.at
|
|
76
75
|
active : binary
|
|
77
76
|
If 1, the job is active and able to be scheduled.
|
|
78
|
-
If 0, the job is inactive and unable to be scheduled. This value is
|
|
77
|
+
If 0, the job is inactive and unable to be scheduled. This value is
|
|
78
|
+
obtained via one of the following avenues
|
|
79
79
|
1) job completed succesfully and was cancelled
|
|
80
|
-
2) job obained this status via cascade from a job that satisfied
|
|
80
|
+
2) job obained this status via cascade from a job that satisfied
|
|
81
|
+
criteria in 1)
|
|
81
82
|
3) job was manually set inactive via TaskMaster.set_inactive method
|
|
82
83
|
expiry : str
|
|
83
84
|
Task.expiry
|
|
@@ -105,7 +106,16 @@ class TaskMaster(object):
|
|
|
105
106
|
|
|
106
107
|
|
|
107
108
|
@classmethod
|
|
108
|
-
def add(
|
|
109
|
+
def add(
|
|
110
|
+
cls,
|
|
111
|
+
func,
|
|
112
|
+
every=None,
|
|
113
|
+
at=None,
|
|
114
|
+
interval=1,
|
|
115
|
+
start=None,
|
|
116
|
+
expiry=None,
|
|
117
|
+
**kwargs
|
|
118
|
+
):
|
|
109
119
|
'''
|
|
110
120
|
Description
|
|
111
121
|
------------
|
|
@@ -124,20 +134,23 @@ class TaskMaster(object):
|
|
|
124
134
|
func : func
|
|
125
135
|
Task func argument
|
|
126
136
|
every : str
|
|
127
|
-
string representation of schedule.Job property (e.g. 'minutes',
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
137
|
+
string representation of schedule.Job property (e.g. 'minutes',
|
|
138
|
+
'hour', 'day' etc.).
|
|
139
|
+
If None and cancel_on_completion kwarg is True, every and interval
|
|
140
|
+
will be set to 'second' and 1, respectively, so that the job will
|
|
141
|
+
run ASAP once the 'start' criteria has been met (if applicable).
|
|
131
142
|
at : str | iter
|
|
132
|
-
time_str argument passed to schedule.Job.at(time_str). Times may be
|
|
133
|
-
'%I:%M%p' format (e.g ['06:15 AM', '12:15 PM', '06:15 PM']).
|
|
134
|
-
iterable then the job will be scheduled at each
|
|
143
|
+
time_str argument passed to schedule.Job.at(time_str). Times may be
|
|
144
|
+
passed in '%I:%M%p' format (e.g ['06:15 AM', '12:15 PM', '06:15 PM']).
|
|
145
|
+
If argument is an iterable then the job will be scheduled at each
|
|
146
|
+
constituent time.
|
|
135
147
|
interval : int
|
|
136
148
|
schedule.Scheduler.every interval argument
|
|
137
149
|
start : Date
|
|
138
150
|
If not None, job will be not be added until this datetime
|
|
139
151
|
expiry : Date
|
|
140
|
-
If not None, job will be set inactive and stop running after this
|
|
152
|
+
If not None, job will be set inactive and stop running after this
|
|
153
|
+
datetime
|
|
141
154
|
kwargs : keyword arguments
|
|
142
155
|
keyword arguments passed to Task.__init__
|
|
143
156
|
|
|
@@ -147,7 +160,8 @@ class TaskMaster(object):
|
|
|
147
160
|
'''
|
|
148
161
|
|
|
149
162
|
if not hasattr(cls, 'db'):
|
|
150
|
-
cls.db = Folder().parent.join('Data', 'SQLite', read_only=False)
|
|
163
|
+
cls.db = Folder().parent.join('Data', 'SQLite', read_only=False)\
|
|
164
|
+
.join('taskmaster.sqlite')
|
|
151
165
|
cls.db.connect()
|
|
152
166
|
cls.db.enable_foreign_keys()
|
|
153
167
|
|
|
@@ -215,7 +229,6 @@ class TaskMaster(object):
|
|
|
215
229
|
return True
|
|
216
230
|
|
|
217
231
|
|
|
218
|
-
|
|
219
232
|
#╭-------------------------------------------------------------------------╮
|
|
220
233
|
#| Assign Class Attribute |
|
|
221
234
|
#╰-------------------------------------------------------------------------╯
|
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
import re
|
|
2
|
-
import time
|
|
3
2
|
from textwrap import wrap as wrap_text
|
|
4
3
|
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
#╭-------------------------------------------------------------------------╮
|
|
8
|
-
#| Functions |
|
|
9
|
-
#╰-------------------------------------------------------------------------╯
|
|
10
|
-
|
|
11
5
|
def elapsed_time(seconds):
|
|
12
6
|
out = []
|
|
13
|
-
for k,v in [('days', 86400), ('hours', 3600), ('minutes', 60)]:
|
|
7
|
+
for k, v in [('days', 86400), ('hours', 3600), ('minutes', 60)]:
|
|
14
8
|
count = int(seconds / v)
|
|
15
9
|
if count > 0:
|
|
16
10
|
out.append(f'{count} {k}')
|
|
@@ -19,20 +13,6 @@ def elapsed_time(seconds):
|
|
|
19
13
|
return ', '.join(out)
|
|
20
14
|
|
|
21
15
|
|
|
22
|
-
def action_timer(func):
|
|
23
|
-
|
|
24
|
-
def wrapper(*args, **kwargs):
|
|
25
|
-
start_time = time.time()
|
|
26
|
-
print(add_border(func.__name__, width=75))
|
|
27
|
-
print()
|
|
28
|
-
out = func(*args, **kwargs)
|
|
29
|
-
print(add_border(f'{func.__name__} complete in {elapsed_time(time.time() - start_time)}', width=75))
|
|
30
|
-
print()
|
|
31
|
-
return out
|
|
32
|
-
|
|
33
|
-
return wrapper
|
|
34
|
-
|
|
35
|
-
|
|
36
16
|
def month_year_iter(start_month, start_year, end_month=None, end_year=None, rng=None, step=0):
|
|
37
17
|
|
|
38
18
|
start = 12 * start_year + start_month - 1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "clockwork"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.2"
|
|
4
4
|
description = "Toolkit for time-related operations including scheduling, logging, date manipulation, and more."
|
|
5
5
|
authors = ["Zachary Einck <zacharyeinck@gmail.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -12,7 +12,7 @@ homepage = "https://github.com/zteinck/clockwork"
|
|
|
12
12
|
python = "^3.8"
|
|
13
13
|
pandas = "*"
|
|
14
14
|
numpy = "*"
|
|
15
|
-
|
|
15
|
+
oddments = "*"
|
|
16
16
|
pathpilot = "*"
|
|
17
17
|
schedule = "*"
|
|
18
18
|
holidays = "*"
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import datetime
|
|
2
|
-
from schedule import Scheduler, CancelJob
|
|
3
|
-
|
|
4
|
-
from .utils import ContinueFailedJob
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class TaskScheduler(Scheduler):
|
|
9
|
-
|
|
10
|
-
def __init__(self):
|
|
11
|
-
super().__init__()
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def _run_job(self, job):
|
|
15
|
-
ret = job.run()
|
|
16
|
-
if ret is CancelJob:
|
|
17
|
-
self.cancel_job(job)
|
|
18
|
-
elif ret is ContinueFailedJob:
|
|
19
|
-
job.last_run = datetime.datetime.now()
|
|
20
|
-
job._schedule_next_run()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|