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.
@@ -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,17 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.egg-info/
5
+ *.egg
6
+ dist/
7
+ build/
8
+ wheels/
9
+ .eggs/
10
+ *.so
11
+ .venv/
12
+ .env
13
+ .pytest_cache/
14
+ .mypy_cache/
15
+ .ruff_cache/
16
+ htmlcov/
17
+ .coverage
@@ -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.
@@ -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