python-dateutil-rs 0.1.4__tar.gz → 0.1.5__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.
- {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/Cargo.lock +2 -2
- {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/Cargo.toml +1 -1
- python_dateutil_rs-0.1.5/PKG-INFO +281 -0
- python_dateutil_rs-0.1.5/README.md +258 -0
- {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/benches/benchmarks.rs +105 -13
- {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/src/error.rs +2 -0
- python_dateutil_rs-0.1.5/crates/dateutil-core/src/parser/compact.rs +169 -0
- python_dateutil_rs-0.1.5/crates/dateutil-core/src/parser/hms.rs +118 -0
- python_dateutil_rs-0.1.5/crates/dateutil-core/src/parser/isoparser.rs +750 -0
- {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/src/parser/parserinfo.rs +153 -53
- {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/src/parser.rs +694 -324
- {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/src/rrule/iter.rs +79 -30
- {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/src/rrule/parse.rs +24 -24
- {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/src/rrule/set.rs +15 -12
- {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/src/rrule.rs +177 -121
- {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/src/tz/file.rs +50 -27
- {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/src/tz/local.rs +50 -10
- {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/src/tz.rs +24 -3
- python_dateutil_rs-0.1.5/crates/dateutil-py/src/py/parser.rs +581 -0
- {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-py/src/py/relativedelta.rs +117 -36
- {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-py/src/py/rrule.rs +45 -39
- {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-py/src/py/tz.rs +120 -48
- {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/pyproject.toml +12 -3
- python_dateutil_rs-0.1.5/python/dateutil/__init__.py +114 -0
- {python_dateutil_rs-0.1.4/python/dateutil_rs → python_dateutil_rs-0.1.5/python/dateutil}/_native.pyi +43 -17
- {python_dateutil_rs-0.1.4/python/dateutil_rs → python_dateutil_rs-0.1.5/python/dateutil}/easter.py +2 -2
- python_dateutil_rs-0.1.5/python/dateutil/parser.py +37 -0
- python_dateutil_rs-0.1.5/python/dateutil/relativedelta.py +5 -0
- {python_dateutil_rs-0.1.4/python/dateutil_rs → python_dateutil_rs-0.1.5/python/dateutil}/rrule.py +16 -2
- python_dateutil_rs-0.1.5/python/dateutil/tz.py +114 -0
- python_dateutil_rs-0.1.5/python/dateutil/utils.py +71 -0
- python_dateutil_rs-0.1.5/python/dateutil/zoneinfo/__init__.py +62 -0
- python_dateutil_rs-0.1.4/PKG-INFO +0 -223
- python_dateutil_rs-0.1.4/README.md +0 -200
- python_dateutil_rs-0.1.4/crates/dateutil-core/src/parser/isoparser.rs +0 -622
- python_dateutil_rs-0.1.4/crates/dateutil-py/src/py/parser.rs +0 -334
- python_dateutil_rs-0.1.4/python/dateutil_rs/__init__.py +0 -77
- python_dateutil_rs-0.1.4/python/dateutil_rs/parser.py +0 -18
- python_dateutil_rs-0.1.4/python/dateutil_rs/relativedelta.py +0 -5
- python_dateutil_rs-0.1.4/python/dateutil_rs/tz.py +0 -23
- {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/LICENSE +0 -0
- {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/CLAUDE.md +0 -0
- {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/Cargo.toml +0 -0
- {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/README.md +0 -0
- {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/src/common.rs +0 -0
- {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/src/easter.rs +0 -0
- {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/src/lib.rs +0 -0
- {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/src/parser/tokenizer.rs +0 -0
- {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/src/relativedelta.rs +0 -0
- {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/src/tz/offset.rs +0 -0
- {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/src/tz/utc.rs +0 -0
- {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-py/Cargo.toml +0 -0
- {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-py/src/lib.rs +0 -0
- {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-py/src/py/common.rs +0 -0
- {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-py/src/py/conv.rs +0 -0
- {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-py/src/py/easter.rs +0 -0
- {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-py/src/py.rs +0 -0
- {python_dateutil_rs-0.1.4/python/dateutil_rs → python_dateutil_rs-0.1.5/python/dateutil}/py.typed +0 -0
|
@@ -220,7 +220,7 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
|
|
220
220
|
|
|
221
221
|
[[package]]
|
|
222
222
|
name = "dateutil"
|
|
223
|
-
version = "0.1.
|
|
223
|
+
version = "0.1.5"
|
|
224
224
|
dependencies = [
|
|
225
225
|
"bitflags",
|
|
226
226
|
"chrono",
|
|
@@ -234,7 +234,7 @@ dependencies = [
|
|
|
234
234
|
|
|
235
235
|
[[package]]
|
|
236
236
|
name = "dateutil-py"
|
|
237
|
-
version = "0.1.
|
|
237
|
+
version = "0.1.5"
|
|
238
238
|
dependencies = [
|
|
239
239
|
"chrono",
|
|
240
240
|
"dateutil",
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-dateutil-rs
|
|
3
|
+
Version: 0.1.5
|
|
4
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
5
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
6
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
9
|
+
Requires-Dist: pytest>=9.0 ; extra == 'dev'
|
|
10
|
+
Requires-Dist: pytest-cov>=7.1 ; extra == 'dev'
|
|
11
|
+
Requires-Dist: pytest-benchmark>=5.2 ; extra == 'dev'
|
|
12
|
+
Requires-Dist: freezegun>=1.5 ; extra == 'dev'
|
|
13
|
+
Requires-Dist: hypothesis>=6.151 ; extra == 'dev'
|
|
14
|
+
Requires-Dist: maturin>=1.13 ; extra == 'dev'
|
|
15
|
+
Requires-Dist: tzdata>=2024.1 ; sys_platform == 'win32' and extra == 'dev'
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Summary: A Rust-backed port of python-dateutil
|
|
19
|
+
License-Expression: MIT
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
22
|
+
|
|
23
|
+
# python-dateutil-rs
|
|
24
|
+
|
|
25
|
+
[](https://pypi.org/project/python-dateutil-rs/)
|
|
26
|
+
[](https://pypi.org/project/python-dateutil-rs/)
|
|
27
|
+
[](https://pypi.org/project/python-dateutil-rs/)
|
|
28
|
+
[](https://github.com/wakita181009/dateutil-rs/actions/workflows/ci.yml)
|
|
29
|
+
[](https://codecov.io/gh/wakita181009/dateutil-rs)
|
|
30
|
+
|
|
31
|
+
A high-performance, **drop-in replacement** for [python-dateutil](https://github.com/dateutil/dateutil) (v2.9.0), powered by Rust.
|
|
32
|
+
|
|
33
|
+
> **Drop-in compatible:** Install `python-dateutil-rs` and your existing `from dateutil.parser import parse`, `from dateutil.tz import tzutc`, etc. continue to work — no code changes required, just **2x–897x faster**.
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
- **True drop-in replacement** — provides `dateutil` package with the same submodule structure (`dateutil.parser`, `dateutil.tz`, `dateutil.relativedelta`, `dateutil.rrule`, `dateutil.easter`)
|
|
38
|
+
- **Zero code changes** — existing imports like `from dateutil.parser import parse` work as-is
|
|
39
|
+
- **Rust-accelerated:** all core modules rewritten in Rust via PyO3/maturin
|
|
40
|
+
- **Optimized core:** zero-copy parser, PHF lookup tables, bitflag filters, buffer-reusing rrule
|
|
41
|
+
- **freezegun compatible** — exposes `dateutil.tz.UTC` constant for seamless time mocking
|
|
42
|
+
- **Comprehensive test suite** validated against python-dateutil behavior
|
|
43
|
+
- **Python 3.10–3.14** supported on Linux, macOS, and Windows
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install python-dateutil-rs
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
> **Note:** This package provides the `dateutil` namespace. If you have `python-dateutil` installed, uninstall it first to avoid conflicts: `pip uninstall python-dateutil`.
|
|
52
|
+
|
|
53
|
+
## Drop-in Replacement
|
|
54
|
+
|
|
55
|
+
Existing code that uses python-dateutil works without modification:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
# These imports work exactly the same as with python-dateutil
|
|
59
|
+
from dateutil.parser import parse, isoparse, parserinfo
|
|
60
|
+
from dateutil.tz import tzutc, tzoffset, tzlocal, gettz, UTC
|
|
61
|
+
from dateutil.relativedelta import relativedelta
|
|
62
|
+
from dateutil.rrule import rrule, rruleset, rrulestr, MONTHLY, WEEKLY, MO, FR
|
|
63
|
+
from dateutil.easter import easter, EASTER_WESTERN
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Usage
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from dateutil.parser import parse, isoparse
|
|
70
|
+
from dateutil.relativedelta import relativedelta
|
|
71
|
+
from dateutil.rrule import rrule, MONTHLY
|
|
72
|
+
from dateutil.tz import gettz, tzutc
|
|
73
|
+
from dateutil.easter import easter
|
|
74
|
+
|
|
75
|
+
# Parse date strings (zero-copy tokenizer)
|
|
76
|
+
dt = parse("2026-01-15T10:30:00+09:00")
|
|
77
|
+
|
|
78
|
+
# ISO-8601 strict parsing
|
|
79
|
+
dt = isoparse("2026-01-15T10:30:00")
|
|
80
|
+
|
|
81
|
+
# Relative deltas
|
|
82
|
+
next_month = dt + relativedelta(months=+1)
|
|
83
|
+
|
|
84
|
+
# Recurrence rules (buffer-reusing iterator)
|
|
85
|
+
monthly = rrule(MONTHLY, count=5, dtstart=parse("2026-01-01"))
|
|
86
|
+
dates = monthly.all()
|
|
87
|
+
dates = list(monthly) # also iterable
|
|
88
|
+
first = monthly[0] # indexing
|
|
89
|
+
subset = monthly[1:3] # slicing
|
|
90
|
+
n = monthly.count() # total occurrences
|
|
91
|
+
dt in monthly # membership test
|
|
92
|
+
|
|
93
|
+
# Timezones
|
|
94
|
+
tokyo = gettz("Asia/Tokyo")
|
|
95
|
+
utc = tzutc()
|
|
96
|
+
|
|
97
|
+
# Easter
|
|
98
|
+
easter_date = easter(2026)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Flat Import Style
|
|
102
|
+
|
|
103
|
+
All symbols are also re-exported from the top-level `dateutil` package:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from dateutil import parse, relativedelta, rrule, gettz, easter
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Development
|
|
110
|
+
|
|
111
|
+
### Prerequisites
|
|
112
|
+
|
|
113
|
+
- Python 3.10+
|
|
114
|
+
- Rust toolchain
|
|
115
|
+
- [uv](https://github.com/astral-sh/uv) (recommended) or pip
|
|
116
|
+
|
|
117
|
+
### Setup
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
git clone https://github.com/wakita181009/dateutil-rs.git
|
|
121
|
+
cd dateutil-rs
|
|
122
|
+
uv sync --extra dev
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Building
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
# Build the native extension
|
|
129
|
+
maturin develop --release
|
|
130
|
+
|
|
131
|
+
# Development build (faster compilation)
|
|
132
|
+
maturin develop -F python
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Running Tests
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
# Run the test suite
|
|
139
|
+
uv run pytest tests/ -x -q
|
|
140
|
+
|
|
141
|
+
# Run with coverage
|
|
142
|
+
uv run pytest tests/ --cov=dateutil
|
|
143
|
+
|
|
144
|
+
# Run Rust tests
|
|
145
|
+
cargo test -p dateutil-core
|
|
146
|
+
cargo test --workspace
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Linting
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
uv run ruff check tests/ python/
|
|
153
|
+
uv run ruff format --check tests/ python/
|
|
154
|
+
uv run mypy python/
|
|
155
|
+
cargo clippy --workspace
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Benchmarks
|
|
159
|
+
|
|
160
|
+
Performance measured against python-dateutil v2.9.0 (before the drop-in rename). Baseline results are preserved in [benchmarks/BASELINE.md](benchmarks/BASELINE.md).
|
|
161
|
+
|
|
162
|
+
#### Summary (vs python-dateutil)
|
|
163
|
+
|
|
164
|
+
| Module | Speedup |
|
|
165
|
+
|--------|---------|
|
|
166
|
+
| Parser (parse) | **19.5x–36.0x** |
|
|
167
|
+
| Parser (isoparse) | **13.0x–38.4x** |
|
|
168
|
+
| RRule | **5.9x–63.7x** |
|
|
169
|
+
| Timezone | **1.0x–896.7x** |
|
|
170
|
+
| RelativeDelta | **2.0x–28.1x** |
|
|
171
|
+
| Easter | **5.0x–7.3x** |
|
|
172
|
+
|
|
173
|
+
> Measured on Apple Silicon (M-series), Python 3.13, release build.
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
# Run benchmarks (Rust dateutil only, since the package now occupies the dateutil namespace)
|
|
177
|
+
make bench
|
|
178
|
+
|
|
179
|
+
# Run and save results as JSON
|
|
180
|
+
make bench-save
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
> **Note:** Since `python-dateutil-rs` provides the same `dateutil` namespace as `python-dateutil`, both cannot be installed simultaneously. The baseline comparison numbers above were captured before the namespace unification.
|
|
184
|
+
|
|
185
|
+
## Project Structure
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
dateutil-rs/
|
|
189
|
+
├── Cargo.toml # Workspace root
|
|
190
|
+
├── pyproject.toml # Python project config (maturin)
|
|
191
|
+
├── crates/
|
|
192
|
+
│ ├── dateutil-core/ # Pure Rust optimized core (crates.io)
|
|
193
|
+
│ │ └── src/
|
|
194
|
+
│ │ ├── lib.rs # Crate root, public API
|
|
195
|
+
│ │ ├── common.rs # Weekday (MO-SU with N-th occurrence)
|
|
196
|
+
│ │ ├── easter.rs # Easter date calculations
|
|
197
|
+
│ │ ├── error.rs # Shared error types
|
|
198
|
+
│ │ ├── relativedelta.rs
|
|
199
|
+
│ │ ├── parser.rs # parse() entry point
|
|
200
|
+
│ │ ├── parser/ # tokenizer, parserinfo, isoparser
|
|
201
|
+
│ │ ├── rrule.rs # RRule entry point
|
|
202
|
+
│ │ ├── rrule/ # iter, parse (rrulestr), set
|
|
203
|
+
│ │ └── tz/ # tzutc, tzoffset, tzfile, tzlocal
|
|
204
|
+
│ └── dateutil-py/ # PyO3 binding layer → PyPI package
|
|
205
|
+
│ └── src/
|
|
206
|
+
│ ├── lib.rs # Module registration
|
|
207
|
+
│ ├── py.rs # Binding root + #[pymodule]
|
|
208
|
+
│ └── py/ # Per-module bindings (common, conv, easter, parser, relativedelta, rrule, tz)
|
|
209
|
+
├── python/dateutil/ # Python package (drop-in replacement for python-dateutil)
|
|
210
|
+
│ ├── __init__.py # Re-exports from Rust native module
|
|
211
|
+
│ ├── _native.pyi # Type stubs for native module
|
|
212
|
+
│ ├── py.typed # PEP 561 marker
|
|
213
|
+
│ ├── parser.py # dateutil.parser (parse, isoparse, parserinfo)
|
|
214
|
+
│ ├── tz.py # dateutil.tz (tzutc, tzoffset, gettz, UTC, ...)
|
|
215
|
+
│ ├── relativedelta.py # dateutil.relativedelta
|
|
216
|
+
│ ├── rrule.py # dateutil.rrule (rrule, rruleset, rrulestr, freq constants)
|
|
217
|
+
│ └── easter.py # dateutil.easter (easter, calendar constants)
|
|
218
|
+
├── tests/ # Python test suite
|
|
219
|
+
├── benchmarks/ # pytest-benchmark comparisons
|
|
220
|
+
├── .github/workflows/ # CI (ci.yml, publish.yml)
|
|
221
|
+
├── Makefile
|
|
222
|
+
└── LICENSE
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Crate Roles
|
|
226
|
+
|
|
227
|
+
| Crate | Purpose | PyO3 | Publish To |
|
|
228
|
+
|-------|---------|------|------------|
|
|
229
|
+
| `dateutil-core` | Pure Rust optimized core | No | crates.io |
|
|
230
|
+
| `dateutil-py` | PyO3 binding layer | Yes | PyPI (`python-dateutil-rs`) |
|
|
231
|
+
|
|
232
|
+
## Compatibility with python-dateutil
|
|
233
|
+
|
|
234
|
+
Target: **python-dateutil v2.9.0**. The goal is covering the **95%+ of real-world usage** — the symbols that actually appear in application code — while intentionally omitting a small number of rarely-used features in exchange for a smaller, faster core. If a symbol below is listed as supported, it is a drop-in for the python-dateutil equivalent in both import path and call signature.
|
|
235
|
+
|
|
236
|
+
### Supported API surface
|
|
237
|
+
|
|
238
|
+
| Submodule | Symbol | Status | Notes |
|
|
239
|
+
|-----------|--------|:------:|-------|
|
|
240
|
+
| `dateutil.parser` | `parse(timestr, ...)` | ✅ | `default`, `ignoretz`, `tzinfos`, `dayfirst`, `yearfirst`, `parserinfo` all honored |
|
|
241
|
+
| `dateutil.parser` | `isoparse` / `isoparser` | ✅ | ISO-8601 strict parsing |
|
|
242
|
+
| `dateutil.parser` | `parserinfo` | ✅ | Customizable via Python subclass (override `WEEKDAYS`, `MONTHS`, `HMS`, `AMPM`, `UTCZONE`, `PERTAIN`, `JUMP`, `TZOFFSET`) |
|
|
243
|
+
| `dateutil.parser` | `ParserError`, `UnknownTimezoneWarning` | ✅ | Same exception hierarchy |
|
|
244
|
+
| `dateutil.tz` | `tzutc`, `tzoffset`, `tzlocal`, `tzfile` | ✅ | Rust-native implementations |
|
|
245
|
+
| `dateutil.tz` | `UTC` | ✅ | Singleton `tzutc()` — works with freezegun |
|
|
246
|
+
| `dateutil.tz` | `gettz(name)` | ✅ | IANA lookup with caching; honors `PYTHONTZPATH`; auto-bootstraps `tzdata` PyPI package on Windows |
|
|
247
|
+
| `dateutil.tz` | `enfold`, `datetime_exists`, `datetime_ambiguous`, `resolve_imaginary` | ✅ | Same semantics for DST gaps/folds |
|
|
248
|
+
| `dateutil.relativedelta` | `relativedelta` | ✅ | All absolute/relative kwargs, weekday N-th occurrence, arithmetic with `date`/`datetime`/`relativedelta` |
|
|
249
|
+
| `dateutil.relativedelta` | `MO`–`SU` weekday constants | ✅ | Same `MO(+1)` / `MO(-1)` API |
|
|
250
|
+
| `dateutil.rrule` | `rrule`, `rruleset`, `rrulestr` | ✅ | RFC 5545 parsing; iteration, indexing, slicing, `count()`, `before`/`after`/`between`, membership |
|
|
251
|
+
| `dateutil.rrule` | `YEARLY`, `MONTHLY`, `WEEKLY`, `DAILY`, `HOURLY`, `MINUTELY`, `SECONDLY` | ✅ | All `freq` constants |
|
|
252
|
+
| `dateutil.rrule` | `MO`–`SU`, `weekday` | ✅ | Re-exported |
|
|
253
|
+
| `dateutil.easter` | `easter(year, method=...)` | ✅ | `EASTER_WESTERN`, `EASTER_ORTHODOX`, `EASTER_JULIAN` |
|
|
254
|
+
| `dateutil.utils` | `today`, `default_tzinfo`, `within_delta` | ✅ | Pure Python, identical behavior |
|
|
255
|
+
|
|
256
|
+
### Intentionally not supported
|
|
257
|
+
|
|
258
|
+
These features target niche use-cases (typically <1% of real-world imports) and are omitted to keep the core small and fast. If you need them, keep `python-dateutil` installed in that project.
|
|
259
|
+
|
|
260
|
+
| Symbol | Reason |
|
|
261
|
+
|--------|--------|
|
|
262
|
+
| `parser.parse(fuzzy=True)` / `fuzzy_with_tokens=True` | Fuzzy natural-language parsing is out of scope — use strict `parse()` or `isoparse()` |
|
|
263
|
+
| `dateutil.tz.tzstr`, `dateutil.tz.tzrange` | POSIX TZ string tzinfo (`EST5EDT,M3.2.0,M11.1.0`). Prefer IANA names via `gettz()` |
|
|
264
|
+
| `dateutil.tz.tzical` | iCalendar `VTIMEZONE` parsing. Prefer `gettz()` |
|
|
265
|
+
| `dateutil.zoneinfo` submodule | Embedded tarball zoneinfo database. The Rust `gettz()` reads the system IANA database (or the `tzdata` PyPI package via `PYTHONTZPATH`) instead |
|
|
266
|
+
| `parser.DEFAULTPARSER`, `DEFAULTTZPARSER` module globals | Module-level mutable singletons; use `parser()` / `isoparser()` instances instead |
|
|
267
|
+
|
|
268
|
+
### Behavior caveats
|
|
269
|
+
|
|
270
|
+
- **Cannot coexist with `python-dateutil`** — both packages provide the `dateutil` top-level namespace. Uninstall one before installing the other.
|
|
271
|
+
- **`tzlocal()`** reads `/etc/localtime` on each call (python-dateutil caches it). This is the only module where Rust can be slower than upstream (~1.0x). All other tz operations are 10x–897x faster.
|
|
272
|
+
- **`parserinfo` subclasses**: override via class attributes (the documented API). Overriding instance methods like `validate()` is not a supported extension point.
|
|
273
|
+
|
|
274
|
+
### Verification
|
|
275
|
+
|
|
276
|
+
The `tests/` directory ports the upstream python-dateutil test suite and is run against the Rust implementation in CI. A test passing there means behavior matches python-dateutil for that input.
|
|
277
|
+
|
|
278
|
+
## License
|
|
279
|
+
|
|
280
|
+
[MIT](LICENSE)
|
|
281
|
+
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# python-dateutil-rs
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/python-dateutil-rs/)
|
|
4
|
+
[](https://pypi.org/project/python-dateutil-rs/)
|
|
5
|
+
[](https://pypi.org/project/python-dateutil-rs/)
|
|
6
|
+
[](https://github.com/wakita181009/dateutil-rs/actions/workflows/ci.yml)
|
|
7
|
+
[](https://codecov.io/gh/wakita181009/dateutil-rs)
|
|
8
|
+
|
|
9
|
+
A high-performance, **drop-in replacement** for [python-dateutil](https://github.com/dateutil/dateutil) (v2.9.0), powered by Rust.
|
|
10
|
+
|
|
11
|
+
> **Drop-in compatible:** Install `python-dateutil-rs` and your existing `from dateutil.parser import parse`, `from dateutil.tz import tzutc`, etc. continue to work — no code changes required, just **2x–897x faster**.
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- **True drop-in replacement** — provides `dateutil` package with the same submodule structure (`dateutil.parser`, `dateutil.tz`, `dateutil.relativedelta`, `dateutil.rrule`, `dateutil.easter`)
|
|
16
|
+
- **Zero code changes** — existing imports like `from dateutil.parser import parse` work as-is
|
|
17
|
+
- **Rust-accelerated:** all core modules rewritten in Rust via PyO3/maturin
|
|
18
|
+
- **Optimized core:** zero-copy parser, PHF lookup tables, bitflag filters, buffer-reusing rrule
|
|
19
|
+
- **freezegun compatible** — exposes `dateutil.tz.UTC` constant for seamless time mocking
|
|
20
|
+
- **Comprehensive test suite** validated against python-dateutil behavior
|
|
21
|
+
- **Python 3.10–3.14** supported on Linux, macOS, and Windows
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install python-dateutil-rs
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
> **Note:** This package provides the `dateutil` namespace. If you have `python-dateutil` installed, uninstall it first to avoid conflicts: `pip uninstall python-dateutil`.
|
|
30
|
+
|
|
31
|
+
## Drop-in Replacement
|
|
32
|
+
|
|
33
|
+
Existing code that uses python-dateutil works without modification:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
# These imports work exactly the same as with python-dateutil
|
|
37
|
+
from dateutil.parser import parse, isoparse, parserinfo
|
|
38
|
+
from dateutil.tz import tzutc, tzoffset, tzlocal, gettz, UTC
|
|
39
|
+
from dateutil.relativedelta import relativedelta
|
|
40
|
+
from dateutil.rrule import rrule, rruleset, rrulestr, MONTHLY, WEEKLY, MO, FR
|
|
41
|
+
from dateutil.easter import easter, EASTER_WESTERN
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from dateutil.parser import parse, isoparse
|
|
48
|
+
from dateutil.relativedelta import relativedelta
|
|
49
|
+
from dateutil.rrule import rrule, MONTHLY
|
|
50
|
+
from dateutil.tz import gettz, tzutc
|
|
51
|
+
from dateutil.easter import easter
|
|
52
|
+
|
|
53
|
+
# Parse date strings (zero-copy tokenizer)
|
|
54
|
+
dt = parse("2026-01-15T10:30:00+09:00")
|
|
55
|
+
|
|
56
|
+
# ISO-8601 strict parsing
|
|
57
|
+
dt = isoparse("2026-01-15T10:30:00")
|
|
58
|
+
|
|
59
|
+
# Relative deltas
|
|
60
|
+
next_month = dt + relativedelta(months=+1)
|
|
61
|
+
|
|
62
|
+
# Recurrence rules (buffer-reusing iterator)
|
|
63
|
+
monthly = rrule(MONTHLY, count=5, dtstart=parse("2026-01-01"))
|
|
64
|
+
dates = monthly.all()
|
|
65
|
+
dates = list(monthly) # also iterable
|
|
66
|
+
first = monthly[0] # indexing
|
|
67
|
+
subset = monthly[1:3] # slicing
|
|
68
|
+
n = monthly.count() # total occurrences
|
|
69
|
+
dt in monthly # membership test
|
|
70
|
+
|
|
71
|
+
# Timezones
|
|
72
|
+
tokyo = gettz("Asia/Tokyo")
|
|
73
|
+
utc = tzutc()
|
|
74
|
+
|
|
75
|
+
# Easter
|
|
76
|
+
easter_date = easter(2026)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Flat Import Style
|
|
80
|
+
|
|
81
|
+
All symbols are also re-exported from the top-level `dateutil` package:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from dateutil import parse, relativedelta, rrule, gettz, easter
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Development
|
|
88
|
+
|
|
89
|
+
### Prerequisites
|
|
90
|
+
|
|
91
|
+
- Python 3.10+
|
|
92
|
+
- Rust toolchain
|
|
93
|
+
- [uv](https://github.com/astral-sh/uv) (recommended) or pip
|
|
94
|
+
|
|
95
|
+
### Setup
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
git clone https://github.com/wakita181009/dateutil-rs.git
|
|
99
|
+
cd dateutil-rs
|
|
100
|
+
uv sync --extra dev
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Building
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# Build the native extension
|
|
107
|
+
maturin develop --release
|
|
108
|
+
|
|
109
|
+
# Development build (faster compilation)
|
|
110
|
+
maturin develop -F python
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Running Tests
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
# Run the test suite
|
|
117
|
+
uv run pytest tests/ -x -q
|
|
118
|
+
|
|
119
|
+
# Run with coverage
|
|
120
|
+
uv run pytest tests/ --cov=dateutil
|
|
121
|
+
|
|
122
|
+
# Run Rust tests
|
|
123
|
+
cargo test -p dateutil-core
|
|
124
|
+
cargo test --workspace
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Linting
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
uv run ruff check tests/ python/
|
|
131
|
+
uv run ruff format --check tests/ python/
|
|
132
|
+
uv run mypy python/
|
|
133
|
+
cargo clippy --workspace
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Benchmarks
|
|
137
|
+
|
|
138
|
+
Performance measured against python-dateutil v2.9.0 (before the drop-in rename). Baseline results are preserved in [benchmarks/BASELINE.md](benchmarks/BASELINE.md).
|
|
139
|
+
|
|
140
|
+
#### Summary (vs python-dateutil)
|
|
141
|
+
|
|
142
|
+
| Module | Speedup |
|
|
143
|
+
|--------|---------|
|
|
144
|
+
| Parser (parse) | **19.5x–36.0x** |
|
|
145
|
+
| Parser (isoparse) | **13.0x–38.4x** |
|
|
146
|
+
| RRule | **5.9x–63.7x** |
|
|
147
|
+
| Timezone | **1.0x–896.7x** |
|
|
148
|
+
| RelativeDelta | **2.0x–28.1x** |
|
|
149
|
+
| Easter | **5.0x–7.3x** |
|
|
150
|
+
|
|
151
|
+
> Measured on Apple Silicon (M-series), Python 3.13, release build.
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
# Run benchmarks (Rust dateutil only, since the package now occupies the dateutil namespace)
|
|
155
|
+
make bench
|
|
156
|
+
|
|
157
|
+
# Run and save results as JSON
|
|
158
|
+
make bench-save
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
> **Note:** Since `python-dateutil-rs` provides the same `dateutil` namespace as `python-dateutil`, both cannot be installed simultaneously. The baseline comparison numbers above were captured before the namespace unification.
|
|
162
|
+
|
|
163
|
+
## Project Structure
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
dateutil-rs/
|
|
167
|
+
├── Cargo.toml # Workspace root
|
|
168
|
+
├── pyproject.toml # Python project config (maturin)
|
|
169
|
+
├── crates/
|
|
170
|
+
│ ├── dateutil-core/ # Pure Rust optimized core (crates.io)
|
|
171
|
+
│ │ └── src/
|
|
172
|
+
│ │ ├── lib.rs # Crate root, public API
|
|
173
|
+
│ │ ├── common.rs # Weekday (MO-SU with N-th occurrence)
|
|
174
|
+
│ │ ├── easter.rs # Easter date calculations
|
|
175
|
+
│ │ ├── error.rs # Shared error types
|
|
176
|
+
│ │ ├── relativedelta.rs
|
|
177
|
+
│ │ ├── parser.rs # parse() entry point
|
|
178
|
+
│ │ ├── parser/ # tokenizer, parserinfo, isoparser
|
|
179
|
+
│ │ ├── rrule.rs # RRule entry point
|
|
180
|
+
│ │ ├── rrule/ # iter, parse (rrulestr), set
|
|
181
|
+
│ │ └── tz/ # tzutc, tzoffset, tzfile, tzlocal
|
|
182
|
+
│ └── dateutil-py/ # PyO3 binding layer → PyPI package
|
|
183
|
+
│ └── src/
|
|
184
|
+
│ ├── lib.rs # Module registration
|
|
185
|
+
│ ├── py.rs # Binding root + #[pymodule]
|
|
186
|
+
│ └── py/ # Per-module bindings (common, conv, easter, parser, relativedelta, rrule, tz)
|
|
187
|
+
├── python/dateutil/ # Python package (drop-in replacement for python-dateutil)
|
|
188
|
+
│ ├── __init__.py # Re-exports from Rust native module
|
|
189
|
+
│ ├── _native.pyi # Type stubs for native module
|
|
190
|
+
│ ├── py.typed # PEP 561 marker
|
|
191
|
+
│ ├── parser.py # dateutil.parser (parse, isoparse, parserinfo)
|
|
192
|
+
│ ├── tz.py # dateutil.tz (tzutc, tzoffset, gettz, UTC, ...)
|
|
193
|
+
│ ├── relativedelta.py # dateutil.relativedelta
|
|
194
|
+
│ ├── rrule.py # dateutil.rrule (rrule, rruleset, rrulestr, freq constants)
|
|
195
|
+
│ └── easter.py # dateutil.easter (easter, calendar constants)
|
|
196
|
+
├── tests/ # Python test suite
|
|
197
|
+
├── benchmarks/ # pytest-benchmark comparisons
|
|
198
|
+
├── .github/workflows/ # CI (ci.yml, publish.yml)
|
|
199
|
+
├── Makefile
|
|
200
|
+
└── LICENSE
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Crate Roles
|
|
204
|
+
|
|
205
|
+
| Crate | Purpose | PyO3 | Publish To |
|
|
206
|
+
|-------|---------|------|------------|
|
|
207
|
+
| `dateutil-core` | Pure Rust optimized core | No | crates.io |
|
|
208
|
+
| `dateutil-py` | PyO3 binding layer | Yes | PyPI (`python-dateutil-rs`) |
|
|
209
|
+
|
|
210
|
+
## Compatibility with python-dateutil
|
|
211
|
+
|
|
212
|
+
Target: **python-dateutil v2.9.0**. The goal is covering the **95%+ of real-world usage** — the symbols that actually appear in application code — while intentionally omitting a small number of rarely-used features in exchange for a smaller, faster core. If a symbol below is listed as supported, it is a drop-in for the python-dateutil equivalent in both import path and call signature.
|
|
213
|
+
|
|
214
|
+
### Supported API surface
|
|
215
|
+
|
|
216
|
+
| Submodule | Symbol | Status | Notes |
|
|
217
|
+
|-----------|--------|:------:|-------|
|
|
218
|
+
| `dateutil.parser` | `parse(timestr, ...)` | ✅ | `default`, `ignoretz`, `tzinfos`, `dayfirst`, `yearfirst`, `parserinfo` all honored |
|
|
219
|
+
| `dateutil.parser` | `isoparse` / `isoparser` | ✅ | ISO-8601 strict parsing |
|
|
220
|
+
| `dateutil.parser` | `parserinfo` | ✅ | Customizable via Python subclass (override `WEEKDAYS`, `MONTHS`, `HMS`, `AMPM`, `UTCZONE`, `PERTAIN`, `JUMP`, `TZOFFSET`) |
|
|
221
|
+
| `dateutil.parser` | `ParserError`, `UnknownTimezoneWarning` | ✅ | Same exception hierarchy |
|
|
222
|
+
| `dateutil.tz` | `tzutc`, `tzoffset`, `tzlocal`, `tzfile` | ✅ | Rust-native implementations |
|
|
223
|
+
| `dateutil.tz` | `UTC` | ✅ | Singleton `tzutc()` — works with freezegun |
|
|
224
|
+
| `dateutil.tz` | `gettz(name)` | ✅ | IANA lookup with caching; honors `PYTHONTZPATH`; auto-bootstraps `tzdata` PyPI package on Windows |
|
|
225
|
+
| `dateutil.tz` | `enfold`, `datetime_exists`, `datetime_ambiguous`, `resolve_imaginary` | ✅ | Same semantics for DST gaps/folds |
|
|
226
|
+
| `dateutil.relativedelta` | `relativedelta` | ✅ | All absolute/relative kwargs, weekday N-th occurrence, arithmetic with `date`/`datetime`/`relativedelta` |
|
|
227
|
+
| `dateutil.relativedelta` | `MO`–`SU` weekday constants | ✅ | Same `MO(+1)` / `MO(-1)` API |
|
|
228
|
+
| `dateutil.rrule` | `rrule`, `rruleset`, `rrulestr` | ✅ | RFC 5545 parsing; iteration, indexing, slicing, `count()`, `before`/`after`/`between`, membership |
|
|
229
|
+
| `dateutil.rrule` | `YEARLY`, `MONTHLY`, `WEEKLY`, `DAILY`, `HOURLY`, `MINUTELY`, `SECONDLY` | ✅ | All `freq` constants |
|
|
230
|
+
| `dateutil.rrule` | `MO`–`SU`, `weekday` | ✅ | Re-exported |
|
|
231
|
+
| `dateutil.easter` | `easter(year, method=...)` | ✅ | `EASTER_WESTERN`, `EASTER_ORTHODOX`, `EASTER_JULIAN` |
|
|
232
|
+
| `dateutil.utils` | `today`, `default_tzinfo`, `within_delta` | ✅ | Pure Python, identical behavior |
|
|
233
|
+
|
|
234
|
+
### Intentionally not supported
|
|
235
|
+
|
|
236
|
+
These features target niche use-cases (typically <1% of real-world imports) and are omitted to keep the core small and fast. If you need them, keep `python-dateutil` installed in that project.
|
|
237
|
+
|
|
238
|
+
| Symbol | Reason |
|
|
239
|
+
|--------|--------|
|
|
240
|
+
| `parser.parse(fuzzy=True)` / `fuzzy_with_tokens=True` | Fuzzy natural-language parsing is out of scope — use strict `parse()` or `isoparse()` |
|
|
241
|
+
| `dateutil.tz.tzstr`, `dateutil.tz.tzrange` | POSIX TZ string tzinfo (`EST5EDT,M3.2.0,M11.1.0`). Prefer IANA names via `gettz()` |
|
|
242
|
+
| `dateutil.tz.tzical` | iCalendar `VTIMEZONE` parsing. Prefer `gettz()` |
|
|
243
|
+
| `dateutil.zoneinfo` submodule | Embedded tarball zoneinfo database. The Rust `gettz()` reads the system IANA database (or the `tzdata` PyPI package via `PYTHONTZPATH`) instead |
|
|
244
|
+
| `parser.DEFAULTPARSER`, `DEFAULTTZPARSER` module globals | Module-level mutable singletons; use `parser()` / `isoparser()` instances instead |
|
|
245
|
+
|
|
246
|
+
### Behavior caveats
|
|
247
|
+
|
|
248
|
+
- **Cannot coexist with `python-dateutil`** — both packages provide the `dateutil` top-level namespace. Uninstall one before installing the other.
|
|
249
|
+
- **`tzlocal()`** reads `/etc/localtime` on each call (python-dateutil caches it). This is the only module where Rust can be slower than upstream (~1.0x). All other tz operations are 10x–897x faster.
|
|
250
|
+
- **`parserinfo` subclasses**: override via class attributes (the documented API). Overriding instance methods like `validate()` is not a supported extension point.
|
|
251
|
+
|
|
252
|
+
### Verification
|
|
253
|
+
|
|
254
|
+
The `tests/` directory ports the upstream python-dateutil test suite and is run against the Rust implementation in CI. A test passing there means behavior matches python-dateutil for that input.
|
|
255
|
+
|
|
256
|
+
## License
|
|
257
|
+
|
|
258
|
+
[MIT](LICENSE)
|