dateflow 0.1.1__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.
- dateflow-0.1.1/.github/workflows/ci.yml +29 -0
- dateflow-0.1.1/.github/workflows/publish.yml +40 -0
- dateflow-0.1.1/.gitignore +17 -0
- dateflow-0.1.1/CHANGELOG.md +53 -0
- dateflow-0.1.1/LICENSE +21 -0
- dateflow-0.1.1/PKG-INFO +412 -0
- dateflow-0.1.1/PLAN.md +255 -0
- dateflow-0.1.1/README.md +389 -0
- dateflow-0.1.1/pyproject.toml +35 -0
- dateflow-0.1.1/src/dateflow/__init__.py +75 -0
- dateflow-0.1.1/src/dateflow/_utils.py +1 -0
- dateflow-0.1.1/src/dateflow/easter.py +60 -0
- dateflow-0.1.1/src/dateflow/parser.py +875 -0
- dateflow-0.1.1/src/dateflow/py.typed +0 -0
- dateflow-0.1.1/src/dateflow/relativedelta.py +507 -0
- dateflow-0.1.1/src/dateflow/rrule.py +1045 -0
- dateflow-0.1.1/src/dateflow/tz.py +337 -0
- dateflow-0.1.1/tests/__init__.py +0 -0
- dateflow-0.1.1/tests/test_compat.py +397 -0
- dateflow-0.1.1/tests/test_easter.py +147 -0
- dateflow-0.1.1/tests/test_parser.py +699 -0
- dateflow-0.1.1/tests/test_relativedelta.py +564 -0
- dateflow-0.1.1/tests/test_rrule.py +731 -0
- dateflow-0.1.1/tests/test_tz.py +389 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ['3.9', '3.10', '3.11', '3.12']
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: ${{ matrix.python-version }}
|
|
21
|
+
|
|
22
|
+
- name: Install dependencies
|
|
23
|
+
run: pip install pytest
|
|
24
|
+
|
|
25
|
+
- name: Install package
|
|
26
|
+
run: pip install -e .
|
|
27
|
+
|
|
28
|
+
- name: Run tests
|
|
29
|
+
run: pytest tests/ -v --tb=short
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
|
|
13
|
+
- uses: actions/setup-python@v5
|
|
14
|
+
with:
|
|
15
|
+
python-version: '3.x'
|
|
16
|
+
|
|
17
|
+
- run: pip install build
|
|
18
|
+
|
|
19
|
+
- run: python -m build
|
|
20
|
+
|
|
21
|
+
- uses: actions/upload-artifact@v4
|
|
22
|
+
with:
|
|
23
|
+
name: dist
|
|
24
|
+
path: dist/
|
|
25
|
+
|
|
26
|
+
publish:
|
|
27
|
+
needs: build
|
|
28
|
+
runs-on: ubuntu-latest
|
|
29
|
+
environment: pypi
|
|
30
|
+
permissions:
|
|
31
|
+
id-token: write
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/download-artifact@v4
|
|
34
|
+
with:
|
|
35
|
+
name: dist
|
|
36
|
+
path: dist/
|
|
37
|
+
|
|
38
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
39
|
+
with:
|
|
40
|
+
attestations: true
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.1] - 2026-03-14
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- CI publish workflow: corrected PyPI Trusted Publisher environment name from `release` to `pypi`
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **Compatibility test suite** (`tests/test_compat.py`) — 54 tests verifying drop-in replacement behavior against python-dateutil API surface across all 5 modules
|
|
17
|
+
- **GitHub Actions CI** (`.github/workflows/ci.yml`) — automated test runs on Python 3.9–3.12 matrix
|
|
18
|
+
|
|
19
|
+
## [0.1.0] - 2026-03-12
|
|
20
|
+
|
|
21
|
+
Initial release of **dateflow** — a zero-dependency, modern Python replacement for python-dateutil. Supports Python 3.9+ with full type annotations and no runtime dependencies.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- **`relativedelta.py`** — month/year-aware date arithmetic with weekday targeting, nth-weekday support, and delta-between-dates computation. Drop-in replacement for `dateutil.relativedelta`.
|
|
26
|
+
- **`easter.py`** — Western (Gregorian), Orthodox, and Julian Easter date computation. Replaces `dateutil.easter`.
|
|
27
|
+
- **`tz.py`** — timezone utilities built on stdlib `zoneinfo`: `gettz`, `UTC`, `tzoffset`, `tzlocal`, `tzutc`, fixed-offset zones, `resolve_imaginary`, `enfold`. Replaces `dateutil.tz` without pytz dependency.
|
|
28
|
+
- **`parser.py`** — fuzzy and strict date/time string parsing: `parse` (natural-language-aware, fuzzy, token-based) and `isoparse` (full ISO 8601 / RFC 3339). Replaces `dateutil.parser`.
|
|
29
|
+
- **`rrule.py`** — RFC 5545 (iCalendar) recurrence rule engine: `rrule`, `rruleset`, `rrulestr`. All 7 frequencies (YEARLY–SECONDLY), all BYXXX modifiers, byeaster, bysetpos, nth-weekday occurrences, negative monthday indexing. Replaces `dateutil.rrule`.
|
|
30
|
+
- **`__init__.py`** — unified public API re-exporting all modules for `from dateflow import parse, relativedelta, rrule, ...` usage.
|
|
31
|
+
- **Zero dependencies** — pure Python, no runtime requirements beyond stdlib.
|
|
32
|
+
- **Python 3.9+** — leverages `zoneinfo` (stdlib), no `six` or Python 2 compatibility shims.
|
|
33
|
+
- **Full type annotations** — complete type hints throughout, `py.typed` marker included.
|
|
34
|
+
- **364 tests** — covering all 5 modules with extensive edge case coverage.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- `relativedelta`: incorrect microsecond negation and leapdays logic in `_compute_delta`
|
|
39
|
+
- `relativedelta`: truncation toward zero in day decomposition
|
|
40
|
+
- `relativedelta`: clip day after absolute year change to prevent leap year `ValueError`
|
|
41
|
+
- `relativedelta`: date/datetime mixed delta and `__init__` export issues
|
|
42
|
+
- `tz`: `tzlocal` crash on extreme datetimes like `datetime.min` (#106)
|
|
43
|
+
- `tz`: optional `tz` parameter added to `datetime_exists` and `datetime_ambiguous` (#105)
|
|
44
|
+
- `tz`: `tzutc`/`tzoffset` `__hash__` correctness to satisfy Python hash invariant (#104)
|
|
45
|
+
- `parser`: greedy timezone offset consumption swallowing year numbers — range validation on `_parse_numeric_offset`
|
|
46
|
+
- `parser`: bare number + AM/PM not recognized as time (e.g., `3pm`, `11am`)
|
|
47
|
+
- `parser`: hyphenated month-name date formats (`15-Jan-2024`, `Jan-15-2024`, `2024-Jan-15`)
|
|
48
|
+
- `parser`: default datetime timezone not propagated to result when parsed string has none
|
|
49
|
+
- `parser`: dotted `a.m.`/`p.m.` variants not recognized — fixed by merging split tokens
|
|
50
|
+
- `rrule`: infinite loop on impossible BYXXX date combinations (e.g., Feb 30) — `max_iter` now counts loop iterations, not yields
|
|
51
|
+
- `rrule`: BYSETPOS not applied in daily, hourly, minutely, secondly, and byeaster iterators
|
|
52
|
+
- `rrule`: timezone offset validation tightened (max ±14h) to prevent year numbers being parsed as offsets
|
|
53
|
+
- `rrule`: ordinal day 366 incorrectly accepted on non-leap years in `isoparse`
|
dateflow-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Agentine
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
dateflow-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dateflow
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: A zero-dependency, modern Python replacement for python-dateutil
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: date,datetime,dateutil,easter,parser,relativedelta,rrule,time,timezone
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# dateflow
|
|
25
|
+
|
|
26
|
+
A zero-dependency, modern Python replacement for python-dateutil.
|
|
27
|
+
|
|
28
|
+
## Why dateflow?
|
|
29
|
+
|
|
30
|
+
`python-dateutil` is downloaded 900M+ times per month but has a single active maintainer, no release in over two years, and a legacy dependency on `six` — a Python 2 compatibility shim — six years after Python 2 reached end-of-life.
|
|
31
|
+
|
|
32
|
+
`dateflow` replaces it entirely:
|
|
33
|
+
|
|
34
|
+
- **Zero dependencies.** No `six`, no `pytz`, nothing. Pure Python using only the standard library.
|
|
35
|
+
- **Python 3.9+ only.** Drops all Python 2 baggage. Uses `zoneinfo` (stdlib), improved `fromisoformat`, and full type annotations.
|
|
36
|
+
- **Drop-in replacement.** `s/dateutil/dateflow/g` works for 90%+ of use cases.
|
|
37
|
+
- **Actively maintained.** Exists precisely because the ecosystem needs a maintained alternative.
|
|
38
|
+
- **MIT licensed.** Same as python-dateutil.
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
pip install dateflow
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Requires Python 3.9 or later. No other dependencies.
|
|
47
|
+
|
|
48
|
+
## Quick Start
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from datetime import datetime
|
|
52
|
+
from dateflow import parse, isoparse, relativedelta, easter
|
|
53
|
+
from dateflow import rrule, DAILY, MO, FR
|
|
54
|
+
|
|
55
|
+
# Parse a date string
|
|
56
|
+
parse("March 11, 2026")
|
|
57
|
+
# datetime(2026, 3, 11, 0, 0)
|
|
58
|
+
|
|
59
|
+
# Month-aware date arithmetic
|
|
60
|
+
datetime(2026, 1, 31) + relativedelta(months=1)
|
|
61
|
+
# datetime(2026, 2, 28)
|
|
62
|
+
|
|
63
|
+
# Recurrence rules — first 5 weekdays of March 2026
|
|
64
|
+
list(rrule(DAILY, count=5, byweekday=[MO, FR], dtstart=datetime(2026, 3, 1)))
|
|
65
|
+
|
|
66
|
+
# Easter
|
|
67
|
+
easter(2026)
|
|
68
|
+
# date(2026, 4, 5)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Usage
|
|
72
|
+
|
|
73
|
+
### Parser
|
|
74
|
+
|
|
75
|
+
The parser handles fuzzy natural-language dates, ambiguous formats, and strict ISO 8601.
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from dateflow import parse, isoparse
|
|
79
|
+
from dateflow.parser import ParserError
|
|
80
|
+
from datetime import datetime
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**`parse()`** — fuzzy date string parsing:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
parse("March 11, 2026")
|
|
87
|
+
# datetime(2026, 3, 11, 0, 0)
|
|
88
|
+
|
|
89
|
+
parse("11/03/2026")
|
|
90
|
+
# datetime(2026, 11, 3, 0, 0)
|
|
91
|
+
|
|
92
|
+
parse("11/03/2026", dayfirst=True)
|
|
93
|
+
# datetime(2026, 3, 11, 0, 0)
|
|
94
|
+
|
|
95
|
+
parse("2026-03-11T14:30:00+05:00")
|
|
96
|
+
# datetime(2026, 3, 11, 14, 30, tzinfo=tzoffset(None, 18000))
|
|
97
|
+
|
|
98
|
+
# Fill missing fields from a default datetime
|
|
99
|
+
parse("March 2026", default=datetime(2026, 1, 1))
|
|
100
|
+
# datetime(2026, 3, 1, 0, 0)
|
|
101
|
+
|
|
102
|
+
# Ignore surrounding text with fuzzy=True
|
|
103
|
+
parse("The meeting is on March 11, 2026 at 3pm", fuzzy=True)
|
|
104
|
+
# datetime(2026, 3, 11, 15, 0)
|
|
105
|
+
|
|
106
|
+
# Get back the non-date tokens with fuzzy_with_tokens=True
|
|
107
|
+
parse("The meeting is on March 11, 2026 at 3pm", fuzzy_with_tokens=True)
|
|
108
|
+
# (datetime(2026, 3, 11, 15, 0), ("The meeting is on ", " at ", ""))
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**`isoparse()`** — strict ISO 8601 / RFC 3339 parsing:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
isoparse("2026-03-11")
|
|
115
|
+
# date(2026, 3, 11)
|
|
116
|
+
|
|
117
|
+
isoparse("2026-03-11T14:30:00Z")
|
|
118
|
+
# datetime(2026, 3, 11, 14, 30, tzinfo=UTC)
|
|
119
|
+
|
|
120
|
+
isoparse("2026-03-11T14:30:00+05:00")
|
|
121
|
+
# datetime(2026, 3, 11, 14, 30, tzinfo=tzoffset(None, 18000))
|
|
122
|
+
|
|
123
|
+
isoparse("2026-W11")
|
|
124
|
+
# date(2026, 3, 9) — ISO week date, Monday of week 11
|
|
125
|
+
|
|
126
|
+
isoparse("2026-070")
|
|
127
|
+
# date(2026, 3, 11) — ISO ordinal date, day 70 of 2026
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**`ParserError`** — raised when a string cannot be parsed:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
try:
|
|
134
|
+
parse("not a date")
|
|
135
|
+
except ParserError as e:
|
|
136
|
+
print(e)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
### RelativeDelta
|
|
142
|
+
|
|
143
|
+
Month- and year-aware date arithmetic that handles edge cases like month-end clipping.
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from dateflow import relativedelta, MO, TU, WE, TH, FR, SA, SU
|
|
147
|
+
from datetime import datetime
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Month arithmetic:**
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
dt = datetime(2026, 1, 31)
|
|
154
|
+
|
|
155
|
+
dt + relativedelta(months=1)
|
|
156
|
+
# datetime(2026, 2, 28) — clips to last day of February
|
|
157
|
+
|
|
158
|
+
dt + relativedelta(months=2)
|
|
159
|
+
# datetime(2026, 3, 31)
|
|
160
|
+
|
|
161
|
+
dt + relativedelta(years=1, months=2, days=3, hours=4)
|
|
162
|
+
# datetime(2027, 4, 3, 4, 0)
|
|
163
|
+
|
|
164
|
+
dt - relativedelta(months=3)
|
|
165
|
+
# datetime(2025, 10, 31)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**Absolute field overrides** (lowercase = absolute, uppercase = relative):
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
# Set the day to 15 after adding a month
|
|
172
|
+
datetime(2026, 1, 31) + relativedelta(months=1, day=15)
|
|
173
|
+
# datetime(2026, 2, 15)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Weekday targeting:**
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
dt = datetime(2026, 3, 11) # Wednesday
|
|
180
|
+
|
|
181
|
+
dt + relativedelta(weekday=FR)
|
|
182
|
+
# datetime(2026, 3, 13) — next Friday
|
|
183
|
+
|
|
184
|
+
dt + relativedelta(weekday=FR(2))
|
|
185
|
+
# datetime(2026, 3, 20) — second Friday from now
|
|
186
|
+
|
|
187
|
+
dt + relativedelta(weekday=MO(-1))
|
|
188
|
+
# datetime(2026, 3, 9) — previous Monday
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**Delta between two dates:**
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
relativedelta(datetime(2026, 3, 11), datetime(2025, 1, 1))
|
|
195
|
+
# relativedelta(years=+1, months=+2, days=+10)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
### Recurrence Rules
|
|
201
|
+
|
|
202
|
+
RFC 5545 (iCalendar) recurrence rule engine with full `BYXXX` support.
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
from dateflow import rrule, rruleset, rrulestr
|
|
206
|
+
from dateflow import YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, SECONDLY
|
|
207
|
+
from dateflow import MO, TU, WE, TH, FR, SA, SU
|
|
208
|
+
from datetime import datetime
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**`rrule()`** — generate recurring dates:
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
# Every day for 5 days
|
|
215
|
+
list(rrule(DAILY, count=5, dtstart=datetime(2026, 3, 11)))
|
|
216
|
+
# [datetime(2026, 3, 11), datetime(2026, 3, 12), ..., datetime(2026, 3, 15)]
|
|
217
|
+
|
|
218
|
+
# Every weekday (Mon–Fri) for 4 occurrences
|
|
219
|
+
list(rrule(WEEKLY, count=4, byweekday=[MO, TU, WE, TH, FR], dtstart=datetime(2026, 3, 11)))
|
|
220
|
+
|
|
221
|
+
# Monthly on the last Friday
|
|
222
|
+
list(rrule(MONTHLY, count=3, byweekday=FR(-1), dtstart=datetime(2026, 1, 1)))
|
|
223
|
+
# Last Friday of January, February, and March 2026
|
|
224
|
+
|
|
225
|
+
# Every year on March 11 until end of 2030
|
|
226
|
+
list(rrule(YEARLY, until=datetime(2030, 12, 31), dtstart=datetime(2026, 3, 11)))
|
|
227
|
+
|
|
228
|
+
# Every hour, 6 times
|
|
229
|
+
list(rrule(HOURLY, count=6, dtstart=datetime(2026, 3, 11, 9, 0)))
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
**`rruleset()`** — combine rules with inclusions and exclusions:
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
rs = rruleset()
|
|
236
|
+
|
|
237
|
+
# Base rule: every day in March 2026
|
|
238
|
+
rs.rrule(rrule(DAILY, count=31, dtstart=datetime(2026, 3, 1)))
|
|
239
|
+
|
|
240
|
+
# Exclude a specific date
|
|
241
|
+
rs.exdate(datetime(2026, 3, 15))
|
|
242
|
+
|
|
243
|
+
# Exclude all Saturdays
|
|
244
|
+
rs.exrule(rrule(WEEKLY, byweekday=SA, dtstart=datetime(2026, 3, 1)))
|
|
245
|
+
|
|
246
|
+
# Include a date that falls outside the base rule
|
|
247
|
+
rs.rdate(datetime(2026, 4, 1))
|
|
248
|
+
|
|
249
|
+
list(rs)[:5]
|
|
250
|
+
# [datetime(2026, 3, 1), datetime(2026, 3, 2), datetime(2026, 3, 3), ...]
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**`rrulestr()`** — parse iCalendar RRULE strings:
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
# Single rule
|
|
257
|
+
rule = rrulestr("RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=10",
|
|
258
|
+
dtstart=datetime(2026, 3, 11))
|
|
259
|
+
|
|
260
|
+
# Full VEVENT block
|
|
261
|
+
rule = rrulestr("""
|
|
262
|
+
DTSTART:20260311T090000
|
|
263
|
+
RRULE:FREQ=MONTHLY;BYDAY=-1FR;COUNT=6
|
|
264
|
+
EXDATE:20260424T090000
|
|
265
|
+
""")
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
### Easter
|
|
271
|
+
|
|
272
|
+
Compute Easter Sunday for any year using Western (Gregorian), Orthodox, or Julian methods.
|
|
273
|
+
|
|
274
|
+
```python
|
|
275
|
+
from dateflow import easter
|
|
276
|
+
from dateflow.easter import EASTER_ORTHODOX, EASTER_JULIAN
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
```python
|
|
280
|
+
easter(2026)
|
|
281
|
+
# date(2026, 4, 5) — Western/Gregorian (default)
|
|
282
|
+
|
|
283
|
+
easter(2026, method=EASTER_ORTHODOX)
|
|
284
|
+
# date(2026, 4, 19) — Orthodox Easter (Gregorian calendar output)
|
|
285
|
+
|
|
286
|
+
easter(2026, method=EASTER_JULIAN)
|
|
287
|
+
# date(2026, 4, 6) — Julian calendar Easter (Julian calendar output)
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
### Timezone Utilities
|
|
293
|
+
|
|
294
|
+
Built on stdlib `zoneinfo`. No `pytz` required.
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
from dateflow import tz
|
|
298
|
+
from datetime import datetime
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
**`gettz()`** — resolve a timezone by name:
|
|
302
|
+
|
|
303
|
+
```python
|
|
304
|
+
eastern = tz.gettz("America/New_York")
|
|
305
|
+
tokyo = tz.gettz("Asia/Tokyo")
|
|
306
|
+
|
|
307
|
+
dt = datetime(2026, 3, 11, 12, 0, tzinfo=eastern)
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
**`UTC`** — the UTC timezone constant:
|
|
311
|
+
|
|
312
|
+
```python
|
|
313
|
+
dt = datetime(2026, 3, 11, 12, 0, tzinfo=tz.UTC)
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
**`tzoffset()`** — fixed UTC offset:
|
|
317
|
+
|
|
318
|
+
```python
|
|
319
|
+
plus5 = tz.tzoffset("IST", 5.5 * 3600)
|
|
320
|
+
minus5 = tz.tzoffset("EST", -5 * 3600)
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
**`tzlocal()`** — local system timezone:
|
|
324
|
+
|
|
325
|
+
```python
|
|
326
|
+
local = tz.tzlocal()
|
|
327
|
+
dt = datetime.now(tz=local)
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
**`tzutc()`** — UTC timezone instance (equivalent to `tz.UTC`):
|
|
331
|
+
|
|
332
|
+
```python
|
|
333
|
+
utc = tz.tzutc()
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
**DST transition helpers:**
|
|
337
|
+
|
|
338
|
+
```python
|
|
339
|
+
# enfold — set the fold flag on an ambiguous datetime (fall-back transition)
|
|
340
|
+
# fold=1 means the second occurrence (post-DST)
|
|
341
|
+
dt_ambiguous = tz.enfold(datetime(2026, 11, 1, 1, 30, tzinfo=eastern), fold=1)
|
|
342
|
+
|
|
343
|
+
# resolve_imaginary — shift a non-existent time forward past a spring-forward gap
|
|
344
|
+
dt_nonexistent = datetime(2026, 3, 8, 2, 30, tzinfo=eastern) # doesn't exist
|
|
345
|
+
resolved = tz.resolve_imaginary(dt_nonexistent)
|
|
346
|
+
# datetime(2026, 3, 8, 3, 30, tzinfo=eastern)
|
|
347
|
+
|
|
348
|
+
# datetime_exists — check whether a local time actually exists
|
|
349
|
+
tz.datetime_exists(datetime(2026, 3, 8, 2, 30, tzinfo=eastern))
|
|
350
|
+
# False (skipped by DST spring-forward)
|
|
351
|
+
|
|
352
|
+
# datetime_ambiguous — check whether a local time occurs twice
|
|
353
|
+
tz.datetime_ambiguous(datetime(2026, 11, 1, 1, 30, tzinfo=eastern))
|
|
354
|
+
# True (occurs in both EDT and EST)
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## Migration from python-dateutil
|
|
360
|
+
|
|
361
|
+
For most projects, migration is a one-liner:
|
|
362
|
+
|
|
363
|
+
```
|
|
364
|
+
s/dateutil/dateflow/g
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
This works for 90%+ of use cases. Here is the full mapping:
|
|
368
|
+
|
|
369
|
+
### Import changes
|
|
370
|
+
|
|
371
|
+
| python-dateutil | dateflow |
|
|
372
|
+
|---|---|
|
|
373
|
+
| `from dateutil.parser import parse` | `from dateflow import parse` |
|
|
374
|
+
| `from dateutil.parser import isoparse` | `from dateflow import isoparse` |
|
|
375
|
+
| `from dateutil.parser import ParserError` | `from dateflow.parser import ParserError` |
|
|
376
|
+
| `from dateutil.relativedelta import relativedelta` | `from dateflow import relativedelta` |
|
|
377
|
+
| `from dateutil.relativedelta import MO, FR, ...` | `from dateflow import MO, FR, ...` |
|
|
378
|
+
| `from dateutil.rrule import rrule, rruleset, rrulestr` | `from dateflow import rrule, rruleset, rrulestr` |
|
|
379
|
+
| `from dateutil.rrule import DAILY, WEEKLY, ...` | `from dateflow import DAILY, WEEKLY, ...` |
|
|
380
|
+
| `from dateutil.easter import easter` | `from dateflow import easter` |
|
|
381
|
+
| `from dateutil.easter import EASTER_ORTHODOX` | `from dateflow.easter import EASTER_ORTHODOX` |
|
|
382
|
+
| `from dateutil import tz` | `from dateflow import tz` |
|
|
383
|
+
| `from dateutil.tz import gettz` | `from dateflow.tz import gettz` |
|
|
384
|
+
| `from dateutil.tz import UTC, tzutc, tzlocal, tzoffset` | `from dateflow.tz import UTC, tzutc, tzlocal, tzoffset` |
|
|
385
|
+
|
|
386
|
+
### No pytz needed
|
|
387
|
+
|
|
388
|
+
`dateflow` uses Python's built-in `zoneinfo` module (available since Python 3.9). If you were using `pytz` only because `dateutil` led you there, you can remove it:
|
|
389
|
+
|
|
390
|
+
```python
|
|
391
|
+
# Before
|
|
392
|
+
import pytz
|
|
393
|
+
eastern = pytz.timezone("America/New_York")
|
|
394
|
+
|
|
395
|
+
# After
|
|
396
|
+
from dateflow import tz
|
|
397
|
+
eastern = tz.gettz("America/New_York")
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### Drop `six` from your dependencies
|
|
401
|
+
|
|
402
|
+
If your project listed `six` because `python-dateutil` required it transitively, remove it. `dateflow` has zero dependencies and does not use `six`.
|
|
403
|
+
|
|
404
|
+
### Behavioral notes
|
|
405
|
+
|
|
406
|
+
- `parse()`, `isoparse()`, `relativedelta`, `rrule`, `rruleset`, `rrulestr`, and `easter()` are API-compatible with their `dateutil` counterparts.
|
|
407
|
+
- `tz.gettz()` returns a `zoneinfo.ZoneInfo` instance (or a `dateflow` shim for fixed offsets and local time) rather than a `dateutil.tz` object. These are fully compatible with `datetime` operations.
|
|
408
|
+
- `tzinfo` objects returned by `dateflow.tz` implement the standard `datetime.tzinfo` interface. Any code that calls `.utcoffset()`, `.dst()`, or `.tzname()` will work unchanged.
|
|
409
|
+
|
|
410
|
+
## License
|
|
411
|
+
|
|
412
|
+
MIT
|