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.
date/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ # Backwards compatibility alias - 'date' re-exports everything from 'opendate'
2
+ from opendate import *
3
+ from opendate import __version__, __all__
opendate/__init__.py ADDED
@@ -0,0 +1,150 @@
1
+ from __future__ import annotations
2
+
3
+ __version__ = '0.1.34'
4
+
5
+ import datetime as _datetime
6
+ import zoneinfo as _zoneinfo
7
+
8
+ from opendate.calendars import Calendar, CustomCalendar, ExchangeCalendar
9
+ from opendate.calendars import available_calendars, get_calendar
10
+ from opendate.calendars import get_default_calendar, register_calendar
11
+ from opendate.calendars import set_default_calendar
12
+ from opendate.constants import EST, GMT, LCL, UTC, WEEKDAY_SHORTNAME, Timezone
13
+ from opendate.constants import WeekDay
14
+ from opendate.date_ import Date
15
+ from opendate.datetime_ import DateTime
16
+ from opendate.decorators import expect_date, expect_date_or_datetime
17
+ from opendate.decorators import expect_datetime, expect_native_timezone
18
+ from opendate.decorators import expect_time, expect_utc_timezone
19
+ from opendate.decorators import prefer_native_timezone, prefer_utc_timezone
20
+ from opendate.extras import create_ics, is_business_day
21
+ from opendate.extras import is_within_business_hours, overlap_days
22
+ from opendate.interval import Interval
23
+ from opendate.time_ import Time
24
+
25
+ timezone = Timezone
26
+
27
+
28
+ def date(year: int, month: int, day: int) -> Date:
29
+ """Create new Date
30
+ """
31
+ return Date(year, month, day)
32
+
33
+
34
+ def datetime(
35
+ year: int,
36
+ month: int,
37
+ day: int,
38
+ hour: int = 0,
39
+ minute: int = 0,
40
+ second: int = 0,
41
+ microsecond: int = 0,
42
+ tzinfo: str | float | _zoneinfo.ZoneInfo | _datetime.tzinfo | None = UTC,
43
+ fold: int = 0,
44
+ ) -> DateTime:
45
+ """Create new DateTime
46
+ """
47
+ return DateTime(
48
+ year,
49
+ month,
50
+ day,
51
+ hour=hour,
52
+ minute=minute,
53
+ second=second,
54
+ microsecond=microsecond,
55
+ tzinfo=tzinfo,
56
+ fold=fold,
57
+ )
58
+
59
+
60
+ def time(
61
+ hour: int,
62
+ minute: int = 0,
63
+ second: int = 0,
64
+ microsecond: int = 0,
65
+ tzinfo: str | float | _zoneinfo.ZoneInfo | _datetime.tzinfo | None = UTC,
66
+ ) -> Time:
67
+ """Create new Time
68
+ """
69
+ return Time(hour, minute, second, microsecond, tzinfo)
70
+
71
+
72
+ def interval(begdate: Date | DateTime, enddate: Date | DateTime) -> Interval:
73
+ """Create new Interval
74
+ """
75
+ return Interval(begdate, enddate)
76
+
77
+
78
+ def parse(s: str | None, calendar: str | Calendar | None = None, raise_err: bool = False) -> DateTime | None:
79
+ """Parse using DateTime.parse
80
+ """
81
+ if calendar is None:
82
+ calendar = get_default_calendar()
83
+ return DateTime.parse(s, calendar=calendar, raise_err=raise_err)
84
+
85
+
86
+ def instance(obj: _datetime.date | _datetime.datetime | _datetime.time) -> DateTime | Date | Time:
87
+ """Create a DateTime/Date/Time instance from a datetime/date/time native one.
88
+ """
89
+ if isinstance(obj, _datetime.date) and not isinstance(obj, _datetime.datetime):
90
+ return Date.instance(obj)
91
+ if isinstance(obj, _datetime.time):
92
+ return Time.instance(obj)
93
+ if isinstance(obj, _datetime.datetime):
94
+ return DateTime.instance(obj)
95
+ raise ValueError(f'opendate `instance` helper cannot parse type {type(obj)}')
96
+
97
+
98
+ def now(tz: str | _zoneinfo.ZoneInfo | None = None) -> DateTime:
99
+ """Returns Datetime.now
100
+ """
101
+ return DateTime.now(tz)
102
+
103
+
104
+ def today(tz: str | _zoneinfo.ZoneInfo | None = None) -> DateTime:
105
+ """Returns DateTime.today
106
+ """
107
+ return DateTime.today(tz)
108
+
109
+
110
+ __all__ = [
111
+ 'Date',
112
+ 'date',
113
+ 'DateTime',
114
+ 'datetime',
115
+ 'Calendar',
116
+ 'ExchangeCalendar',
117
+ 'CustomCalendar',
118
+ 'get_calendar',
119
+ 'get_default_calendar',
120
+ 'set_default_calendar',
121
+ 'available_calendars',
122
+ 'register_calendar',
123
+ 'expect_date',
124
+ 'expect_datetime',
125
+ 'expect_time',
126
+ 'expect_date_or_datetime',
127
+ 'expect_native_timezone',
128
+ 'expect_utc_timezone',
129
+ 'instance',
130
+ 'Interval',
131
+ 'interval',
132
+ 'is_business_day',
133
+ 'is_within_business_hours',
134
+ 'LCL',
135
+ 'now',
136
+ 'overlap_days',
137
+ 'parse',
138
+ 'prefer_native_timezone',
139
+ 'prefer_utc_timezone',
140
+ 'Time',
141
+ 'time',
142
+ 'timezone',
143
+ 'today',
144
+ 'WeekDay',
145
+ 'EST',
146
+ 'GMT',
147
+ 'UTC',
148
+ 'WEEKDAY_SHORTNAME',
149
+ 'create_ics',
150
+ ]
Binary file
opendate/calendars.py ADDED
@@ -0,0 +1,350 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime as _datetime
4
+ import zoneinfo as _zoneinfo
5
+ from abc import ABC, abstractmethod
6
+
7
+ import pandas as pd
8
+ import pandas_market_calendars as mcal
9
+
10
+ import opendate as _date
11
+ from opendate.constants import MAX_YEAR, UTC, Timezone
12
+ from opendate.helpers import _BusinessCalendar, _get_decade_bounds
13
+
14
+
15
+ class Calendar(ABC):
16
+ """Abstract base class for calendar definitions.
17
+
18
+ Provides business day information including trading days,
19
+ market hours, and holidays. Use string-based calendars for
20
+ exchanges (via get_calendar()) or CustomCalendar for user-defined.
21
+ """
22
+
23
+ name: str = 'calendar'
24
+ tz: _zoneinfo.ZoneInfo = UTC
25
+
26
+ @abstractmethod
27
+ def business_days(self, begdate: _datetime.date, enddate: _datetime.date) -> set:
28
+ """Returns all business days over a range.
29
+ """
30
+
31
+ @abstractmethod
32
+ def business_hours(self, begdate: _datetime.date, enddate: _datetime.date) -> dict:
33
+ """Returns market open/close times for each business day.
34
+ """
35
+
36
+ @abstractmethod
37
+ def business_holidays(self, begdate: _datetime.date, enddate: _datetime.date) -> set:
38
+ """Returns holidays over a range.
39
+ """
40
+
41
+ @abstractmethod
42
+ def _get_calendar(self, date: _datetime.date):
43
+ """Get Rust BusinessCalendar for O(1) operations.
44
+ """
45
+
46
+
47
+ class ExchangeCalendar(Calendar):
48
+ """Calendar backed by pandas_market_calendars.
49
+
50
+ Provides access to 150+ exchange calendars including NYSE, LSE,
51
+ NASDAQ, TSX, etc. Use get_calendar('NYSE') or available_calendars()
52
+ to discover options.
53
+ """
54
+
55
+ BEGDATE = _datetime.date(2000, 1, 1)
56
+ ENDDATE = _datetime.date(2050, 1, 1)
57
+
58
+ def __init__(self, name: str):
59
+ self._name = name.upper()
60
+ self._mcal = mcal.get_calendar(self._name)
61
+ tz_str = str(self._mcal.tz)
62
+ self._tz = Timezone(tz_str)
63
+ self._business_days_cache: dict[tuple, set] = {}
64
+ self._business_hours_cache: dict[tuple, dict] = {}
65
+ self._business_holidays_cache: dict[tuple, set] = {}
66
+ self._fast_calendar_cache: dict[tuple, object] = {}
67
+
68
+ @property
69
+ def name(self) -> str:
70
+ return self._name
71
+
72
+ @property
73
+ def tz(self) -> _zoneinfo.ZoneInfo:
74
+ return self._tz
75
+
76
+ def business_days(self, begdate: _datetime.date = None, enddate: _datetime.date = None) -> set:
77
+ """Get business days for a date range (loads and caches by decade).
78
+ """
79
+ if begdate is None:
80
+ begdate = self.BEGDATE
81
+ if enddate is None:
82
+ enddate = self.ENDDATE
83
+
84
+ if begdate.year > MAX_YEAR:
85
+ return set()
86
+
87
+ decade_start = _datetime.date(begdate.year // 10 * 10, 1, 1)
88
+ next_decade_year = (enddate.year // 10 + 1) * 10
89
+ if next_decade_year > MAX_YEAR:
90
+ decade_end = _datetime.date(MAX_YEAR, 12, 31)
91
+ else:
92
+ decade_end = _datetime.date(next_decade_year, 1, 1)
93
+
94
+ return self._get_business_days_cached(decade_start, decade_end)
95
+
96
+ def _get_business_days_cached(self, begdate: _datetime.date, enddate: _datetime.date) -> set:
97
+ """Internal method to load and cache business days by decade.
98
+ """
99
+ key = (begdate, enddate)
100
+ if key not in self._business_days_cache:
101
+ self._business_days_cache[key] = {
102
+ _date.Date.instance(d.date())
103
+ for d in self._mcal.valid_days(begdate, enddate)
104
+ }
105
+ return self._business_days_cache[key]
106
+
107
+ def business_hours(self, begdate: _datetime.date = None, enddate: _datetime.date = None) -> dict:
108
+ """Get market hours for a date range.
109
+ """
110
+ if begdate is None:
111
+ begdate = self.BEGDATE
112
+ if enddate is None:
113
+ enddate = self.ENDDATE
114
+
115
+ key = (begdate, enddate)
116
+ if key not in self._business_hours_cache:
117
+ df = self._mcal.schedule(begdate, enddate, tz=self._tz)
118
+ open_close = [
119
+ (_date.DateTime.instance(o.to_pydatetime()),
120
+ _date.DateTime.instance(c.to_pydatetime()))
121
+ for o, c in zip(df.market_open, df.market_close)
122
+ ]
123
+ self._business_hours_cache[key] = dict(zip(df.index.date, open_close))
124
+ return self._business_hours_cache[key]
125
+
126
+ def business_holidays(self, begdate: _datetime.date = None, enddate: _datetime.date = None) -> set:
127
+ """Get business holidays for a date range.
128
+ """
129
+ if begdate is None:
130
+ begdate = self.BEGDATE
131
+ if enddate is None:
132
+ enddate = self.ENDDATE
133
+
134
+ key = (begdate, enddate)
135
+ if key not in self._business_holidays_cache:
136
+ self._business_holidays_cache[key] = {
137
+ _date.Date.instance(d.date())
138
+ for d in map(pd.to_datetime, self._mcal.holidays().holidays)
139
+ if begdate <= d.date() <= enddate
140
+ }
141
+ return self._business_holidays_cache[key]
142
+
143
+ def _get_fast_calendar(self, decade_start: _datetime.date, decade_end: _datetime.date):
144
+ """Get a BusinessCalendar for O(1) business day operations.
145
+ """
146
+ key = (decade_start, decade_end)
147
+ if key not in self._fast_calendar_cache:
148
+ business_days = self._get_business_days_cached(decade_start, decade_end)
149
+ ordinals = sorted(d.toordinal() for d in business_days)
150
+ self._fast_calendar_cache[key] = _BusinessCalendar(ordinals)
151
+ return self._fast_calendar_cache[key]
152
+
153
+ def _get_calendar(self, date: _datetime.date):
154
+ """Get the business calendar covering the decade containing the given date.
155
+ """
156
+ bounds = _get_decade_bounds(date.year)
157
+ if bounds is None:
158
+ return None
159
+ return self._get_fast_calendar(*bounds)
160
+
161
+
162
+ class CustomCalendar(Calendar):
163
+ """User-defined calendar with custom holidays and hours.
164
+
165
+ Example:
166
+ holidays = {Date(2024, 12, 26), Date(2024, 12, 27)}
167
+ cal = CustomCalendar(
168
+ name='MyCompany',
169
+ holidays=holidays,
170
+ tz=Timezone('US/Eastern'),
171
+ )
172
+ d = Date(2024, 12, 25).calendar(cal).b.add(days=1)
173
+ """
174
+
175
+ def __init__(
176
+ self,
177
+ name: str = 'custom',
178
+ holidays: set[_datetime.date] | callable = None,
179
+ tz: _zoneinfo.ZoneInfo = UTC,
180
+ weekmask: str = 'Mon Tue Wed Thu Fri',
181
+ open_time: _datetime.time = _datetime.time(9, 30),
182
+ close_time: _datetime.time = _datetime.time(16, 0),
183
+ ):
184
+ self._name = name
185
+ self._holidays = holidays or set()
186
+ self._tz = tz
187
+ self._weekmask = weekmask
188
+ self._open_time = open_time
189
+ self._close_time = close_time
190
+ self._weekday_set = self._parse_weekmask(weekmask)
191
+ self._fast_calendar_cache: dict[tuple, object] = {}
192
+
193
+ def _parse_weekmask(self, weekmask: str) -> set[int]:
194
+ """Parse weekmask string into set of weekday numbers (0=Mon, 6=Sun).
195
+ """
196
+ day_map = {
197
+ 'mon': 0, 'tue': 1, 'wed': 2, 'thu': 3,
198
+ 'fri': 4, 'sat': 5, 'sun': 6,
199
+ }
200
+ return {day_map[d.lower()] for d in weekmask.split() if d.lower() in day_map}
201
+
202
+ @property
203
+ def name(self) -> str:
204
+ return self._name
205
+
206
+ @property
207
+ def tz(self) -> _zoneinfo.ZoneInfo:
208
+ return self._tz
209
+
210
+ def _get_holidays(self, begdate: _datetime.date, enddate: _datetime.date) -> set:
211
+ """Get holidays for the date range.
212
+ """
213
+ if callable(self._holidays):
214
+ return self._holidays(begdate, enddate)
215
+ return {h for h in self._holidays if begdate <= h <= enddate}
216
+
217
+ def business_days(self, begdate: _datetime.date = None, enddate: _datetime.date = None) -> set:
218
+ """Get business days for a date range.
219
+ """
220
+ if begdate is None:
221
+ begdate = _datetime.date(2000, 1, 1)
222
+ if enddate is None:
223
+ enddate = _datetime.date(2050, 1, 1)
224
+
225
+ holidays = self._get_holidays(begdate, enddate)
226
+ result = set()
227
+ current = begdate
228
+ while current <= enddate:
229
+ if current.weekday() in self._weekday_set and current not in holidays:
230
+ result.add(_date.Date.instance(current))
231
+ current += _datetime.timedelta(days=1)
232
+ return result
233
+
234
+ def business_hours(self, begdate: _datetime.date = None, enddate: _datetime.date = None) -> dict:
235
+ """Get market hours for a date range.
236
+ """
237
+ business_days = self.business_days(begdate, enddate)
238
+ result = {}
239
+ for d in business_days:
240
+ open_dt = _date.DateTime(
241
+ d.year, d.month, d.day,
242
+ self._open_time.hour, self._open_time.minute, self._open_time.second,
243
+ tzinfo=self._tz,
244
+ )
245
+ close_dt = _date.DateTime(
246
+ d.year, d.month, d.day,
247
+ self._close_time.hour, self._close_time.minute, self._close_time.second,
248
+ tzinfo=self._tz,
249
+ )
250
+ result[d] = (open_dt, close_dt)
251
+ return result
252
+
253
+ def business_holidays(self, begdate: _datetime.date = None, enddate: _datetime.date = None) -> set:
254
+ """Get business holidays for a date range.
255
+ """
256
+ if begdate is None:
257
+ begdate = _datetime.date(2000, 1, 1)
258
+ if enddate is None:
259
+ enddate = _datetime.date(2050, 1, 1)
260
+ return {_date.Date.instance(h) if not isinstance(h, _date.Date) else h
261
+ for h in self._get_holidays(begdate, enddate)}
262
+
263
+ def _get_calendar(self, date: _datetime.date):
264
+ """Get the business calendar for O(1) operations.
265
+ """
266
+ bounds = _get_decade_bounds(date.year)
267
+ if bounds is None:
268
+ return None
269
+ decade_start, decade_end = bounds
270
+ key = (decade_start, decade_end)
271
+ if key not in self._fast_calendar_cache:
272
+ business_days = self.business_days(decade_start, decade_end)
273
+ ordinals = sorted(d.toordinal() for d in business_days)
274
+ self._fast_calendar_cache[key] = _BusinessCalendar(ordinals)
275
+ return self._fast_calendar_cache[key]
276
+
277
+
278
+ _calendar_cache: dict[str, Calendar] = {}
279
+ _default_calendar: str = 'NYSE'
280
+
281
+
282
+ def get_default_calendar() -> str:
283
+ """Get the default calendar name used when no calendar is specified.
284
+
285
+ Returns
286
+ Current default calendar name (initially 'NYSE')
287
+ """
288
+ return _default_calendar
289
+
290
+
291
+ def set_default_calendar(name: str) -> None:
292
+ """Set the default calendar used when no calendar is specified.
293
+
294
+ Parameters
295
+ name: Calendar name (e.g., 'NYSE', 'LSE', or a registered custom name)
296
+
297
+ Raises
298
+ ValueError: If calendar name is not recognized
299
+ """
300
+ global _default_calendar
301
+ get_calendar(name)
302
+ _default_calendar = name.upper()
303
+
304
+
305
+ def get_calendar(name: str) -> Calendar:
306
+ """Get or create a calendar instance by name.
307
+
308
+ Parameters
309
+ name: Exchange name (e.g., 'NYSE', 'LSE') or registered custom name
310
+
311
+ Returns
312
+ Calendar instance
313
+
314
+ Raises
315
+ ValueError: If calendar name is not recognized
316
+ """
317
+ name_upper = name.upper()
318
+
319
+ if name_upper in _calendar_cache:
320
+ return _calendar_cache[name_upper]
321
+
322
+ valid_names = set(mcal.get_calendar_names())
323
+ if name_upper in valid_names or name in valid_names:
324
+ cal = ExchangeCalendar(name)
325
+ _calendar_cache[name_upper] = cal
326
+ return cal
327
+
328
+ raise ValueError(
329
+ f'Unknown calendar: {name}. '
330
+ f'Use available_calendars() to see valid options.'
331
+ )
332
+
333
+
334
+ def available_calendars() -> list[str]:
335
+ """List all available exchange calendar names.
336
+
337
+ Returns
338
+ Sorted list of calendar names (e.g., ['NYSE', 'LSE', 'NASDAQ', ...])
339
+ """
340
+ return sorted(mcal.get_calendar_names())
341
+
342
+
343
+ def register_calendar(name: str, calendar: Calendar) -> None:
344
+ """Register a custom calendar for use by name.
345
+
346
+ Parameters
347
+ name: Name to register the calendar under
348
+ calendar: Calendar instance
349
+ """
350
+ _calendar_cache[name.upper()] = calendar
opendate/constants.py ADDED
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import re
5
+ import zoneinfo as _zoneinfo
6
+
7
+ import pendulum as _pendulum
8
+
9
+ _IS_WINDOWS = os.name == 'nt'
10
+
11
+ MIN_YEAR = 1900
12
+ MAX_YEAR = 2100
13
+
14
+
15
+ def Timezone(name: str = 'US/Eastern') -> _zoneinfo.ZoneInfo:
16
+ """Create a timezone object with the specified name.
17
+
18
+ Simple wrapper around Pendulum's Timezone function that ensures
19
+ consistent timezone handling across the library. Note that 'US/Eastern'
20
+ is equivalent to 'America/New_York' for all dates.
21
+ """
22
+ return _pendulum.tz.Timezone(name)
23
+
24
+
25
+ UTC = Timezone('UTC')
26
+ GMT = Timezone('GMT')
27
+ EST = Timezone('US/Eastern')
28
+ LCL = _pendulum.tz.Timezone(_pendulum.tz.get_local_timezone().name)
29
+
30
+ WeekDay = _pendulum.day.WeekDay
31
+
32
+ WEEKDAY_SHORTNAME = {
33
+ 'MO': WeekDay.MONDAY,
34
+ 'TU': WeekDay.TUESDAY,
35
+ 'WE': WeekDay.WEDNESDAY,
36
+ 'TH': WeekDay.THURSDAY,
37
+ 'FR': WeekDay.FRIDAY,
38
+ 'SA': WeekDay.SATURDAY,
39
+ 'SU': WeekDay.SUNDAY
40
+ }
41
+
42
+
43
+ MONTH_SHORTNAME = {
44
+ 'jan': 1,
45
+ 'feb': 2,
46
+ 'mar': 3,
47
+ 'apr': 4,
48
+ 'may': 5,
49
+ 'jun': 6,
50
+ 'jul': 7,
51
+ 'aug': 8,
52
+ 'sep': 9,
53
+ 'oct': 10,
54
+ 'nov': 11,
55
+ 'dec': 12,
56
+ }
57
+
58
+ DATEMATCH = re.compile(r'^(?P<d>N|T|Y|P|M)(?P<n>[-+]?\d+)?(?P<b>b?)?$')