Compute with dated monetary values.
Project description
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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file dated_money-2.1.0.tar.gz.
File metadata
- Download URL: dated_money-2.1.0.tar.gz
- Upload date:
- Size: 88.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.7.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8a4fad80c38ba7ce39bad449c6626fc7061d2afce9b04333e5c7db9d0a4d2548
|
|
| MD5 |
617027dcb353f33b64256041aac78be8
|
|
| BLAKE2b-256 |
9c22f4043dfa0dd83d9bdd76d541d2e376a703ea974e953cdeeff21f50250d28
|
File details
Details for the file dated_money-2.1.0-py3-none-any.whl.
File metadata
- Download URL: dated_money-2.1.0-py3-none-any.whl
- Upload date:
- Size: 18.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.7.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9189a07baea5b2b74e5284f47bdad34b792607969e4754e93d99095df3e0d24b
|
|
| MD5 |
498e114cfed7f3d65f604783e4de5647
|
|
| BLAKE2b-256 |
8fea73e22c33bb31bc434cd67729efa97cab38b9012a0f3b32e9abff9854972b
|