clockwork 0.7.1__tar.gz → 0.8.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.1 → clockwork-0.8.0}/PKG-INFO +2 -1
- {clockwork-0.7.1 → clockwork-0.8.0}/pyproject.toml +28 -27
- {clockwork-0.7.1 → clockwork-0.8.0/src}/clockwork/__init__.py +4 -3
- {clockwork-0.7.1/clockwork/timestamp → clockwork-0.8.0/src/clockwork}/_decorators.py +3 -3
- clockwork-0.8.0/src/clockwork/constants.py +6 -0
- clockwork-0.7.1/clockwork/timestamp/core.py → clockwork-0.8.0/src/clockwork/date_time.py +203 -89
- {clockwork-0.7.1 → clockwork-0.8.0/src}/clockwork/month_end.py +14 -13
- {clockwork-0.7.1 → clockwork-0.8.0/src}/clockwork/quarter_end.py +12 -12
- {clockwork-0.7.1 → clockwork-0.8.0/src}/clockwork/taskmaster/_task.py +7 -7
- {clockwork-0.7.1 → clockwork-0.8.0/src}/clockwork/taskmaster/taskmaster.py +4 -4
- {clockwork-0.7.1 → clockwork-0.8.0}/uv.lock +15 -1
- clockwork-0.7.1/clockwork/constants.py +0 -1
- clockwork-0.7.1/clockwork/timestamp/__init__.py +0 -1
- {clockwork-0.7.1 → clockwork-0.8.0}/.gitignore +0 -0
- {clockwork-0.7.1 → clockwork-0.8.0}/.python-version +0 -0
- {clockwork-0.7.1 → clockwork-0.8.0}/LICENSE +0 -0
- {clockwork-0.7.1 → clockwork-0.8.0}/README.md +0 -0
- {clockwork-0.7.1 → clockwork-0.8.0/src}/clockwork/decorators.py +0 -0
- {clockwork-0.7.1 → clockwork-0.8.0/src}/clockwork/taskmaster/__init__.py +0 -0
- {clockwork-0.7.1 → clockwork-0.8.0/src}/clockwork/taskmaster/_scheduler.py +0 -0
- {clockwork-0.7.1 → clockwork-0.8.0/src}/clockwork/taskmaster/file_monitor.py +0 -0
- {clockwork-0.7.1 → clockwork-0.8.0/src}/clockwork/taskmaster/logger.py +0 -0
- {clockwork-0.7.1 → clockwork-0.8.0/src}/clockwork/taskmaster/utils.py +0 -0
- {clockwork-0.7.1 → clockwork-0.8.0/src}/clockwork/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: clockwork
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.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
|
|
@@ -15,6 +15,7 @@ Requires-Dist: pandas>=3.0.6
|
|
|
15
15
|
Requires-Dist: polars>=2.0.0
|
|
16
16
|
Requires-Dist: python-dateutil>=2.9.0.post0
|
|
17
17
|
Requires-Dist: schedule>=1.2.2
|
|
18
|
+
Requires-Dist: tzlocal>=5.4.4
|
|
18
19
|
Description-Content-Type: text/markdown
|
|
19
20
|
|
|
20
21
|
# clockwork
|
|
@@ -1,27 +1,28 @@
|
|
|
1
|
-
[project]
|
|
2
|
-
name = "clockwork"
|
|
3
|
-
version = "0.
|
|
4
|
-
description = "Toolkit for time-related operations including scheduling, logging, date manipulation, and more."
|
|
5
|
-
authors = [
|
|
6
|
-
{ name = "Zachary Einck", email = "zacharyeinck@gmail.com" }
|
|
7
|
-
]
|
|
8
|
-
license = { text = "MIT" }
|
|
9
|
-
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.13"
|
|
11
|
-
dependencies = [
|
|
12
|
-
"holidays>=0.106",
|
|
13
|
-
"numpy>=2.5.4",
|
|
14
|
-
"oddments>=0.10.1",
|
|
15
|
-
"pandas>=3.0.6",
|
|
16
|
-
"polars>=2.0.0",
|
|
17
|
-
"python-dateutil>=2.9.0.post0",
|
|
18
|
-
"schedule>=1.2.2",
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
1
|
+
[project]
|
|
2
|
+
name = "clockwork"
|
|
3
|
+
version = "0.8.0"
|
|
4
|
+
description = "Toolkit for time-related operations including scheduling, logging, date manipulation, and more."
|
|
5
|
+
authors = [
|
|
6
|
+
{ name = "Zachary Einck", email = "zacharyeinck@gmail.com" }
|
|
7
|
+
]
|
|
8
|
+
license = { text = "MIT" }
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.13"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"holidays>=0.106",
|
|
13
|
+
"numpy>=2.5.4",
|
|
14
|
+
"oddments>=0.10.1",
|
|
15
|
+
"pandas>=3.0.6",
|
|
16
|
+
"polars>=2.0.0",
|
|
17
|
+
"python-dateutil>=2.9.0.post0",
|
|
18
|
+
"schedule>=1.2.2",
|
|
19
|
+
"tzlocal>=5.4.4",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
repository = "https://github.com/zteinck/clockwork"
|
|
24
|
+
homepage = "https://github.com/zteinck/clockwork"
|
|
25
|
+
|
|
26
|
+
[build-system]
|
|
27
|
+
requires = ["hatchling"]
|
|
28
|
+
build-backend = "hatchling.build"
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
from .
|
|
1
|
+
from .constants import ISO_DATE_FORMAT, UTC
|
|
2
|
+
from .date_time import Datetime
|
|
3
|
+
from .decorators import print_duration
|
|
2
4
|
from .month_end import MonthEnd
|
|
3
5
|
from .quarter_end import QuarterEnd
|
|
4
|
-
from .decorators import print_duration
|
|
5
6
|
|
|
6
7
|
from .utils import (
|
|
7
8
|
format_duration,
|
|
8
9
|
temporal_format_to_regex,
|
|
9
10
|
)
|
|
10
11
|
|
|
11
|
-
__version__ = '0.
|
|
12
|
+
__version__ = '0.8.0'
|
|
12
13
|
__author__ = 'Zachary Einck <zacharyeinck@gmail.com>'
|
|
@@ -32,7 +32,7 @@ def other_to_delta(func):
|
|
|
32
32
|
return delta
|
|
33
33
|
|
|
34
34
|
# Otherwise, 'other' is assumed to be a delta and the resulting
|
|
35
|
-
#
|
|
35
|
+
# Datetime object is returned.
|
|
36
36
|
delta = self._build_delta(other)
|
|
37
37
|
dt = func(self, delta)
|
|
38
38
|
ts = self._make_base(dt)
|
|
@@ -84,8 +84,8 @@ def handle_next_last(func):
|
|
|
84
84
|
|
|
85
85
|
Returns
|
|
86
86
|
------------
|
|
87
|
-
shifted :
|
|
88
|
-
|
|
87
|
+
shifted : Datetime
|
|
88
|
+
Datetime instance
|
|
89
89
|
'''
|
|
90
90
|
|
|
91
91
|
(
|
|
@@ -1,19 +1,31 @@
|
|
|
1
|
-
import datetime
|
|
2
1
|
import calendar
|
|
3
|
-
|
|
2
|
+
import datetime
|
|
3
|
+
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
import
|
|
5
|
+
from dateutil.relativedelta import relativedelta as relative_delta
|
|
6
|
+
from dateutil.parser import parse as parse_dt_string
|
|
7
|
+
from holidays import UnitedStates
|
|
7
8
|
import numpy as np
|
|
8
9
|
from oddments import Validator, UNSET
|
|
9
|
-
|
|
10
|
-
from
|
|
10
|
+
import pandas as pd
|
|
11
|
+
from tzlocal import get_localzone_name
|
|
12
|
+
|
|
13
|
+
from .constants import (
|
|
14
|
+
ISO_DATE_FORMAT,
|
|
15
|
+
MONTHS_IN_YEAR,
|
|
16
|
+
UTC,
|
|
17
|
+
)
|
|
11
18
|
|
|
12
|
-
from
|
|
13
|
-
|
|
19
|
+
from ._decorators import (
|
|
20
|
+
other_to_dt,
|
|
21
|
+
other_to_delta,
|
|
22
|
+
handle_next_last,
|
|
23
|
+
to_period_end,
|
|
24
|
+
skip_shift,
|
|
25
|
+
)
|
|
14
26
|
|
|
15
27
|
|
|
16
|
-
class
|
|
28
|
+
class Datetime:
|
|
17
29
|
'''
|
|
18
30
|
Description
|
|
19
31
|
--------------------
|
|
@@ -22,7 +34,7 @@ class Timestamp:
|
|
|
22
34
|
|
|
23
35
|
Class Attributes
|
|
24
36
|
--------------------
|
|
25
|
-
|
|
37
|
+
display_format : str
|
|
26
38
|
String of datetime format codes used to render the object's string
|
|
27
39
|
representation.
|
|
28
40
|
business_hours : tuple[int]
|
|
@@ -47,7 +59,7 @@ class Timestamp:
|
|
|
47
59
|
#| Class Attributes |
|
|
48
60
|
#╰-------------------------------------------------------------------------╯
|
|
49
61
|
|
|
50
|
-
|
|
62
|
+
display_format = '%Y-%m-%d %I:%M:%S.%f %p'
|
|
51
63
|
business_hours = (8, 17) # 8am - 5pm
|
|
52
64
|
weekday_map = None
|
|
53
65
|
holiday_calendar = None
|
|
@@ -145,10 +157,21 @@ class Timestamp:
|
|
|
145
157
|
return self.time_zone
|
|
146
158
|
|
|
147
159
|
|
|
160
|
+
@property
|
|
161
|
+
def tz_key(self):
|
|
162
|
+
''' time zone key '''
|
|
163
|
+
return None if self.tz is None else self.tz.key
|
|
164
|
+
|
|
165
|
+
|
|
148
166
|
@property
|
|
149
167
|
def ymd(self):
|
|
150
168
|
''' Date string in ISO 8601 format (i.e. YYYY-MM-DD) '''
|
|
151
|
-
return self.to_string(
|
|
169
|
+
return self.to_string(ISO_DATE_FORMAT)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def display(self):
|
|
174
|
+
return self.to_string(self.display_format)
|
|
152
175
|
|
|
153
176
|
|
|
154
177
|
@property
|
|
@@ -225,7 +248,7 @@ class Timestamp:
|
|
|
225
248
|
@property
|
|
226
249
|
def is_weekend(self):
|
|
227
250
|
''' returns True if the date falls on the weekend '''
|
|
228
|
-
return self.
|
|
251
|
+
return self.weekday_index >= 5
|
|
229
252
|
|
|
230
253
|
|
|
231
254
|
@property
|
|
@@ -274,9 +297,9 @@ class Timestamp:
|
|
|
274
297
|
|
|
275
298
|
|
|
276
299
|
@property
|
|
277
|
-
def
|
|
278
|
-
'''
|
|
279
|
-
return self.time
|
|
300
|
+
def has_time_component(self):
|
|
301
|
+
''' whether the date has a time component (i.e. not 00:00:00) '''
|
|
302
|
+
return self.time != datetime.time.min
|
|
280
303
|
|
|
281
304
|
|
|
282
305
|
@property
|
|
@@ -348,6 +371,8 @@ class Timestamp:
|
|
|
348
371
|
|
|
349
372
|
|
|
350
373
|
def __getattr__(self, name):
|
|
374
|
+
if name.startswith('_'):
|
|
375
|
+
raise AttributeError(name)
|
|
351
376
|
return getattr(self.dt, name)
|
|
352
377
|
|
|
353
378
|
|
|
@@ -360,10 +385,10 @@ class Timestamp:
|
|
|
360
385
|
|
|
361
386
|
|
|
362
387
|
def __repr__(self):
|
|
363
|
-
return '{0}(
|
|
388
|
+
return '{0}({1!r}, tz_key={2!r})'.format(
|
|
364
389
|
self.__class__.__name__,
|
|
365
|
-
self.
|
|
366
|
-
self.
|
|
390
|
+
self.display,
|
|
391
|
+
self.tz_key,
|
|
367
392
|
)
|
|
368
393
|
|
|
369
394
|
|
|
@@ -423,11 +448,11 @@ class Timestamp:
|
|
|
423
448
|
#| Instance Methods |
|
|
424
449
|
#╰-------------------------------------------------------------------------╯
|
|
425
450
|
|
|
426
|
-
def shift(self, *,
|
|
451
|
+
def shift(self, *, business_days=None, **kwargs):
|
|
427
452
|
'''
|
|
428
453
|
Description
|
|
429
454
|
------------
|
|
430
|
-
Returns a
|
|
455
|
+
Returns a Datetime object representing self shifted by a delta.
|
|
431
456
|
|
|
432
457
|
Parameters
|
|
433
458
|
------------
|
|
@@ -437,16 +462,16 @@ class Timestamp:
|
|
|
437
462
|
available:
|
|
438
463
|
|
|
439
464
|
relative : bool
|
|
440
|
-
If True, kwargs are passed to '
|
|
465
|
+
If True, kwargs are passed to 'relativedelta' instead of
|
|
441
466
|
'datetime.timedelta'.
|
|
442
|
-
|
|
467
|
+
business_days : int
|
|
443
468
|
Similar to the datetime.timedelta days argument, except
|
|
444
469
|
weekends and holidays are excluded from the count.
|
|
445
470
|
|
|
446
471
|
Returns
|
|
447
472
|
------------
|
|
448
|
-
shifted :
|
|
449
|
-
|
|
473
|
+
shifted : Datetime
|
|
474
|
+
Datetime object representing self post-shift.
|
|
450
475
|
'''
|
|
451
476
|
|
|
452
477
|
obj = (
|
|
@@ -455,13 +480,19 @@ class Timestamp:
|
|
|
455
480
|
else self.clone()
|
|
456
481
|
)
|
|
457
482
|
|
|
458
|
-
|
|
483
|
+
business_days = self._ensure_int(
|
|
484
|
+
value=business_days,
|
|
485
|
+
name='business_days',
|
|
486
|
+
allow_none=True,
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
if business_days is None or business_days == 0:
|
|
459
490
|
return obj
|
|
460
491
|
|
|
461
|
-
delta = int(np.sign(
|
|
492
|
+
delta = int(np.sign(business_days))
|
|
462
493
|
counter = 0
|
|
463
494
|
|
|
464
|
-
while counter < abs(
|
|
495
|
+
while counter < abs(business_days):
|
|
465
496
|
obj = obj._shift(days=delta)
|
|
466
497
|
if obj.is_business_day:
|
|
467
498
|
counter += 1
|
|
@@ -503,13 +534,17 @@ class Timestamp:
|
|
|
503
534
|
|
|
504
535
|
|
|
505
536
|
def to_utc(self):
|
|
506
|
-
return self.to_time_zone(
|
|
537
|
+
return self.to_time_zone(UTC)
|
|
507
538
|
|
|
508
539
|
|
|
509
540
|
def to_naive(self):
|
|
510
541
|
return self.to_time_zone(None)
|
|
511
542
|
|
|
512
543
|
|
|
544
|
+
def localize(self):
|
|
545
|
+
return self.to_time_zone('Local')
|
|
546
|
+
|
|
547
|
+
|
|
513
548
|
def clone(self):
|
|
514
549
|
''' returns a deep copy of self '''
|
|
515
550
|
return self._spawn(self)
|
|
@@ -522,7 +557,7 @@ class Timestamp:
|
|
|
522
557
|
|
|
523
558
|
def to_datetime(self):
|
|
524
559
|
''' returns a copy of the underlying datetime.datetime object '''
|
|
525
|
-
return
|
|
560
|
+
return self.dt
|
|
526
561
|
|
|
527
562
|
|
|
528
563
|
def to_pandas_timestamp(self):
|
|
@@ -576,10 +611,10 @@ class Timestamp:
|
|
|
576
611
|
pass
|
|
577
612
|
|
|
578
613
|
|
|
579
|
-
def
|
|
614
|
+
def to_midnight(self):
|
|
580
615
|
dt = self.to_datetime()
|
|
581
|
-
|
|
582
|
-
return self._spawn(
|
|
616
|
+
dt = self._to_midnight(dt)
|
|
617
|
+
return self._spawn(dt)
|
|
583
618
|
|
|
584
619
|
|
|
585
620
|
def replace(self, **kwargs):
|
|
@@ -608,7 +643,7 @@ class Timestamp:
|
|
|
608
643
|
)
|
|
609
644
|
|
|
610
645
|
delta_cls = (
|
|
611
|
-
|
|
646
|
+
relative_delta
|
|
612
647
|
if relative
|
|
613
648
|
else datetime.timedelta
|
|
614
649
|
)
|
|
@@ -624,10 +659,17 @@ class Timestamp:
|
|
|
624
659
|
|
|
625
660
|
|
|
626
661
|
def _to_time_zone(self, dt, tz):
|
|
627
|
-
tz = self._resolve_time_zone(
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
662
|
+
tz = self._resolve_time_zone(
|
|
663
|
+
value=tz,
|
|
664
|
+
name='tz',
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
dt = (
|
|
668
|
+
dt.replace(tzinfo=None)
|
|
669
|
+
if tz is None
|
|
670
|
+
else dt.astimezone(tz)
|
|
671
|
+
)
|
|
672
|
+
|
|
631
673
|
return dt
|
|
632
674
|
|
|
633
675
|
|
|
@@ -654,7 +696,7 @@ class Timestamp:
|
|
|
654
696
|
Value to convert to datetime. Supported formats include:
|
|
655
697
|
• None → returns the current datetime (i.e. now)
|
|
656
698
|
• pd.Timestamp
|
|
657
|
-
• cw.
|
|
699
|
+
• cw.Datetime or subclass
|
|
658
700
|
• datetime.datetime
|
|
659
701
|
• datetime.date
|
|
660
702
|
• int or float (expressed in seconds)
|
|
@@ -669,9 +711,9 @@ class Timestamp:
|
|
|
669
711
|
source_format : str | None
|
|
670
712
|
Datetime format code(s) used to parse 'source' when it is a string
|
|
671
713
|
(e.g. '%Y%m%d %H%M%S').
|
|
672
|
-
source_tz : UNSET | None | str | datetime.timezone
|
|
714
|
+
source_tz : UNSET | None | str | datetime.timezone | ZoneInfo
|
|
673
715
|
Source time zone.
|
|
674
|
-
target_tz : UNSET | None | str | datetime.timezone
|
|
716
|
+
target_tz : UNSET | None | str | datetime.timezone | ZoneInfo
|
|
675
717
|
Desired time zone.
|
|
676
718
|
offset : int
|
|
677
719
|
If a day of the week is provided (e.g., 'Monday'), the current week
|
|
@@ -702,7 +744,7 @@ class Timestamp:
|
|
|
702
744
|
now = datetime.datetime.now(tz=_target_tz)
|
|
703
745
|
days = weekday - now.weekday()
|
|
704
746
|
dt = now + datetime.timedelta(days=days, weeks=offset)
|
|
705
|
-
dt = self.
|
|
747
|
+
dt = self._to_midnight(dt)
|
|
706
748
|
return dt
|
|
707
749
|
|
|
708
750
|
|
|
@@ -733,16 +775,43 @@ class Timestamp:
|
|
|
733
775
|
return dt
|
|
734
776
|
|
|
735
777
|
|
|
736
|
-
def
|
|
778
|
+
def resolve_tz(dt):
|
|
779
|
+
|
|
780
|
+
def normalize_tz(dt):
|
|
781
|
+
if (
|
|
782
|
+
dt.tzinfo is None
|
|
783
|
+
or isinstance(dt.tzinfo, ZoneInfo)
|
|
784
|
+
):
|
|
785
|
+
return dt
|
|
786
|
+
|
|
787
|
+
if dt.tzinfo == datetime.timezone.utc:
|
|
788
|
+
return dt.replace(tzinfo=UTC)
|
|
789
|
+
|
|
790
|
+
return dt.astimezone(UTC)
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
dt = normalize_tz(dt)
|
|
794
|
+
|
|
795
|
+
(
|
|
796
|
+
Validator(
|
|
797
|
+
types=ZoneInfo,
|
|
798
|
+
allow_none=True,
|
|
799
|
+
)
|
|
800
|
+
.validate(
|
|
801
|
+
tzinfo=dt.tzinfo
|
|
802
|
+
)
|
|
803
|
+
)
|
|
804
|
+
|
|
737
805
|
if source_tz is not UNSET and source_tz != dt.tzinfo:
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
else:
|
|
806
|
+
|
|
807
|
+
if dt.tzinfo is not None:
|
|
741
808
|
raise ValueError(
|
|
742
|
-
f"
|
|
743
|
-
f"not align with 'source_tz' ({source_tz})."
|
|
809
|
+
f"The resolved datetime's time zone ({dt.tzinfo!r}) "
|
|
810
|
+
f"does not align with 'source_tz' ({source_tz!r})."
|
|
744
811
|
)
|
|
745
812
|
|
|
813
|
+
dt = dt.replace(tzinfo=source_tz)
|
|
814
|
+
|
|
746
815
|
if target_tz is not UNSET and target_tz != dt.tzinfo:
|
|
747
816
|
dt = self._to_time_zone(dt=dt, tz=target_tz)
|
|
748
817
|
|
|
@@ -754,8 +823,14 @@ class Timestamp:
|
|
|
754
823
|
for tz in (source_tz, target_tz)
|
|
755
824
|
)
|
|
756
825
|
|
|
757
|
-
|
|
758
826
|
if source is None:
|
|
827
|
+
|
|
828
|
+
if source_tz is not UNSET:
|
|
829
|
+
raise ValueError(
|
|
830
|
+
"'source_tz' must be UNSET when 'source' is None, got: "
|
|
831
|
+
f"{source_tz!r}"
|
|
832
|
+
)
|
|
833
|
+
|
|
759
834
|
dt = datetime.datetime.now(tz=_target_tz)
|
|
760
835
|
return dt
|
|
761
836
|
|
|
@@ -763,7 +838,7 @@ class Timestamp:
|
|
|
763
838
|
|
|
764
839
|
if source_format is not None:
|
|
765
840
|
dt = datetime.datetime.strptime(source, source_format)
|
|
766
|
-
dt =
|
|
841
|
+
dt = resolve_tz(dt)
|
|
767
842
|
return dt
|
|
768
843
|
|
|
769
844
|
for func in (try_weekday, try_quarter_end):
|
|
@@ -771,7 +846,9 @@ class Timestamp:
|
|
|
771
846
|
if dt is not None:
|
|
772
847
|
return dt
|
|
773
848
|
|
|
774
|
-
dt =
|
|
849
|
+
dt = parse_dt_string(source)
|
|
850
|
+
dt = resolve_tz(dt)
|
|
851
|
+
|
|
775
852
|
return dt
|
|
776
853
|
|
|
777
854
|
if source_format is not None:
|
|
@@ -791,7 +868,7 @@ class Timestamp:
|
|
|
791
868
|
tzinfo=_source_tz,
|
|
792
869
|
)
|
|
793
870
|
|
|
794
|
-
elif isinstance(source,
|
|
871
|
+
elif isinstance(source, Datetime):
|
|
795
872
|
dt = source.to_datetime()
|
|
796
873
|
|
|
797
874
|
elif hasattr(source, 'to_pydatetime'):
|
|
@@ -804,28 +881,17 @@ class Timestamp:
|
|
|
804
881
|
)
|
|
805
882
|
.validate(
|
|
806
883
|
type(dt),
|
|
807
|
-
|
|
884
|
+
"'dt' type",
|
|
808
885
|
)
|
|
809
886
|
)
|
|
810
887
|
|
|
811
|
-
|
|
812
|
-
raise ValueError(
|
|
813
|
-
f"The resolved datetime's Time zone ({dt.tzinfo}) does "
|
|
814
|
-
f"not align with 'source_tz' ({source_tz})."
|
|
815
|
-
)
|
|
816
|
-
|
|
817
|
-
if target_tz is not UNSET and target_tz != dt.tzinfo:
|
|
818
|
-
dt = self._to_time_zone(dt=dt, tz=target_tz)
|
|
819
|
-
|
|
888
|
+
dt = resolve_tz(dt)
|
|
820
889
|
return dt
|
|
821
890
|
|
|
822
891
|
|
|
823
892
|
# timestamp expressed in seconds
|
|
824
893
|
elif isinstance(source, (float, int)):
|
|
825
|
-
if not (
|
|
826
|
-
source_tz is UNSET
|
|
827
|
-
or source_tz == datetime.timezone.utc
|
|
828
|
-
):
|
|
894
|
+
if source_tz not in (UNSET, UTC):
|
|
829
895
|
raise ValueError(
|
|
830
896
|
"'source_tz' must be UNSET or UTC when 'source' is a "
|
|
831
897
|
f"timestamp, got: {source_tz!r}"
|
|
@@ -874,11 +940,11 @@ class Timestamp:
|
|
|
874
940
|
|
|
875
941
|
@staticmethod
|
|
876
942
|
def _make_base(*args, **kwargs):
|
|
877
|
-
return
|
|
943
|
+
return Datetime(*args, **kwargs)
|
|
878
944
|
|
|
879
945
|
|
|
880
946
|
@staticmethod
|
|
881
|
-
def
|
|
947
|
+
def _to_midnight(value):
|
|
882
948
|
''' Sets the time component to zero (midnight) '''
|
|
883
949
|
return value.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
884
950
|
|
|
@@ -887,8 +953,8 @@ class Timestamp:
|
|
|
887
953
|
def _validate_month(month):
|
|
888
954
|
if not (1 <= month <= MONTHS_IN_YEAR):
|
|
889
955
|
raise ValueError(
|
|
890
|
-
"'month' must be between 1 and "
|
|
891
|
-
f"{
|
|
956
|
+
f"'month' must be between 1 and {MONTHS_IN_YEAR}, got: "
|
|
957
|
+
f"{month}."
|
|
892
958
|
)
|
|
893
959
|
|
|
894
960
|
|
|
@@ -899,29 +965,64 @@ class Timestamp:
|
|
|
899
965
|
|
|
900
966
|
|
|
901
967
|
@staticmethod
|
|
902
|
-
def _ensure_int(
|
|
903
|
-
|
|
968
|
+
def _ensure_int(
|
|
969
|
+
value,
|
|
970
|
+
name=None,
|
|
971
|
+
whitelist=None,
|
|
972
|
+
allow_none=False,
|
|
973
|
+
positive_only=False,
|
|
974
|
+
):
|
|
904
975
|
|
|
905
|
-
if
|
|
906
|
-
|
|
907
|
-
|
|
976
|
+
if allow_none and value is None:
|
|
977
|
+
return value
|
|
978
|
+
|
|
979
|
+
if isinstance(value, bool):
|
|
908
980
|
raise TypeError(
|
|
909
|
-
f
|
|
981
|
+
f'{name!r} must be an int or str, got bool: {value!r}'
|
|
910
982
|
)
|
|
911
983
|
|
|
984
|
+
if isinstance(value, str):
|
|
985
|
+
try:
|
|
986
|
+
return int(value)
|
|
987
|
+
except ValueError as exc:
|
|
988
|
+
raise ValueError(
|
|
989
|
+
f'Integer conversion failed for {name!r}: {value!r}'
|
|
990
|
+
) from exc
|
|
991
|
+
|
|
992
|
+
params = {}
|
|
993
|
+
|
|
994
|
+
if positive_only:
|
|
995
|
+
params.update({
|
|
996
|
+
'min_value': 0,
|
|
997
|
+
'min_inclusive': False,
|
|
998
|
+
})
|
|
999
|
+
|
|
1000
|
+
(
|
|
1001
|
+
Validator(
|
|
1002
|
+
types=int,
|
|
1003
|
+
whitelist=whitelist,
|
|
1004
|
+
**params,
|
|
1005
|
+
)
|
|
1006
|
+
.validate(value, name)
|
|
1007
|
+
)
|
|
1008
|
+
|
|
912
1009
|
return value
|
|
913
1010
|
|
|
914
1011
|
|
|
915
1012
|
@staticmethod
|
|
916
1013
|
def _resolve_time_zone(value, name):
|
|
917
1014
|
|
|
918
|
-
cls = datetime.timezone
|
|
919
|
-
|
|
920
1015
|
(
|
|
921
1016
|
Validator(
|
|
922
|
-
types=(
|
|
1017
|
+
types=(
|
|
1018
|
+
str,
|
|
1019
|
+
ZoneInfo,
|
|
1020
|
+
datetime.timezone,
|
|
1021
|
+
),
|
|
923
1022
|
allow_none=True,
|
|
924
1023
|
allow_unset=True,
|
|
1024
|
+
allow_blank=False,
|
|
1025
|
+
allow_whitespace=False,
|
|
925
1026
|
)
|
|
926
1027
|
.validate(value, name)
|
|
927
1028
|
)
|
|
@@ -930,17 +1031,30 @@ class Timestamp:
|
|
|
930
1031
|
return value
|
|
931
1032
|
|
|
932
1033
|
if isinstance(value, str):
|
|
933
|
-
if value.
|
|
934
|
-
return
|
|
935
|
-
|
|
1034
|
+
if value.upper() == UTC.key:
|
|
1035
|
+
return UTC
|
|
1036
|
+
if value.lower() == 'local':
|
|
1037
|
+
value = get_localzone_name()
|
|
1038
|
+
try:
|
|
1039
|
+
return ZoneInfo(value)
|
|
1040
|
+
except ZoneInfoNotFoundError:
|
|
936
1041
|
raise ValueError(
|
|
937
|
-
|
|
938
|
-
|
|
1042
|
+
'String values must be valid ZoneInfo keys. Invalid '
|
|
1043
|
+
f'value for {name!r}: {value!r}.'
|
|
1044
|
+
) from None
|
|
939
1045
|
|
|
940
|
-
if isinstance(value,
|
|
1046
|
+
if isinstance(value, ZoneInfo):
|
|
941
1047
|
return value
|
|
942
1048
|
|
|
943
|
-
|
|
1049
|
+
utc = datetime.timezone.utc
|
|
1050
|
+
|
|
1051
|
+
if value == utc:
|
|
1052
|
+
return UTC
|
|
1053
|
+
|
|
1054
|
+
raise ValueError(
|
|
1055
|
+
f'Expected {utc!r} or a valid ZoneInfo key; fixed offsets are '
|
|
1056
|
+
f'not supported. Unsupported value for {name!r}: {value!r}.'
|
|
1057
|
+
)
|
|
944
1058
|
|
|
945
1059
|
|
|
946
1060
|
#╭-------------------------------------------------------------------------╮
|
|
@@ -960,7 +1074,7 @@ class Timestamp:
|
|
|
960
1074
|
@classmethod
|
|
961
1075
|
def _get_holiday_calendar(cls):
|
|
962
1076
|
if cls.holiday_calendar is None:
|
|
963
|
-
cls.holiday_calendar =
|
|
1077
|
+
cls.holiday_calendar = UnitedStates()
|
|
964
1078
|
return cls.holiday_calendar
|
|
965
1079
|
|
|
966
1080
|
|
|
@@ -982,7 +1096,7 @@ class Timestamp:
|
|
|
982
1096
|
@classmethod
|
|
983
1097
|
def _get_month_end_cls(cls):
|
|
984
1098
|
if cls._month_end_cls is None:
|
|
985
|
-
from
|
|
1099
|
+
from .month_end import MonthEnd
|
|
986
1100
|
cls._month_end_cls = MonthEnd
|
|
987
1101
|
return cls._month_end_cls
|
|
988
1102
|
|
|
@@ -990,6 +1104,6 @@ class Timestamp:
|
|
|
990
1104
|
@classmethod
|
|
991
1105
|
def _get_quarter_end_cls(cls):
|
|
992
1106
|
if cls._quarter_end_cls is None:
|
|
993
|
-
from
|
|
1107
|
+
from .quarter_end import QuarterEnd
|
|
994
1108
|
cls._quarter_end_cls = QuarterEnd
|
|
995
1109
|
return cls._quarter_end_cls
|
|
@@ -2,11 +2,11 @@ import datetime
|
|
|
2
2
|
|
|
3
3
|
from oddments import Validator, UNSET
|
|
4
4
|
|
|
5
|
-
from .
|
|
6
|
-
from .
|
|
5
|
+
from .constants import ISO_DATE_FORMAT, MONTHS_IN_YEAR
|
|
6
|
+
from .date_time import Datetime
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
class MonthEnd(
|
|
9
|
+
class MonthEnd(Datetime):
|
|
10
10
|
'''
|
|
11
11
|
Description
|
|
12
12
|
--------------------
|
|
@@ -26,7 +26,7 @@ class MonthEnd(Timestamp):
|
|
|
26
26
|
#| Class Attributes |
|
|
27
27
|
#╰-------------------------------------------------------------------------╯
|
|
28
28
|
|
|
29
|
-
repr_format =
|
|
29
|
+
repr_format = ISO_DATE_FORMAT
|
|
30
30
|
_increment = 1
|
|
31
31
|
|
|
32
32
|
|
|
@@ -44,7 +44,7 @@ class MonthEnd(Timestamp):
|
|
|
44
44
|
|
|
45
45
|
@property
|
|
46
46
|
def long_label(self):
|
|
47
|
-
return self.to_string(
|
|
47
|
+
return self.to_string(ISO_DATE_FORMAT)
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
@property
|
|
@@ -147,11 +147,8 @@ class MonthEnd(Timestamp):
|
|
|
147
147
|
f'of {self.month_name} {self.year}'
|
|
148
148
|
)
|
|
149
149
|
|
|
150
|
-
if
|
|
151
|
-
return
|
|
152
|
-
'the time component must be normalized (i.e. all zoroes), '
|
|
153
|
-
f'got: {self.time}'
|
|
154
|
-
)
|
|
150
|
+
if self.has_time_component:
|
|
151
|
+
return f'it includes a time component: {self.time}'
|
|
155
152
|
|
|
156
153
|
|
|
157
154
|
def _init_dt(
|
|
@@ -179,7 +176,7 @@ class MonthEnd(Timestamp):
|
|
|
179
176
|
parameters are provided. Use positive values to move forward in
|
|
180
177
|
time and negative values to move backward in time.
|
|
181
178
|
kwargs : dict
|
|
182
|
-
Additional keyword arguments are forwarded to the
|
|
179
|
+
Additional keyword arguments are forwarded to the Datetime
|
|
183
180
|
constructor.
|
|
184
181
|
'''
|
|
185
182
|
|
|
@@ -208,7 +205,11 @@ class MonthEnd(Timestamp):
|
|
|
208
205
|
|
|
209
206
|
if has_year_or_month:
|
|
210
207
|
year, month = (
|
|
211
|
-
self._ensure_int(
|
|
208
|
+
self._ensure_int(
|
|
209
|
+
value=v,
|
|
210
|
+
name=k,
|
|
211
|
+
positive_only=True,
|
|
212
|
+
)
|
|
212
213
|
for k, v in {
|
|
213
214
|
'year': year,
|
|
214
215
|
'month': month,
|
|
@@ -239,5 +240,5 @@ class MonthEnd(Timestamp):
|
|
|
239
240
|
tzinfo=_target_tz,
|
|
240
241
|
)
|
|
241
242
|
|
|
242
|
-
self._dt = self.
|
|
243
|
+
self._dt = self._to_midnight(self._dt)
|
|
243
244
|
self._validate()
|
|
@@ -102,7 +102,7 @@ class QuarterEnd(MonthEnd):
|
|
|
102
102
|
when no other parameters are provided. Use positive values to move
|
|
103
103
|
forward in time and negative values to move backward in time.
|
|
104
104
|
kwargs : dict
|
|
105
|
-
Additional keyword arguments forwarded to the
|
|
105
|
+
Additional keyword arguments forwarded to the Datetime
|
|
106
106
|
constructor.
|
|
107
107
|
'''
|
|
108
108
|
|
|
@@ -132,23 +132,17 @@ class QuarterEnd(MonthEnd):
|
|
|
132
132
|
"simultaneously."
|
|
133
133
|
)
|
|
134
134
|
|
|
135
|
-
month = self._ensure_int(
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
(
|
|
139
|
-
Validator(
|
|
140
|
-
types=int,
|
|
135
|
+
month = self._ensure_int(
|
|
136
|
+
value=month,
|
|
137
|
+
name='month',
|
|
141
138
|
whitelist=self.scheme,
|
|
142
139
|
)
|
|
143
|
-
.validate(
|
|
144
|
-
month=month
|
|
145
|
-
)
|
|
146
|
-
)
|
|
147
140
|
|
|
148
141
|
if has_quarter:
|
|
149
142
|
quarter = self._ensure_int(
|
|
150
143
|
value=quarter,
|
|
151
144
|
name='quarter',
|
|
145
|
+
whitelist=[1, 2, 3, 4],
|
|
152
146
|
)
|
|
153
147
|
month = self.scheme[quarter - 1]
|
|
154
148
|
|
|
@@ -304,7 +298,13 @@ class QuarterEnd(MonthEnd):
|
|
|
304
298
|
if parsed is None:
|
|
305
299
|
return None
|
|
306
300
|
|
|
307
|
-
year, quarter = (
|
|
301
|
+
year, quarter = (
|
|
302
|
+
cls._ensure_int(
|
|
303
|
+
value=value,
|
|
304
|
+
positive_only=True,
|
|
305
|
+
)
|
|
306
|
+
for value in parsed
|
|
307
|
+
)
|
|
308
308
|
|
|
309
309
|
if not (1 <= quarter <= 4):
|
|
310
310
|
raise ValueError(
|
|
@@ -4,7 +4,7 @@ from contextlib import redirect_stdout
|
|
|
4
4
|
|
|
5
5
|
from schedule import CancelJob
|
|
6
6
|
|
|
7
|
-
from ..
|
|
7
|
+
from ..date_time import Datetime
|
|
8
8
|
from ..utils import format_duration
|
|
9
9
|
|
|
10
10
|
from .utils import (
|
|
@@ -38,7 +38,7 @@ class Task:
|
|
|
38
38
|
name of job
|
|
39
39
|
at : str
|
|
40
40
|
at time string
|
|
41
|
-
expiry :
|
|
41
|
+
expiry : Datetime
|
|
42
42
|
If not None, job will be set inactive and stop running after this
|
|
43
43
|
datetime
|
|
44
44
|
func : func
|
|
@@ -173,12 +173,12 @@ class Task:
|
|
|
173
173
|
|
|
174
174
|
self.update_table(active=0 if set_inactive else 1)
|
|
175
175
|
|
|
176
|
-
if self.expiry and self.expiry <
|
|
176
|
+
if self.expiry and self.expiry < Datetime():
|
|
177
177
|
update_status('cancelled on expiration', set_inactive=True)
|
|
178
178
|
if self.verbose:
|
|
179
179
|
print('Killing', end=' ')
|
|
180
180
|
print(self.name, end='')
|
|
181
|
-
print(f' @ {
|
|
181
|
+
print(f' @ {Datetime()} ->', end=' ')
|
|
182
182
|
print('Job Cancelled')
|
|
183
183
|
|
|
184
184
|
# cancel job if it was cancelled on cascasde or
|
|
@@ -195,16 +195,16 @@ class Task:
|
|
|
195
195
|
if self.verbose:
|
|
196
196
|
print('Running', end=' ')
|
|
197
197
|
print(self.name, end='')
|
|
198
|
-
print(f' @ {
|
|
198
|
+
print(f' @ {Datetime()} ->', end=' ')
|
|
199
199
|
|
|
200
200
|
if self.restrict_to_business_hours \
|
|
201
|
-
and not
|
|
201
|
+
and not Datetime().is_business_hours:
|
|
202
202
|
raise PrerequisiteError(
|
|
203
203
|
'cannot execute outside of business hours'
|
|
204
204
|
)
|
|
205
205
|
|
|
206
206
|
if self.restrict_to_business_days \
|
|
207
|
-
and not
|
|
207
|
+
and not Datetime().is_business_day:
|
|
208
208
|
raise PrerequisiteError(
|
|
209
209
|
'cannot execute during the weekend'
|
|
210
210
|
)
|
|
@@ -4,7 +4,7 @@ import uuid
|
|
|
4
4
|
|
|
5
5
|
import oddments as odd
|
|
6
6
|
|
|
7
|
-
from ..
|
|
7
|
+
from ..date_time import Datetime
|
|
8
8
|
from ._scheduler import TaskScheduler
|
|
9
9
|
from ._task import Task
|
|
10
10
|
|
|
@@ -147,9 +147,9 @@ class TaskMaster:
|
|
|
147
147
|
constituent time.
|
|
148
148
|
interval : int
|
|
149
149
|
schedule.Scheduler.every interval argument
|
|
150
|
-
start :
|
|
150
|
+
start : Datetime
|
|
151
151
|
If not None, job will be not be added until this datetime
|
|
152
|
-
expiry :
|
|
152
|
+
expiry : Datetime
|
|
153
153
|
If not None, job will be set inactive and stop running after this
|
|
154
154
|
datetime
|
|
155
155
|
kwargs : keyword arguments
|
|
@@ -168,7 +168,7 @@ class TaskMaster:
|
|
|
168
168
|
if not hasattr(cls, 'scheduler'):
|
|
169
169
|
cls.scheduler = TaskScheduler()
|
|
170
170
|
|
|
171
|
-
now =
|
|
171
|
+
now = Datetime()
|
|
172
172
|
|
|
173
173
|
if (start and start > now) or (expiry and expiry < now):
|
|
174
174
|
return
|
|
@@ -12,7 +12,7 @@ resolution-markers = [
|
|
|
12
12
|
|
|
13
13
|
[[package]]
|
|
14
14
|
name = "clockwork"
|
|
15
|
-
version = "0.
|
|
15
|
+
version = "0.8.0"
|
|
16
16
|
source = { editable = "." }
|
|
17
17
|
dependencies = [
|
|
18
18
|
{ name = "holidays" },
|
|
@@ -22,6 +22,7 @@ dependencies = [
|
|
|
22
22
|
{ name = "polars" },
|
|
23
23
|
{ name = "python-dateutil" },
|
|
24
24
|
{ name = "schedule" },
|
|
25
|
+
{ name = "tzlocal" },
|
|
25
26
|
]
|
|
26
27
|
|
|
27
28
|
[package.metadata]
|
|
@@ -33,6 +34,7 @@ requires-dist = [
|
|
|
33
34
|
{ name = "polars", specifier = ">=2.0.0" },
|
|
34
35
|
{ name = "python-dateutil", specifier = ">=2.9.0.post0" },
|
|
35
36
|
{ name = "schedule", specifier = ">=1.2.2" },
|
|
37
|
+
{ name = "tzlocal", specifier = ">=5.4.4" },
|
|
36
38
|
]
|
|
37
39
|
|
|
38
40
|
[[package]]
|
|
@@ -286,3 +288,15 @@ sdist = { url = "https://files.pythonhosted.org/packages/d9/68/f1b440335057bfce7
|
|
|
286
288
|
wheels = [
|
|
287
289
|
{ url = "https://files.pythonhosted.org/packages/94/21/1e5995a1c920cce14e4bffae20c665ec10e7ed03ab25e006cd741092b718/tzdata-2026.5-py2.py3-none-any.whl", hash = "sha256:b683bd1b6659ddcd810ff02ad09ba821d4bf1065072805063eb35c49617905ac", size = 347996, upload-time = "2026-10-03T09:23:12.535Z" },
|
|
288
290
|
]
|
|
291
|
+
|
|
292
|
+
[[package]]
|
|
293
|
+
name = "tzlocal"
|
|
294
|
+
version = "5.4.5"
|
|
295
|
+
source = { registry = "https://pypi.org/simple" }
|
|
296
|
+
dependencies = [
|
|
297
|
+
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
|
298
|
+
]
|
|
299
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d8/89/2bd035df331499589fc92e59375c17c6cea6a500453b2c5f6a6ec9572825/tzlocal-5.4.5.tar.gz", hash = "sha256:ba976898bad27945178c9ca25c222c19e322b30c10342b9f40b46714f70d2d5f", size = 31483, upload-time = "2026-10-22T09:09:26.38Z" }
|
|
300
|
+
wheels = [
|
|
301
|
+
{ url = "https://files.pythonhosted.org/packages/7b/c1/9be4e1a8a30db8729a86628073910e56f85aaf8a659e053f376a2e6ea5a8/tzlocal-5.4.5-py3-none-any.whl", hash = "sha256:ecab534f6d65d9deb4c4ea52c121ab6dd5958e9b878e71dcb102578269752454", size = 18215, upload-time = "2026-10-22T09:09:24.447Z" },
|
|
302
|
+
]
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
MONTHS_IN_YEAR = 12
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
from .core import Timestamp
|
|
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
|
|
File without changes
|