clockwork 0.2.1__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {clockwork-0.2.1 → clockwork-0.3.0}/PKG-INFO +2 -4
- clockwork-0.3.0/clockwork/__init__.py +12 -0
- clockwork-0.3.0/clockwork/constants.py +1 -0
- clockwork-0.3.0/clockwork/decorators.py +22 -0
- clockwork-0.3.0/clockwork/month_end.py +188 -0
- clockwork-0.3.0/clockwork/quarter_end.py +244 -0
- clockwork-0.3.0/clockwork/taskmaster/_scheduler.py +27 -0
- {clockwork-0.2.1 → clockwork-0.3.0}/clockwork/taskmaster/_task.py +11 -10
- {clockwork-0.2.1 → clockwork-0.3.0}/clockwork/taskmaster/file_monitor.py +5 -6
- {clockwork-0.2.1 → clockwork-0.3.0}/clockwork/taskmaster/logger.py +2 -6
- {clockwork-0.2.1 → clockwork-0.3.0}/clockwork/taskmaster/taskmaster.py +9 -12
- clockwork-0.3.0/clockwork/timestamp.py +917 -0
- clockwork-0.3.0/clockwork/utils.py +107 -0
- {clockwork-0.2.1 → clockwork-0.3.0}/pyproject.toml +2 -4
- clockwork-0.2.1/clockwork/__init__.py +0 -4
- clockwork-0.2.1/clockwork/_base.py +0 -497
- clockwork-0.2.1/clockwork/_month_end.py +0 -77
- clockwork-0.2.1/clockwork/_quarter_end.py +0 -66
- clockwork-0.2.1/clockwork/core.py +0 -214
- clockwork-0.2.1/clockwork/decorators.py +0 -19
- clockwork-0.2.1/clockwork/taskmaster/_scheduler.py +0 -20
- clockwork-0.2.1/clockwork/utils.py +0 -140
- {clockwork-0.2.1 → clockwork-0.3.0}/LICENSE +0 -0
- {clockwork-0.2.1 → clockwork-0.3.0}/README.md +0 -0
- {clockwork-0.2.1 → clockwork-0.3.0}/clockwork/taskmaster/__init__.py +0 -0
- {clockwork-0.2.1 → clockwork-0.3.0}/clockwork/taskmaster/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: clockwork
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
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,12 +15,10 @@ 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 (>=0.3.0)
|
|
20
20
|
Requires-Dist: pandas
|
|
21
|
-
Requires-Dist: pathpilot
|
|
22
21
|
Requires-Dist: schedule
|
|
23
|
-
Requires-Dist: textwrap3
|
|
24
22
|
Project-URL: Repository, https://github.com/zteinck/clockwork
|
|
25
23
|
Description-Content-Type: text/markdown
|
|
26
24
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .timestamp import Timestamp
|
|
2
|
+
from .month_end import MonthEnd
|
|
3
|
+
from .quarter_end import QuarterEnd
|
|
4
|
+
from .decorators import action_timer
|
|
5
|
+
|
|
6
|
+
from .utils import (
|
|
7
|
+
format_elapsed_seconds,
|
|
8
|
+
date_format_to_regex,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__version__ = '0.3.0'
|
|
12
|
+
__author__ = 'Zachary Einck <zacharyeinck@gmail.com>'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
MONTHS_IN_YEAR = 12
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from functools import wraps
|
|
2
|
+
from oddments import add_border
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
from .utils import format_elapsed_seconds
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def action_timer(func):
|
|
9
|
+
|
|
10
|
+
@wraps(func)
|
|
11
|
+
def wrapper(*args, **kwargs):
|
|
12
|
+
start_time = time.time()
|
|
13
|
+
action = func.__name__
|
|
14
|
+
header = add_border(action, width=75, fixed_width=True)
|
|
15
|
+
print(header + '\n')
|
|
16
|
+
out = func(*args, **kwargs)
|
|
17
|
+
duration = format_elapsed_seconds(time.time() - start_time)
|
|
18
|
+
trailer = add_border(f'{action} complete in {duration}.')
|
|
19
|
+
print(trailer + '\n')
|
|
20
|
+
return out
|
|
21
|
+
|
|
22
|
+
return wrapper
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
|
|
2
|
+
import datetime
|
|
3
|
+
import oddments as odd
|
|
4
|
+
|
|
5
|
+
from .timestamp import Timestamp
|
|
6
|
+
from .constants import MONTHS_IN_YEAR
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MonthEnd(Timestamp):
|
|
10
|
+
'''
|
|
11
|
+
Description
|
|
12
|
+
--------------------
|
|
13
|
+
Month end timestamp object.
|
|
14
|
+
|
|
15
|
+
Class Attributes
|
|
16
|
+
--------------------
|
|
17
|
+
_increment : int
|
|
18
|
+
Number of months to increment during offsets.
|
|
19
|
+
|
|
20
|
+
Instance Attributes
|
|
21
|
+
--------------------
|
|
22
|
+
...
|
|
23
|
+
'''
|
|
24
|
+
|
|
25
|
+
#╭-------------------------------------------------------------------------╮
|
|
26
|
+
#| Class Attributes |
|
|
27
|
+
#╰-------------------------------------------------------------------------╯
|
|
28
|
+
|
|
29
|
+
_increment = 1
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
#╭-------------------------------------------------------------------------╮
|
|
33
|
+
#| Initialize Instance |
|
|
34
|
+
#╰-------------------------------------------------------------------------╯
|
|
35
|
+
|
|
36
|
+
def __init__(self, arg=None, **kwargs):
|
|
37
|
+
'''
|
|
38
|
+
Parameters
|
|
39
|
+
------------
|
|
40
|
+
arg : None | any
|
|
41
|
+
A scalar value to be interpreted as a month end date. Must be None
|
|
42
|
+
if 'year' or 'month' are provided. Refer to '_to_datetime()' for
|
|
43
|
+
supported input formats.
|
|
44
|
+
year : int
|
|
45
|
+
The calendar year of the month end date.
|
|
46
|
+
month : int
|
|
47
|
+
The calendar month of the month end date (1 to 12).
|
|
48
|
+
offset : int
|
|
49
|
+
Number of months to shift from the base month end. Base month end
|
|
50
|
+
defaults to the most recently completed month end when no other
|
|
51
|
+
parameters are provided. Use positive values to move forward and
|
|
52
|
+
negative values to move backward in time.
|
|
53
|
+
kwargs : dict
|
|
54
|
+
Additional keyword arguments are forwarded to the Timestamp
|
|
55
|
+
constructor. Refer to its documentation for details.
|
|
56
|
+
'''
|
|
57
|
+
offset = self._try_int(kwargs.pop('offset', 0))
|
|
58
|
+
year, month = [kwargs.pop(k, None) for k in ['year','month']]
|
|
59
|
+
year_or_month = not (year is None and month is None)
|
|
60
|
+
has_arg = arg is not None
|
|
61
|
+
|
|
62
|
+
if has_arg:
|
|
63
|
+
if year_or_month:
|
|
64
|
+
raise ValueError(
|
|
65
|
+
"Both 'year' and 'month' must be "
|
|
66
|
+
"None when 'arg' is not None."
|
|
67
|
+
)
|
|
68
|
+
else:
|
|
69
|
+
if year_or_month:
|
|
70
|
+
year, month = map(self._try_int, (year, month))
|
|
71
|
+
self._validate_month(month)
|
|
72
|
+
else:
|
|
73
|
+
now = datetime.datetime.now()
|
|
74
|
+
year = now.year
|
|
75
|
+
month = now.month - 1 # default to last month
|
|
76
|
+
|
|
77
|
+
year, month = self._offset(
|
|
78
|
+
year=year,
|
|
79
|
+
month=month,
|
|
80
|
+
offset=offset * self._increment
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
day = self.days_in_month(year, month)
|
|
84
|
+
arg = datetime.datetime(year, month, day)
|
|
85
|
+
|
|
86
|
+
super().__init__(arg, **kwargs)
|
|
87
|
+
self.validate_instance()
|
|
88
|
+
|
|
89
|
+
if has_arg and offset != 0:
|
|
90
|
+
self.offset(periods=offset, inplace=True)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
#╭-------------------------------------------------------------------------╮
|
|
94
|
+
#| Properties |
|
|
95
|
+
#╰-------------------------------------------------------------------------╯
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def long_label(self):
|
|
99
|
+
return self.str('%Y-%m-%d')
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def compact_label(self):
|
|
104
|
+
return self.str('%Y-%m')
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def short_label(self):
|
|
109
|
+
return self.str('%b')
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def is_year_end(self):
|
|
114
|
+
return self.month == MONTHS_IN_YEAR
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def relative_offset(self):
|
|
119
|
+
'''
|
|
120
|
+
Description
|
|
121
|
+
------------
|
|
122
|
+
Returns the number of periods the instance is offset relative to
|
|
123
|
+
the most recent period end. Periods are defined by the '_increment'
|
|
124
|
+
class attribute.
|
|
125
|
+
|
|
126
|
+
Returns
|
|
127
|
+
------------
|
|
128
|
+
q : int
|
|
129
|
+
Number of offset periods.
|
|
130
|
+
'''
|
|
131
|
+
a, b = [
|
|
132
|
+
self._total_months(obj.year, obj.month)
|
|
133
|
+
for obj in (self, self.__class__(offset=0))
|
|
134
|
+
]
|
|
135
|
+
q, r = divmod(a - b, self._increment)
|
|
136
|
+
if r == 0: return int(q)
|
|
137
|
+
raise ValueError(f"Unexpected remainder: {r}")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
#╭-------------------------------------------------------------------------╮
|
|
141
|
+
#| Instance Methods |
|
|
142
|
+
#╰-------------------------------------------------------------------------╯
|
|
143
|
+
|
|
144
|
+
def offset(self, periods, inplace=False):
|
|
145
|
+
''' offsets instance by desired number of periods '''
|
|
146
|
+
odd.validate_value(
|
|
147
|
+
value=periods,
|
|
148
|
+
attr='periods',
|
|
149
|
+
types=int
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
out = self.__class__(offset=self.relative_offset + periods)
|
|
153
|
+
|
|
154
|
+
if inplace:
|
|
155
|
+
self.dt = out.dt
|
|
156
|
+
else:
|
|
157
|
+
return out
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _offset(self, year, month, offset):
|
|
161
|
+
total_months = self._total_months(year, month + offset)
|
|
162
|
+
y, m = divmod(total_months, MONTHS_IN_YEAR)
|
|
163
|
+
return (y - 1, MONTHS_IN_YEAR) if m == 0 else (y, m)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def validate_instance(self):
|
|
167
|
+
''' raises an error if the instance fails validation '''
|
|
168
|
+
error_msg = self._validate_instance()
|
|
169
|
+
|
|
170
|
+
if error_msg is not None:
|
|
171
|
+
raise ValueError(
|
|
172
|
+
f"Failed to initialize a {self.__class__.__name__} "
|
|
173
|
+
f"instance because the {error_msg}. "
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _validate_instance(self):
|
|
178
|
+
if not self.is_last_day_of_month:
|
|
179
|
+
return (
|
|
180
|
+
f"day ({self.day}) is not the last day "
|
|
181
|
+
f"({self.last_day_of_month}) of {self.month_name} {self.year}"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
if not self.is_normalized:
|
|
185
|
+
return (
|
|
186
|
+
"date must have no time component. Use "
|
|
187
|
+
"'normalize=True' when creating the date"
|
|
188
|
+
)
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import oddments as odd
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import datetime
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
from .month_end import MonthEnd
|
|
7
|
+
from .constants import MONTHS_IN_YEAR
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class QuarterEnd(MonthEnd):
|
|
11
|
+
'''
|
|
12
|
+
Description
|
|
13
|
+
--------------------
|
|
14
|
+
Quarter end timestamp object.
|
|
15
|
+
|
|
16
|
+
Class Attributes
|
|
17
|
+
--------------------
|
|
18
|
+
scheme : tuple
|
|
19
|
+
Quarter end months.
|
|
20
|
+
|
|
21
|
+
Instance Attributes
|
|
22
|
+
--------------------
|
|
23
|
+
...
|
|
24
|
+
'''
|
|
25
|
+
|
|
26
|
+
#╭-------------------------------------------------------------------------╮
|
|
27
|
+
#| Class Attributes |
|
|
28
|
+
#╰-------------------------------------------------------------------------╯
|
|
29
|
+
|
|
30
|
+
_increment = 3
|
|
31
|
+
scheme = (3, 6, 9, 12)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
#╭-------------------------------------------------------------------------╮
|
|
35
|
+
#| Initialize Instance |
|
|
36
|
+
#╰-------------------------------------------------------------------------╯
|
|
37
|
+
|
|
38
|
+
def __init__(self, arg=None, **kwargs):
|
|
39
|
+
'''
|
|
40
|
+
Parameters
|
|
41
|
+
------------
|
|
42
|
+
arg : None | any
|
|
43
|
+
A scalar value to be interpreted as a quarter end date. Must be
|
|
44
|
+
None if 'year', 'month', or 'quarter' are provided. Refer to
|
|
45
|
+
'_to_datetime()' for supported input formats.
|
|
46
|
+
year : int
|
|
47
|
+
The calendar year of the quarter end date.
|
|
48
|
+
month : int
|
|
49
|
+
The calendar month of the quarter end date (1 to 12).
|
|
50
|
+
quarter : int
|
|
51
|
+
The quarter number (1 to 4). Cannot be used together with the
|
|
52
|
+
'month' argument.
|
|
53
|
+
offset : int
|
|
54
|
+
Number of quarters to shift from the base quarter end. Base
|
|
55
|
+
quarter end defaults to the most recently completed quarter end
|
|
56
|
+
when no other parameters are provided. Use positive values to move
|
|
57
|
+
forward and negative values to move backward in time.
|
|
58
|
+
kwargs : dict
|
|
59
|
+
Additional keyword arguments are forwarded to the Timestamp
|
|
60
|
+
constructor. Refer to its documentation for details.
|
|
61
|
+
'''
|
|
62
|
+
qtr = kwargs.pop('quarter', None)
|
|
63
|
+
parsed = self.parse_label(arg)
|
|
64
|
+
|
|
65
|
+
if parsed is not None:
|
|
66
|
+
for k in ['year','quarter']:
|
|
67
|
+
if kwargs.get(k) is not None:
|
|
68
|
+
raise ValueError(
|
|
69
|
+
f"'{k}' must be None when 'arg' "
|
|
70
|
+
"is a quarter label: {arg!r}."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
kwargs['year'], qtr = parsed
|
|
74
|
+
arg = None
|
|
75
|
+
|
|
76
|
+
if qtr is not None:
|
|
77
|
+
if kwargs.get('month') is not None:
|
|
78
|
+
raise ValueError(
|
|
79
|
+
"Cannot pass 'quarter' and 'month' "
|
|
80
|
+
"arguments simultaneously."
|
|
81
|
+
)
|
|
82
|
+
qtr = self._try_int(qtr)
|
|
83
|
+
kwargs['month'] = self.scheme[qtr - 1]
|
|
84
|
+
|
|
85
|
+
super().__init__(arg, **kwargs)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
#╭-------------------------------------------------------------------------╮
|
|
89
|
+
#| Properties |
|
|
90
|
+
#╰-------------------------------------------------------------------------╯
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def long_label(self):
|
|
94
|
+
return f'{self.year}Q{self.qtr}'
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def compact_label(self):
|
|
99
|
+
return f'{self.qtr}Q' + self.str('%y')
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def short_label(self):
|
|
104
|
+
return f'Q{self.qtr}'
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def quarter(self):
|
|
109
|
+
''' the quarter number (1 to 4) '''
|
|
110
|
+
return int(self.scheme.index(self.month) + 1)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def qtr(self):
|
|
115
|
+
''' quarter alias '''
|
|
116
|
+
return self.quarter
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
#╭-------------------------------------------------------------------------╮
|
|
120
|
+
#| Instance Methods |
|
|
121
|
+
#╰-------------------------------------------------------------------------╯
|
|
122
|
+
|
|
123
|
+
def _offset(self, year, month, offset):
|
|
124
|
+
year, month = self._backtrack_to_scheme(year, month)
|
|
125
|
+
return super()._offset(year, month, offset)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _validate_instance(self):
|
|
129
|
+
if self.month not in self.scheme:
|
|
130
|
+
return f'month ({self.month}) is not in scheme: {self.scheme}'
|
|
131
|
+
return super()._validate_instance()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
#╭-------------------------------------------------------------------------╮
|
|
135
|
+
#| Class Methods |
|
|
136
|
+
#╰-------------------------------------------------------------------------╯
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def _backtrack_to_scheme(cls, year, month):
|
|
140
|
+
''' Backtracks from the given year and month, moving one month at a
|
|
141
|
+
time, until a month that is part of the scheme is found. '''
|
|
142
|
+
while month not in cls.scheme:
|
|
143
|
+
year, month = cls._get_prior_month(year, month)
|
|
144
|
+
return year, month
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@classmethod
|
|
148
|
+
def set_scheme(cls, value):
|
|
149
|
+
''' safely sets 'scheme' class attribute '''
|
|
150
|
+
|
|
151
|
+
odd.validate_value(
|
|
152
|
+
value=value,
|
|
153
|
+
attr='scheme',
|
|
154
|
+
types=tuple
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
if len(value) != 4:
|
|
158
|
+
raise ValueError(
|
|
159
|
+
"'scheme' must contain 4 elements, "
|
|
160
|
+
f"got: {len(value):,}"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
value = tuple(map(cls._try_int, value))
|
|
164
|
+
s = pd.Series(value)
|
|
165
|
+
|
|
166
|
+
if not (s.diff().dropna() == 3).all():
|
|
167
|
+
raise ValueError(
|
|
168
|
+
"'scheme' must be ascending in "
|
|
169
|
+
f"increments of 3. got: {value}."
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
if not s.between(
|
|
173
|
+
left=1,
|
|
174
|
+
right=MONTHS_IN_YEAR,
|
|
175
|
+
inclusive='both'
|
|
176
|
+
).all():
|
|
177
|
+
raise ValueError(
|
|
178
|
+
"'scheme' values must be between 1 "
|
|
179
|
+
f"and {MONTHS_IN_YEAR}, got: {value}."
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
cls.scheme = value
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@classmethod
|
|
186
|
+
def parse_label(cls, x):
|
|
187
|
+
'''
|
|
188
|
+
Description
|
|
189
|
+
------------
|
|
190
|
+
Parses a quarter end label into its year and quarter components.
|
|
191
|
+
Supports input patterns like YYYYQ#, #QYY, and Q#. When the year
|
|
192
|
+
is not provided in the label, the current year is used by default.
|
|
193
|
+
|
|
194
|
+
Parameters
|
|
195
|
+
------------
|
|
196
|
+
x : str
|
|
197
|
+
Quarter end label to parse.
|
|
198
|
+
|
|
199
|
+
Returns
|
|
200
|
+
------------
|
|
201
|
+
Returns None if input is not a string or parsing failed.
|
|
202
|
+
Otherwise:
|
|
203
|
+
|
|
204
|
+
out : tuple
|
|
205
|
+
year : int
|
|
206
|
+
The four-digit year.
|
|
207
|
+
qtr : int
|
|
208
|
+
The quarter number (1 to 4).
|
|
209
|
+
'''
|
|
210
|
+
|
|
211
|
+
def extract_year_qtr(x):
|
|
212
|
+
if not isinstance(x, str): return
|
|
213
|
+
x = x.strip().upper()
|
|
214
|
+
|
|
215
|
+
# YYYYQ#
|
|
216
|
+
match = re.fullmatch(r'(\d{4})Q(\d)', x)
|
|
217
|
+
if match: return match.groups()
|
|
218
|
+
|
|
219
|
+
now = datetime.datetime.now()
|
|
220
|
+
|
|
221
|
+
# #QYY
|
|
222
|
+
match = re.fullmatch(r'(\d)Q(\d{2})', x)
|
|
223
|
+
if match:
|
|
224
|
+
qtr, yy = match.groups()
|
|
225
|
+
return f'{now.year // 100}{yy}', qtr
|
|
226
|
+
|
|
227
|
+
# Q#
|
|
228
|
+
match = re.fullmatch(r'Q(\d)', x)
|
|
229
|
+
if match:
|
|
230
|
+
qtr = match.group(1)
|
|
231
|
+
return str(now.year), qtr
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
parsed = extract_year_qtr(x)
|
|
235
|
+
if parsed is None: return
|
|
236
|
+
year, qtr = map(cls._try_int, parsed)
|
|
237
|
+
|
|
238
|
+
if not (1 <= qtr <= 4):
|
|
239
|
+
raise ValueError(
|
|
240
|
+
"Quarter must be between "
|
|
241
|
+
f"1 and 4, got: {qtr}"
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
return year, qtr
|
|
@@ -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()
|
|
@@ -3,12 +3,11 @@ import os
|
|
|
3
3
|
from contextlib import redirect_stdout
|
|
4
4
|
from schedule import CancelJob
|
|
5
5
|
|
|
6
|
-
from ..
|
|
6
|
+
from ..timestamp import Timestamp
|
|
7
7
|
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
|
|
@@ -34,8 +33,9 @@ class Task(object):
|
|
|
34
33
|
name of job
|
|
35
34
|
at : str
|
|
36
35
|
at time string
|
|
37
|
-
expiry :
|
|
38
|
-
If not None, job will be set inactive and stop running after this
|
|
36
|
+
expiry : Timestamp
|
|
37
|
+
If not None, job will be set inactive and stop running after this
|
|
38
|
+
datetime
|
|
39
39
|
func : func
|
|
40
40
|
function to run
|
|
41
41
|
args : tuple
|
|
@@ -164,15 +164,16 @@ class Task(object):
|
|
|
164
164
|
if logger: logger.info(f"{self.name} status update: '{self.status}'")
|
|
165
165
|
self.update_table(active=0 if set_inactive else 1)
|
|
166
166
|
|
|
167
|
-
if self.expiry and self.expiry <
|
|
167
|
+
if self.expiry and self.expiry < Timestamp():
|
|
168
168
|
update_status('cancelled on expiration', set_inactive=True)
|
|
169
169
|
if self.verbose:
|
|
170
170
|
print('Killing', end=' ')
|
|
171
171
|
print(self.name, end='')
|
|
172
|
-
print(f' @ {
|
|
172
|
+
print(f' @ {Timestamp()} ->', end=' ')
|
|
173
173
|
print('Job Cancelled')
|
|
174
174
|
|
|
175
|
-
# cancel job if it was cancelled on cascasde or
|
|
175
|
+
# cancel job if it was cancelled on cascasde or
|
|
176
|
+
# if it was set inactive after being scheduled
|
|
176
177
|
if self.status == self.cascade_status or \
|
|
177
178
|
not self.master.is_active(self.name, self.at):
|
|
178
179
|
return CancelJob
|
|
@@ -185,12 +186,12 @@ class Task(object):
|
|
|
185
186
|
if self.verbose:
|
|
186
187
|
print('Running', end=' ')
|
|
187
188
|
print(self.name, end='')
|
|
188
|
-
print(f' @ {
|
|
189
|
+
print(f' @ {Timestamp()} ->', end=' ')
|
|
189
190
|
|
|
190
|
-
if self.restrict_to_business_hours and not
|
|
191
|
+
if self.restrict_to_business_hours and not Timestamp().is_business_hours:
|
|
191
192
|
raise PrerequisiteError('cannot execute outside of business hours')
|
|
192
193
|
|
|
193
|
-
if self.restrict_to_business_days and not
|
|
194
|
+
if self.restrict_to_business_days and not Timestamp().is_business_day:
|
|
194
195
|
raise PrerequisiteError('cannot execute during the weekend')
|
|
195
196
|
|
|
196
197
|
if self.disable_print:
|
|
@@ -1,15 +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
8
|
Monitors a folder for new files and passes latest and 2nd latest file
|
|
10
|
-
names to user-defined function. Class is intended to be used in
|
|
11
|
-
with TaskMaster as a func argument which allows for file
|
|
12
|
-
intervals.
|
|
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.
|
|
13
12
|
|
|
14
13
|
Class Attributes
|
|
15
14
|
--------------------
|
|
@@ -18,8 +17,8 @@ class FileMonitor(object):
|
|
|
18
17
|
Instance Attributes
|
|
19
18
|
--------------------
|
|
20
19
|
func : func
|
|
21
|
-
Custom function that takes the latest and second-latest file names
|
|
22
|
-
folder as 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.
|
|
23
22
|
folder : Folder object
|
|
24
23
|
folder to monitor for new files.
|
|
25
24
|
filter_kwargs : dict
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import datetime
|
|
3
|
-
from pathpilot import Folder
|
|
4
3
|
|
|
5
4
|
|
|
6
5
|
#╭-------------------------------------------------------------------------╮
|
|
@@ -21,7 +20,6 @@ class CustomLogFormatter(logging.Formatter):
|
|
|
21
20
|
.format('%03d' % record.msecs)
|
|
22
21
|
|
|
23
22
|
|
|
24
|
-
|
|
25
23
|
class Logger(object):
|
|
26
24
|
|
|
27
25
|
#╭-------------------------------------------------------------------------╮
|
|
@@ -56,12 +54,10 @@ class Logger(object):
|
|
|
56
54
|
# create custom formatter
|
|
57
55
|
# https://docs.python.org/3/library/logging.html#logrecord-attributes
|
|
58
56
|
formatter = CustomLogFormatter(fmt='%(asctime)s %(levelname)s %(message)s')
|
|
59
|
-
#formatter = CustomLogFormatter(fmt='%(asctime)s %(name)s %(levelname)s %(message)s')
|
|
57
|
+
# formatter = CustomLogFormatter(fmt='%(asctime)s %(name)s %(levelname)s %(message)s')
|
|
60
58
|
|
|
61
59
|
# create file handler which logs even debug messages
|
|
62
|
-
self.file =
|
|
63
|
-
.join('Data', 'Logger', read_only=False)\
|
|
64
|
-
.join(f'{name}.log').path
|
|
60
|
+
self.file = self.folder.join(f'{name}.log', read_only=False).path
|
|
65
61
|
if clear: self.clear()
|
|
66
62
|
fh = logging.FileHandler(self.file)
|
|
67
63
|
fh.setLevel(logging.DEBUG)
|