Metadata-Version: 2.4
Name: fasti-dates
Version: 0.2.1
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: Operating System :: MacOS
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3 :: Only
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
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Free Threading :: 2 - Beta
Classifier: Programming Language :: Rust
Classifier: Topic :: Office/Business :: Financial
Classifier: Typing :: Typed
License-File: licenses/LICENSE-APACHE
License-File: licenses/LICENSE-MIT
License-File: licenses/THIRD-PARTY-NOTICES
Summary: Dates, calendars, business-day conventions and day-count fractions for financial code. Python bindings for the fasti Rust crate.
Keywords: date,calendar,business-day,day-count,finance
Author: fasti contributors
License-Expression: Apache-2.0 OR MIT
Requires-Python: >=3.10
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Changelog, https://github.com/hugof38/fasti/blob/main/CHANGELOG.md
Project-URL: Documentation, https://github.com/hugof38/fasti/blob/main/bindings/python/README-py.md
Project-URL: Issues, https://github.com/hugof38/fasti/issues
Project-URL: Repository, https://github.com/hugof38/fasti

# fasti-dates

[![PyPI](https://img.shields.io/pypi/v/fasti-dates.svg)](https://pypi.org/project/fasti-dates/)

Python bindings for [fasti](https://github.com/hugof38/fasti) — dates,
calendars, business-day conventions and day-count fractions for
financial code, in native Rust with no floating-point arithmetic
anywhere.

The distribution is `fasti-dates`; the import name is `fasti`, matching
the crate. `fasti` on PyPI is an unrelated FastAPI project manager, and
`fasti-py` is refused as too similar to it — PyPI strips a `py` affix
before comparing names.

That project also installs its own top-level `fasti` package, so having
both in one environment would leave whichever was installed second. It
has published two `0.0.0.dev` releases and nothing since, and matching
the crate name is worth more here than insuring against a collision with
a placeholder — but if you need both, install them in separate
environments.

```console
$ pip install fasti-dates
```

Wheels are `abi3` from CPython 3.10 up, so one wheel per platform covers
every version, and a new CPython needs no new wheel.

## The boundary

This is a translation layer, not a second library. Every name maps to
one in the crate, with the same method names, the same semantics and the
same argument order.

- **Dates in and out are `datetime.date`, and nothing else.** A `str` is
  refused, naming `datetime.date.fromisoformat`. A `datetime.datetime`
  is refused too: which day a moment falls on is a time-zone decision,
  and this library has no time-zone concept — call `.date()` yourself.
- **Year fractions come back as `fractions.Fraction`.** Never a float.
  The crate is float-free, and that has to survive the boundary.
- **Errors are `FastiError`, a `ValueError`.** Type mistakes are
  `TypeError`.

```python
>>> import datetime
>>> from fasti.calendars import us
>>> us.SETTLEMENT.is_holiday("2024-07-04")
Traceback (most recent call last):
TypeError: fasti takes a datetime.date, not a str: ...
>>> us.SETTLEMENT.is_holiday(datetime.datetime(2024, 7, 4, 23, 30))
Traceback (most recent call last):
TypeError: fasti takes a datetime.date, not a datetime.datetime: ...

```

- **Values are immutable.** Every mutator returns a new value; every
  value compares, hashes and pickles.

The operators the crate defines are spelled the Python way, and only
those: a date steps by a period, and the two enums the crate orders —
`Weekday` and `Frequency` — are the two that sort.

```python
>>> import datetime
>>> from fasti import Frequency, Period, Weekday
>>> datetime.date(2026, 1, 15) + Period.months(6)
datetime.date(2026, 7, 15)
>>> datetime.date(2026, 1, 31) + Period.months(1)   # Add clamps; it never snaps
datetime.date(2026, 2, 28)
>>> sorted([Weekday.SUN, Weekday.MON]), Frequency.ANNUAL < Frequency.MONTHLY
([Weekday.MON, Weekday.SUN], True)

```

End-of-month preservation is `Calendar.advance`'s business, not `+`'s —
the same split the crate makes between `Add` and `Date::advance`. A
`BusinessDayConvention` refuses `<` because the crate gives it no order.

## Quickstart

```python
>>> import datetime
>>> from fasti import DayCount, Period, Rule, Schedule
>>> from fasti.calendars import us

>>> us.SETTLEMENT.is_business_day(datetime.date(2024, 7, 4))
False
>>> us.SETTLEMENT.adjust(datetime.date(2024, 7, 4), "following")
datetime.date(2024, 7, 5)
>>> us.SETTLEMENT.advance(datetime.date(2025, 1, 31), Period.months(1),
...                       "modified following", True)
datetime.date(2025, 2, 28)

>>> schedule = Schedule(datetime.date(2025, 1, 15), datetime.date(2026, 1, 15),
...                     "semiannual", us.GOVERNMENT_BOND)
>>> schedule.dates()
[datetime.date(2025, 1, 15), datetime.date(2025, 7, 15), datetime.date(2026, 1, 15)]

>>> DayCount.act_act_icma("semiannual").bind(schedule).year_fraction(
...     datetime.date(2025, 1, 15), datetime.date(2025, 4, 15))
Fraction(45, 181)

```

A `Schedule` is its coupon dates: it has a length, indexes, slices,
iterates and reverses, because the crate's schedule derefs to `[Date]`.
Slicing gives you the dates; `after` and `until` are what give you back a
schedule, since a bare run of dates names no lattice.

```python
>>> schedule[1:], schedule[-1], len(schedule)
([datetime.date(2025, 7, 15), datetime.date(2026, 1, 15)], datetime.date(2026, 1, 15), 3)

```

Calendars compose:

```python
>>> from fasti.calendars import france, us
>>> joint = us.SETTLEMENT.union(france.SETTLEMENT)
>>> joint.is_holiday(datetime.date(2026, 7, 14)), joint.is_holiday(datetime.date(2026, 11, 26))
(True, True)
>>> quiet = joint.with_rule(Rule.one_off(datetime.date(2026, 8, 3))).with_name("Acme")
>>> quiet.name
'Acme'

```

## Reading the API

One rule settles what is a property and what is a method: **a field in the
crate is a property here, a method there is a method here.** So
`calendar.name` and `period.unit` are properties, while
`day_count.name()`, `period.length()` and `frequency.per_year()` are
calls. It is not the usual Python instinct — the usual instinct is that
anything cheap is a property — but it means you can read the Rust docs and
know what to type without a second table.

## Equality and pickling

`Calendar` and `Rule` compare **structurally** — two values are equal
when they were built the same way. That is not "these two agree on every
date": settling that means walking 1901 through 2199. (`Rule` cannot
derive equality in the crate at all, since its escape hatch holds a
function pointer.)

Everything pickles. A built-in calendar travels as its registry name,
because it holds those function-pointer rules; a derived one travels as
the operations applied to it; a generated `Schedule` replays its
generation arguments, because its stub reference grid is not recoverable
from the dates alone.

## Typing

The package ships type stubs and `py.typed`. Date positions are spelled
`datetime.date`.

A type checker cannot reject a `datetime.datetime` where a
`datetime.date` is wanted — `datetime` subclasses `date`, so it type-checks.
The runtime rejects it.

## What is not here

- `Rule.Custom`. The crate's escape hatch is a bare `fn(Date) -> bool`
  pointer, which no Python callable can be.
- `Date`, `Year`, `Month`, `Ordinal`, `Weekend`, `Fraction`. Python
  already has `datetime.date` and `fractions.Fraction`; months, years
  and ordinals are `int`s; a weekend is a sequence of `Weekday`.

## Development

See [CONTRIBUTING.md](../../CONTRIBUTING.md). In short:

```console
$ uv sync            # or: pip install maturin pytest mypy
$ maturin develop
$ pytest && mypy
```

## License

`Apache-2.0 OR MIT`, the same as the crate.

