Metadata-Version: 2.4
Name: ecbfx
Version: 0.3.0
Summary: ECB foreign exchange rate lookup — CLI and Python API with gap-fill, strict mode, and scripting support
Project-URL: Homepage, https://github.com/edvinassvedas-dev/ecbfx
Project-URL: Repository, https://github.com/edvinassvedas-dev/ecbfx
Project-URL: Issues, https://github.com/edvinassvedas-dev/ecbfx/issues
Author: Edvinas Švedas
License: MIT
License-File: LICENSE
Keywords: cli,currency,ecb,exchange-rate,finance,forex,fx
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Office/Business :: Financial
Classifier: Topic :: Utilities
Requires-Python: >=3.9
Requires-Dist: requests>=2.28
Provides-Extra: dev
Requires-Dist: pytest-cov; extra == 'dev'
Requires-Dist: pytest>=7; extra == 'dev'
Requires-Dist: responses; extra == 'dev'
Requires-Dist: ruff; extra == 'dev'
Description-Content-Type: text/markdown

# ecbfx

[![PyPI version](https://img.shields.io/pypi/v/ecbfx.svg)](https://pypi.org/project/ecbfx/)
[![Python](https://img.shields.io/pypi/pyversions/ecbfx.svg)](https://pypi.org/project/ecbfx/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/edvinassvedas-dev/ecbfx/blob/main/LICENSE)

A minimal command-line tool and Python library for fetching EUR foreign exchange
rates directly from the [ECB SDMX API](https://data-api.ecb.europa.eu).

---

## Install

```bash
pip install ecbfx
```

Working on `ecbfx`:

```bash
git clone https://github.com/edvinassvedas-dev/ecbfx.git
cd ecbfx && pip install -e ".[dev]"
pytest
```

## CLI usage

```bash
# Today's rate for USD (indirect: USD per 1 EUR — ECB native)
# Note: ECB publishes rates ~16:00 CET on trading days.
# If today's rate isn't available yet, use --latest instead.
ecbfx USD

# Specific date, and multiple currencies at once
ecbfx USD 2025-01-15
ecbfx USD GBP CHF 2025-01-15

# Date range
ecbfx USD GBP --from 2025-01-01 --to 2025-03-31

# Direct convention: EUR per 1 foreign unit (inverted)
ecbfx USD 2025-01-15 --direct

# Most recent available rate (ignores today being a weekend/holiday)
ecbfx USD --latest

# Single value only — ideal for shell scripting
RATE=$(ecbfx USD --quiet)

# CSV output (pipe-friendly)
ecbfx USD --from 2025-01-01 --to 2025-01-31 --csv > rates.csv

# Read (date, currency) pairs from a file or stdin — one HTTP call per currency
ecbfx --pairs transactions.csv --direct --csv
cat transactions.csv | ecbfx --pairs - --direct --csv
```

### Convention

| Flag | Formula | Example |
|---|---|---|
| *(default)* | foreign units per 1 EUR | `1 EUR = 1.0830 USD` |
| `--direct` | EUR per 1 foreign unit | `1 USD = 0.9234 EUR` |

ECB publishes indirect natively. `--direct` inverts the rate.
The `convention` column in CSV output (`USD/EUR` or `EUR/USD`) makes the
direction explicit for downstream pipelines.

### Flags reference

| Flag | Default | Description |
|---|---|---|
| `--from YYYY-MM-DD` | — | Start of date range (inclusive). Also filters `--pairs` input |
| `--to YYYY-MM-DD` | — | End of date range (inclusive). Requires `--from` in range mode |
| `--direct` | off | EUR per 1 foreign unit instead of ECB native |
| `--latest` | off | Most recent available rate, regardless of date |
| `--quiet` / `-q` | off | Print rate value(s) only — ideal for scripting |
| `--decimal N` / `--decimals N` | 4 | Output precision. Decimal places normally; with `--direct` a floor on *significant* digits, so high-ratio currencies (JPY, HUF, KRW) keep full precision instead of collapsing to `0.0007` |
| `--timeout N` | 30 | HTTP timeout in seconds, per attempt (3 attempts with backoff) |
| `--no-gap-fill` | off | Raise an error on weekends/holidays instead of substituting the nearest rate |
| `--csv` | off | CSV output instead of formatted table |
| `--pairs FILE\|-` | — | Read `date,currency` pairs from a file or stdin. Comma, tab or space separated; surrounding quotes are tolerated, so spreadsheet exports work as-is |
| `--version` | — | Print the installed version and exit |

### Weekend and holiday gap-filling

ECB only publishes rates on trading days. By default, `ecbfx` automatically
uses the most recent prior trading day's rate (Last Observation Carried Forward)
when a requested date falls on a weekend or public holiday — including the first
date in a range that starts on a holiday such as January 1st.

A future date is never substituted, so a rate is never influenced by information
that did not exist on the date requested. If no trading day exists within 5
calendar days before the target — the widest real gap in the ECB calendar — the
lookup fails rather than reaching further back for a staler rate.

Use `--no-gap-fill` to disable substitution entirely and receive an explicit
error instead — useful in audit workflows where a substituted rate is not
acceptable.

### Exit codes

| Code | Meaning |
|---|---|
| `0` | Success |
| `1` | Runtime error (ECB API failure, network issue, no data returned) |
| `2` | Usage error (invalid arguments, bad date format, missing required flag) |

Useful for scripting:
```bash
ecbfx USD --quiet || echo "fetch failed, exit $?"
```

---

## Python API

```python
from datetime import date
from ecbfx import fetch_rates, fetch_rates_for_pairs, fetch_latest

# Indirect (default) — foreign units per 1 EUR, contiguous range
rows = fetch_rates(["USD", "GBP"], date(2025, 1, 1), date(2025, 1, 31))

# Direct — EUR per 1 foreign unit
rows = fetch_rates(["USD"], date(2025, 1, 15), date(2025, 1, 15), direct=True)

# Most recent available rate
rows = fetch_latest(["USD", "CHF"])

# Sparse transaction dates — one HTTP call per currency regardless of pair count
pairs = [
    (date(2025, 1, 15), "USD"),
    (date(2025, 1, 20), "GBP"),
    (date(2025, 2,  3), "USD"),
]
rows = fetch_rates_for_pairs(pairs, direct=True)

# Strict mode — raises ECBError on weekends/holidays instead of substituting
rows = fetch_rates(["USD"], date(2025, 1, 13), date(2025, 1, 13), gap_fill=False)

for r in rows:
    print(r["date"], r["currency"], r["convention"], r["rate"])
```

Both fetch functions accept `direct`, `decimals`, `gap_fill`, `timeout` and an
optional `session` for connection reuse.

### Behaviour worth knowing

- **`fetch_rates` is all-or-nothing across currencies.** A malformed code raises
  before any request. A well-formed code the ECB will not serve raises partway
  through: rows already fetched are discarded and later currencies are never
  requested. Call once per currency if you need per-currency isolation.
- **Duplicate currencies collapse.** `["USD", "usd"]` is one HTTP call and one
  row per date.
- **Output order differs between the two functions** — see the table below.

### Input validation

```python
from ecbfx import validate_currency, ECBError

# Normalises and validates a currency code — raises ECBError if invalid
print(validate_currency("usd"))    # → "USD"
print(validate_currency("  GBP "))  # → "GBP"

try:
    validate_currency("US$")
except ECBError as e:
    print(e)
    # Invalid currency code 'US$'. Expected a 3-letter ISO 4217 code, e.g. USD, GBP, CHF.
```

Use `ECBError` in `try/except` blocks when calling any `ecbfx` function
to handle API failures, network errors, or invalid inputs cleanly.

### fetch_rates vs fetch_rates_for_pairs

| | `fetch_rates` | `fetch_rates_for_pairs` |
|---|---|---|
| Input | currency list + date range | list of `(date, currency)` tuples |
| Returns | every calendar day in range | exactly the requested dates |
| Order | sorted by `(date, currency)` | matches input order, repeats included |
| Best for | daily pipelines, backfill | transaction enrichment, broker CSVs |

Both make one HTTP call per currency.

> **Note:** `fetch_rates_for_pairs` fetches the full span from the earliest to
> the latest date per currency in that single call. For very sparse data (e.g.
> two transactions 10 years apart) this pulls the entire intervening range; a
> warning is logged when the span exceeds one year.

---

## License

MIT
