clockwork 0.6.0__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.7.0/.gitignore +162 -0
- clockwork-0.7.0/.python-version +1 -0
- clockwork-0.7.0/PKG-INFO +36 -0
- {clockwork-0.6.0 → clockwork-0.7.0}/clockwork/__init__.py +1 -1
- {clockwork-0.6.0 → 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.0 → clockwork-0.7.0}/clockwork/taskmaster/_task.py +4 -4
- {clockwork-0.6.0 → clockwork-0.7.0}/clockwork/taskmaster/file_monitor.py +1 -1
- {clockwork-0.6.0 → clockwork-0.7.0}/clockwork/taskmaster/logger.py +3 -3
- {clockwork-0.6.0 → clockwork-0.7.0}/clockwork/taskmaster/taskmaster.py +7 -3
- {clockwork-0.6.0 → clockwork-0.7.0}/clockwork/taskmaster/utils.py +1 -1
- {clockwork-0.6.0 → clockwork-0.7.0}/clockwork/timestamp/_decorators.py +40 -40
- clockwork-0.7.0/clockwork/timestamp/core.py +995 -0
- {clockwork-0.6.0 → clockwork-0.7.0}/clockwork/utils.py +9 -8
- clockwork-0.7.0/pyproject.toml +27 -0
- clockwork-0.7.0/uv.lock +274 -0
- clockwork-0.6.0/PKG-INFO +0 -46
- clockwork-0.6.0/clockwork/month_end.py +0 -187
- clockwork-0.6.0/clockwork/quarter_end.py +0 -256
- clockwork-0.6.0/clockwork/timestamp/core.py +0 -769
- clockwork-0.6.0/pyproject.toml +0 -22
- {clockwork-0.6.0 → clockwork-0.7.0}/LICENSE +0 -0
- {clockwork-0.6.0 → clockwork-0.7.0}/README.md +0 -0
- {clockwork-0.6.0 → clockwork-0.7.0}/clockwork/constants.py +0 -0
- {clockwork-0.6.0 → clockwork-0.7.0}/clockwork/taskmaster/__init__.py +0 -0
- {clockwork-0.6.0 → clockwork-0.7.0}/clockwork/taskmaster/_scheduler.py +0 -0
- {clockwork-0.6.0 → clockwork-0.7.0}/clockwork/timestamp/__init__.py +0 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# https://github.com/github/gitignore/blob/main/Python.gitignore
|
|
2
|
+
|
|
3
|
+
# Byte-compiled / optimized / DLL files
|
|
4
|
+
__pycache__/
|
|
5
|
+
*.py[cod]
|
|
6
|
+
*$py.class
|
|
7
|
+
|
|
8
|
+
# C extensions
|
|
9
|
+
*.so
|
|
10
|
+
|
|
11
|
+
# Distribution / packaging
|
|
12
|
+
.Python
|
|
13
|
+
build/
|
|
14
|
+
develop-eggs/
|
|
15
|
+
dist/
|
|
16
|
+
downloads/
|
|
17
|
+
eggs/
|
|
18
|
+
.eggs/
|
|
19
|
+
lib/
|
|
20
|
+
lib64/
|
|
21
|
+
parts/
|
|
22
|
+
sdist/
|
|
23
|
+
var/
|
|
24
|
+
wheels/
|
|
25
|
+
share/python-wheels/
|
|
26
|
+
*.egg-info/
|
|
27
|
+
.installed.cfg
|
|
28
|
+
*.egg
|
|
29
|
+
MANIFEST
|
|
30
|
+
|
|
31
|
+
# PyInstaller
|
|
32
|
+
# Usually these files are written by a python script from a template
|
|
33
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
34
|
+
*.manifest
|
|
35
|
+
*.spec
|
|
36
|
+
|
|
37
|
+
# Installer logs
|
|
38
|
+
pip-log.txt
|
|
39
|
+
pip-delete-this-directory.txt
|
|
40
|
+
|
|
41
|
+
# Unit test / coverage reports
|
|
42
|
+
htmlcov/
|
|
43
|
+
.tox/
|
|
44
|
+
.nox/
|
|
45
|
+
.coverage
|
|
46
|
+
.coverage.*
|
|
47
|
+
.cache
|
|
48
|
+
nosetests.xml
|
|
49
|
+
coverage.xml
|
|
50
|
+
*.cover
|
|
51
|
+
*.py,cover
|
|
52
|
+
.hypothesis/
|
|
53
|
+
.pytest_cache/
|
|
54
|
+
cover/
|
|
55
|
+
|
|
56
|
+
# Translations
|
|
57
|
+
*.mo
|
|
58
|
+
*.pot
|
|
59
|
+
|
|
60
|
+
# Django stuff:
|
|
61
|
+
*.log
|
|
62
|
+
local_settings.py
|
|
63
|
+
db.sqlite3
|
|
64
|
+
db.sqlite3-journal
|
|
65
|
+
|
|
66
|
+
# Flask stuff:
|
|
67
|
+
instance/
|
|
68
|
+
.webassets-cache
|
|
69
|
+
|
|
70
|
+
# Scrapy stuff:
|
|
71
|
+
.scrapy
|
|
72
|
+
|
|
73
|
+
# Sphinx documentation
|
|
74
|
+
docs/_build/
|
|
75
|
+
|
|
76
|
+
# PyBuilder
|
|
77
|
+
.pybuilder/
|
|
78
|
+
target/
|
|
79
|
+
|
|
80
|
+
# Jupyter Notebook
|
|
81
|
+
.ipynb_checkpoints
|
|
82
|
+
|
|
83
|
+
# IPython
|
|
84
|
+
profile_default/
|
|
85
|
+
ipython_config.py
|
|
86
|
+
|
|
87
|
+
# pyenv
|
|
88
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
89
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
90
|
+
# .python-version
|
|
91
|
+
|
|
92
|
+
# pipenv
|
|
93
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
94
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
95
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
96
|
+
# install all needed dependencies.
|
|
97
|
+
#Pipfile.lock
|
|
98
|
+
|
|
99
|
+
# poetry
|
|
100
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
101
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
102
|
+
# commonly ignored for libraries.
|
|
103
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
104
|
+
#poetry.lock
|
|
105
|
+
|
|
106
|
+
# pdm
|
|
107
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
108
|
+
#pdm.lock
|
|
109
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
110
|
+
# in version control.
|
|
111
|
+
# https://pdm.fming.dev/#use-with-ide
|
|
112
|
+
.pdm.toml
|
|
113
|
+
|
|
114
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
115
|
+
__pypackages__/
|
|
116
|
+
|
|
117
|
+
# Celery stuff
|
|
118
|
+
celerybeat-schedule
|
|
119
|
+
celerybeat.pid
|
|
120
|
+
|
|
121
|
+
# SageMath parsed files
|
|
122
|
+
*.sage.py
|
|
123
|
+
|
|
124
|
+
# Environments
|
|
125
|
+
.env
|
|
126
|
+
.venv
|
|
127
|
+
env/
|
|
128
|
+
venv/
|
|
129
|
+
ENV/
|
|
130
|
+
env.bak/
|
|
131
|
+
venv.bak/
|
|
132
|
+
|
|
133
|
+
# Spyder project settings
|
|
134
|
+
.spyderproject
|
|
135
|
+
.spyproject
|
|
136
|
+
|
|
137
|
+
# Rope project settings
|
|
138
|
+
.ropeproject
|
|
139
|
+
|
|
140
|
+
# mkdocs documentation
|
|
141
|
+
/site
|
|
142
|
+
|
|
143
|
+
# mypy
|
|
144
|
+
.mypy_cache/
|
|
145
|
+
.dmypy.json
|
|
146
|
+
dmypy.json
|
|
147
|
+
|
|
148
|
+
# Pyre type checker
|
|
149
|
+
.pyre/
|
|
150
|
+
|
|
151
|
+
# pytype static type analyzer
|
|
152
|
+
.pytype/
|
|
153
|
+
|
|
154
|
+
# Cython debug symbols
|
|
155
|
+
cython_debug/
|
|
156
|
+
|
|
157
|
+
# PyCharm
|
|
158
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
159
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
160
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
161
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
162
|
+
#.idea/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
clockwork-0.7.0/PKG-INFO
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: clockwork
|
|
3
|
+
Version: 0.7.0
|
|
4
|
+
Summary: Toolkit for time-related operations including scheduling, logging, date manipulation, and more.
|
|
5
|
+
Project-URL: repository, https://github.com/zteinck/clockwork
|
|
6
|
+
Project-URL: homepage, https://github.com/zteinck/clockwork
|
|
7
|
+
Author-email: Zachary Einck <zacharyeinck@gmail.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Python: >=3.13
|
|
11
|
+
Requires-Dist: holidays>=0.94
|
|
12
|
+
Requires-Dist: numpy>=2.4.4
|
|
13
|
+
Requires-Dist: oddments>=0.10.0
|
|
14
|
+
Requires-Dist: pandas>=3.0.2
|
|
15
|
+
Requires-Dist: polars>=1.39.3
|
|
16
|
+
Requires-Dist: python-dateutil>=2.9.0.post0
|
|
17
|
+
Requires-Dist: schedule>=1.2.2
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# clockwork
|
|
21
|
+
|
|
22
|
+
<div>
|
|
23
|
+
|
|
24
|
+
[](https://pypi.org/project/clockwork/)
|
|
25
|
+
[](https://github.com/zteinck/clockwork/blob/master/LICENSE)
|
|
26
|
+
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
`clockwork` is a Python package that provides a multitude of time-related functionalities, facilitating tasks such as scheduling, logging, date manipulation, and more.
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
```sh
|
|
35
|
+
pip install clockwork
|
|
36
|
+
```
|
|
@@ -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()
|