A Python library for currency conversion with historical exchange rates.
The library provides monetary values that combine:
- An amount (stored as Decimal, in cents)
- A currency (ISO 4217 code)
- An optional date for historical conversions
Exchange rates are fetched from multiple sources with automatic fallback.
You can install dated-money using uv (recommended):
uv add dated-moneyor pip:
pip install dated-moneyFor development, clone the repository and install with development dependencies:
git clone https://github.com/juanre/dated-money
cd dated-money
uv syncfrom dated_money import DM, DatedMoney, Currency
# Create factories using currency codes or symbols
Eur = DM('EUR', '2022-07-14') # Using ISO code
Usd = DM('$', '2022-07-14') # Using currency symbol
Gbp = DM('£', '2022-07-14') # Common symbols: $, €, £, ¥, etc.
# All amounts created with Eur are in EUR base currency
price = Eur(100) # €100
payment = Eur(50, Currency.USD) # $50 converted to EUR (~ €47)
fee = Eur(20, '£') # £20 converted to EUR (~ €23) - symbols work here too
# Addition is straightforward - all in EUR
total = price + payment + fee
assert total.currency == Currency.EUR
# Direct instantiation keeps original currency
usd_amount = DatedMoney(50, '$', '2022-07-14') # Using symbol
gbp_amount = DatedMoney(20, 'GBP', '2022-07-14') # Using ISO code
# Operations with DatedMoney instances
# Result is in the last operand's currency and date
result = usd_amount + gbp_amount # Result in GBP
assert result.currency == Currency.GBP
# String representation and parsing
money = DatedMoney(100.50, 'EUR', '2022-07-14')
print(str(money)) # €100.50
print(repr(money)) # 2022-07-14 EUR 100.50
# Parse from string representation
parsed = DatedMoney.parse('2022-07-14 EUR 100.50')
assert parsed == money
# Parse without date
parsed_no_date = DatedMoney.parse('EUR 100.50')
assert parsed_no_date.currency == Currency.EURCreates a convenience function for instantiating monetary values with a default currency and date.
DM(base_currency, base_date=None)Parameters:
base_currency: Default currency - accepts:- ISO code: 'EUR', 'USD', 'GBP' (case-insensitive)
- Currency symbol: '$', '€', '£', '¥', etc.
- Currency enum: Currency.EUR
base_date: Default date for conversions (optional)
Returns a function that creates DatedMoney instances:
# Various ways to create factories
Eur = DM('EUR', '2024-01-01') # ISO code
Usd = DM('$', '2024-01-01') # Symbol with date
Gbp = DM(Currency.GBP, '2024-01-01') # Enum with date
# Create monetary values
price = Eur(100) # €100
payment = Eur(50, 'USD') # $50 → EURCore class representing a monetary value.
DatedMoney(amount, currency, on_date=None)Parameters:
amount: Numeric value or string. Append 'c' for cents (e.g., '1234c')currency: Accepts:- ISO code: 'EUR', 'USD', 'GBP' (case-insensitive)
- Currency symbol: '$', '€', '£', '¥', etc.
- Currency enum: Currency.EUR
on_date: Date string 'YYYY-MM-DD' or date object (optional)
Methods:
cents(in_currency=None, on_date=None): Get amount in centsamount(currency=None, rounding=False): Get decimal amountto(currency, on_date=None): Convert to another currencyon(date): Create new instance with different dateparse(string): Parse from string representation (class method)
String representations:
str(money): Display format with symbol, e.g., "€100.50"repr(money): Parseable format, e.g., "2022-07-14 EUR 100.50" or "EUR 100.50"
- Addition/subtraction converts to the second operand's currency
- Multiplication/division with scalars preserves currency
- Division between DatedMoney instances returns a Decimal ratio
- Comparisons use the second operand's currency for conversion
a = DatedMoney(100, 'EUR', '2024-01-01')
b = DatedMoney(50, 'USD', '2024-01-01')
# Result is in USD (second operand)
result = a + b # Converts EUR to USD, adds
assert result.currency == Currency.USD
# Scalar operations
doubled = a * 2
assert doubled.cents() == 20000
assert doubled.currency == Currency.EURRates are fetched in order:
- Local SQLite cache
- Git repository (if configured)
- Supabase (if configured)
- exchangerate-api.com
Missing rates trigger automatic fallback to previous dates (up to 10 days).
-
DMON_RATES_CACHE: Directory for the SQLite cache database (default: platform-specific cache directory - see below) -
DMON_RATES_REPO: Directory containing a git repository with exchange rates in amoneysubdirectory -
SUPABASE_URLandSUPABASE_KEY: Credentials for Supabase integration -
DMON_EXCHANGERATE_API_KEY: API key for exchangerate-api.com (required for historical rates on paid plans)
Rate files: yyyy-mm-dd-rates.json with structure:
{"conversion_rates": {"USD": 1, "EUR": 0.85, ...}}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
# Create cache table
dmon-rates --create-table
# Fetch historical rates (requires paid API key)
dmon-rates --fetch-rates 2021-10-10:2021-10-20DatedMoney objects can be stored in SQLite and PostgreSQL databases.
DatedMoney implements the SQLite adapter protocol via __conform__, allowing automatic serialization:
import sqlite3
from dated_money import DatedMoney, register_sqlite_converters
# Enable automatic conversion
register_sqlite_converters()
# Create connection with type detection
conn = sqlite3.connect(':memory:', detect_types=sqlite3.PARSE_DECLTYPES)
cursor = conn.cursor()
# Create table with DATEDMONEY type
cursor.execute('''
CREATE TABLE transactions (
id INTEGER PRIMARY KEY,
amount DATEDMONEY,
description TEXT
)
''')
# Store DatedMoney objects
money = DatedMoney(100.50, 'EUR', '2024-01-01')
cursor.execute("INSERT INTO transactions (amount, description) VALUES (?, ?)",
(money, "Payment"))
# Retrieve with automatic conversion
cursor.execute("SELECT amount FROM transactions")
retrieved = cursor.fetchone()[0]
assert isinstance(retrieved, DatedMoney)
assert retrieved == moneyFor PostgreSQL, use the helper functions for conversion:
import psycopg2
from dated_money import DatedMoney
from dated_money.db_serialization import to_postgres, from_postgres
# Connect to PostgreSQL
conn = psycopg2.connect(dbname='your_db', user='your_user', host='localhost')
cursor = conn.cursor()
# Create table
cursor.execute('''
CREATE TABLE IF NOT EXISTS transactions (
id SERIAL PRIMARY KEY,
amount TEXT,
description TEXT
)
''')
conn.commit()
# Store DatedMoney
money = DatedMoney(100.50, 'EUR', '2024-01-01')
cursor.execute(
"INSERT INTO transactions (amount, description) VALUES (%s, %s)",
(to_postgres(money), "Payment")
)
conn.commit()
# Retrieve and convert
cursor.execute("SELECT amount FROM transactions WHERE id = %s", (1,))
row = cursor.fetchone()
retrieved = from_postgres(row[0])
assert retrieved == money
conn.close()This project uses:
- uv for package management
- black for code formatting
- ruff for linting
- mypy for type checking
- pytest for testing
uv run pytest# Format code
uv run black src/ test/
# Run linter
uv run ruff check src/ test/
# Type checking
uv run mypy src/Money is maintained as an alias to DM for backwards compatibility.
Contributions are welcome! If you find any issues or have suggestions for improvements, please open an issue or submit a pull request on the GitHub repository.
dated-money is released under the MIT License.