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.
Files changed (58) hide show
  1. {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/Cargo.lock +2 -2
  2. {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/Cargo.toml +1 -1
  3. python_dateutil_rs-0.1.5/PKG-INFO +281 -0
  4. python_dateutil_rs-0.1.5/README.md +258 -0
  5. {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/benches/benchmarks.rs +105 -13
  6. {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/src/error.rs +2 -0
  7. python_dateutil_rs-0.1.5/crates/dateutil-core/src/parser/compact.rs +169 -0
  8. python_dateutil_rs-0.1.5/crates/dateutil-core/src/parser/hms.rs +118 -0
  9. python_dateutil_rs-0.1.5/crates/dateutil-core/src/parser/isoparser.rs +750 -0
  10. {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/src/parser/parserinfo.rs +153 -53
  11. {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/src/parser.rs +694 -324
  12. {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/src/rrule/iter.rs +79 -30
  13. {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/src/rrule/parse.rs +24 -24
  14. {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/src/rrule/set.rs +15 -12
  15. {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/src/rrule.rs +177 -121
  16. {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/src/tz/file.rs +50 -27
  17. {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/src/tz/local.rs +50 -10
  18. {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/src/tz.rs +24 -3
  19. python_dateutil_rs-0.1.5/crates/dateutil-py/src/py/parser.rs +581 -0
  20. {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-py/src/py/relativedelta.rs +117 -36
  21. {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-py/src/py/rrule.rs +45 -39
  22. {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-py/src/py/tz.rs +120 -48
  23. {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/pyproject.toml +12 -3
  24. python_dateutil_rs-0.1.5/python/dateutil/__init__.py +114 -0
  25. {python_dateutil_rs-0.1.4/python/dateutil_rs → python_dateutil_rs-0.1.5/python/dateutil}/_native.pyi +43 -17
  26. {python_dateutil_rs-0.1.4/python/dateutil_rs → python_dateutil_rs-0.1.5/python/dateutil}/easter.py +2 -2
  27. python_dateutil_rs-0.1.5/python/dateutil/parser.py +37 -0
  28. python_dateutil_rs-0.1.5/python/dateutil/relativedelta.py +5 -0
  29. {python_dateutil_rs-0.1.4/python/dateutil_rs → python_dateutil_rs-0.1.5/python/dateutil}/rrule.py +16 -2
  30. python_dateutil_rs-0.1.5/python/dateutil/tz.py +114 -0
  31. python_dateutil_rs-0.1.5/python/dateutil/utils.py +71 -0
  32. python_dateutil_rs-0.1.5/python/dateutil/zoneinfo/__init__.py +62 -0
  33. python_dateutil_rs-0.1.4/PKG-INFO +0 -223
  34. python_dateutil_rs-0.1.4/README.md +0 -200
  35. python_dateutil_rs-0.1.4/crates/dateutil-core/src/parser/isoparser.rs +0 -622
  36. python_dateutil_rs-0.1.4/crates/dateutil-py/src/py/parser.rs +0 -334
  37. python_dateutil_rs-0.1.4/python/dateutil_rs/__init__.py +0 -77
  38. python_dateutil_rs-0.1.4/python/dateutil_rs/parser.py +0 -18
  39. python_dateutil_rs-0.1.4/python/dateutil_rs/relativedelta.py +0 -5
  40. python_dateutil_rs-0.1.4/python/dateutil_rs/tz.py +0 -23
  41. {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/LICENSE +0 -0
  42. {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/CLAUDE.md +0 -0
  43. {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/Cargo.toml +0 -0
  44. {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/README.md +0 -0
  45. {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/src/common.rs +0 -0
  46. {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/src/easter.rs +0 -0
  47. {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/src/lib.rs +0 -0
  48. {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/src/parser/tokenizer.rs +0 -0
  49. {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/src/relativedelta.rs +0 -0
  50. {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/src/tz/offset.rs +0 -0
  51. {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-core/src/tz/utc.rs +0 -0
  52. {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-py/Cargo.toml +0 -0
  53. {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-py/src/lib.rs +0 -0
  54. {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-py/src/py/common.rs +0 -0
  55. {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-py/src/py/conv.rs +0 -0
  56. {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-py/src/py/easter.rs +0 -0
  57. {python_dateutil_rs-0.1.4 → python_dateutil_rs-0.1.5}/crates/dateutil-py/src/py.rs +0 -0
  58. {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.4"
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.4"
237
+ version = "0.1.5"
238
238
  dependencies = [
239
239
  "chrono",
240
240
  "dateutil",
@@ -3,7 +3,7 @@ members = ["crates/dateutil-core", "crates/dateutil-py"]
3
3
  resolver = "2"
4
4
 
5
5
  [workspace.package]
6
- version = "0.1.4"
6
+ version = "0.1.5"
7
7
  edition = "2021"
8
8
  license = "MIT"
9
9
  repository = "https://github.com/wakita181009/dateutil-rs"
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/python-dateutil-rs.svg?style=flat-square)](https://pypi.org/project/python-dateutil-rs/)
26
+ [![Python](https://img.shields.io/pypi/pyversions/python-dateutil-rs.svg?style=flat-square)](https://pypi.org/project/python-dateutil-rs/)
27
+ [![License](https://img.shields.io/pypi/l/python-dateutil-rs.svg?style=flat-square)](https://pypi.org/project/python-dateutil-rs/)
28
+ [![CI](https://github.com/wakita181009/dateutil-rs/actions/workflows/ci.yml/badge.svg)](https://github.com/wakita181009/dateutil-rs/actions/workflows/ci.yml)
29
+ [![Coverage](https://codecov.io/gh/wakita181009/dateutil-rs/branch/main/graph/badge.svg)](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
+ [![PyPI](https://img.shields.io/pypi/v/python-dateutil-rs.svg?style=flat-square)](https://pypi.org/project/python-dateutil-rs/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/python-dateutil-rs.svg?style=flat-square)](https://pypi.org/project/python-dateutil-rs/)
5
+ [![License](https://img.shields.io/pypi/l/python-dateutil-rs.svg?style=flat-square)](https://pypi.org/project/python-dateutil-rs/)
6
+ [![CI](https://github.com/wakita181009/dateutil-rs/actions/workflows/ci.yml/badge.svg)](https://github.com/wakita181009/dateutil-rs/actions/workflows/ci.yml)
7
+ [![Coverage](https://codecov.io/gh/wakita181009/dateutil-rs/branch/main/graph/badge.svg)](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)