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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: clockwork
3
- Version: 0.2.0
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
@@ -0,0 +1,4 @@
1
+ from .core import *
2
+
3
+ __version__ = '0.2.2'
4
+ __author__ = 'Zachary Einck <zacharyeinck@gmail.com>'
@@ -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 it's a leap year
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 the week relative to self. For example self.next('Mon')
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 characters (e.g. 'Friday' or 'Fri') not case-sensitive.
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 week and -1 is last week).
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 datetimes otherwise
360
- other is treated as timedelta '''
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 datetimes otherwise
366
- other is treated as timedelta '''
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 zero (midnight) '''
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
- super().__init__(dt.replace(day=self.n_days_in_month(dt.year, dt.month)))
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
- obj = self.offset(delta=delta)
37
- if hasattr(obj, 'quarter'): return obj
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, qtr, *args, **kwargs):
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.qtr
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, qtr)
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 include:
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 (not case sensitive)
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, seconds, microseconds are set to zero)
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 (e.g. '%Y%m%d %H%S').
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 the date returned will be that day of the week
42
- for the current week. This argument is used to override this behavior by shifting the week backwards or forwards
43
- (e.g. if arg='Monday' and week_offset=-1 then the Monday of last week will be returned).
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
- qtr_map = {k: i for i,k in enumerate([(3,31), (6,30), (9,30), (12,31)], 1)}
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(f"When 'format' argument is not None, 'arg' must be a string, not {type(arg)}.")
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: dt = datetime.datetime(dt.year, dt.month, dt.day)
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, qtr) if qtr else MonthEnd(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 recent month end and -1 is the 2nd most recent month end).
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)): return Date(delta)
137
- y, m = divmod(datetime.date.today().year * 12 + datetime.date.today().month + delta - 1, 12)
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
- clockwork.date() -> 'date' argument.
155
- Integer values are treated as offsets +/- from the most recent quarter end (e.g. 0 is the most recent quarter
156
- end and -1 is the 2nd most recent quarter end).
157
- String values represent specific quarters. Acceptable formats include: '#QYY' or 'YYYYQ#'
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 isinstance(delta, (DateBase, str)): return Date(delta)
166
- if scheme is not None: raise NotImplementedError
167
- scheme = (3, 6, 9, 12)
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
- y, m = divmod(candidates[np.digitize(((cy * 12) + cm + (delta * 3)), candidates, right=True) - 1], 12)
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(dt=datetime.datetime(y, m, 1), qtr=scheme.index(m) + 1)
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 'job' argument
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 is True.
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 datetime
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 (i.e. job will run only once).
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 traceback. cancel_on_failure
50
- and cascade are automatically set to True if this argument is True (prevents endless spam).
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 restrictive version of restrict_to_business_days.
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 multiple times in the jobs table
57
- due to having multiple 'at' values, changes in activiation in one will cascade to all others. For example,
58
- consider the job named 'my job' which is scheduled at 8:00 AM and 5:00 PM that is cancelled on completion.
59
- If the 8:00 AM completes successfully then the job with that 'at' time will be set to inactive and have its
60
- status updated in the table accordingly. Under default behavior, the 5:00 PM run will be unaffected by the
61
- completion of the 8:00 AM run, however, if cascade is set to True then the 5:00 PM run will also receive
62
- the same updates.
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 being cancelled.
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 if it was set inactive after being scheduled
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 names to
10
- user-defined function. Class is intended to be used in conjunction with TaskMaster
11
- as a func argument which allows for file monitoring at regular intervals.
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 in a folder as
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 timestamps to be formatted how I want '''
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: raise TypeError('datefmt argument must be None')
17
- return self.converter(record.created).strftime('%Y-%m-%d %I:%M:%S.{} %p').format('%03d' % record.msecs)
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.join('Data', 'Logger', read_only=False).join(f'{name}.log').path
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 iterlab import to_iter
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 implementation of
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 obtained via one of the following avenues
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 criteria in 1)
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(cls, func, every=None, at=None, interval=1, start=None, expiry=None, **kwargs):
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', 'hour', 'day' etc.).
128
- If None and cancel_on_completion kwarg is True, every and interval will be set to
129
- 'second' and 1, respectively, so that the job will run ASAP once the 'start' criteria
130
- has been met (if applicable).
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 passed in
133
- '%I:%M%p' format (e.g ['06:15 AM', '12:15 PM', '06:15 PM']). If argument is an
134
- iterable then the job will be scheduled at each constituent time.
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 datetime
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).join('taskmaster.sqlite')
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.0"
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
- iterlab = "*"
15
+ oddments = "*"
16
16
  pathpilot = "*"
17
17
  schedule = "*"
18
18
  holidays = "*"
@@ -1,4 +0,0 @@
1
- from .core import *
2
-
3
- __version__ = '0.2.0'
4
- __author__ = 'Zachary Einck <zacharyeinck@gmail.com>'
@@ -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