dated-money
A Python library for currency conversion with historical exchange rates.
Overview
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.
Installation
You can install dated-money using uv (recommended):
uv add dated-money
or pip:
pip install dated-money
Development Installation
For development, clone the repository and install with development dependencies:
git clone https://github.com/juanre/dated-money
cd dated-money
uv sync
Usage
Basic Usage
from 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.EUR
API Reference
DM Factory Function
Creates 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 → EUR
DatedMoney Class
Core 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"
Arithmetic Operations
- 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.EUR
Exchange Rate Sources
Rates 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).
Environment Variables
-
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
Cache Management
# Create cache table
dmon-rates --create-table
# Fetch historical rates (requires paid API key)
dmon-rates --fetch-rates 2021-10-10:2021-10-20
Database Serialization
DatedMoney objects can be stored in SQLite and PostgreSQL databases.
SQLite
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 == money
PostgreSQL
For 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()
Development
This project uses:
- uv for package management
- black for code formatting
- ruff for linting
- mypy for type checking
- pytest for testing
Running Tests
uv run pytest
Code Quality
# Format code
uv run black src/ test/
# Run linter
uv run ruff check src/ test/
# Type checking
uv run mypy src/
Backwards Compatibility
Money is maintained as an alias to DM for backwards compatibility.
Contributing
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.
License
dated-money is released under the MIT License.
Metadata
Release files for dated-money 2.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| dated_money-2.1.0.tar.gz | 88.0 kB | Without provenance | Details for dated_money-2.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| dated_money-2.1.0-py3-none-any.whl | Python 3 | none | any | Details for dated_money-2.1.0-py3-none-any.whl |
Total release size: 106.9 kB
Release files / dated_money-2.1.0.tar.gz
| Download URL | dated_money-2.1.0.tar.gz |
|---|---|
| Size | 88.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8a4fad80c38ba7ce39bad449c6626fc7061d2afce9b04333e5c7db9d0a4d2548
|
|
BLAKE2b-256 checksum How to use checksums |
9c22f4043dfa0dd83d9bdd76d541d2e376a703ea974e953cdeeff21f50250d28
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.7.4
|
Release files / dated_money-2.1.0-py3-none-any.whl
| Download URL | dated_money-2.1.0-py3-none-any.whl |
|---|---|
| Size | 18.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9189a07baea5b2b74e5284f47bdad34b792607969e4754e93d99095df3e0d24b
|
|
BLAKE2b-256 checksum How to use checksums |
8fea73e22c33bb31bc434cd67729efa97cab38b9012a0f3b32e9abff9854972b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.7.4
|
Security for dated-money 2.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.