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