clockwork 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- clockwork/__init__.py +4 -0
- clockwork/chronicle.py +97 -0
- clockwork/clockwork.py +780 -0
- clockwork/taskmaster.py +537 -0
- clockwork-0.1.0.dist-info/LICENSE +21 -0
- clockwork-0.1.0.dist-info/METADATA +26 -0
- clockwork-0.1.0.dist-info/RECORD +8 -0
- clockwork-0.1.0.dist-info/WHEEL +4 -0
clockwork/clockwork.py
ADDED
|
@@ -0,0 +1,780 @@
|
|
|
1
|
+
import calendar
|
|
2
|
+
import holidays
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import numpy as np
|
|
5
|
+
import re
|
|
6
|
+
import datetime
|
|
7
|
+
import time
|
|
8
|
+
from textwrap import wrap as wrap_text
|
|
9
|
+
from dateutil.relativedelta import relativedelta
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
#+---------------------------------------------------------------------------+
|
|
15
|
+
# Freestanding functions
|
|
16
|
+
#+---------------------------------------------------------------------------+
|
|
17
|
+
|
|
18
|
+
def Date(arg=None, normalize=False, week_offset=0):
|
|
19
|
+
'''
|
|
20
|
+
Description
|
|
21
|
+
----------
|
|
22
|
+
assigns new date instances to the correct class polymorphism
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
arg : str | object
|
|
27
|
+
object to convert to DateBase object. Currently supported formats include:
|
|
28
|
+
• None (the current date and time will be used)
|
|
29
|
+
• pandas._libs.tslibs.timestamps.Timestamp
|
|
30
|
+
• datetime.datetime
|
|
31
|
+
• datetime.date
|
|
32
|
+
• integer (in seconds)
|
|
33
|
+
• string
|
|
34
|
+
-> day of the week fully spelled out or first 3 letters (not case sensitive)
|
|
35
|
+
(e.g. 'Monday', 'monday', 'mon')
|
|
36
|
+
-> quarter in #QYY or YYYYQ# format
|
|
37
|
+
-> any string format supported by pd.to_datetime
|
|
38
|
+
• DateBase object or DateBase polymorphism
|
|
39
|
+
normalize : bool
|
|
40
|
+
if True, only the year, month, and day are retained (hours, minutes, seconds, microseconds are set to zero)
|
|
41
|
+
week_offset : int
|
|
42
|
+
by default, if a day of the week is supplied (e.g. 'Monday') then the date returned will be that day of the week
|
|
43
|
+
for the current week. This argument is used to override this behavior by shifting the week backwards or forwards
|
|
44
|
+
(e.g. if arg='Monday' and week_offset=-1 then the Monday of last week will be returned).
|
|
45
|
+
|
|
46
|
+
Returns
|
|
47
|
+
----------
|
|
48
|
+
out : DateBase | DateBase polymorphism
|
|
49
|
+
DateBase object or polymorphism
|
|
50
|
+
'''
|
|
51
|
+
|
|
52
|
+
qtr_map = {k: i for i,k in enumerate([(3,31), (6,30), (9,30), (12,31)], 1)}
|
|
53
|
+
|
|
54
|
+
def qtr_label_to_dt(x):
|
|
55
|
+
''' attempts to convert quarter expressed as string to datetime. Suported formats include #QYY and YYYYQ# '''
|
|
56
|
+
try:
|
|
57
|
+
qtr, year = re.findall(r'(\d{1})Q(\d{2})', x)[0]
|
|
58
|
+
except:
|
|
59
|
+
try:
|
|
60
|
+
year, qtr = re.findall(r'(\d{4})Q(\d{1})', x)[0]
|
|
61
|
+
except:
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
inverse = {v: k for k,v in qtr_map.items()}
|
|
65
|
+
month, day = inverse[int(qtr)]
|
|
66
|
+
out = DateBase.to_datetime(f'{month}-{day}-{year}')
|
|
67
|
+
return out
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
if arg is None:
|
|
71
|
+
dt = datetime.datetime.now()
|
|
72
|
+
elif hasattr(arg, 'to_pydatetime'): # pandas
|
|
73
|
+
dt = arg.to_pydatetime()
|
|
74
|
+
elif isinstance(arg, datetime.datetime):
|
|
75
|
+
dt = arg
|
|
76
|
+
elif isinstance(arg, datetime.date):
|
|
77
|
+
dt = datetime.datetime(arg.year, arg.month, arg.day)
|
|
78
|
+
elif isinstance(arg, int): # is timestamp expressed in seconds
|
|
79
|
+
dt = datetime.datetime.fromtimestamp(arg)
|
|
80
|
+
elif isinstance(arg, str):
|
|
81
|
+
desired_day = DateBase.weekdays.get(arg.title())
|
|
82
|
+
if desired_day is None:
|
|
83
|
+
dt = qtr_label_to_dt(arg) or DateBase.to_datetime(arg)
|
|
84
|
+
else:
|
|
85
|
+
normalize, now = True, datetime.datetime.now()
|
|
86
|
+
dt = now - datetime.timedelta(days=now.weekday()) + \
|
|
87
|
+
datetime.timedelta(days=desired_day, weeks=week_offset)
|
|
88
|
+
elif isinstance(arg, DateBase):
|
|
89
|
+
dt = arg.dt
|
|
90
|
+
else:
|
|
91
|
+
raise ValueError(f'date argument {arg} of type {type(arg)} is not supported.')
|
|
92
|
+
|
|
93
|
+
if normalize: dt = datetime.datetime(dt.year, dt.month, dt.day)
|
|
94
|
+
|
|
95
|
+
if dt.day == n_days_in_month(dt.year, dt.month):
|
|
96
|
+
qtr = qtr_map.get((dt.month, dt.day))
|
|
97
|
+
return QuarterEnd(dt, qtr) if qtr else MonthEnd(dt)
|
|
98
|
+
else:
|
|
99
|
+
return DateBase(dt)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def add_border(text, width=100, fixed_width=False, align='left'):
|
|
104
|
+
'''
|
|
105
|
+
Description
|
|
106
|
+
----------
|
|
107
|
+
Adds a border around text.
|
|
108
|
+
|
|
109
|
+
Parameters
|
|
110
|
+
----------
|
|
111
|
+
text : str
|
|
112
|
+
text to encase
|
|
113
|
+
width : int
|
|
114
|
+
wrap_text width argument. If width=1, the text is printed vertically.
|
|
115
|
+
fixed_width : bool
|
|
116
|
+
• True -> the width of the border will equal the 'width' argument value.
|
|
117
|
+
• False -> the width of the border is capped at the length of the longest
|
|
118
|
+
line in the text.
|
|
119
|
+
align : str
|
|
120
|
+
• 'left' -> aligns text along the left margin
|
|
121
|
+
• 'center' -> aligns text in the center between the left and right margins
|
|
122
|
+
• 'right' -> aligns text along the right margin
|
|
123
|
+
|
|
124
|
+
Returns
|
|
125
|
+
----------
|
|
126
|
+
out : str
|
|
127
|
+
text encased within a border
|
|
128
|
+
'''
|
|
129
|
+
lines = wrap_text(' '.join(text.split()), width)
|
|
130
|
+
max_width = width if fixed_width else len(max(lines, key=len))
|
|
131
|
+
border = ('-' * (max_width + 2)).join(['+'] * 2)
|
|
132
|
+
|
|
133
|
+
if align in 'left':
|
|
134
|
+
content = [('| ' + line + ''.join([' '] * (max_width - len(line))) + ' |') for line in lines]
|
|
135
|
+
elif align == 'right':
|
|
136
|
+
content = [('| ' + ''.join([' '] * (max_width - len(line))) + line + ' |') for line in lines]
|
|
137
|
+
elif align == 'center':
|
|
138
|
+
content = [('| ' + ''.join([' '] * ((max_width - len(line)) // 2)) + line +
|
|
139
|
+
''.join([' '] * ((max_width - len(line) + 1) // 2)) + ' |') for line in lines]
|
|
140
|
+
|
|
141
|
+
out = '\n'.join([border, '\n'.join(content), border])
|
|
142
|
+
return out
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def elapsed_time(seconds):
|
|
147
|
+
'''
|
|
148
|
+
Parameters
|
|
149
|
+
----------
|
|
150
|
+
seconds : float
|
|
151
|
+
seconds
|
|
152
|
+
'''
|
|
153
|
+
out = []
|
|
154
|
+
for k,v in [('days', 86400), ('hours', 3600), ('minutes', 60)]:
|
|
155
|
+
count = int(seconds / v)
|
|
156
|
+
if count > 0:
|
|
157
|
+
out.append(f'{count} {k}')
|
|
158
|
+
seconds -= count * v
|
|
159
|
+
out.append(f'{round(seconds, 2)} seconds')
|
|
160
|
+
return ', '.join(out)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def action_timer(func):
|
|
165
|
+
|
|
166
|
+
def wrapper(*args, **kwargs):
|
|
167
|
+
start_time = time.time()
|
|
168
|
+
print(add_border(func.__name__, width=75))
|
|
169
|
+
print()
|
|
170
|
+
out = func(*args, **kwargs)
|
|
171
|
+
print(add_border(f'{func.__name__} complete in {elapsed_time(time.time() - start_time)}', width=75))
|
|
172
|
+
print()
|
|
173
|
+
return out
|
|
174
|
+
|
|
175
|
+
return wrapper
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def n_days_in_month(year, month):
|
|
180
|
+
'''
|
|
181
|
+
Description
|
|
182
|
+
----------
|
|
183
|
+
Returns the # of days in a month for a given year.
|
|
184
|
+
For example, if year=2024 and month=2, 29 days is returned since it's a leap year
|
|
185
|
+
|
|
186
|
+
Parameters
|
|
187
|
+
----------
|
|
188
|
+
year : int
|
|
189
|
+
year in YYYY format
|
|
190
|
+
month : int
|
|
191
|
+
month
|
|
192
|
+
|
|
193
|
+
Returns
|
|
194
|
+
----------
|
|
195
|
+
out : int
|
|
196
|
+
number of days
|
|
197
|
+
'''
|
|
198
|
+
out = calendar.monthrange(year, month)[1]
|
|
199
|
+
return out
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def last_day_of_month(year, month):
|
|
204
|
+
'''
|
|
205
|
+
Description
|
|
206
|
+
----------
|
|
207
|
+
Returns date of the last day of the month for a given year and month
|
|
208
|
+
|
|
209
|
+
Parameters
|
|
210
|
+
----------
|
|
211
|
+
year : int
|
|
212
|
+
year in YYYY format
|
|
213
|
+
month : int
|
|
214
|
+
month
|
|
215
|
+
|
|
216
|
+
Returns
|
|
217
|
+
----------
|
|
218
|
+
clockwork.{MonthEnd}|{QuarterEnd}
|
|
219
|
+
'''
|
|
220
|
+
day = n_days_in_month(year, month)
|
|
221
|
+
return Date(datetime.datetime(year, month, day))
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def month_year_iter(start_month, start_year, end_month=None, end_year=None, rng=None, step=0):
|
|
226
|
+
|
|
227
|
+
start = 12 * start_year + start_month - 1
|
|
228
|
+
|
|
229
|
+
if not any((end_month, end_year, rng)):
|
|
230
|
+
while True:
|
|
231
|
+
y,m = divmod(start, 12)
|
|
232
|
+
start += 1
|
|
233
|
+
yield y, m + 1
|
|
234
|
+
else:
|
|
235
|
+
if all((end_month, end_year)):
|
|
236
|
+
end = 12 * end_year + end_month
|
|
237
|
+
elif rng:
|
|
238
|
+
rng += (1 if rng > 0 else -1)
|
|
239
|
+
end = start + rng
|
|
240
|
+
else:
|
|
241
|
+
raise ValueError('end_month and end_year arguments must both be provided.')
|
|
242
|
+
|
|
243
|
+
if end < start:
|
|
244
|
+
if step == 0: step = -1
|
|
245
|
+
if step > 0: step *= -1
|
|
246
|
+
|
|
247
|
+
month_range = range(start,end,step) if step else range(start, end)
|
|
248
|
+
|
|
249
|
+
for x in month_range:
|
|
250
|
+
y,m = divmod(x, 12)
|
|
251
|
+
yield y, m + 1
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def day_of_week(day, delta=0):
|
|
256
|
+
''' see DateBase.first_last decorator for documentation '''
|
|
257
|
+
return Date(normalize=True).last(day, delta)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def month_end(delta=0):
|
|
262
|
+
'''
|
|
263
|
+
Description
|
|
264
|
+
----------
|
|
265
|
+
Returns month end date
|
|
266
|
+
|
|
267
|
+
Parameters
|
|
268
|
+
----------
|
|
269
|
+
delta : int | str | DateBase
|
|
270
|
+
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).
|
|
271
|
+
|
|
272
|
+
Returns
|
|
273
|
+
----------
|
|
274
|
+
clockwork.MonthEnd object
|
|
275
|
+
'''
|
|
276
|
+
if isinstance(delta, (DateBase, str)): return Date(delta)
|
|
277
|
+
y, m = divmod(datetime.date.today().year * 12 + datetime.date.today().month + delta - 1, 12)
|
|
278
|
+
if m == 0:
|
|
279
|
+
y -= 1
|
|
280
|
+
m = 12
|
|
281
|
+
return MonthEnd(datetime.datetime(y, m, 1))
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def quarter_end(delta=0, scheme=None):
|
|
286
|
+
'''
|
|
287
|
+
Description
|
|
288
|
+
----------
|
|
289
|
+
Returns quarter end date
|
|
290
|
+
|
|
291
|
+
Parameters
|
|
292
|
+
----------
|
|
293
|
+
delta : int | str | DateBase
|
|
294
|
+
clockwork.date() -> 'date' argument.
|
|
295
|
+
Integer values are treated as offsets +/- from the most recent quarter end (e.g. 0 is the most recent quarter
|
|
296
|
+
end and -1 is the 2nd most recent quarter end).
|
|
297
|
+
String values represent specific quarters. Acceptable formats include: '#QYY' or 'YYYYQ#'
|
|
298
|
+
scheme : tuple
|
|
299
|
+
Tuple listing the quarter end months. Defaults to calendar year-end.
|
|
300
|
+
|
|
301
|
+
Returns
|
|
302
|
+
----------
|
|
303
|
+
clockwork.QuarterEnd object
|
|
304
|
+
'''
|
|
305
|
+
if isinstance(delta, (DateBase, str)): return Date(delta)
|
|
306
|
+
if scheme is not None: raise NotImplementedError
|
|
307
|
+
scheme = (3, 6, 9, 12)
|
|
308
|
+
today = datetime.datetime.now()
|
|
309
|
+
cy, cm, cd = today.year, today.month, today.day
|
|
310
|
+
if cd <= n_days_in_month(cy, cm): cm -= 1
|
|
311
|
+
candidates = [((cy * 12) + m) + (delta * 3) - 1 for m in scheme]
|
|
312
|
+
candidates.insert(0,((cy - 1) * 12) + scheme[-1] + (delta * 3) - 1)
|
|
313
|
+
y, m = divmod(candidates[np.digitize(((cy * 12) + cm + (delta * 3)), candidates, right=True) - 1], 12)
|
|
314
|
+
m += 1
|
|
315
|
+
return QuarterEnd(dt=datetime.datetime(y, m, 1), qtr=scheme.index(m) + 1)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def year_end(delta=0, **kwargs):
|
|
320
|
+
''' returns year end date as a QuarterEnd object '''
|
|
321
|
+
quarter = quarter_end().qtr
|
|
322
|
+
delta = (4 * delta) + (4 - quarter)
|
|
323
|
+
return quarter_end(delta, **kwargs)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
#+---------------------------------------------------------------------------+
|
|
328
|
+
# Classes
|
|
329
|
+
#+---------------------------------------------------------------------------+
|
|
330
|
+
|
|
331
|
+
class DateBase(object):
|
|
332
|
+
'''
|
|
333
|
+
Description
|
|
334
|
+
--------------------
|
|
335
|
+
user-friendly object-oriented representation of a date
|
|
336
|
+
|
|
337
|
+
Class Attributes
|
|
338
|
+
--------------------
|
|
339
|
+
weekdays : dict
|
|
340
|
+
dictionary where keys are the days of the week and values are the corresponding index values
|
|
341
|
+
holidays : holidays.countries.united_states.UnitedStates
|
|
342
|
+
comprehensive list of U.S. holidays
|
|
343
|
+
|
|
344
|
+
Instance Attributes
|
|
345
|
+
--------------------
|
|
346
|
+
datetime : datetime.datetime
|
|
347
|
+
date and time (if applicable)
|
|
348
|
+
'''
|
|
349
|
+
|
|
350
|
+
weekdays = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3, 'Friday': 4, 'Saturday': 5, 'Sunday': 6}
|
|
351
|
+
weekdays.update({k[:3]: v for k,v in weekdays.items()})
|
|
352
|
+
holidays = holidays.UnitedStates()
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def __init__(self, arg=None):
|
|
356
|
+
self.datetime = arg
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
#+---------------------------------------------------------------------------+
|
|
360
|
+
# Static Methods
|
|
361
|
+
#+---------------------------------------------------------------------------+
|
|
362
|
+
|
|
363
|
+
@staticmethod
|
|
364
|
+
def to_timestamp(arg, **kwargs):
|
|
365
|
+
''' converts date in any format to timestamp '''
|
|
366
|
+
if not isinstance(arg, datetime.datetime):
|
|
367
|
+
arg = DateBase.to_datetime(arg, **kwargs)
|
|
368
|
+
return int(time.mktime(arg.timetuple()))
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
@staticmethod
|
|
372
|
+
def to_datetime(arg, **kwargs):
|
|
373
|
+
''' converts date in any format to datetime.datetime '''
|
|
374
|
+
return pd.to_datetime(arg, **kwargs).to_pydatetime()
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
#+---------------------------------------------------------------------------+
|
|
379
|
+
# Classes
|
|
380
|
+
#+---------------------------------------------------------------------------+
|
|
381
|
+
|
|
382
|
+
class Decorators(object):
|
|
383
|
+
|
|
384
|
+
@classmethod
|
|
385
|
+
def other_to_dt(cls, func):
|
|
386
|
+
''' decorator converts other argument used in magic methods to datetime '''
|
|
387
|
+
def wrapper(self, other):
|
|
388
|
+
if hasattr(other, 'dt'):
|
|
389
|
+
other = other.dt
|
|
390
|
+
else:
|
|
391
|
+
other = DateBase.to_datetime(other)
|
|
392
|
+
return func(self, other)
|
|
393
|
+
return wrapper
|
|
394
|
+
|
|
395
|
+
@classmethod
|
|
396
|
+
def arithmetic_other(cls, func):
|
|
397
|
+
''' decorator converts other argument used in magic methods to datetime '''
|
|
398
|
+
def wrapper(self, other):
|
|
399
|
+
try:
|
|
400
|
+
other = datetime.timedelta(float(other))
|
|
401
|
+
return Date(func(self, other))
|
|
402
|
+
except:
|
|
403
|
+
other = Date(other).dt
|
|
404
|
+
return func(self, other)
|
|
405
|
+
|
|
406
|
+
return wrapper
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
@classmethod
|
|
410
|
+
def next_last(cls, func):
|
|
411
|
+
''' decorator performs next/last logic '''
|
|
412
|
+
def wrapper(self, weekday, delta=0):
|
|
413
|
+
'''
|
|
414
|
+
returns DateBase object representing the next or last day of the week relative to self. For example self.next('Mon')
|
|
415
|
+
would return the date of the following monday.
|
|
416
|
+
|
|
417
|
+
Attributes
|
|
418
|
+
-----------------------
|
|
419
|
+
weekday : str
|
|
420
|
+
Day of the week either fully spelled out or the first three characters (e.g. 'Friday' or 'Fri') not case-sensitive.
|
|
421
|
+
delta : int
|
|
422
|
+
Offset value +/- from the current week (e.g. 0 is the current week and -1 is last week).
|
|
423
|
+
'''
|
|
424
|
+
day, desired_day = self.dt.weekday(), self.weekdays[weekday.title()]
|
|
425
|
+
delta += func(self, day - desired_day)
|
|
426
|
+
return self.minus(days=day).plus(days=desired_day, weeks=delta)
|
|
427
|
+
return wrapper
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
#+---------------------------------------------------------------------------+
|
|
432
|
+
# Class Methods
|
|
433
|
+
#+---------------------------------------------------------------------------+
|
|
434
|
+
# None
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
#+---------------------------------------------------------------------------+
|
|
438
|
+
# Properties
|
|
439
|
+
#+---------------------------------------------------------------------------+
|
|
440
|
+
|
|
441
|
+
@property
|
|
442
|
+
def dt(self):
|
|
443
|
+
''' datetime alias '''
|
|
444
|
+
return self.datetime
|
|
445
|
+
|
|
446
|
+
@property
|
|
447
|
+
def pandas(self):
|
|
448
|
+
''' pandas format '''
|
|
449
|
+
return pd.to_datetime(self.dt)
|
|
450
|
+
|
|
451
|
+
@property
|
|
452
|
+
def sql_server(self):
|
|
453
|
+
''' string in YYYY-MM-DD format '''
|
|
454
|
+
return self.str('%Y-%m-%d')
|
|
455
|
+
|
|
456
|
+
@property
|
|
457
|
+
def sqlsvr(self):
|
|
458
|
+
''' sql_server alias '''
|
|
459
|
+
return self.sql_server
|
|
460
|
+
|
|
461
|
+
@property
|
|
462
|
+
def oracle(self):
|
|
463
|
+
''' string in DD-%b-YY (e.g. 30-Sep-19) format '''
|
|
464
|
+
return self.str('%d-%b-%y')
|
|
465
|
+
|
|
466
|
+
@property
|
|
467
|
+
def timestamp(self):
|
|
468
|
+
''' integer '''
|
|
469
|
+
return self.to_timestamp(self.dt)
|
|
470
|
+
|
|
471
|
+
@property
|
|
472
|
+
def year(self):
|
|
473
|
+
return self.dt.year
|
|
474
|
+
|
|
475
|
+
@property
|
|
476
|
+
def month(self):
|
|
477
|
+
return self.dt.month
|
|
478
|
+
|
|
479
|
+
@property
|
|
480
|
+
def month_name(self):
|
|
481
|
+
return self.dt.strftime('%B')
|
|
482
|
+
|
|
483
|
+
@property
|
|
484
|
+
def day(self):
|
|
485
|
+
return self.dt.day
|
|
486
|
+
|
|
487
|
+
@property
|
|
488
|
+
def yesterday(self):
|
|
489
|
+
return self - 1
|
|
490
|
+
|
|
491
|
+
@property
|
|
492
|
+
def tomorrow(self):
|
|
493
|
+
return self + 1
|
|
494
|
+
|
|
495
|
+
@property
|
|
496
|
+
def month_start(self):
|
|
497
|
+
''' return date object representing the first day of the month '''
|
|
498
|
+
return Date(datetime.datetime(self.year, self.month, 1))
|
|
499
|
+
|
|
500
|
+
@property
|
|
501
|
+
def month_end(self):
|
|
502
|
+
''' last day of the month as a MonthEnd instance '''
|
|
503
|
+
return last_day_of_month(self.year, self.month)
|
|
504
|
+
|
|
505
|
+
@property
|
|
506
|
+
def last_business_day_of_month(self):
|
|
507
|
+
dt = self.month_end
|
|
508
|
+
dt -= {'Saturday': 1, 'Sunday': 2}.get(dt.weekday, 0)
|
|
509
|
+
return dt
|
|
510
|
+
|
|
511
|
+
@property
|
|
512
|
+
def weekday(self):
|
|
513
|
+
out = {
|
|
514
|
+
0: 'Monday',
|
|
515
|
+
1: 'Tuesday',
|
|
516
|
+
2: 'Wednesday',
|
|
517
|
+
3: 'Thursday',
|
|
518
|
+
4: 'Friday',
|
|
519
|
+
5: 'Saturday',
|
|
520
|
+
6: 'Sunday'
|
|
521
|
+
}[self.dt.weekday()]
|
|
522
|
+
return out
|
|
523
|
+
|
|
524
|
+
@property
|
|
525
|
+
def is_weekend(self):
|
|
526
|
+
''' returns True if date does not fall on a weekend '''
|
|
527
|
+
return self.weekday in ('Saturday','Sunday')
|
|
528
|
+
|
|
529
|
+
@property
|
|
530
|
+
def is_holiday(self):
|
|
531
|
+
''' returns True if date is a U.S. holiday '''
|
|
532
|
+
return self.sqlsvr in self.holidays
|
|
533
|
+
|
|
534
|
+
@property
|
|
535
|
+
def is_business_day(self):
|
|
536
|
+
''' returns True if date does not fall on a weekend '''
|
|
537
|
+
return not (self.is_weekend or self.is_holiday)
|
|
538
|
+
|
|
539
|
+
@property
|
|
540
|
+
def is_business_hours(self):
|
|
541
|
+
''' returns True if date is within business hours (8am - 9pm) '''
|
|
542
|
+
set_hour = lambda hour: datetime.datetime(self.year, self.month, self.day, hour)
|
|
543
|
+
out = self.is_business_day and self.dt >= set_hour(8) and self.dt <= set_hour(21)
|
|
544
|
+
return out
|
|
545
|
+
|
|
546
|
+
@property
|
|
547
|
+
def is_today(self):
|
|
548
|
+
''' returns True if date is the current day '''
|
|
549
|
+
return self.sqlsvr == Date().sqlsvr
|
|
550
|
+
|
|
551
|
+
@property
|
|
552
|
+
def holiday(self):
|
|
553
|
+
''' returns the current holiday, if applicable '''
|
|
554
|
+
return self.holidays.get(self.sqlsvr)
|
|
555
|
+
|
|
556
|
+
@property
|
|
557
|
+
def is_quarter_end(self):
|
|
558
|
+
''' returns True if the date is a quarter end date '''
|
|
559
|
+
return isinstance(Date(self.sqlsvr), QuarterEnd)
|
|
560
|
+
|
|
561
|
+
@property
|
|
562
|
+
def is_month_end(self):
|
|
563
|
+
'''' returns True if the date is a month end date '''
|
|
564
|
+
return isinstance(Date(self.sqlsvr), MonthEnd)
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
#+---------------------------------------------------------------------------+
|
|
569
|
+
# Magic Methods
|
|
570
|
+
#+---------------------------------------------------------------------------+
|
|
571
|
+
|
|
572
|
+
def __repr__(self):
|
|
573
|
+
return str(self)
|
|
574
|
+
|
|
575
|
+
def __str__(self):
|
|
576
|
+
components = ['%Y-%m-%d']
|
|
577
|
+
if self.dt.hour + self.dt.second + self.dt.microsecond > 0:
|
|
578
|
+
components.append('%I:%M:%S.%f %p')
|
|
579
|
+
return self.str(' '.join(components))
|
|
580
|
+
|
|
581
|
+
def __int__(self):
|
|
582
|
+
return self.timestamp
|
|
583
|
+
|
|
584
|
+
@Decorators.other_to_dt
|
|
585
|
+
def __eq__(self, other):
|
|
586
|
+
return self.dt == other
|
|
587
|
+
|
|
588
|
+
@Decorators.other_to_dt
|
|
589
|
+
def __ne__(self, other):
|
|
590
|
+
return self.dt != other
|
|
591
|
+
|
|
592
|
+
@Decorators.other_to_dt
|
|
593
|
+
def __lt__(self, other):
|
|
594
|
+
return self.dt < other
|
|
595
|
+
|
|
596
|
+
@Decorators.other_to_dt
|
|
597
|
+
def __gt__(self, other):
|
|
598
|
+
return self.dt > other
|
|
599
|
+
|
|
600
|
+
@Decorators.other_to_dt
|
|
601
|
+
def __le__(self, other):
|
|
602
|
+
return self.dt <= other
|
|
603
|
+
|
|
604
|
+
@Decorators.other_to_dt
|
|
605
|
+
def __ge__(self, other):
|
|
606
|
+
return self.dt >= other
|
|
607
|
+
|
|
608
|
+
@Decorators.arithmetic_other
|
|
609
|
+
def __add__(self, other):
|
|
610
|
+
''' if other is date-like then implements default behavior for adding datetimes otherwise
|
|
611
|
+
other is treated as timedelta '''
|
|
612
|
+
return self.dt + other
|
|
613
|
+
|
|
614
|
+
@Decorators.arithmetic_other
|
|
615
|
+
def __sub__(self, other):
|
|
616
|
+
''' if other is date-like then implements default behavior for subtracting datetimes otherwise
|
|
617
|
+
other is treated as timedelta '''
|
|
618
|
+
return self.dt - other
|
|
619
|
+
|
|
620
|
+
@Decorators.other_to_dt
|
|
621
|
+
def __contains__(self, item):
|
|
622
|
+
return self.normalize(item).dt <= self.dt < (self.normalize(item) + 1).dt
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
#+---------------------------------------------------------------------------+
|
|
627
|
+
# Instance Methods
|
|
628
|
+
#+---------------------------------------------------------------------------+
|
|
629
|
+
|
|
630
|
+
def skip_weekend(self, forward=True):
|
|
631
|
+
x = 2 if forward else -1
|
|
632
|
+
if self.weekday == 'Saturday':
|
|
633
|
+
return self + x
|
|
634
|
+
elif self.weekday == 'Sunday':
|
|
635
|
+
return self + (x - 1)
|
|
636
|
+
else:
|
|
637
|
+
return self
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def business_day_delta(self, delta):
|
|
641
|
+
out = Date(self.dt)
|
|
642
|
+
sign = np.sign(delta)
|
|
643
|
+
|
|
644
|
+
counter = 0
|
|
645
|
+
while counter < abs(delta):
|
|
646
|
+
out += sign
|
|
647
|
+
while not out.is_business_day:
|
|
648
|
+
out += sign
|
|
649
|
+
counter += 1
|
|
650
|
+
|
|
651
|
+
return out
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def skip_holiday(self, forward=True):
|
|
655
|
+
if self.is_holiday:
|
|
656
|
+
return (self + (1 if forward else -1)).skip_holiday(forward=forward)
|
|
657
|
+
else:
|
|
658
|
+
return self
|
|
659
|
+
|
|
660
|
+
def shift(self, **kwargs):
|
|
661
|
+
return Date(self.dt + relativedelta(**kwargs))
|
|
662
|
+
|
|
663
|
+
def replace(self, **kwargs):
|
|
664
|
+
return Date(self.dt.replace(**kwargs))
|
|
665
|
+
|
|
666
|
+
def str(self, fmt):
|
|
667
|
+
''' strftime shortcut '''
|
|
668
|
+
return self.dt.strftime(fmt)
|
|
669
|
+
|
|
670
|
+
def plus(self, **kwargs):
|
|
671
|
+
''' timedelta kwargs = weeks, days, hours, minutes, seconds, seconds, etc '''
|
|
672
|
+
return Date(self.dt + datetime.timedelta(**kwargs))
|
|
673
|
+
|
|
674
|
+
def minus(self, **kwargs):
|
|
675
|
+
''' timedelta kwargs = weeks, days, hours, minutes, seconds, seconds, etc '''
|
|
676
|
+
return Date(self.dt - datetime.timedelta(**kwargs))
|
|
677
|
+
|
|
678
|
+
@Decorators.next_last
|
|
679
|
+
def next(self, x):
|
|
680
|
+
''' see decorator for documentation '''
|
|
681
|
+
return +1 if x >= 0 else 0
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
@Decorators.next_last
|
|
685
|
+
def last(self, x):
|
|
686
|
+
''' see decorator for documentation '''
|
|
687
|
+
return -1 if x <= 0 else 0
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def normalize(self, inplace=False):
|
|
691
|
+
''' the time component (hours, minutes, seconds, microseconds) is set to zero (midnight) '''
|
|
692
|
+
out = Date(datetime.datetime(self.year, self.month, self.day))
|
|
693
|
+
if inplace:
|
|
694
|
+
self = out
|
|
695
|
+
else:
|
|
696
|
+
return out
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
class MonthEnd(DateBase):
|
|
701
|
+
|
|
702
|
+
def __init__(self, dt):
|
|
703
|
+
super().__init__(dt.replace(day=n_days_in_month(dt.year, dt.month)))
|
|
704
|
+
|
|
705
|
+
@property
|
|
706
|
+
def label(self):
|
|
707
|
+
return self.str('%Y%b')
|
|
708
|
+
|
|
709
|
+
@property
|
|
710
|
+
def short(self):
|
|
711
|
+
return self.str('%b-%y')
|
|
712
|
+
|
|
713
|
+
@property
|
|
714
|
+
def mid(self):
|
|
715
|
+
return self.str('%b%y')
|
|
716
|
+
|
|
717
|
+
@property
|
|
718
|
+
def last_quarter_end(self):
|
|
719
|
+
''' returns most recent quarter end '''
|
|
720
|
+
delta = 0
|
|
721
|
+
while True:
|
|
722
|
+
dt = self.offset(delta=delta)
|
|
723
|
+
if isinstance(dt, QuarterEnd): return dt
|
|
724
|
+
delta -= 1
|
|
725
|
+
|
|
726
|
+
def offset(self, delta):
|
|
727
|
+
''' returns the month end 'delta' months away from the instance '''
|
|
728
|
+
if not isinstance(delta, int):
|
|
729
|
+
raise TypeError("'delta' argument must be an integer")
|
|
730
|
+
if delta == 0: return self
|
|
731
|
+
dt = self.shift(months=delta).dt
|
|
732
|
+
return MonthEnd(dt)
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
class QuarterEnd(MonthEnd):
|
|
737
|
+
|
|
738
|
+
def __init__(self, dt, qtr, *args, **kwargs):
|
|
739
|
+
super().__init__(dt)
|
|
740
|
+
self.qtr = qtr
|
|
741
|
+
|
|
742
|
+
@property
|
|
743
|
+
def label(self):
|
|
744
|
+
return f'{self.year}Q{self.qtr}'
|
|
745
|
+
|
|
746
|
+
@property
|
|
747
|
+
def strqtr(self):
|
|
748
|
+
return self.label
|
|
749
|
+
|
|
750
|
+
@property
|
|
751
|
+
def quarter(self):
|
|
752
|
+
return self.qtr
|
|
753
|
+
|
|
754
|
+
@property
|
|
755
|
+
def short(self):
|
|
756
|
+
return f'Q{self.qtr}'
|
|
757
|
+
|
|
758
|
+
@property
|
|
759
|
+
def mid(self):
|
|
760
|
+
return f'{self.qtr}Q{str(self.year)[2:]}'
|
|
761
|
+
|
|
762
|
+
def to_month_end(self):
|
|
763
|
+
return MonthEnd(self.dt)
|
|
764
|
+
|
|
765
|
+
def offset(self, delta):
|
|
766
|
+
''' returns the quarter end 'delta' quarters away from the instance '''
|
|
767
|
+
if not isinstance(delta, int):
|
|
768
|
+
raise TypeError("'delta' argument must be an integer")
|
|
769
|
+
if delta == 0: return self
|
|
770
|
+
dt = self.shift(months=delta * 3).dt
|
|
771
|
+
qtr = ((self.qtr - 1 + delta) % 4) + 1
|
|
772
|
+
return QuarterEnd(dt, qtr)
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
if __name__ == '__main__':
|
|
780
|
+
pass
|