clockwork 0.6.1__tar.gz → 0.7.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.6.1 → clockwork-0.7.0}/.python-version +1 -1
- {clockwork-0.6.1 → clockwork-0.7.0}/PKG-INFO +4 -3
- {clockwork-0.6.1 → clockwork-0.7.0}/clockwork/__init__.py +1 -1
- {clockwork-0.6.1 → clockwork-0.7.0}/clockwork/decorators.py +12 -13
- clockwork-0.7.0/clockwork/month_end.py +243 -0
- clockwork-0.7.0/clockwork/quarter_end.py +314 -0
- {clockwork-0.6.1 → clockwork-0.7.0}/clockwork/taskmaster/_task.py +4 -4
- {clockwork-0.6.1 → clockwork-0.7.0}/clockwork/taskmaster/file_monitor.py +1 -1
- {clockwork-0.6.1 → clockwork-0.7.0}/clockwork/taskmaster/logger.py +3 -3
- {clockwork-0.6.1 → clockwork-0.7.0}/clockwork/taskmaster/taskmaster.py +7 -3
- {clockwork-0.6.1 → clockwork-0.7.0}/clockwork/taskmaster/utils.py +1 -1
- {clockwork-0.6.1 → clockwork-0.7.0}/clockwork/timestamp/_decorators.py +40 -40
- clockwork-0.7.0/clockwork/timestamp/core.py +995 -0
- {clockwork-0.6.1 → clockwork-0.7.0}/clockwork/utils.py +9 -8
- {clockwork-0.6.1 → clockwork-0.7.0}/pyproject.toml +4 -3
- clockwork-0.7.0/uv.lock +274 -0
- clockwork-0.6.1/clockwork/month_end.py +0 -187
- clockwork-0.6.1/clockwork/quarter_end.py +0 -256
- clockwork-0.6.1/clockwork/timestamp/core.py +0 -769
- clockwork-0.6.1/uv.lock +0 -258
- {clockwork-0.6.1 → clockwork-0.7.0}/.gitignore +0 -0
- {clockwork-0.6.1 → clockwork-0.7.0}/LICENSE +0 -0
- {clockwork-0.6.1 → clockwork-0.7.0}/README.md +0 -0
- {clockwork-0.6.1 → clockwork-0.7.0}/clockwork/constants.py +0 -0
- {clockwork-0.6.1 → clockwork-0.7.0}/clockwork/taskmaster/__init__.py +0 -0
- {clockwork-0.6.1 → clockwork-0.7.0}/clockwork/taskmaster/_scheduler.py +0 -0
- {clockwork-0.6.1 → clockwork-0.7.0}/clockwork/timestamp/__init__.py +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
3.13
|
|
1
|
+
3.13
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: clockwork
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Toolkit for time-related operations including scheduling, logging, date manipulation, and more.
|
|
5
5
|
Project-URL: repository, https://github.com/zteinck/clockwork
|
|
6
6
|
Project-URL: homepage, https://github.com/zteinck/clockwork
|
|
@@ -10,9 +10,10 @@ License-File: LICENSE
|
|
|
10
10
|
Requires-Python: >=3.13
|
|
11
11
|
Requires-Dist: holidays>=0.94
|
|
12
12
|
Requires-Dist: numpy>=2.4.4
|
|
13
|
-
Requires-Dist: oddments>=0.
|
|
13
|
+
Requires-Dist: oddments>=0.10.0
|
|
14
14
|
Requires-Dist: pandas>=3.0.2
|
|
15
15
|
Requires-Dist: polars>=1.39.3
|
|
16
|
+
Requires-Dist: python-dateutil>=2.9.0.post0
|
|
16
17
|
Requires-Dist: schedule>=1.2.2
|
|
17
18
|
Description-Content-Type: text/markdown
|
|
18
19
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from functools import wraps
|
|
2
2
|
from time import perf_counter
|
|
3
3
|
|
|
4
|
-
from oddments import
|
|
4
|
+
from oddments import Validator
|
|
5
5
|
|
|
6
6
|
from .utils import format_duration
|
|
7
7
|
|
|
@@ -25,7 +25,7 @@ def print_duration(indents=1, in_place=True):
|
|
|
25
25
|
|
|
26
26
|
Returns
|
|
27
27
|
------------
|
|
28
|
-
|
|
28
|
+
result : any
|
|
29
29
|
The decorated method's return value.
|
|
30
30
|
'''
|
|
31
31
|
|
|
@@ -95,19 +95,18 @@ def print_duration(indents=1, in_place=True):
|
|
|
95
95
|
|
|
96
96
|
|
|
97
97
|
# validate decorator arguments
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
name='indents',
|
|
98
|
+
(
|
|
99
|
+
Validator(
|
|
101
100
|
types=int,
|
|
102
101
|
min_value=0,
|
|
103
|
-
min_inclusive=True
|
|
102
|
+
min_inclusive=True,
|
|
104
103
|
)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
value=in_place,
|
|
108
|
-
name='in_place',
|
|
109
|
-
types=bool,
|
|
104
|
+
.validate(
|
|
105
|
+
indents=indents
|
|
110
106
|
)
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
Validator(types=bool).validate(in_place=in_place)
|
|
111
110
|
|
|
112
111
|
if self.verbose:
|
|
113
112
|
words = [
|
|
@@ -119,13 +118,13 @@ def print_duration(indents=1, in_place=True):
|
|
|
119
118
|
print_status()
|
|
120
119
|
start_time = perf_counter()
|
|
121
120
|
|
|
122
|
-
|
|
121
|
+
result = func(self, *args, **kwargs)
|
|
123
122
|
|
|
124
123
|
if self.verbose:
|
|
125
124
|
duration = perf_counter() - start_time
|
|
126
125
|
print_status(duration)
|
|
127
126
|
|
|
128
|
-
return
|
|
127
|
+
return result
|
|
129
128
|
|
|
130
129
|
return wrapper
|
|
131
130
|
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
|
|
3
|
+
from oddments import Validator, UNSET
|
|
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 date.
|
|
14
|
+
|
|
15
|
+
Class Attributes
|
|
16
|
+
--------------------
|
|
17
|
+
_increment : int
|
|
18
|
+
Number of months to increment during offsets.
|
|
19
|
+
|
|
20
|
+
Instance Attributes
|
|
21
|
+
--------------------
|
|
22
|
+
None
|
|
23
|
+
'''
|
|
24
|
+
|
|
25
|
+
#╭-------------------------------------------------------------------------╮
|
|
26
|
+
#| Class Attributes |
|
|
27
|
+
#╰-------------------------------------------------------------------------╯
|
|
28
|
+
|
|
29
|
+
repr_format = '%Y-%m-%d'
|
|
30
|
+
_increment = 1
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
#╭-------------------------------------------------------------------------╮
|
|
34
|
+
#| Initialize Instance |
|
|
35
|
+
#╰-------------------------------------------------------------------------╯
|
|
36
|
+
|
|
37
|
+
def __init__(self, *args, **kwargs):
|
|
38
|
+
super().__init__(*args, **kwargs)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
#╭-------------------------------------------------------------------------╮
|
|
42
|
+
#| Properties |
|
|
43
|
+
#╰-------------------------------------------------------------------------╯
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def long_label(self):
|
|
47
|
+
return self.to_string('%Y-%m-%d')
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def compact_label(self):
|
|
52
|
+
return self.to_string('%Y-%m')
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def short_label(self):
|
|
57
|
+
return self.to_string('%b')
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def is_year_end(self):
|
|
62
|
+
return self.month == MONTHS_IN_YEAR
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def relative_offset(self):
|
|
67
|
+
return self._compute_relative_offset()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
#╭-------------------------------------------------------------------------╮
|
|
71
|
+
#| Instance Methods |
|
|
72
|
+
#╰-------------------------------------------------------------------------╯
|
|
73
|
+
|
|
74
|
+
def offset(self, periods):
|
|
75
|
+
''' offsets instance by desired number of periods '''
|
|
76
|
+
Validator(types=int).validate(periods=periods)
|
|
77
|
+
offset = self.relative_offset + periods
|
|
78
|
+
result = self._spawn(offset=offset, target_tz=self.tz)
|
|
79
|
+
return result
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _offset(self, year, month, offset):
|
|
83
|
+
total_months = self._total_months(
|
|
84
|
+
years=year,
|
|
85
|
+
months=(month + offset),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
y, m = divmod(total_months, MONTHS_IN_YEAR)
|
|
89
|
+
|
|
90
|
+
if m == 0:
|
|
91
|
+
return y - 1, MONTHS_IN_YEAR
|
|
92
|
+
|
|
93
|
+
return y, m
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _compute_relative_offset(self):
|
|
97
|
+
'''
|
|
98
|
+
Description
|
|
99
|
+
------------
|
|
100
|
+
Returns the number of periods the instance is offset relative to
|
|
101
|
+
the most recent period end. Periods are defined by the '_increment'
|
|
102
|
+
class attribute.
|
|
103
|
+
|
|
104
|
+
Returns
|
|
105
|
+
------------
|
|
106
|
+
q : int
|
|
107
|
+
Number of offset periods.
|
|
108
|
+
'''
|
|
109
|
+
a, b = (
|
|
110
|
+
self._total_months(
|
|
111
|
+
years=obj.year,
|
|
112
|
+
months=obj.month,
|
|
113
|
+
)
|
|
114
|
+
for obj in (
|
|
115
|
+
self,
|
|
116
|
+
self._spawn(
|
|
117
|
+
offset=0,
|
|
118
|
+
target_tz=self.tz,
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
q, r = divmod(a - b, self._increment)
|
|
124
|
+
|
|
125
|
+
if r != 0:
|
|
126
|
+
raise AssertionError(
|
|
127
|
+
f'Unexpected remainder: {r}'
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
return int(q)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _validate(self):
|
|
134
|
+
''' raises an error if the instance fails validation '''
|
|
135
|
+
msg = self._find_error()
|
|
136
|
+
|
|
137
|
+
if msg is not None:
|
|
138
|
+
raise ValueError(
|
|
139
|
+
f'{self.__class__.__name__} instance is invalid because {msg}'
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _find_error(self):
|
|
144
|
+
if not self.is_last_day_of_month:
|
|
145
|
+
return (
|
|
146
|
+
f'{self.day} is not the last day ({self.last_day_of_month}) '
|
|
147
|
+
f'of {self.month_name} {self.year}'
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if not self.is_normalized:
|
|
151
|
+
return (
|
|
152
|
+
'the time component must be normalized (i.e. all zoroes), '
|
|
153
|
+
f'got: {self.time}'
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _init_dt(
|
|
158
|
+
self,
|
|
159
|
+
source,
|
|
160
|
+
target_tz,
|
|
161
|
+
offset,
|
|
162
|
+
year=None,
|
|
163
|
+
month=None,
|
|
164
|
+
**kwargs
|
|
165
|
+
):
|
|
166
|
+
'''
|
|
167
|
+
Parameters
|
|
168
|
+
------------
|
|
169
|
+
source : None | any
|
|
170
|
+
A value representing a month end date. Must be None if 'year' or
|
|
171
|
+
'month' arguments are provided.
|
|
172
|
+
year : int
|
|
173
|
+
The calendar year of the month end date.
|
|
174
|
+
month : int
|
|
175
|
+
The calendar month of the month end date (1 to 12).
|
|
176
|
+
offset : int
|
|
177
|
+
Number of months to shift from the base month end. Base month end
|
|
178
|
+
defaults to the most recently completed month end when no other
|
|
179
|
+
parameters are provided. Use positive values to move forward in
|
|
180
|
+
time and negative values to move backward in time.
|
|
181
|
+
kwargs : dict
|
|
182
|
+
Additional keyword arguments are forwarded to the Timestamp
|
|
183
|
+
constructor.
|
|
184
|
+
'''
|
|
185
|
+
|
|
186
|
+
has_source = source is not None
|
|
187
|
+
has_year_or_month = year is not None or month is not None
|
|
188
|
+
|
|
189
|
+
if has_source:
|
|
190
|
+
if has_year_or_month:
|
|
191
|
+
raise ValueError(
|
|
192
|
+
"Both 'year' and 'month' must be None when 'source' is "
|
|
193
|
+
"not None."
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
self._dt = self._resolve_dt(
|
|
197
|
+
source=source,
|
|
198
|
+
target_tz=target_tz,
|
|
199
|
+
offset=offset,
|
|
200
|
+
**kwargs
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
if offset != 0:
|
|
204
|
+
self._dt = self.offset(periods=offset).to_datetime()
|
|
205
|
+
|
|
206
|
+
else:
|
|
207
|
+
_target_tz = None if target_tz is UNSET else target_tz
|
|
208
|
+
|
|
209
|
+
if has_year_or_month:
|
|
210
|
+
year, month = (
|
|
211
|
+
self._ensure_int(value=v, name=k)
|
|
212
|
+
for k, v in {
|
|
213
|
+
'year': year,
|
|
214
|
+
'month': month,
|
|
215
|
+
}.items()
|
|
216
|
+
)
|
|
217
|
+
self._validate_month(month)
|
|
218
|
+
|
|
219
|
+
else:
|
|
220
|
+
now = datetime.datetime.now(tz=_target_tz)
|
|
221
|
+
|
|
222
|
+
year, month = self._get_prior_month(
|
|
223
|
+
year=now.year,
|
|
224
|
+
month=now.month,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
year, month = self._offset(
|
|
228
|
+
year=year,
|
|
229
|
+
month=month,
|
|
230
|
+
offset=(offset * self._increment),
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
day = self.days_in_month(year, month)
|
|
234
|
+
|
|
235
|
+
self._dt = datetime.datetime(
|
|
236
|
+
year=year,
|
|
237
|
+
month=month,
|
|
238
|
+
day=day,
|
|
239
|
+
tzinfo=_target_tz,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
self._dt = self._normalize(self._dt)
|
|
243
|
+
self._validate()
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
from oddments import Validator, UNSET
|
|
5
|
+
import polars as pl
|
|
6
|
+
|
|
7
|
+
from .month_end import MonthEnd
|
|
8
|
+
from .constants import MONTHS_IN_YEAR
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class QuarterEnd(MonthEnd):
|
|
12
|
+
'''
|
|
13
|
+
Description
|
|
14
|
+
--------------------
|
|
15
|
+
Quarter end date.
|
|
16
|
+
|
|
17
|
+
Class Attributes
|
|
18
|
+
--------------------
|
|
19
|
+
_scheme : tuple
|
|
20
|
+
Quarter end months.
|
|
21
|
+
|
|
22
|
+
Instance Attributes
|
|
23
|
+
--------------------
|
|
24
|
+
None
|
|
25
|
+
'''
|
|
26
|
+
|
|
27
|
+
#╭-------------------------------------------------------------------------╮
|
|
28
|
+
#| Class Attributes |
|
|
29
|
+
#╰-------------------------------------------------------------------------╯
|
|
30
|
+
|
|
31
|
+
_increment = 3
|
|
32
|
+
_scheme = (3, 6, 9, 12)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
#╭-------------------------------------------------------------------------╮
|
|
36
|
+
#| Initialize Instance |
|
|
37
|
+
#╰-------------------------------------------------------------------------╯
|
|
38
|
+
|
|
39
|
+
def __init__(self, *args, **kwargs):
|
|
40
|
+
super().__init__(*args, **kwargs)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
#╭-------------------------------------------------------------------------╮
|
|
44
|
+
#| Properties |
|
|
45
|
+
#╰-------------------------------------------------------------------------╯
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def scheme(self):
|
|
49
|
+
return self._scheme
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def long_label(self):
|
|
54
|
+
return f'{self.year}Q{self.quarter}'
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def compact_label(self):
|
|
59
|
+
return f'{self.quarter}Q' + self.to_string('%y')
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def short_label(self):
|
|
64
|
+
return f'Q{self.quarter}'
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def quarter(self):
|
|
69
|
+
''' the quarter number (1 to 4) '''
|
|
70
|
+
return int(self.scheme.index(self.month) + 1)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
#╭-------------------------------------------------------------------------╮
|
|
74
|
+
#| Instance Methods |
|
|
75
|
+
#╰-------------------------------------------------------------------------╯
|
|
76
|
+
|
|
77
|
+
def _init_dt(
|
|
78
|
+
self,
|
|
79
|
+
source,
|
|
80
|
+
target_tz,
|
|
81
|
+
year=None,
|
|
82
|
+
month=None,
|
|
83
|
+
quarter=None,
|
|
84
|
+
**kwargs
|
|
85
|
+
):
|
|
86
|
+
'''
|
|
87
|
+
Parameters
|
|
88
|
+
------------
|
|
89
|
+
source : None | any
|
|
90
|
+
A value representing a quarter end date. Must be None if 'year',
|
|
91
|
+
'month', or 'quarter' are provided.
|
|
92
|
+
year : int
|
|
93
|
+
The calendar year of the quarter end date.
|
|
94
|
+
month : int
|
|
95
|
+
The calendar month of the quarter end date (1 to 12).
|
|
96
|
+
quarter : int
|
|
97
|
+
The quarter number (1 to 4). Cannot be used together with the
|
|
98
|
+
'month' argument.
|
|
99
|
+
offset : int
|
|
100
|
+
Number of quarters to shift from the base quarter end. Base
|
|
101
|
+
quarter end defaults to the most recently completed quarter end
|
|
102
|
+
when no other parameters are provided. Use positive values to move
|
|
103
|
+
forward in time and negative values to move backward in time.
|
|
104
|
+
kwargs : dict
|
|
105
|
+
Additional keyword arguments forwarded to the Timestamp
|
|
106
|
+
constructor.
|
|
107
|
+
'''
|
|
108
|
+
|
|
109
|
+
parsed_label = self._parse_label(source, target_tz)
|
|
110
|
+
|
|
111
|
+
if parsed_label is not None:
|
|
112
|
+
for k, v in {
|
|
113
|
+
'year': year,
|
|
114
|
+
'month': month,
|
|
115
|
+
'quarter': quarter,
|
|
116
|
+
}.items():
|
|
117
|
+
if v is not None:
|
|
118
|
+
raise ValueError(
|
|
119
|
+
f"{k!r} must be None when 'source' is a quarter end "
|
|
120
|
+
f"label: {source!r}."
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
year, quarter = parsed_label
|
|
124
|
+
source = None
|
|
125
|
+
|
|
126
|
+
has_quarter = quarter is not None
|
|
127
|
+
|
|
128
|
+
if month is not None:
|
|
129
|
+
if has_quarter:
|
|
130
|
+
raise ValueError(
|
|
131
|
+
"Cannot pass 'quarter' and 'month' arguments "
|
|
132
|
+
"simultaneously."
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
month = self._ensure_int(month)
|
|
136
|
+
|
|
137
|
+
# validate month
|
|
138
|
+
(
|
|
139
|
+
Validator(
|
|
140
|
+
types=int,
|
|
141
|
+
whitelist=self.scheme,
|
|
142
|
+
)
|
|
143
|
+
.validate(
|
|
144
|
+
month=month
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
if has_quarter:
|
|
149
|
+
quarter = self._ensure_int(
|
|
150
|
+
value=quarter,
|
|
151
|
+
name='quarter',
|
|
152
|
+
)
|
|
153
|
+
month = self.scheme[quarter - 1]
|
|
154
|
+
|
|
155
|
+
super()._init_dt(
|
|
156
|
+
source=source,
|
|
157
|
+
target_tz=target_tz,
|
|
158
|
+
year=year,
|
|
159
|
+
month=month,
|
|
160
|
+
**kwargs
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _offset(self, year, month, offset):
|
|
165
|
+
year, month = self._backtrack_to_scheme(
|
|
166
|
+
year=year,
|
|
167
|
+
month=month,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
result = super()._offset(
|
|
171
|
+
year=year,
|
|
172
|
+
month=month,
|
|
173
|
+
offset=offset,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
return result
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _find_error(self):
|
|
180
|
+
if self.month not in self.scheme:
|
|
181
|
+
return f'the month ({self.month}) is not in scheme: {self.scheme}'
|
|
182
|
+
return super()._find_error()
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
#╭-------------------------------------------------------------------------╮
|
|
186
|
+
#| Class Methods |
|
|
187
|
+
#╰-------------------------------------------------------------------------╯
|
|
188
|
+
|
|
189
|
+
@classmethod
|
|
190
|
+
def set_scheme(cls, value):
|
|
191
|
+
''' safely sets 'scheme' class attribute '''
|
|
192
|
+
|
|
193
|
+
name = 'scheme'
|
|
194
|
+
|
|
195
|
+
Validator(types=tuple).validate(value, name)
|
|
196
|
+
|
|
197
|
+
if len(value) != 4:
|
|
198
|
+
raise ValueError(
|
|
199
|
+
f"'scheme' must contain 4 elements, got: {len(value):,}"
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
value = tuple(map(cls._ensure_int, value))
|
|
203
|
+
s = pl.Series(name=name, values=value)
|
|
204
|
+
|
|
205
|
+
valid_increments = (
|
|
206
|
+
s.diff(null_behavior='drop') == 3
|
|
207
|
+
).all()
|
|
208
|
+
|
|
209
|
+
if not valid_increments:
|
|
210
|
+
raise ValueError(
|
|
211
|
+
"'scheme' must be ascending in increments of 3, got: "
|
|
212
|
+
f"{value!r}"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
valid_months = s.is_between(
|
|
216
|
+
lower_bound=1,
|
|
217
|
+
upper_bound=MONTHS_IN_YEAR,
|
|
218
|
+
closed='both'
|
|
219
|
+
).all()
|
|
220
|
+
|
|
221
|
+
if not valid_months:
|
|
222
|
+
raise ValueError(
|
|
223
|
+
f"'scheme' values must be between 1 and {MONTHS_IN_YEAR}, "
|
|
224
|
+
f"got: {value!r}."
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
cls._scheme = value
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@classmethod
|
|
231
|
+
def _backtrack_to_scheme(cls, year, month):
|
|
232
|
+
''' Backtracks from the given year and month, moving one month at a
|
|
233
|
+
time, until a month that is part of the scheme is found. '''
|
|
234
|
+
|
|
235
|
+
while month not in cls._scheme:
|
|
236
|
+
year, month = cls._get_prior_month(
|
|
237
|
+
year=year,
|
|
238
|
+
month=month,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
return year, month
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@classmethod
|
|
245
|
+
def _parse_label(cls, source, target_tz):
|
|
246
|
+
'''
|
|
247
|
+
Description
|
|
248
|
+
------------
|
|
249
|
+
Parses a quarter end label into its year and quarter components.
|
|
250
|
+
Supports input patterns like YYYYQ#, #QYY, and Q#. When the year
|
|
251
|
+
is not provided in the label, the current year is used by default.
|
|
252
|
+
|
|
253
|
+
Parameters
|
|
254
|
+
------------
|
|
255
|
+
source : str
|
|
256
|
+
Quarter end label to parse.
|
|
257
|
+
target_tz : str | datetime.timezone
|
|
258
|
+
Refer to '_resolve_dt()' documentation.
|
|
259
|
+
|
|
260
|
+
Returns
|
|
261
|
+
------------
|
|
262
|
+
Returns None if input is not a string or parsing failed.
|
|
263
|
+
Otherwise:
|
|
264
|
+
|
|
265
|
+
result : tuple
|
|
266
|
+
year : int
|
|
267
|
+
The four-digit year.
|
|
268
|
+
quarter : int
|
|
269
|
+
The quarter number (1 to 4).
|
|
270
|
+
'''
|
|
271
|
+
|
|
272
|
+
def extract_year_and_quarter(value):
|
|
273
|
+
if not isinstance(value, str):
|
|
274
|
+
return None
|
|
275
|
+
|
|
276
|
+
value = value.strip().upper()
|
|
277
|
+
|
|
278
|
+
# 'YYYYQ#' | 'YYYY Q#'
|
|
279
|
+
match = re.fullmatch(r'(\d{4})\s?Q(\d)', value)
|
|
280
|
+
|
|
281
|
+
if match:
|
|
282
|
+
return match.groups()
|
|
283
|
+
|
|
284
|
+
tz = None if target_tz is UNSET else target_tz
|
|
285
|
+
now = datetime.datetime.now(tz=tz)
|
|
286
|
+
|
|
287
|
+
# '#QYY'
|
|
288
|
+
match = re.fullmatch(r'(\d)Q(\d{2})', value)
|
|
289
|
+
|
|
290
|
+
if match:
|
|
291
|
+
quarter, year = match.groups()
|
|
292
|
+
return f'{now.year // 100}{year}', quarter
|
|
293
|
+
|
|
294
|
+
# 'Q#'
|
|
295
|
+
match = re.fullmatch(r'Q(\d)', value)
|
|
296
|
+
|
|
297
|
+
if match:
|
|
298
|
+
quarter = match.group(1)
|
|
299
|
+
return f'{now.year}', quarter
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
parsed = extract_year_and_quarter(source)
|
|
303
|
+
|
|
304
|
+
if parsed is None:
|
|
305
|
+
return None
|
|
306
|
+
|
|
307
|
+
year, quarter = (cls._ensure_int(x) for x in parsed)
|
|
308
|
+
|
|
309
|
+
if not (1 <= quarter <= 4):
|
|
310
|
+
raise ValueError(
|
|
311
|
+
f"Quarter must be between 1 and 4, got: {quarter}"
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
return year, quarter
|
|
@@ -13,7 +13,7 @@ from .utils import (
|
|
|
13
13
|
)
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
class Task
|
|
16
|
+
class Task:
|
|
17
17
|
'''
|
|
18
18
|
Description
|
|
19
19
|
--------------------
|
|
@@ -211,9 +211,9 @@ class Task(object):
|
|
|
211
211
|
|
|
212
212
|
if self.disable_print:
|
|
213
213
|
with redirect_stdout(open(os.devnull, 'w')):
|
|
214
|
-
|
|
214
|
+
result = self.func(*self.args, **self.kwargs)
|
|
215
215
|
else:
|
|
216
|
-
|
|
216
|
+
result = self.func(*self.args, **self.kwargs)
|
|
217
217
|
|
|
218
218
|
if self.verbose:
|
|
219
219
|
elapsed = format_duration(time.time() - start)
|
|
@@ -230,7 +230,7 @@ class Task(object):
|
|
|
230
230
|
if self.cancel_on_completion:
|
|
231
231
|
update_status('cancelled on completion', set_inactive=True)
|
|
232
232
|
|
|
233
|
-
return CancelJob if self.cancel_on_completion else
|
|
233
|
+
return CancelJob if self.cancel_on_completion else result
|
|
234
234
|
|
|
235
235
|
except Exception as e:
|
|
236
236
|
|