A Python library for date-aware currency operations
Updated 2025-08-15: v2.x with
DM
factory, robust fallback for historical rates, and SQLite/PostgreSQL serialization helpers.
dated-money represents monetary values with an optional date and performs conversions using historical exchange rates with automatic fallback.
https://github.com/juanre/dated-money
Installation
uv add dated-money
or
pip install dated-money
Quickstart
from dated_money import DM, DatedMoney, Currency
# Factory: default currency/date
Eur = DM('EUR', '2024-01-01')
# Create amounts
price = Eur(100) # €100.00
from_usd = Eur(50, '$') # $50 → EUR (on 2024-01-01)
# Arithmetic: result in the second operand's currency/date
a = DatedMoney(100, 'EUR', '2024-01-01')
b = DatedMoney(50, 'USD', '2024-01-01')
res = a + b # result in USD on 2024-01-01
# Convert and format
print(res.to('EUR')) # €...
print(str(res)) # e.g. $... (rounded to cents)
print(repr(res)) # '2024-01-01 USD ...' (parseable)
# Parse from repr
parsed = DatedMoney.parse('2024-01-01 EUR 100.50')
assert parsed == DatedMoney(100.50, 'EUR', '2024-01-01')
Notes:
- Amounts accept strings with trailing ‘c’ for cents, e.g.
DatedMoney('1234c', 'EUR')
. Money
is kept as an alias toDM
for backwards compatibility.
Exchange rates and caching
Sources (in order):
- SQLite cache (auto-created)
- Local git repo
DMON_RATES_REPO
(expectsmoney/yyyy-mm-dd-rates.json
) - Supabase (
SUPABASE_URL
,SUPABASE_KEY
) - exchangerate-api.com (
DMON_EXCHANGERATE_API_KEY
)
Fallback: if a date is missing, it searches up to 10 days back and logs the date used. Once fetched, rates are cached locally.
Rate file format (yyyy-mm-dd-rates.json
):
{"conversion_rates": {"USD": 1, "EUR": 0.85, "GBP": 0.73}}
Default cache locations:
- macOS:
~/Library/Caches/dated_money/exchange-rates.db
- Linux:
~/.cache/dated_money/exchange-rates.db
- Windows:
%LOCALAPPDATA%/dated_money/cache/exchange-rates.db
- Override with
DMON_RATES_CACHE
CLI
# Create cache table
dmon-rates --create-table
# Fill cache from local repo
dmon-rates -C --update-cache
# Download historical rates (paid API key required)
dmon-rates --fetch-rates 2023-01-01:2023-12-31
# Inspect rates on a date
dmon-rates --rate-on 2024-01-15
dmon-rates --rate-on 2024-01-15 --currency EUR
Environment variables:
DMON_RATES_CACHE
,DMON_RATES_REPO
,SUPABASE_URL
,SUPABASE_KEY
,DMON_EXCHANGERATE_API_KEY
Database serialization
SQLite (automatic adapters):
import sqlite3
from dated_money import DatedMoney
from dated_money.db_serialization import register_sqlite_converters
register_sqlite_converters()
conn = sqlite3.connect(':memory:', detect_types=sqlite3.PARSE_DECLTYPES)
cur = conn.cursor()
cur.execute('CREATE TABLE t (amount DATEDMONEY)')
cur.execute('INSERT INTO t (amount) VALUES (?)', (DatedMoney(100.50, 'EUR', '2024-01-01'),))
retrieved = cur.execute('SELECT amount FROM t').fetchone()[0]
assert isinstance(retrieved, DatedMoney)
PostgreSQL (helpers):
from dated_money import DatedMoney
from dated_money.db_serialization import to_postgres, from_postgres
value = to_postgres(DatedMoney(100.50, 'EUR', '2024-01-01'))
restored = from_postgres(value)
API reference
Exported symbols
Currency
— ISO 4217 currency enum (e.g.,Currency.EUR
,Currency.USD
)DM
— factory for creating currency/date-specific constructorsMoney
— alias ofDM
(backwards compatibility)DatedMoney
— monetary value with optional dateregister_sqlite_converters
— SQLite adapter/convertor registration
DM
DM(base_currency, base_date: str | date | None = None) -> (amount, currency=None, on_date=None) -> DatedMoney
- base_currency: accepts ISO code (e.g.
'EUR'
), symbol ('€'
,'$'
,'£'
,'¥'
), orCurrency
enum. - base_date: optional default date (
'YYYY-MM-DD'
ordate
). - Returns a function that:
- When called as
Factory(amount)
, createsDatedMoney(amount, base_currency, base_date)
. - When called as
Factory(amount, other_currency, on_date)
, creates then converts tobase_currency
.
- When called as
Examples:
Eur = DM('EUR', '2024-01-01')
Eur(50) # €50.00 on 2024-01-01
Eur(50, '$') # $50 → €… on 2024-01-01
Usd = DM('$')
Usd(20, 'EUR', '2024-02-10') # €20 → $… on 2024-02-10
DatedMoney
DatedMoney(amount, currency, on_date: str | date | None = None)
Arguments:
- amount:
int | float | Decimal | str
. A trailing'c'
means cents (e.g.'1234c'
). - currency: ISO code, symbol, or
Currency
enum. - on_date:
'YYYY-MM-DD'
ordate
, optional.
Attributes:
currency: Currency
on_date: date | None
precision: int
(class var) — cents-level tolerance for equality (default0
).
Methods:
cents(in_currency: str|Currency|None = None, on_date: str|date|None = None) -> Decimal
- Converts to cents in target currency/date (defaults to instance).
amount(currency: str|Currency|None = None, rounding: bool = False) -> Decimal
- Major units (e.g., euros). Rounds to cents if
rounding=True
.
- Major units (e.g., euros). Rounds to cents if
to(currency, on_date: str|date|None = None) -> DatedMoney
- New instance in
currency
(date preserved unless overridden).
- New instance in
on(on_date: str) -> DatedMoney
- New instance with a different date.
parse(string: str) -> DatedMoney
(classmethod)- Accepts
'YYYY-MM-DD CODE AMOUNT'
or'CODE AMOUNT'
.
- Accepts
Operators:
+
and-
between money: both sides normalize to the second operand’s currency/date.*
and scalar/
: scale amount, preserve currency/date./
money-to-money: returnsDecimal
ratio.- Comparisons (
==
,!=
,<
,<=
,>
,>=
) normalize to the second operand’s currency/date.==
usesprecision
.
Display/serialization:
str(m)
: symbol + amount rounded to cents (e.g.,€100.00
).repr(m)
: parseable:'YYYY-MM-DD CODE 100.00'
or'CODE 100.00'
.- SQLite:
__conform__
returnsrepr()
for storage (see SQLite section).
Errors:
ValueError
for invalid parse formats or invalid currency codes.TypeError
for wrong argument types.RuntimeError
when exchange rates are unavailable for requested date/currencies.
License
MIT