opendate 0.1.34__cp39-cp39-win_amd64.whl

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.
opendate/date_.py ADDED
@@ -0,0 +1,255 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import datetime as _datetime
5
+ import sys
6
+ from typing import TYPE_CHECKING
7
+
8
+ import numpy as np
9
+ import pandas as pd
10
+ import pendulum as _pendulum
11
+
12
+ from opendate.constants import _IS_WINDOWS, DATEMATCH, LCL, UTC
13
+ from opendate.helpers import _rust_parse_datetime
14
+ from opendate.metaclass import DATE_METHODS_RETURNING_DATE, DateContextMeta
15
+ from opendate.mixins import DateBusinessMixin, DateExtrasMixin
16
+
17
+ if sys.version_info >= (3, 11):
18
+ from typing import Self
19
+ else:
20
+ from typing_extensions import Self
21
+
22
+ if TYPE_CHECKING:
23
+ from opendate.calendars import Calendar
24
+
25
+
26
+ class Date(
27
+ DateExtrasMixin,
28
+ DateBusinessMixin,
29
+ _pendulum.Date,
30
+ metaclass=DateContextMeta,
31
+ methods_to_wrap=DATE_METHODS_RETURNING_DATE
32
+ ):
33
+ """Date class extending pendulum.Date with business day and additional functionality.
34
+
35
+ This class inherits all pendulum.Date functionality while adding:
36
+ - Business day calculations with NYSE calendar integration
37
+ - Additional date navigation methods
38
+ - Enhanced parsing capabilities
39
+ - Custom financial date utilities
40
+
41
+ Unlike pendulum.Date, methods that create new instances return Date objects
42
+ that preserve business status and entity association when chained.
43
+ """
44
+
45
+ def to_string(self, fmt: str) -> str:
46
+ """Format date to string, handling platform-specific format codes.
47
+
48
+ Automatically converts '%-' format codes to '%#' on Windows.
49
+ """
50
+ return self.strftime(fmt.replace('%-', '%#') if _IS_WINDOWS else fmt)
51
+
52
+ @classmethod
53
+ def fromordinal(cls, *args, **kwargs) -> Self:
54
+ """Create a Date from an ordinal.
55
+
56
+ Parameters
57
+ n: The ordinal value
58
+
59
+ Returns
60
+ Date instance
61
+ """
62
+ result = _pendulum.Date.fromordinal(*args, **kwargs)
63
+ return cls.instance(result)
64
+
65
+ @classmethod
66
+ def fromtimestamp(cls, timestamp, tz=None) -> Self:
67
+ """Create a Date from a timestamp.
68
+
69
+ Parameters
70
+ timestamp: Unix timestamp
71
+ tz: Optional timezone (defaults to UTC)
72
+
73
+ Returns
74
+ Date instance
75
+ """
76
+ tz = tz or UTC
77
+ dt = _datetime.datetime.fromtimestamp(timestamp, tz=tz)
78
+ return cls(dt.year, dt.month, dt.day)
79
+
80
+ @classmethod
81
+ def parse(
82
+ cls,
83
+ s: str | None,
84
+ calendar: str | Calendar = 'NYSE',
85
+ raise_err: bool = False,
86
+ ) -> Self | None:
87
+ """Convert a string to a date handling many different formats.
88
+
89
+ Supports various date formats including:
90
+ - Standard formats: YYYY-MM-DD, MM/DD/YYYY, MM/DD/YY, YYYYMMDD
91
+ - Named months: DD-MON-YYYY, MON-DD-YYYY, Month DD, YYYY
92
+ - Special codes: T (today), Y (yesterday), P (previous business day)
93
+ - Business day offsets: T-3b, P+2b (add/subtract business days)
94
+
95
+ Parameters
96
+ s: String to parse or None
97
+ calendar: Calendar name or instance for business day calculations (default 'NYSE')
98
+ raise_err: If True, raises ValueError on parse failure instead of returning None
99
+
100
+ Returns
101
+ Date instance or None if parsing fails and raise_err is False
102
+
103
+ Examples
104
+ Standard numeric formats:
105
+ Date.parse('2020-01-15') → Date(2020, 1, 15)
106
+ Date.parse('01/15/2020') → Date(2020, 1, 15)
107
+ Date.parse('01/15/20') → Date(2020, 1, 15)
108
+ Date.parse('20200115') → Date(2020, 1, 15)
109
+
110
+ Named month formats:
111
+ Date.parse('15-Jan-2020') → Date(2020, 1, 15)
112
+ Date.parse('Jan 15, 2020') → Date(2020, 1, 15)
113
+ Date.parse('15JAN2020') → Date(2020, 1, 15)
114
+
115
+ Special codes:
116
+ Date.parse('T') → today's date
117
+ Date.parse('Y') → yesterday's date
118
+ Date.parse('P') → previous business day
119
+ Date.parse('M') → last day of previous month
120
+
121
+ Business day offsets:
122
+ Date.parse('T-3b') → 3 business days ago
123
+ Date.parse('P+2b') → 2 business days after previous business day
124
+ Date.parse('T+5') → 5 calendar days from today
125
+ """
126
+
127
+ def date_for_symbol(s):
128
+ if s == 'N':
129
+ return cls.today()
130
+ if s == 'T':
131
+ return cls.today()
132
+ if s == 'Y':
133
+ return cls.today().subtract(days=1)
134
+ if s == 'P':
135
+ return cls.today().calendar(calendar).business().subtract(days=1)
136
+ if s == 'M':
137
+ return cls.today().start_of('month').subtract(days=1)
138
+
139
+ if not s:
140
+ if raise_err:
141
+ raise ValueError('Empty value')
142
+ return
143
+
144
+ if not isinstance(s, str):
145
+ raise TypeError(f'Invalid type for date parse: {s.__class__}')
146
+
147
+ with contextlib.suppress(ValueError):
148
+ if float(s) and len(s) != 8: # 20000101
149
+ if raise_err:
150
+ raise ValueError('Invalid date: %s', s)
151
+ return
152
+
153
+ # special shortcode symbolic values: T, Y-2, P-1b
154
+ if m := DATEMATCH.match(s):
155
+ d = date_for_symbol(m.groupdict().get('d'))
156
+ n = m.groupdict().get('n')
157
+ if not n:
158
+ return d
159
+ n = int(n)
160
+ b = m.groupdict().get('b')
161
+ if b:
162
+ if b != 'b':
163
+ raise ValueError(f"Expected 'b' for business day modifier, got '{b}'")
164
+ d = d.calendar(calendar).business().add(days=n)
165
+ else:
166
+ d = d.add(days=n)
167
+ return d
168
+ if 'today' in s.lower():
169
+ return cls.today()
170
+ if 'yester' in s.lower():
171
+ return cls.today().subtract(days=1)
172
+
173
+ parsed = _rust_parse_datetime(s)
174
+ if parsed is not None:
175
+ return cls.instance(parsed)
176
+
177
+ if raise_err:
178
+ raise ValueError('Failed to parse date: %s', s)
179
+
180
+ @classmethod
181
+ def instance(
182
+ cls,
183
+ obj: _datetime.date
184
+ | _datetime.datetime
185
+ | _datetime.time
186
+ | pd.Timestamp
187
+ | np.datetime64
188
+ | Self
189
+ | None,
190
+ raise_err: bool = False,
191
+ ) -> Self | None:
192
+ """Create a Date instance from various date-like objects.
193
+
194
+ Converts datetime.date, datetime.datetime, pandas Timestamp,
195
+ numpy datetime64, and other date-like objects to Date instances.
196
+
197
+ Parameters
198
+ obj: Date-like object to convert
199
+ raise_err: If True, raises ValueError for None/NA values instead of returning None
200
+
201
+ Returns
202
+ Date instance or None if obj is None/NA and raise_err is False
203
+ """
204
+ if pd.isna(obj):
205
+ if raise_err:
206
+ raise ValueError('Empty value')
207
+ return
208
+
209
+ if type(obj) is cls:
210
+ return obj
211
+
212
+ if isinstance(obj, pd.Timestamp):
213
+ obj = obj.to_pydatetime()
214
+ return cls(obj.year, obj.month, obj.day)
215
+
216
+ if isinstance(obj, np.datetime64):
217
+ obj = np.datetime64(obj, 'us').astype(_datetime.datetime)
218
+ return cls(obj.year, obj.month, obj.day)
219
+
220
+ return cls(obj.year, obj.month, obj.day)
221
+
222
+ @classmethod
223
+ def today(cls) -> Self:
224
+ d = _datetime.datetime.now(LCL)
225
+ return cls(d.year, d.month, d.day)
226
+
227
+ def isoweek(self) -> int | None:
228
+ """Get ISO week number (1-52/53) following ISO week-numbering standard.
229
+ """
230
+ with contextlib.suppress(Exception):
231
+ return self.isocalendar()[1]
232
+
233
+ def lookback(self, unit='last') -> Self:
234
+ """Get date in the past based on lookback unit.
235
+
236
+ Supported units: 'last'/'day' (1 day), 'week', 'month', 'quarter', 'year'.
237
+ Respects business day mode if enabled.
238
+ """
239
+ def _lookback(years=0, months=0, weeks=0, days=0):
240
+ _business = self._business
241
+ self._business = False
242
+ d = self\
243
+ .subtract(years=years, months=months, weeks=weeks, days=days)
244
+ if _business:
245
+ return d._business_or_previous()
246
+ return d
247
+
248
+ return {
249
+ 'day': _lookback(days=1),
250
+ 'last': _lookback(days=1),
251
+ 'week': _lookback(weeks=1),
252
+ 'month': _lookback(months=1),
253
+ 'quarter': _lookback(months=3),
254
+ 'year': _lookback(years=1),
255
+ }.get(unit)
opendate/datetime_.py ADDED
@@ -0,0 +1,339 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime as _datetime
4
+ import sys
5
+ import zoneinfo as _zoneinfo
6
+ from typing import TYPE_CHECKING
7
+
8
+ import numpy as np
9
+ import pandas as pd
10
+ import pendulum as _pendulum
11
+
12
+ from opendate.constants import LCL, UTC, Timezone
13
+ from opendate.helpers import _rust_parse_datetime
14
+ from opendate.metaclass import DATETIME_METHODS_RETURNING_DATETIME, DateContextMeta
15
+ from opendate.mixins import DateBusinessMixin
16
+
17
+ if sys.version_info >= (3, 11):
18
+ from typing import Self
19
+ else:
20
+ from typing_extensions import Self
21
+
22
+ if TYPE_CHECKING:
23
+ from opendate.calendars import Calendar
24
+ from opendate.date_ import Date
25
+ from opendate.time_ import Time
26
+
27
+
28
+ class DateTime(
29
+ DateBusinessMixin,
30
+ _pendulum.DateTime,
31
+ metaclass=DateContextMeta,
32
+ methods_to_wrap=DATETIME_METHODS_RETURNING_DATETIME
33
+ ):
34
+ """DateTime class extending pendulum.DateTime with business day and additional functionality.
35
+
36
+ This class inherits all pendulum.DateTime functionality while adding:
37
+ - Business day calculations with NYSE calendar integration
38
+ - Enhanced timezone handling
39
+ - Extended parsing capabilities
40
+ - Custom utility methods for financial applications
41
+
42
+ Unlike pendulum.DateTime:
43
+ - today() returns start of day rather than current time
44
+ - Methods preserve business status and entity when chaining
45
+ - Has timezone handling helpers not present in pendulum
46
+ """
47
+
48
+ def epoch(self) -> float:
49
+ """Translate a datetime object into unix seconds since epoch
50
+ """
51
+ return self.timestamp()
52
+
53
+ @classmethod
54
+ def fromordinal(cls, *args, **kwargs) -> Self:
55
+ """Create a DateTime from an ordinal.
56
+
57
+ Parameters
58
+ n: The ordinal value
59
+
60
+ Returns
61
+ DateTime instance
62
+ """
63
+ result = _pendulum.DateTime.fromordinal(*args, **kwargs)
64
+ return cls.instance(result)
65
+
66
+ @classmethod
67
+ def fromtimestamp(cls, timestamp, tz=None) -> Self:
68
+ """Create a DateTime from a timestamp.
69
+
70
+ Parameters
71
+ timestamp: Unix timestamp
72
+ tz: Optional timezone
73
+
74
+ Returns
75
+ DateTime instance
76
+ """
77
+ tz = tz or UTC
78
+ result = _pendulum.DateTime.fromtimestamp(timestamp, tz)
79
+ return cls.instance(result)
80
+
81
+ @classmethod
82
+ def strptime(cls, time_str, fmt) -> Self:
83
+ """Parse a string into a DateTime according to a format.
84
+
85
+ Parameters
86
+ time_str: String to parse
87
+ fmt: Format string
88
+
89
+ Returns
90
+ DateTime instance
91
+ """
92
+ result = _pendulum.DateTime.strptime(time_str, fmt)
93
+ return cls.instance(result)
94
+
95
+ @classmethod
96
+ def utcfromtimestamp(cls, timestamp) -> Self:
97
+ """Create a UTC DateTime from a timestamp.
98
+
99
+ Parameters
100
+ timestamp: Unix timestamp
101
+
102
+ Returns
103
+ DateTime instance
104
+ """
105
+ result = _pendulum.DateTime.utcfromtimestamp(timestamp)
106
+ return cls.instance(result)
107
+
108
+ @classmethod
109
+ def utcnow(cls) -> Self:
110
+ """Create a DateTime representing current UTC time.
111
+
112
+ Returns
113
+ DateTime instance
114
+ """
115
+ result = _pendulum.DateTime.utcnow()
116
+ return cls.instance(result)
117
+
118
+ @classmethod
119
+ def now(cls, tz: str | _zoneinfo.ZoneInfo | _datetime.tzinfo | None = None) -> Self:
120
+ """Get a DateTime instance for the current date and time.
121
+ """
122
+ if tz is None or tz == 'local':
123
+ d = _datetime.datetime.now(LCL)
124
+ elif tz is UTC or tz == 'UTC':
125
+ d = _datetime.datetime.now(UTC)
126
+ else:
127
+ d = _datetime.datetime.now(UTC)
128
+ tz = _pendulum._safe_timezone(tz)
129
+ d = d.astimezone(tz)
130
+ return cls(d.year, d.month, d.day, d.hour, d.minute, d.second,
131
+ d.microsecond, tzinfo=d.tzinfo, fold=d.fold)
132
+
133
+ @classmethod
134
+ def today(cls, tz: str | _zoneinfo.ZoneInfo | None = None) -> Self:
135
+ """Create a DateTime object representing today at the start of day.
136
+
137
+ Unlike pendulum.today() which returns current time, this method
138
+ returns a DateTime object at 00:00:00 of the current day.
139
+
140
+ Parameters
141
+ tz: Optional timezone (defaults to local timezone)
142
+
143
+ Returns
144
+ DateTime instance representing start of current day
145
+ """
146
+ return DateTime.now(tz).start_of('day')
147
+
148
+ def date(self) -> Date:
149
+ from opendate.date_ import Date
150
+ return Date(self.year, self.month, self.day)
151
+
152
+ @classmethod
153
+ def combine(
154
+ cls,
155
+ date: _datetime.date,
156
+ time: _datetime.time,
157
+ tzinfo: _zoneinfo.ZoneInfo | None = None,
158
+ ) -> Self:
159
+ """Combine date and time (*behaves differently from Pendulum `combine`*).
160
+ """
161
+ _tzinfo = tzinfo or time.tzinfo
162
+ return DateTime.instance(_datetime.datetime.combine(date, time, tzinfo=_tzinfo))
163
+
164
+ def rfc3339(self) -> str:
165
+ """Return RFC 3339 formatted string (same as isoformat()).
166
+ """
167
+ return self.isoformat()
168
+
169
+ def time(self) -> Time:
170
+ """Extract time component from datetime (preserving timezone).
171
+ """
172
+ from opendate.time_ import Time
173
+ return Time.instance(self)
174
+
175
+ @classmethod
176
+ def parse(
177
+ cls, s: str | int | None,
178
+ calendar: str | Calendar = 'NYSE',
179
+ raise_err: bool = False
180
+ ) -> Self | None:
181
+ """Convert a string or timestamp to a DateTime with extended format support.
182
+
183
+ Unlike pendulum's parse, this method supports:
184
+ - Unix timestamps (int/float, handles milliseconds automatically)
185
+ - Special codes: T (today), Y (yesterday), P (previous business day)
186
+ - Business day offsets: T-3b, P+2b (add/subtract business days)
187
+ - Multiple date-time formats beyond ISO 8601
188
+ - Combined date and time strings with various separators
189
+
190
+ Parameters
191
+ s: String or timestamp to parse
192
+ calendar: Calendar name or instance for business day calculations (default 'NYSE')
193
+ raise_err: If True, raises ValueError on parse failure instead of returning None
194
+
195
+ Returns
196
+ DateTime instance or None if parsing fails and raise_err is False
197
+
198
+ Examples
199
+ Unix timestamps:
200
+ DateTime.parse(1609459200) → DateTime(2021, 1, 1, 0, 0, 0, tzinfo=LCL)
201
+ DateTime.parse(1609459200000) → DateTime(2021, 1, 1, 0, 0, 0, tzinfo=LCL)
202
+
203
+ ISO 8601 format:
204
+ DateTime.parse('2020-01-15T14:30:00') → DateTime(2020, 1, 15, 14, 30, 0)
205
+
206
+ Date and time separated:
207
+ DateTime.parse('2020-01-15 14:30:00') → DateTime(2020, 1, 15, 14, 30, 0, tzinfo=LCL)
208
+ DateTime.parse('01/15/2020:14:30:00') → DateTime(2020, 1, 15, 14, 30, 0, tzinfo=LCL)
209
+
210
+ Date only (time defaults to 00:00:00):
211
+ DateTime.parse('2020-01-15') → DateTime(2020, 1, 15, 0, 0, 0)
212
+ DateTime.parse('01/15/2020') → DateTime(2020, 1, 15, 0, 0, 0)
213
+
214
+ Time only (uses today's date):
215
+ DateTime.parse('14:30:00') → DateTime(today's year, month, day, 14, 30, 0, tzinfo=LCL)
216
+
217
+ Special codes:
218
+ DateTime.parse('T') → today at 00:00:00
219
+ DateTime.parse('Y') → yesterday at 00:00:00
220
+ DateTime.parse('P') → previous business day at 00:00:00
221
+ """
222
+ from opendate.date_ import Date
223
+ from opendate.time_ import Time
224
+
225
+ if not s:
226
+ if raise_err:
227
+ raise ValueError('Empty value')
228
+ return
229
+
230
+ if not isinstance(s, (str, int, float)):
231
+ raise TypeError(f'Invalid type for datetime parse: {s.__class__}')
232
+
233
+ if isinstance(s, (int, float)):
234
+ if len(str(int(s))) == 13:
235
+ s /= 1000 # Convert from milliseconds to seconds
236
+ dt = _datetime.datetime.fromtimestamp(s)
237
+ return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute,
238
+ dt.second, dt.microsecond, tzinfo=LCL)
239
+
240
+ parsed = _rust_parse_datetime(s)
241
+ if parsed is not None:
242
+ return cls.instance(parsed)
243
+
244
+ for delim in (' ', ':'):
245
+ bits = s.split(delim, 1)
246
+ if len(bits) == 2:
247
+ d = Date.parse(bits[0])
248
+ t = Time.parse(bits[1])
249
+ if d is not None and t is not None:
250
+ return DateTime.combine(d, t, LCL)
251
+
252
+ d = Date.parse(s, calendar=calendar)
253
+ if d is not None:
254
+ return cls(d.year, d.month, d.day, 0, 0, 0)
255
+
256
+ current = Date.today()
257
+ t = Time.parse(s)
258
+ if t is not None:
259
+ return cls.combine(current, t, LCL)
260
+
261
+ if raise_err:
262
+ raise ValueError('Invalid date-time format: %s', s)
263
+
264
+ @classmethod
265
+ def instance(
266
+ cls,
267
+ obj: _datetime.date
268
+ | _datetime.time
269
+ | pd.Timestamp
270
+ | np.datetime64
271
+ | Self
272
+ | None,
273
+ tz: str | _zoneinfo.ZoneInfo | _datetime.tzinfo | None = None,
274
+ raise_err: bool = False,
275
+ ) -> Self | None:
276
+ """Create a DateTime instance from various datetime-like objects.
277
+
278
+ Provides unified interface for converting different date/time types
279
+ including pandas and numpy datetime objects into DateTime instances.
280
+
281
+ Unlike pendulum, this method:
282
+ - Handles pandas Timestamp and numpy datetime64 objects
283
+ - Adds timezone (UTC by default) when none is specified
284
+ - Has special handling for Time objects (combines with current date)
285
+
286
+ Parameters
287
+ obj: Date, datetime, time, or compatible object to convert
288
+ tz: Optional timezone to apply (if None, uses obj's timezone or UTC)
289
+ raise_err: If True, raises ValueError for None/NA values instead of returning None
290
+
291
+ Returns
292
+ DateTime instance or None if obj is None/NA and raise_err is False
293
+ """
294
+ from opendate.date_ import Date
295
+ from opendate.time_ import Time
296
+
297
+ if pd.isna(obj):
298
+ if raise_err:
299
+ raise ValueError('Empty value')
300
+ return
301
+
302
+ if type(obj) is cls and not tz:
303
+ return obj
304
+
305
+ if isinstance(obj, pd.Timestamp):
306
+ obj = obj.to_pydatetime()
307
+ tz = tz or obj.tzinfo or UTC
308
+ if tz is _datetime.timezone.utc:
309
+ tz = UTC
310
+ elif hasattr(tz, 'zone'):
311
+ tz = Timezone(tz.zone)
312
+ elif isinstance(tz, str):
313
+ tz = Timezone(tz)
314
+ return cls(obj.year, obj.month, obj.day, obj.hour, obj.minute,
315
+ obj.second, obj.microsecond, tzinfo=tz)
316
+
317
+ if isinstance(obj, np.datetime64):
318
+ obj = np.datetime64(obj, 'us').astype(_datetime.datetime)
319
+ tz = tz or UTC
320
+ return cls(obj.year, obj.month, obj.day, obj.hour, obj.minute,
321
+ obj.second, obj.microsecond, tzinfo=tz)
322
+
323
+ if type(obj) is Date:
324
+ return cls(obj.year, obj.month, obj.day, tzinfo=tz or UTC)
325
+
326
+ if isinstance(obj, _datetime.date) and not isinstance(obj, _datetime.datetime):
327
+ return cls(obj.year, obj.month, obj.day, tzinfo=tz or UTC)
328
+
329
+ tz = tz or obj.tzinfo or UTC
330
+
331
+ if type(obj) is Time:
332
+ return cls.combine(Date.today(), obj, tzinfo=tz)
333
+
334
+ if isinstance(obj, _datetime.time):
335
+ from opendate.date_ import Date
336
+ return cls.combine(Date.today(), obj, tzinfo=tz)
337
+
338
+ return cls(obj.year, obj.month, obj.day, obj.hour, obj.minute,
339
+ obj.second, obj.microsecond, tzinfo=tz)