clockwork 0.1.2__tar.gz → 0.1.3__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.1.2
3
+ Version: 0.1.3
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
@@ -16,6 +16,8 @@ Classifier: Programming Language :: Python :: 3.11
16
16
  Classifier: Programming Language :: Python :: 3.12
17
17
  Requires-Dist: holidays
18
18
  Requires-Dist: iterlab
19
+ Requires-Dist: numpy
20
+ Requires-Dist: pandas
19
21
  Requires-Dist: pathpilot
20
22
  Requires-Dist: schedule
21
23
  Requires-Dist: textwrap3
@@ -0,0 +1,4 @@
1
+ from .core import *
2
+
3
+ __version__ = '0.1.3'
4
+ __author__ = 'Zachary Einck <zacharyeinck@gmail.com>'
@@ -0,0 +1,452 @@
1
+ import datetime
2
+ import calendar
3
+ import time
4
+ from functools import cached_property
5
+ from dateutil.relativedelta import relativedelta
6
+ import holidays
7
+ import pandas as pd
8
+ import numpy as np
9
+
10
+
11
+
12
+ class DateBase(object):
13
+ '''
14
+ Description
15
+ --------------------
16
+ date base class
17
+
18
+ Class Attributes
19
+ --------------------
20
+ factory : func
21
+ initializes new instances as the correct subclass
22
+
23
+ Instance Attributes
24
+ --------------------
25
+ datetime : datetime.datetime
26
+ date and time (if applicable)
27
+ '''
28
+
29
+ #╭-------------------------------------------------------------------------╮
30
+ #| Initialize Instance |
31
+ #╰-------------------------------------------------------------------------╯
32
+
33
+ def __init__(self, arg=None):
34
+ self.datetime = arg
35
+
36
+
37
+ #╭-------------------------------------------------------------------------╮
38
+ #| Static Methods |
39
+ #╰-------------------------------------------------------------------------╯
40
+
41
+ @staticmethod
42
+ def to_timestamp(arg, **kwargs):
43
+ ''' converts date in any format to timestamp '''
44
+ if not isinstance(arg, datetime.datetime):
45
+ arg = DateBase.to_datetime(arg, **kwargs)
46
+ return int(time.mktime(arg.timetuple()))
47
+
48
+
49
+ @staticmethod
50
+ def to_datetime(arg, **kwargs):
51
+ ''' converts date in any format to datetime.datetime '''
52
+ return pd.to_datetime(arg, **kwargs).to_pydatetime()
53
+
54
+
55
+ @staticmethod
56
+ def n_days_in_month(year, month):
57
+ '''
58
+ Description
59
+ ----------
60
+ Returns the # of days in a month for a given year.
61
+ For example, if year=2024 and month=2, 29 days is returned since it's a leap year
62
+
63
+ Parameters
64
+ ----------
65
+ year : int
66
+ year in YYYY format
67
+ month : int
68
+ month
69
+
70
+ Returns
71
+ ----------
72
+ out : int
73
+ number of days
74
+ '''
75
+ out = calendar.monthrange(year, month)[1]
76
+ return out
77
+
78
+
79
+ #╭-------------------------------------------------------------------------╮
80
+ #| Class Methods |
81
+ #╰-------------------------------------------------------------------------╯
82
+
83
+ @classmethod
84
+ def spawn(cls, *args, **kwargs):
85
+ ''' Spawn a new instance. The factory function assigns the correct subclass '''
86
+ return cls.factory(*args, **kwargs)
87
+
88
+
89
+ @classmethod
90
+ def last_day_of_month(cls, year, month):
91
+ '''
92
+ Description
93
+ ----------
94
+ Returns date of the last day of the month for a given year and month
95
+
96
+ Parameters
97
+ ----------
98
+ year : int
99
+ year in YYYY format
100
+ month : int
101
+ month
102
+
103
+ Returns
104
+ ----------
105
+ clockwork.{MonthEnd}|{QuarterEnd}
106
+ '''
107
+ day = cls.n_days_in_month(year, month)
108
+ return cls.spawn(datetime.datetime(year, month, day))
109
+
110
+
111
+ #╭-------------------------------------------------------------------------╮
112
+ #| Classes |
113
+ #╰-------------------------------------------------------------------------╯
114
+
115
+ class Decorators(object):
116
+
117
+ @classmethod
118
+ def spawn(cls, func):
119
+ ''' spawn new date instance '''
120
+
121
+ def wrapper(self, *args, **kwargs):
122
+ return cls.spawn(func(self, *args, **kwargs))
123
+
124
+ return wrapper
125
+
126
+
127
+ @classmethod
128
+ def other_to_dt(cls, func):
129
+ ''' decorator converts other argument used in magic methods to datetime '''
130
+
131
+ def wrapper(self, other):
132
+ if hasattr(other, 'dt'):
133
+ other = other.dt
134
+ else:
135
+ other = DateBase.to_datetime(other)
136
+ return func(self, other)
137
+
138
+ return wrapper
139
+
140
+
141
+ @classmethod
142
+ def arithmetic_other(cls, func):
143
+ ''' decorator converts other argument used in magic methods to datetime '''
144
+
145
+ def wrapper(self, other):
146
+ try:
147
+ other = datetime.timedelta(float(other))
148
+ return cls.spawn(func(self, other))
149
+ except:
150
+ other = cls.spawn(other).dt
151
+ return func(self, other)
152
+
153
+ return wrapper
154
+
155
+
156
+ @classmethod
157
+ def next_last(cls, func):
158
+ ''' decorator performs next/last logic '''
159
+
160
+ def wrapper(self, weekday, delta=0):
161
+ '''
162
+ returns DateBase object representing the next or last day of the week relative to self. For example self.next('Mon')
163
+ would return the date of the following monday.
164
+
165
+ Attributes
166
+ -----------------------
167
+ weekday : str
168
+ Day of the week either fully spelled out or the first three characters (e.g. 'Friday' or 'Fri') not case-sensitive.
169
+ delta : int
170
+ Offset value +/- from the current week (e.g. 0 is the current week and -1 is last week).
171
+ '''
172
+ day, desired_day = self.dt.weekday(), self.weekdays[weekday.title()]
173
+ delta += func(self, day - desired_day)
174
+ return self.minus(days=day).plus(days=desired_day, weeks=delta)
175
+
176
+ return wrapper
177
+
178
+
179
+ #╭-------------------------------------------------------------------------╮
180
+ #| Properties |
181
+ #╰-------------------------------------------------------------------------╯
182
+
183
+ @property
184
+ def date(self):
185
+ ''' returns normalized instance '''
186
+ return self.normalize()
187
+
188
+ @property
189
+ def d(self):
190
+ ''' converts datetime.datetime to datetime.date '''
191
+ return self.datetime.date()
192
+
193
+ @property
194
+ def dt(self):
195
+ ''' datetime alias '''
196
+ return self.datetime
197
+
198
+ @property
199
+ def pandas(self):
200
+ ''' pandas format '''
201
+ return pd.to_datetime(self.dt)
202
+
203
+ @property
204
+ def ymd(self):
205
+ ''' string in YYYY-MM-DD format '''
206
+ return self.str('%Y-%m-%d')
207
+
208
+ @property
209
+ def sql_server(self):
210
+ ''' ymd alias '''
211
+ return self.ymd
212
+
213
+ @property
214
+ def oracle(self):
215
+ ''' string in DD-%b-YY (e.g. 30-Sep-19) format '''
216
+ return self.str('%d-%b-%y')
217
+
218
+ @property
219
+ def timestamp(self):
220
+ ''' integer '''
221
+ return self.to_timestamp(self.dt)
222
+
223
+ @property
224
+ def year(self):
225
+ return self.dt.year
226
+
227
+ @property
228
+ def month(self):
229
+ return self.dt.month
230
+
231
+ @property
232
+ def month_name(self):
233
+ return self.dt.strftime('%B')
234
+
235
+ @property
236
+ def day(self):
237
+ return self.dt.day
238
+
239
+ @property
240
+ def yesterday(self):
241
+ return self - 1
242
+
243
+ @property
244
+ def tomorrow(self):
245
+ return self + 1
246
+
247
+ @property
248
+ def month_start(self):
249
+ ''' return date object representing the first day of the month '''
250
+ return self.spawn(datetime.datetime(self.year, self.month, 1))
251
+
252
+ @property
253
+ def month_end(self):
254
+ ''' last day of the month as a MonthEnd instance '''
255
+ return self.last_day_of_month(self.year, self.month)
256
+
257
+ @property
258
+ def last_business_day_of_month(self):
259
+ dt = self.month_end
260
+ dt -= {'Saturday': 1, 'Sunday': 2}.get(dt.weekday, 0)
261
+ return dt
262
+
263
+ @property
264
+ def weekday(self):
265
+ return self.str('%A')
266
+
267
+ @property
268
+ def weekday_short(self):
269
+ return self.str('%a')
270
+
271
+ @property
272
+ def is_weekend(self):
273
+ ''' returns True if date does not fall on a weekend '''
274
+ return self.weekday in ('Saturday','Sunday')
275
+
276
+ @property
277
+ def is_holiday(self):
278
+ ''' returns True if date is a U.S. holiday '''
279
+ return self.ymd in self.holidays
280
+
281
+ @property
282
+ def is_business_day(self):
283
+ ''' returns True if date does not fall on a weekend '''
284
+ return not (self.is_weekend or self.is_holiday)
285
+
286
+ @property
287
+ def is_business_hours(self):
288
+ ''' returns True if date is within business hours (8am - 9pm) '''
289
+ set_hour = lambda hour: datetime.datetime(self.year, self.month, self.day, hour)
290
+ out = self.is_business_day and self.dt >= set_hour(8) and self.dt <= set_hour(21)
291
+ return out
292
+
293
+ @property
294
+ def is_today(self):
295
+ ''' returns True if date is the current day '''
296
+ return self.ymd == self.spawn().ymd
297
+
298
+ @property
299
+ def holiday(self):
300
+ ''' returns the current holiday, if applicable '''
301
+ return self.holidays.get(self.ymd)
302
+
303
+ @cached_property
304
+ def weekdays(self):
305
+ ''' dictionary where keys are the days of the week and values are the corresponding index values '''
306
+ out = {k: i for i, k in enumerate((
307
+ 'Monday','Tuesday','Wednesday','Thursday',
308
+ 'Friday','Saturday','Sunday'))}
309
+ out.update({k[:3]: v for k, v in out.items()})
310
+ return out
311
+
312
+ @cached_property
313
+ def holidays(self):
314
+ ''' comprehensive list of U.S. holidays '''
315
+ return holidays.UnitedStates()
316
+
317
+
318
+ #╭-------------------------------------------------------------------------╮
319
+ #| Magic Methods |
320
+ #╰-------------------------------------------------------------------------╯
321
+
322
+ def __repr__(self):
323
+ return str(self)
324
+
325
+ def __str__(self):
326
+ components = ['%Y-%m-%d']
327
+ if self.dt.hour + self.dt.second + self.dt.microsecond > 0:
328
+ components.append('%I:%M:%S.%f %p')
329
+ return self.str(' '.join(components))
330
+
331
+ def __int__(self):
332
+ return self.timestamp
333
+
334
+ @Decorators.other_to_dt
335
+ def __eq__(self, other):
336
+ return self.dt == other
337
+
338
+ @Decorators.other_to_dt
339
+ def __ne__(self, other):
340
+ return self.dt != other
341
+
342
+ @Decorators.other_to_dt
343
+ def __lt__(self, other):
344
+ return self.dt < other
345
+
346
+ @Decorators.other_to_dt
347
+ def __gt__(self, other):
348
+ return self.dt > other
349
+
350
+ @Decorators.other_to_dt
351
+ def __le__(self, other):
352
+ return self.dt <= other
353
+
354
+ @Decorators.other_to_dt
355
+ def __ge__(self, other):
356
+ return self.dt >= other
357
+
358
+ @Decorators.arithmetic_other
359
+ def __add__(self, other):
360
+ ''' if other is date-like then implements default behavior for adding datetimes otherwise
361
+ other is treated as timedelta '''
362
+ return self.dt + other
363
+
364
+ @Decorators.arithmetic_other
365
+ def __sub__(self, other):
366
+ ''' if other is date-like then implements default behavior for subtracting datetimes otherwise
367
+ other is treated as timedelta '''
368
+ return self.dt - other
369
+
370
+ @Decorators.other_to_dt
371
+ def __contains__(self, item):
372
+ return self.normalize(item).dt <= self.dt < (self.normalize(item) + 1).dt
373
+
374
+
375
+ #╭-------------------------------------------------------------------------╮
376
+ #| Instance Methods |
377
+ #╰-------------------------------------------------------------------------╯
378
+
379
+ def skip_weekend(self, forward=True):
380
+ x = 2 if forward else -1
381
+ if self.weekday == 'Saturday':
382
+ return self + x
383
+ elif self.weekday == 'Sunday':
384
+ return self + (x - 1)
385
+ else:
386
+ return self
387
+
388
+
389
+ def business_day_delta(self, delta):
390
+ out = self.spawn(self.dt)
391
+ sign = np.sign(delta)
392
+
393
+ counter = 0
394
+ while counter < abs(delta):
395
+ out += sign
396
+ while not out.is_business_day:
397
+ out += sign
398
+ counter += 1
399
+
400
+ return out
401
+
402
+
403
+ def skip_holiday(self, forward=True):
404
+ if self.is_holiday:
405
+ return (self + (1 if forward else -1)).skip_holiday(forward=forward)
406
+ else:
407
+ return self
408
+
409
+
410
+ def str(self, fmt):
411
+ ''' strftime shortcut '''
412
+ return self.dt.strftime(fmt)
413
+
414
+
415
+ @Decorators.spawn
416
+ def shift(self, **kwargs):
417
+ return self.dt + relativedelta(**kwargs)
418
+
419
+
420
+ @Decorators.spawn
421
+ def replace(self, **kwargs):
422
+ return self.dt.replace(**kwargs)
423
+
424
+
425
+ @Decorators.spawn
426
+ def plus(self, **kwargs):
427
+ ''' timedelta kwargs = weeks, days, hours, minutes, seconds, seconds, etc '''
428
+ return self.dt + datetime.timedelta(**kwargs)
429
+
430
+
431
+ @Decorators.spawn
432
+ def minus(self, **kwargs):
433
+ ''' timedelta kwargs = weeks, days, hours, minutes, seconds, seconds, etc '''
434
+ return self.dt - datetime.timedelta(**kwargs)
435
+
436
+
437
+ @Decorators.next_last
438
+ def next(self, x):
439
+ ''' see decorator for documentation '''
440
+ return +1 if x >= 0 else 0
441
+
442
+
443
+ @Decorators.next_last
444
+ def last(self, x):
445
+ ''' see decorator for documentation '''
446
+ return -1 if x <= 0 else 0
447
+
448
+
449
+ @Decorators.spawn
450
+ def normalize(self):
451
+ ''' the time component (hours, minutes, seconds, microseconds) is set to zero (midnight) '''
452
+ return datetime.datetime(self.year, self.month, self.day)
@@ -0,0 +1,51 @@
1
+ from ._base import DateBase
2
+
3
+
4
+
5
+ class MonthEnd(DateBase):
6
+
7
+ #╭-------------------------------------------------------------------------╮
8
+ #| Initialize Instance |
9
+ #╰-------------------------------------------------------------------------╯
10
+
11
+ def __init__(self, dt):
12
+ super().__init__(dt.replace(day=self.n_days_in_month(dt.year, dt.month)))
13
+
14
+
15
+ #╭-------------------------------------------------------------------------╮
16
+ #| Properties |
17
+ #╰-------------------------------------------------------------------------╯
18
+
19
+ @property
20
+ def long(self):
21
+ return self.str('%Y-%m-%d')
22
+
23
+ @property
24
+ def compact(self):
25
+ return self.str('%Y-%m')
26
+
27
+ @property
28
+ def short(self):
29
+ return self.str('%b')
30
+
31
+ @property
32
+ def last_quarter_end(self):
33
+ ''' returns most recent quarter end '''
34
+ delta = 0
35
+ while True:
36
+ obj = self.offset(delta=delta)
37
+ if hasattr(obj, 'quarter'): return obj
38
+ delta -= 1
39
+
40
+
41
+ #╭-------------------------------------------------------------------------╮
42
+ #| Instance Methods |
43
+ #╰-------------------------------------------------------------------------╯
44
+
45
+ def offset(self, delta):
46
+ ''' returns the month end 'delta' months away from the instance '''
47
+ if not isinstance(delta, int):
48
+ raise TypeError("'delta' argument must be an integer")
49
+ if delta == 0: return self
50
+ dt = self.shift(months=delta).dt
51
+ return self.__class__(dt)
@@ -0,0 +1,52 @@
1
+ from ._month_end import MonthEnd
2
+
3
+
4
+
5
+ class QuarterEnd(MonthEnd):
6
+
7
+ #╭-------------------------------------------------------------------------╮
8
+ #| Initialize Instance |
9
+ #╰-------------------------------------------------------------------------╯
10
+
11
+ def __init__(self, dt, qtr, *args, **kwargs):
12
+ super().__init__(dt)
13
+ self.qtr = qtr
14
+
15
+
16
+ #╭-------------------------------------------------------------------------╮
17
+ #| Properties |
18
+ #╰-------------------------------------------------------------------------╯
19
+
20
+ @property
21
+ def long(self):
22
+ return f'{self.year}Q{self.qtr}'
23
+
24
+ @property
25
+ def compact(self):
26
+ return f'{self.qtr}Q' + self.str('%y')
27
+
28
+ @property
29
+ def short(self):
30
+ return f'Q{self.qtr}'
31
+
32
+ @property
33
+ def quarter(self):
34
+ return self.qtr
35
+
36
+
37
+ #╭-------------------------------------------------------------------------╮
38
+ #| Instance Methods |
39
+ #╰-------------------------------------------------------------------------╯
40
+
41
+ def to_month_end(self):
42
+ return MonthEnd(self.dt)
43
+
44
+
45
+ def offset(self, delta):
46
+ ''' returns the quarter end 'delta' quarters away from the instance '''
47
+ if not isinstance(delta, int):
48
+ raise TypeError("'delta' argument must be an integer")
49
+ if delta == 0: return self
50
+ dt = self.shift(months=delta * 3).dt
51
+ qtr = ((self.qtr - 1 + delta) % 4) + 1
52
+ return self.__class__(dt, qtr)