Skip to content

juanre/dated-money

Repository files navigation

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 cents
  • amount(currency=None, rounding=False): Get decimal amount
  • to(currency, on_date=None): Convert to another currency
  • on(date): Create new instance with different date
  • parse(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:

  1. Local SQLite cache
  2. Git repository (if configured)
  3. Supabase (if configured)
  4. 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 a money subdirectory

  • SUPABASE_URL and SUPABASE_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.

About

Compute with monetary values.

Resources

License

Stars

2 stars

Watchers

1 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages