Metadata-Version: 2.4
Name: python-dateutil-rs
Version: 0.1.6
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Dist: pytest>=9.1.0 ; extra == 'dev'
Requires-Dist: pytest-cov>=7.1 ; extra == 'dev'
Requires-Dist: pytest-benchmark>=5.2 ; extra == 'dev'
Requires-Dist: freezegun>=1.5 ; extra == 'dev'
Requires-Dist: hypothesis>=6.155 ; extra == 'dev'
Requires-Dist: maturin>=1.14 ; extra == 'dev'
Requires-Dist: tzdata>=2024.1 ; sys_platform == 'win32' and extra == 'dev'
Provides-Extra: dev
License-File: LICENSE
Summary: A Rust-backed port of python-dateutil
License-Expression: MIT
Requires-Python: >=3.10
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM

# python-dateutil-rs

[![PyPI](https://img.shields.io/pypi/v/python-dateutil-rs.svg?style=flat-square)](https://pypi.org/project/python-dateutil-rs/)
[![Python](https://img.shields.io/pypi/pyversions/python-dateutil-rs.svg?style=flat-square)](https://pypi.org/project/python-dateutil-rs/)
[![License](https://img.shields.io/pypi/l/python-dateutil-rs.svg?style=flat-square)](https://pypi.org/project/python-dateutil-rs/)
[![CI](https://github.com/wakita181009/dateutil-rs/actions/workflows/ci.yml/badge.svg)](https://github.com/wakita181009/dateutil-rs/actions/workflows/ci.yml)
[![Coverage](https://codecov.io/gh/wakita181009/dateutil-rs/branch/main/graph/badge.svg)](https://codecov.io/gh/wakita181009/dateutil-rs)

A high-performance, **drop-in replacement** for [python-dateutil](https://github.com/dateutil/dateutil) (v2.9.0), powered by Rust.

> **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**.

## Features

- **True drop-in replacement** — provides `dateutil` package with the same submodule structure (`dateutil.parser`, `dateutil.tz`, `dateutil.relativedelta`, `dateutil.rrule`, `dateutil.easter`)
- **Zero code changes** — existing imports like `from dateutil.parser import parse` work as-is
- **Rust-accelerated:** all core modules rewritten in Rust via PyO3/maturin
- **Optimized core:** zero-copy parser, PHF lookup tables, bitflag filters, buffer-reusing rrule
- **freezegun compatible** — exposes `dateutil.tz.UTC` constant for seamless time mocking
- **Comprehensive test suite** validated against python-dateutil behavior
- **Python 3.10–3.14** supported on Linux, macOS, and Windows

## Installation

```bash
pip install python-dateutil-rs
```

> **Note:** This package provides the `dateutil` namespace. If you have `python-dateutil` installed, uninstall it first to avoid conflicts: `pip uninstall python-dateutil`.

## Drop-in Replacement

Existing code that uses python-dateutil works without modification:

```python
# These imports work exactly the same as with python-dateutil
from dateutil.parser import parse, isoparse, parserinfo
from dateutil.tz import tzutc, tzoffset, tzlocal, gettz, UTC
from dateutil.relativedelta import relativedelta
from dateutil.rrule import rrule, rruleset, rrulestr, MONTHLY, WEEKLY, MO, FR
from dateutil.easter import easter, EASTER_WESTERN
```

## Usage

```python
from dateutil.parser import parse, isoparse
from dateutil.relativedelta import relativedelta
from dateutil.rrule import rrule, MONTHLY
from dateutil.tz import gettz, tzutc
from dateutil.easter import easter

# Parse date strings (zero-copy tokenizer)
dt = parse("2026-01-15T10:30:00+09:00")

# ISO-8601 strict parsing
dt = isoparse("2026-01-15T10:30:00")

# Relative deltas
next_month = dt + relativedelta(months=+1)

# Recurrence rules (buffer-reusing iterator)
monthly = rrule(MONTHLY, count=5, dtstart=parse("2026-01-01"))
dates = monthly.all()
dates = list(monthly)           # also iterable
first = monthly[0]              # indexing
subset = monthly[1:3]           # slicing
n = monthly.count()             # total occurrences
dt in monthly                   # membership test

# Timezones
tokyo = gettz("Asia/Tokyo")
utc = tzutc()

# Easter
easter_date = easter(2026)
```

### Flat Import Style

All symbols are also re-exported from the top-level `dateutil` package:

```python
from dateutil import parse, relativedelta, rrule, gettz, easter
```

## Development

### Prerequisites

- Python 3.10+
- Rust toolchain
- [uv](https://github.com/astral-sh/uv) (recommended) or pip

### Setup

```bash
git clone https://github.com/wakita181009/dateutil-rs.git
cd dateutil-rs
uv sync --extra dev
```

### Building

```bash
# Build the native extension
maturin develop --release

# Development build (faster compilation)
maturin develop -F python
```

### Running Tests

```bash
# Run the test suite
uv run pytest tests/ -x -q

# Run with coverage
uv run pytest tests/ --cov=dateutil

# Run Rust tests
cargo test -p dateutil-core
cargo test --workspace
```

### Linting

```bash
uv run ruff check tests/ python/
uv run ruff format --check tests/ python/
uv run mypy python/
cargo clippy --workspace
```

### Benchmarks

Performance measured against python-dateutil v2.9.0 (before the drop-in rename). Baseline results are preserved in [benchmarks/BASELINE.md](benchmarks/BASELINE.md).

#### Summary (vs python-dateutil)

| Module | Speedup |
|--------|---------|
| Parser (parse) | **19.5x–36.0x** |
| Parser (isoparse) | **13.0x–38.4x** |
| RRule | **5.9x–63.7x** |
| Timezone | **1.0x–896.7x** |
| RelativeDelta | **2.0x–28.1x** |
| Easter | **5.0x–7.3x** |

> Measured on Apple Silicon (M-series), Python 3.13, release build.

```bash
# Run benchmarks (Rust dateutil only, since the package now occupies the dateutil namespace)
make bench

# Run and save results as JSON
make bench-save
```

> **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.

## Project Structure

```
dateutil-rs/
├── Cargo.toml                 # Workspace root
├── pyproject.toml             # Python project config (maturin)
├── crates/
│   ├── dateutil-core/         # Pure Rust optimized core (crates.io)
│   │   └── src/
│   │       ├── lib.rs         # Crate root, public API
│   │       ├── common.rs      # Weekday (MO-SU with N-th occurrence)
│   │       ├── easter.rs      # Easter date calculations
│   │       ├── error.rs       # Shared error types
│   │       ├── relativedelta.rs
│   │       ├── parser.rs      # parse() entry point
│   │       ├── parser/        # tokenizer, parserinfo, isoparser
│   │       ├── rrule.rs       # RRule entry point
│   │       ├── rrule/         # iter, parse (rrulestr), set
│   │       └── tz/            # tzutc, tzoffset, tzfile, tzlocal
│   └── dateutil-py/           # PyO3 binding layer → PyPI package
│       └── src/
│           ├── lib.rs         # Module registration
│           ├── py.rs          # Binding root + #[pymodule]
│           └── py/            # Per-module bindings (common, conv, easter, parser, relativedelta, rrule, tz)
├── python/dateutil/        # Python package (drop-in replacement for python-dateutil)
│   ├── __init__.py            # Re-exports from Rust native module
│   ├── _native.pyi            # Type stubs for native module
│   ├── py.typed               # PEP 561 marker
│   ├── parser.py              # dateutil.parser (parse, isoparse, parserinfo)
│   ├── tz.py                  # dateutil.tz (tzutc, tzoffset, gettz, UTC, ...)
│   ├── relativedelta.py       # dateutil.relativedelta
│   ├── rrule.py               # dateutil.rrule (rrule, rruleset, rrulestr, freq constants)
│   └── easter.py              # dateutil.easter (easter, calendar constants)
├── tests/                     # Python test suite
├── benchmarks/                # pytest-benchmark comparisons
├── .github/workflows/         # CI (ci.yml, publish.yml)
├── Makefile
└── LICENSE
```

### Crate Roles

| Crate | Purpose | PyO3 | Publish To |
|-------|---------|------|------------|
| `dateutil-core` | Pure Rust optimized core | No | crates.io |
| `dateutil-py` | PyO3 binding layer | Yes | PyPI (`python-dateutil-rs`) |

## Compatibility with python-dateutil

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.

### Supported API surface

| Submodule | Symbol | Status | Notes |
|-----------|--------|:------:|-------|
| `dateutil.parser` | `parse(timestr, ...)` | ✅ | `default`, `ignoretz`, `tzinfos`, `dayfirst`, `yearfirst`, `parserinfo` all honored |
| `dateutil.parser` | `isoparse` / `isoparser` | ✅ | ISO-8601 strict parsing |
| `dateutil.parser` | `parserinfo` | ✅ | Customizable via Python subclass (override `WEEKDAYS`, `MONTHS`, `HMS`, `AMPM`, `UTCZONE`, `PERTAIN`, `JUMP`, `TZOFFSET`) |
| `dateutil.parser` | `ParserError`, `UnknownTimezoneWarning` | ✅ | Same exception hierarchy |
| `dateutil.tz` | `tzutc`, `tzoffset`, `tzlocal`, `tzfile` | ✅ | Rust-native implementations |
| `dateutil.tz` | `UTC` | ✅ | Singleton `tzutc()` — works with freezegun |
| `dateutil.tz` | `gettz(name)` | ✅ | IANA lookup with caching; honors `PYTHONTZPATH`; auto-bootstraps `tzdata` PyPI package on Windows |
| `dateutil.tz` | `enfold`, `datetime_exists`, `datetime_ambiguous`, `resolve_imaginary` | ✅ | Same semantics for DST gaps/folds |
| `dateutil.relativedelta` | `relativedelta` | ✅ | All absolute/relative kwargs, weekday N-th occurrence, arithmetic with `date`/`datetime`/`relativedelta` |
| `dateutil.relativedelta` | `MO`–`SU` weekday constants | ✅ | Same `MO(+1)` / `MO(-1)` API |
| `dateutil.rrule` | `rrule`, `rruleset`, `rrulestr` | ✅ | RFC 5545 parsing; iteration, indexing, slicing, `count()`, `before`/`after`/`between`, membership |
| `dateutil.rrule` | `YEARLY`, `MONTHLY`, `WEEKLY`, `DAILY`, `HOURLY`, `MINUTELY`, `SECONDLY` | ✅ | All `freq` constants |
| `dateutil.rrule` | `MO`–`SU`, `weekday` | ✅ | Re-exported |
| `dateutil.easter` | `easter(year, method=...)` | ✅ | `EASTER_WESTERN`, `EASTER_ORTHODOX`, `EASTER_JULIAN` |
| `dateutil.utils` | `today`, `default_tzinfo`, `within_delta` | ✅ | Pure Python, identical behavior |

### Intentionally not supported

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.

| Symbol | Reason |
|--------|--------|
| `parser.parse(fuzzy=True)` / `fuzzy_with_tokens=True` | Fuzzy natural-language parsing is out of scope — use strict `parse()` or `isoparse()` |
| `dateutil.tz.tzstr`, `dateutil.tz.tzrange` | POSIX TZ string tzinfo (`EST5EDT,M3.2.0,M11.1.0`). Prefer IANA names via `gettz()` |
| `dateutil.tz.tzical` | iCalendar `VTIMEZONE` parsing. Prefer `gettz()` |
| `dateutil.zoneinfo` submodule | Embedded tarball zoneinfo database. The Rust `gettz()` reads the system IANA database (or the `tzdata` PyPI package via `PYTHONTZPATH`) instead |
| `parser.DEFAULTPARSER`, `DEFAULTTZPARSER` module globals | Module-level mutable singletons; use `parser()` / `isoparser()` instances instead |

### Behavior caveats

- **Cannot coexist with `python-dateutil`** — both packages provide the `dateutil` top-level namespace. Uninstall one before installing the other.
- **`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.
- **`parserinfo` subclasses**: override via class attributes (the documented API). Overriding instance methods like `validate()` is not a supported extension point.

### Verification

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.

## License

[MIT](LICENSE)

