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