clockwork 0.3.3__tar.gz → 0.3.5__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.3.3 → clockwork-0.3.5}/PKG-INFO +2 -2
- {clockwork-0.3.3 → clockwork-0.3.5}/clockwork/__init__.py +2 -2
- {clockwork-0.3.3 → clockwork-0.3.5}/clockwork/taskmaster/_task.py +41 -21
- {clockwork-0.3.3 → clockwork-0.3.5}/clockwork/taskmaster/logger.py +3 -2
- {clockwork-0.3.3 → clockwork-0.3.5}/clockwork/taskmaster/taskmaster.py +9 -7
- {clockwork-0.3.3 → clockwork-0.3.5}/clockwork/timestamp.py +25 -27
- {clockwork-0.3.3 → clockwork-0.3.5}/clockwork/utils.py +8 -5
- {clockwork-0.3.3 → clockwork-0.3.5}/pyproject.toml +2 -2
- {clockwork-0.3.3 → clockwork-0.3.5}/LICENSE +0 -0
- {clockwork-0.3.3 → clockwork-0.3.5}/README.md +0 -0
- {clockwork-0.3.3 → clockwork-0.3.5}/clockwork/constants.py +0 -0
- {clockwork-0.3.3 → clockwork-0.3.5}/clockwork/decorators.py +0 -0
- {clockwork-0.3.3 → clockwork-0.3.5}/clockwork/month_end.py +0 -0
- {clockwork-0.3.3 → clockwork-0.3.5}/clockwork/quarter_end.py +0 -0
- {clockwork-0.3.3 → clockwork-0.3.5}/clockwork/taskmaster/__init__.py +0 -0
- {clockwork-0.3.3 → clockwork-0.3.5}/clockwork/taskmaster/_scheduler.py +0 -0
- {clockwork-0.3.3 → clockwork-0.3.5}/clockwork/taskmaster/file_monitor.py +0 -0
- {clockwork-0.3.3 → clockwork-0.3.5}/clockwork/taskmaster/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: clockwork
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.5
|
|
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,7 +16,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
17
17
|
Requires-Dist: holidays
|
|
18
18
|
Requires-Dist: numpy
|
|
19
|
-
Requires-Dist: oddments (>=0.3.
|
|
19
|
+
Requires-Dist: oddments (>=0.3.1)
|
|
20
20
|
Requires-Dist: pandas
|
|
21
21
|
Requires-Dist: schedule
|
|
22
22
|
Project-URL: Repository, https://github.com/zteinck/clockwork
|
|
@@ -4,7 +4,7 @@ from contextlib import redirect_stdout
|
|
|
4
4
|
from schedule import CancelJob
|
|
5
5
|
|
|
6
6
|
from ..timestamp import Timestamp
|
|
7
|
-
from ..utils import
|
|
7
|
+
from ..utils import format_elapsed_seconds
|
|
8
8
|
from .utils import PrerequisiteError, ContinueFailedJob
|
|
9
9
|
|
|
10
10
|
|
|
@@ -22,8 +22,8 @@ class Task(object):
|
|
|
22
22
|
verbose : bool
|
|
23
23
|
If True, class content is printed to console.
|
|
24
24
|
disable_print : bool
|
|
25
|
-
If True, printing is suppressed while func runs. Must be True if
|
|
26
|
-
is True.
|
|
25
|
+
If True, printing is suppressed while func runs. Must be True if
|
|
26
|
+
verbose is True.
|
|
27
27
|
cascade_status : str
|
|
28
28
|
cascade status
|
|
29
29
|
|
|
@@ -52,8 +52,8 @@ class Task(object):
|
|
|
52
52
|
traceback. cancel_on_failure and cascade are automatically set to True
|
|
53
53
|
if this argument is True (prevents endless spam).
|
|
54
54
|
restrict_to_business_hours : bool
|
|
55
|
-
If True, the job will only execute during business hours. This is a
|
|
56
|
-
restrictive version of restrict_to_business_days.
|
|
55
|
+
If True, the job will only execute during business hours. This is a
|
|
56
|
+
more restrictive version of restrict_to_business_days.
|
|
57
57
|
restrict_to_business_days : bool
|
|
58
58
|
If True, the job will only execute during weekdays (i.e. not weekends).
|
|
59
59
|
cascade : bool
|
|
@@ -61,14 +61,15 @@ class Task(object):
|
|
|
61
61
|
multiple times in the jobs table due to having multiple 'at' values,
|
|
62
62
|
changes in activiation in one will cascade to all others. For example,
|
|
63
63
|
consider the job named 'my job' which is scheduled at 8:00 AM and 5:00
|
|
64
|
-
PM that is cancelled on completion. If the 8:00 AM completes
|
|
65
|
-
then the job with that 'at' time will be set to inactive
|
|
66
|
-
updated in the table accordingly. Under default
|
|
67
|
-
will be unaffected by the completion of the
|
|
68
|
-
is set to True then the 5:00 PM run
|
|
64
|
+
PM that is cancelled on completion. If the 8:00 AM completes
|
|
65
|
+
successfully then the job with that 'at' time will be set to inactive
|
|
66
|
+
and have its status updated in the table accordingly. Under default
|
|
67
|
+
behavior, the 5:00 PM run will be unaffected by the completion of the
|
|
68
|
+
8:00 AM run, however, if cascade is set to True then the 5:00 PM run
|
|
69
|
+
will also receive the same updates.
|
|
69
70
|
attempts : int
|
|
70
|
-
If greater than 1, the job will be attempted this number of times
|
|
71
|
-
being cancelled.
|
|
71
|
+
If greater than 1, the job will be attempted this number of times
|
|
72
|
+
before being cancelled.
|
|
72
73
|
status : str | None
|
|
73
74
|
current status of the job
|
|
74
75
|
'''
|
|
@@ -161,7 +162,10 @@ class Task(object):
|
|
|
161
162
|
|
|
162
163
|
def update_status(status, set_inactive=False):
|
|
163
164
|
self.status = status
|
|
164
|
-
|
|
165
|
+
|
|
166
|
+
if logger:
|
|
167
|
+
logger.info(f"{self.name} status update: '{self.status}'")
|
|
168
|
+
|
|
165
169
|
self.update_table(active=0 if set_inactive else 1)
|
|
166
170
|
|
|
167
171
|
if self.expiry and self.expiry < Timestamp():
|
|
@@ -188,11 +192,17 @@ class Task(object):
|
|
|
188
192
|
print(self.name, end='')
|
|
189
193
|
print(f' @ {Timestamp()} ->', end=' ')
|
|
190
194
|
|
|
191
|
-
if self.restrict_to_business_hours
|
|
192
|
-
|
|
195
|
+
if self.restrict_to_business_hours \
|
|
196
|
+
and not Timestamp().is_business_hours:
|
|
197
|
+
raise PrerequisiteError(
|
|
198
|
+
'cannot execute outside of business hours'
|
|
199
|
+
)
|
|
193
200
|
|
|
194
|
-
if self.restrict_to_business_days
|
|
195
|
-
|
|
201
|
+
if self.restrict_to_business_days \
|
|
202
|
+
and not Timestamp().is_business_day:
|
|
203
|
+
raise PrerequisiteError(
|
|
204
|
+
'cannot execute during the weekend'
|
|
205
|
+
)
|
|
196
206
|
|
|
197
207
|
if self.disable_print:
|
|
198
208
|
with redirect_stdout(open(os.devnull, 'w')):
|
|
@@ -201,10 +211,16 @@ class Task(object):
|
|
|
201
211
|
out = self.func(*self.args, **self.kwargs)
|
|
202
212
|
|
|
203
213
|
if self.verbose:
|
|
204
|
-
|
|
205
|
-
print('
|
|
214
|
+
elapsed = format_elapsed_seconds(time.time() - start)
|
|
215
|
+
print(f'Complete {elapsed}' , end='')
|
|
216
|
+
print(
|
|
217
|
+
' (Job Cancelled)'
|
|
218
|
+
if self.cancel_on_completion
|
|
219
|
+
else ''
|
|
220
|
+
)
|
|
206
221
|
|
|
207
|
-
if logger:
|
|
222
|
+
if logger:
|
|
223
|
+
logger.info(f'{self.name} complete')
|
|
208
224
|
|
|
209
225
|
if self.cancel_on_completion:
|
|
210
226
|
update_status('cancelled on completion', set_inactive=True)
|
|
@@ -240,4 +256,8 @@ class Task(object):
|
|
|
240
256
|
if self.notify_on_failure:
|
|
241
257
|
self.send_email_notification()
|
|
242
258
|
|
|
243
|
-
return
|
|
259
|
+
return (
|
|
260
|
+
CancelJob
|
|
261
|
+
if self.cancel_on_failure
|
|
262
|
+
else ContinueFailedJob
|
|
263
|
+
)
|
|
@@ -53,8 +53,9 @@ class Logger(object):
|
|
|
53
53
|
|
|
54
54
|
# create custom formatter
|
|
55
55
|
# https://docs.python.org/3/library/logging.html#logrecord-attributes
|
|
56
|
-
formatter = CustomLogFormatter(
|
|
57
|
-
|
|
56
|
+
formatter = CustomLogFormatter(
|
|
57
|
+
fmt='%(asctime)s %(levelname)s %(message)s'
|
|
58
|
+
)
|
|
58
59
|
|
|
59
60
|
# create file handler which logs even debug messages
|
|
60
61
|
self.file = self.folder.join(f'{name}.log', read_only=False).path
|
|
@@ -78,7 +78,8 @@ class TaskMaster(object):
|
|
|
78
78
|
1) job completed succesfully and was cancelled
|
|
79
79
|
2) job obained this status via cascade from a job that satisfied
|
|
80
80
|
criteria in 1)
|
|
81
|
-
3) job was manually set inactive via TaskMaster.set_inactive
|
|
81
|
+
3) job was manually set inactive via TaskMaster.set_inactive
|
|
82
|
+
method
|
|
82
83
|
expiry : str
|
|
83
84
|
Task.expiry
|
|
84
85
|
status : str
|
|
@@ -139,8 +140,8 @@ class TaskMaster(object):
|
|
|
139
140
|
will be set to 'second' and 1, respectively, so that the job will
|
|
140
141
|
run ASAP once the 'start' criteria has been met (if applicable).
|
|
141
142
|
at : str | iter
|
|
142
|
-
|
|
143
|
-
passed in '%I:%M%p' format (e.g ['06:15 AM', '12:15 PM'
|
|
143
|
+
time_string argument passed to schedule.Job.at(time_string). Times may be
|
|
144
|
+
passed in '%I:%M%p' format (e.g ['06:15 AM', '12:15 PM']).
|
|
144
145
|
If argument is an iterable then the job will be scheduled at each
|
|
145
146
|
constituent time.
|
|
146
147
|
interval : int
|
|
@@ -185,12 +186,13 @@ class TaskMaster(object):
|
|
|
185
186
|
job = getattr(cls.scheduler.every(interval), every)
|
|
186
187
|
if at is not None:
|
|
187
188
|
try:
|
|
188
|
-
|
|
189
|
-
at.upper().replace(' ',''),
|
|
189
|
+
time_string = datetime.datetime.strptime(
|
|
190
|
+
at.upper().replace(' ', ''),
|
|
191
|
+
'%I:%M%p'
|
|
190
192
|
).strftime('%H:%M')
|
|
191
193
|
except:
|
|
192
|
-
|
|
193
|
-
job = job.at(
|
|
194
|
+
time_string = at
|
|
195
|
+
job = job.at(time_string)
|
|
194
196
|
else:
|
|
195
197
|
at = 'N/A'
|
|
196
198
|
|
|
@@ -5,9 +5,9 @@ import holidays as hd
|
|
|
5
5
|
import pandas as pd
|
|
6
6
|
import numpy as np
|
|
7
7
|
import oddments as odd
|
|
8
|
-
from copy import deepcopy
|
|
9
|
-
from dateutil.relativedelta import
|
|
10
|
-
|
|
8
|
+
from copy import deepcopy
|
|
9
|
+
from dateutil.relativedelta import relativedelta
|
|
10
|
+
from dateutil.parser import parse
|
|
11
11
|
|
|
12
12
|
from .constants import MONTHS_IN_YEAR
|
|
13
13
|
|
|
@@ -111,7 +111,8 @@ class Timestamp(object):
|
|
|
111
111
|
|
|
112
112
|
@wraps(func)
|
|
113
113
|
def wrapper(self, other):
|
|
114
|
-
# Subtracting a date-like 'other' returns a datetime.timedelta
|
|
114
|
+
# Subtracting a date-like 'other' returns a datetime.timedelta
|
|
115
|
+
# object.
|
|
115
116
|
if func.__name__.replace('_', '') == 'sub' and \
|
|
116
117
|
not isinstance(other, (dict, float, int)):
|
|
117
118
|
return func(self, self._to_datetime(other))
|
|
@@ -163,9 +164,9 @@ class Timestamp(object):
|
|
|
163
164
|
characters (e.g. 'Friday' or 'Fri') not case-sensitive. Also
|
|
164
165
|
supports period ends (e.g. 'QE', 'ME').
|
|
165
166
|
offset : int
|
|
166
|
-
Offset value +/- indicating how many additional weeks to
|
|
167
|
-
For example, self.next('Mon', offset=+1) would
|
|
168
|
-
two weeks from self.
|
|
167
|
+
Offset value +/- indicating how many additional weeks to
|
|
168
|
+
shift. For example, self.next('Mon', offset=+1) would
|
|
169
|
+
return the Monday two weeks from self.
|
|
169
170
|
|
|
170
171
|
Returns
|
|
171
172
|
------------
|
|
@@ -565,7 +566,7 @@ class Timestamp(object):
|
|
|
565
566
|
|
|
566
567
|
def to_datetime(self):
|
|
567
568
|
''' returns a copy of the underlying datetime.datetime object '''
|
|
568
|
-
return
|
|
569
|
+
return deepcopy(self.dt)
|
|
569
570
|
|
|
570
571
|
|
|
571
572
|
def to_pandas(self):
|
|
@@ -588,14 +589,9 @@ class Timestamp(object):
|
|
|
588
589
|
return self.dt.time()
|
|
589
590
|
|
|
590
591
|
|
|
591
|
-
def to_string(self,
|
|
592
|
+
def to_string(self, format):
|
|
592
593
|
''' strftime alias '''
|
|
593
|
-
return self.strftime(
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
def str(self, *args, **kwargs):
|
|
597
|
-
''' to_string() alias '''
|
|
598
|
-
return self.to_string(*args, **kwargs)
|
|
594
|
+
return self.strftime(format)
|
|
599
595
|
|
|
600
596
|
|
|
601
597
|
@Decorators.skip_shift
|
|
@@ -639,7 +635,7 @@ class Timestamp(object):
|
|
|
639
635
|
if not kwargs:
|
|
640
636
|
raise ValueError("'kwargs' is empty?")
|
|
641
637
|
|
|
642
|
-
return (
|
|
638
|
+
return (relativedelta if relative else
|
|
643
639
|
datetime.timedelta)(**kwargs)
|
|
644
640
|
|
|
645
641
|
|
|
@@ -836,7 +832,7 @@ class Timestamp(object):
|
|
|
836
832
|
|
|
837
833
|
|
|
838
834
|
@classmethod
|
|
839
|
-
def _to_datetime(cls, arg,
|
|
835
|
+
def _to_datetime(cls, arg, format=None, offset=0):
|
|
840
836
|
'''
|
|
841
837
|
Description
|
|
842
838
|
------------
|
|
@@ -888,8 +884,8 @@ class Timestamp(object):
|
|
|
888
884
|
qe_cls = cls._get_quarter_end_cls()
|
|
889
885
|
parsed = qe_cls.parse_label(arg)
|
|
890
886
|
if parsed is None: return
|
|
891
|
-
year,
|
|
892
|
-
qe = qe_cls(year=year,
|
|
887
|
+
year, quarter = parsed
|
|
888
|
+
qe = qe_cls(year=year, quarter=quarter, offset=offset)
|
|
893
889
|
return qe.dt
|
|
894
890
|
|
|
895
891
|
|
|
@@ -900,14 +896,16 @@ class Timestamp(object):
|
|
|
900
896
|
raise NotImplementedError
|
|
901
897
|
|
|
902
898
|
if isinstance(arg, str):
|
|
903
|
-
if not
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
899
|
+
if format is not None:
|
|
900
|
+
return datetime.datetime.strptime(arg, format)
|
|
901
|
+
|
|
902
|
+
offset = cls._try_int(offset)
|
|
903
|
+
for func in (try_weekday, try_quarter_end):
|
|
904
|
+
result = func(arg, offset)
|
|
905
|
+
if result is not None:
|
|
906
|
+
return result
|
|
907
|
+
|
|
908
|
+
return parse(arg)
|
|
911
909
|
|
|
912
910
|
# Timestamp instance (or subclass)
|
|
913
911
|
if isinstance(arg, Timestamp):
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from oddments import validate_value
|
|
1
2
|
import re
|
|
2
3
|
|
|
3
4
|
|
|
@@ -48,7 +49,7 @@ def format_elapsed_seconds(seconds, n_digits=2):
|
|
|
48
49
|
return ', '.join(parts)
|
|
49
50
|
|
|
50
51
|
|
|
51
|
-
def
|
|
52
|
+
def temporal_format_to_regex(format, encase=False):
|
|
52
53
|
'''
|
|
53
54
|
Description
|
|
54
55
|
------------
|
|
@@ -63,7 +64,7 @@ def date_format_to_regex(date_format, encase=False):
|
|
|
63
64
|
|
|
64
65
|
Parameters
|
|
65
66
|
------------
|
|
66
|
-
|
|
67
|
+
format : str
|
|
67
68
|
String of datetime format codes (e.g. '%Y-%m-%d').
|
|
68
69
|
encase : bool
|
|
69
70
|
If True, the output will be encased in parenthesis.
|
|
@@ -73,9 +74,11 @@ def date_format_to_regex(date_format, encase=False):
|
|
|
73
74
|
pattern : str
|
|
74
75
|
regex pattern
|
|
75
76
|
'''
|
|
76
|
-
|
|
77
|
+
validate_value(value=format, attr='format', types=str)
|
|
78
|
+
validate_value(value=encase, attr='encase', types=bool)
|
|
77
79
|
|
|
78
80
|
digit_pattern = lambda x: r'\d{%d}' % x
|
|
81
|
+
mapping = {}
|
|
79
82
|
|
|
80
83
|
for k, v in [('w', 1), ('j', 3), ('Y', 4), ('f', 6)]:
|
|
81
84
|
mapping['%' + k] = digit_pattern(v)
|
|
@@ -96,10 +99,10 @@ def date_format_to_regex(date_format, encase=False):
|
|
|
96
99
|
mapping['%' + k] = r'(?:%s)' % '|'.join(v)
|
|
97
100
|
|
|
98
101
|
for k in ['z','Z','c','x','X']:
|
|
99
|
-
if '%' + k in
|
|
102
|
+
if '%' + k in format:
|
|
100
103
|
raise NotImplementedError
|
|
101
104
|
|
|
102
|
-
pattern = re.escape(
|
|
105
|
+
pattern = re.escape(format)
|
|
103
106
|
|
|
104
107
|
for k, v in mapping.items():
|
|
105
108
|
pattern = pattern.replace(re.escape(k), v)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "clockwork"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.5"
|
|
4
4
|
description = "Toolkit for time-related operations including scheduling, logging, date manipulation, and more."
|
|
5
5
|
authors = ["Zachary Einck <zacharyeinck@gmail.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -14,7 +14,7 @@ pandas = "*"
|
|
|
14
14
|
numpy = "*"
|
|
15
15
|
schedule = "*"
|
|
16
16
|
holidays = "*"
|
|
17
|
-
oddments = ">=0.3.
|
|
17
|
+
oddments = ">=0.3.1"
|
|
18
18
|
|
|
19
19
|
[build-system]
|
|
20
20
|
requires = ["poetry-core"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|