ecbfx 0.2.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ecbfx-0.3.0/.github/workflows/ci.yml +76 -0
- {ecbfx-0.2.0 → ecbfx-0.3.0}/.gitignore +5 -0
- ecbfx-0.3.0/CHANGELOG.md +40 -0
- {ecbfx-0.2.0 → ecbfx-0.3.0}/PKG-INFO +45 -39
- {ecbfx-0.2.0 → ecbfx-0.3.0}/README.md +42 -38
- {ecbfx-0.2.0 → ecbfx-0.3.0}/pyproject.toml +19 -1
- {ecbfx-0.2.0 → ecbfx-0.3.0}/src/ecbfx/__init__.py +3 -5
- {ecbfx-0.2.0 → ecbfx-0.3.0}/src/ecbfx/api.py +165 -46
- ecbfx-0.3.0/src/ecbfx/cli.py +599 -0
- ecbfx-0.3.0/src/ecbfx/py.typed +0 -0
- {ecbfx-0.2.0 → ecbfx-0.3.0}/tests/test_ecbfx.py +745 -14
- ecbfx-0.2.0/CHANGELOG.md +0 -10
- ecbfx-0.2.0/src/ecbfx/cli.py +0 -490
- {ecbfx-0.2.0 → ecbfx-0.3.0}/LICENSE +0 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
# One job per advertised Python. The classifiers in pyproject.toml claim
|
|
12
|
+
# 3.9-3.14; until this matrix went green the claim was never checked, and
|
|
13
|
+
# date.fromisoformat had already been shown to behave differently across it.
|
|
14
|
+
# Keep the matrix and the classifier list in sync — a version in one and not
|
|
15
|
+
# the other is the bug this job exists to prevent.
|
|
16
|
+
name: test (py${{ matrix.python-version }})
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
strategy:
|
|
19
|
+
# Report every failing version, not just the first: knowing whether 3.9
|
|
20
|
+
# alone breaks or the whole lower half does is the point of the matrix.
|
|
21
|
+
fail-fast: false
|
|
22
|
+
matrix:
|
|
23
|
+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v4
|
|
26
|
+
|
|
27
|
+
- uses: actions/setup-python@v5
|
|
28
|
+
with:
|
|
29
|
+
python-version: ${{ matrix.python-version }}
|
|
30
|
+
|
|
31
|
+
- name: Install
|
|
32
|
+
run: python -m pip install --upgrade pip && python -m pip install -e ".[dev]"
|
|
33
|
+
|
|
34
|
+
- name: Test
|
|
35
|
+
run: python -m pytest
|
|
36
|
+
|
|
37
|
+
lint:
|
|
38
|
+
# Ruff's result does not vary by interpreter, so it runs once rather than
|
|
39
|
+
# six times. The pinned version is the linter's runtime, not the package's.
|
|
40
|
+
name: lint
|
|
41
|
+
runs-on: ubuntu-latest
|
|
42
|
+
steps:
|
|
43
|
+
- uses: actions/checkout@v4
|
|
44
|
+
|
|
45
|
+
- uses: actions/setup-python@v5
|
|
46
|
+
with:
|
|
47
|
+
python-version: "3.13"
|
|
48
|
+
|
|
49
|
+
- name: Install
|
|
50
|
+
run: python -m pip install --upgrade pip && python -m pip install -e ".[dev]"
|
|
51
|
+
|
|
52
|
+
- name: Ruff
|
|
53
|
+
run: python -m ruff check src/ tests/
|
|
54
|
+
|
|
55
|
+
build:
|
|
56
|
+
# Catches packaging breakage before a tag rather than after: the wheel must
|
|
57
|
+
# install and the console script must run in an environment that never saw
|
|
58
|
+
# the source tree.
|
|
59
|
+
name: build + smoke-test artifacts
|
|
60
|
+
runs-on: ubuntu-latest
|
|
61
|
+
steps:
|
|
62
|
+
- uses: actions/checkout@v4
|
|
63
|
+
|
|
64
|
+
- uses: actions/setup-python@v5
|
|
65
|
+
with:
|
|
66
|
+
python-version: "3.13"
|
|
67
|
+
|
|
68
|
+
- name: Build
|
|
69
|
+
run: python -m pip install --upgrade pip build && python -m build
|
|
70
|
+
|
|
71
|
+
- name: Install the wheel into a clean environment
|
|
72
|
+
run: |
|
|
73
|
+
python -m venv /tmp/smoke
|
|
74
|
+
/tmp/smoke/bin/pip install dist/*.whl
|
|
75
|
+
/tmp/smoke/bin/python -c "import ecbfx; print(ecbfx.__version__)"
|
|
76
|
+
/tmp/smoke/bin/ecbfx --version
|
ecbfx-0.3.0/CHANGELOG.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.0 — 2026-08-01
|
|
4
|
+
- Removed `_validate_currency`, the deprecated alias for `validate_currency`
|
|
5
|
+
- Added `--timeout N`, and a `timeout` parameter on the three fetch functions
|
|
6
|
+
- Added `--decimals` as an alias for `--decimal`
|
|
7
|
+
- Added `py.typed` — annotations are now visible to type checkers
|
|
8
|
+
- Added Python 3.14 classifier; CI runs the suite on 3.9–3.14
|
|
9
|
+
- Added `User-Agent: ecbfx/<version>` on outgoing requests
|
|
10
|
+
- `--pairs` accepts quoted fields — spreadsheet exports work as-is
|
|
11
|
+
- Currency codes must now be exactly three letters (`US`, `USDD` were accepted)
|
|
12
|
+
- Dates must now be `YYYY-MM-DD` — `20250115` and ISO week dates parsed only on 3.11+
|
|
13
|
+
- `fetch_rates()` collapses duplicate currencies, matching `fetch_rates_for_pairs()`
|
|
14
|
+
- Library installs a `logging.NullHandler`; skipped non-positive rates are logged
|
|
15
|
+
- Fixed: CLI was unusable on Python 3.9 — argparse rejected the `nargs="*"` positional
|
|
16
|
+
- Fixed: `--direct` lost precision above ratio 10 — KRW gave `0.0007`, not `0.0006656`
|
|
17
|
+
- Fixed: table output misaligned in both tty and non-tty modes
|
|
18
|
+
- Fixed: mistyped dates reported as invalid currency codes, exit 1 instead of 2
|
|
19
|
+
- Fixed: `USD 2025-01-15 --latest` exited 1 — the unreachable `DATE` positional is gone
|
|
20
|
+
- Fixed: `--pairs` with a malformed `--from`/`--to` crashed with a traceback
|
|
21
|
+
- Fixed: `fetch_latest()` rejected padded codes (`" usd "`)
|
|
22
|
+
- Fixed: `fetch_rates()` with `start` after `end` fetched, then returned `[]`
|
|
23
|
+
- Fixed: HTML error pages from the ECB are retried instead of failing outright
|
|
24
|
+
- Fixed: a UTF-8 BOM could be reported as a bad payload
|
|
25
|
+
- Fixed: HTTP session is closed on every exit path (`ResourceWarning` under `-W error`)
|
|
26
|
+
- Fixed: gap-fill error says "calendar days"; `resolve_rate()` docstring matches the code
|
|
27
|
+
|
|
28
|
+
Minor bump, not a patch: currency and date validation both narrowed, duplicate
|
|
29
|
+
currencies collapse, an inverted range raises instead of returning `[]`, and
|
|
30
|
+
direct-mode output changes for rates above 10 (JPY, HUF, KRW). Rates below 10 —
|
|
31
|
+
USD, GBP, CHF and the rest — are byte-identical to 0.2.0.
|
|
32
|
+
|
|
33
|
+
## 0.2.0 — 2026-05-01
|
|
34
|
+
- Added `fetch_rates_for_pairs()` — sparse transaction date support
|
|
35
|
+
- Added `fetch_latest()` — most recent available rate
|
|
36
|
+
- Added `validate_currency()` — public input validation
|
|
37
|
+
- Added `--pairs`, `--latest`, `--quiet`, `--decimal`, `--no-gap-fill` CLI flags
|
|
38
|
+
- LOCF gap-filling — always uses prior trading day, never look-ahead
|
|
39
|
+
- HTTP session reuse, exponential backoff with jitter
|
|
40
|
+
- Fixed: `re.fullmatch` for currency validation (trailing newline bypass)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ecbfx
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: ECB foreign exchange rate lookup — CLI and Python API with gap-fill, strict mode, and scripting support
|
|
5
5
|
Project-URL: Homepage, https://github.com/edvinassvedas-dev/ecbfx
|
|
6
6
|
Project-URL: Repository, https://github.com/edvinassvedas-dev/ecbfx
|
|
@@ -20,6 +20,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.11
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.12
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
24
|
Classifier: Topic :: Office/Business :: Financial
|
|
24
25
|
Classifier: Topic :: Utilities
|
|
25
26
|
Requires-Python: >=3.9
|
|
@@ -28,6 +29,7 @@ Provides-Extra: dev
|
|
|
28
29
|
Requires-Dist: pytest-cov; extra == 'dev'
|
|
29
30
|
Requires-Dist: pytest>=7; extra == 'dev'
|
|
30
31
|
Requires-Dist: responses; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
31
33
|
Description-Content-Type: text/markdown
|
|
32
34
|
|
|
33
35
|
# ecbfx
|
|
@@ -47,12 +49,11 @@ rates directly from the [ECB SDMX API](https://data-api.ecb.europa.eu).
|
|
|
47
49
|
pip install ecbfx
|
|
48
50
|
```
|
|
49
51
|
|
|
50
|
-
|
|
52
|
+
Working on `ecbfx`:
|
|
51
53
|
|
|
52
54
|
```bash
|
|
53
55
|
git clone https://github.com/edvinassvedas-dev/ecbfx.git
|
|
54
|
-
cd ecbfx
|
|
55
|
-
pip install -e ".[dev]"
|
|
56
|
+
cd ecbfx && pip install -e ".[dev]"
|
|
56
57
|
pytest
|
|
57
58
|
```
|
|
58
59
|
|
|
@@ -64,10 +65,8 @@ pytest
|
|
|
64
65
|
# If today's rate isn't available yet, use --latest instead.
|
|
65
66
|
ecbfx USD
|
|
66
67
|
|
|
67
|
-
# Specific date
|
|
68
|
+
# Specific date, and multiple currencies at once
|
|
68
69
|
ecbfx USD 2025-01-15
|
|
69
|
-
|
|
70
|
-
# Multiple currencies, specific date
|
|
71
70
|
ecbfx USD GBP CHF 2025-01-15
|
|
72
71
|
|
|
73
72
|
# Date range
|
|
@@ -80,27 +79,14 @@ ecbfx USD 2025-01-15 --direct
|
|
|
80
79
|
ecbfx USD --latest
|
|
81
80
|
|
|
82
81
|
# Single value only — ideal for shell scripting
|
|
83
|
-
ecbfx USD --latest --quiet
|
|
84
82
|
RATE=$(ecbfx USD --quiet)
|
|
85
83
|
|
|
86
|
-
# Control decimal precision (default: 4)
|
|
87
|
-
ecbfx USD 2025-01-15 --decimal 6
|
|
88
|
-
|
|
89
|
-
# Strict mode — error instead of gap-filling on weekends/holidays
|
|
90
|
-
ecbfx USD 2025-01-15 --no-gap-fill
|
|
91
|
-
|
|
92
84
|
# CSV output (pipe-friendly)
|
|
93
|
-
ecbfx USD --from 2025-01-01 --to 2025-01-31 --csv
|
|
94
85
|
ecbfx USD --from 2025-01-01 --to 2025-01-31 --csv > rates.csv
|
|
95
86
|
|
|
96
|
-
# Read pairs from a file — one HTTP call per currency
|
|
87
|
+
# Read (date, currency) pairs from a file or stdin — one HTTP call per currency
|
|
97
88
|
ecbfx --pairs transactions.csv --direct --csv
|
|
98
|
-
|
|
99
|
-
# Read pairs from stdin
|
|
100
89
|
cat transactions.csv | ecbfx --pairs - --direct --csv
|
|
101
|
-
|
|
102
|
-
# Combine with date filter
|
|
103
|
-
ecbfx --pairs transactions.csv --from 2025-01-01 --to 2025-03-31 --csv
|
|
104
90
|
```
|
|
105
91
|
|
|
106
92
|
### Convention
|
|
@@ -118,13 +104,17 @@ direction explicit for downstream pipelines.
|
|
|
118
104
|
|
|
119
105
|
| Flag | Default | Description |
|
|
120
106
|
|---|---|---|
|
|
107
|
+
| `--from YYYY-MM-DD` | — | Start of date range (inclusive). Also filters `--pairs` input |
|
|
108
|
+
| `--to YYYY-MM-DD` | — | End of date range (inclusive). Requires `--from` in range mode |
|
|
121
109
|
| `--direct` | off | EUR per 1 foreign unit instead of ECB native |
|
|
122
110
|
| `--latest` | off | Most recent available rate, regardless of date |
|
|
123
111
|
| `--quiet` / `-q` | off | Print rate value(s) only — ideal for scripting |
|
|
124
|
-
| `--decimal N` | 4 | Decimal places
|
|
112
|
+
| `--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` |
|
|
113
|
+
| `--timeout N` | 30 | HTTP timeout in seconds, per attempt (3 attempts with backoff) |
|
|
125
114
|
| `--no-gap-fill` | off | Raise an error on weekends/holidays instead of substituting the nearest rate |
|
|
126
115
|
| `--csv` | off | CSV output instead of formatted table |
|
|
127
|
-
| `--pairs FILE\|-` | — | Read `date,currency` pairs from a file or stdin |
|
|
116
|
+
| `--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 |
|
|
117
|
+
| `--version` | — | Print the installed version and exit |
|
|
128
118
|
|
|
129
119
|
### Weekend and holiday gap-filling
|
|
130
120
|
|
|
@@ -133,11 +123,14 @@ uses the most recent prior trading day's rate (Last Observation Carried Forward)
|
|
|
133
123
|
when a requested date falls on a weekend or public holiday — including the first
|
|
134
124
|
date in a range that starts on a holiday such as January 1st.
|
|
135
125
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
126
|
+
A future date is never substituted, so a rate is never influenced by information
|
|
127
|
+
that did not exist on the date requested. If no trading day exists within 5
|
|
128
|
+
calendar days before the target — the widest real gap in the ECB calendar — the
|
|
129
|
+
lookup fails rather than reaching further back for a staler rate.
|
|
140
130
|
|
|
131
|
+
Use `--no-gap-fill` to disable substitution entirely and receive an explicit
|
|
132
|
+
error instead — useful in audit workflows where a substituted rate is not
|
|
133
|
+
acceptable.
|
|
141
134
|
|
|
142
135
|
### Exit codes
|
|
143
136
|
|
|
@@ -152,6 +145,8 @@ Useful for scripting:
|
|
|
152
145
|
ecbfx USD --quiet || echo "fetch failed, exit $?"
|
|
153
146
|
```
|
|
154
147
|
|
|
148
|
+
---
|
|
149
|
+
|
|
155
150
|
## Python API
|
|
156
151
|
|
|
157
152
|
```python
|
|
@@ -172,22 +167,28 @@ pairs = [
|
|
|
172
167
|
(date(2025, 1, 15), "USD"),
|
|
173
168
|
(date(2025, 1, 20), "GBP"),
|
|
174
169
|
(date(2025, 2, 3), "USD"),
|
|
175
|
-
(date(2025, 2, 3), "CHF"),
|
|
176
170
|
]
|
|
177
171
|
rows = fetch_rates_for_pairs(pairs, direct=True)
|
|
178
172
|
|
|
179
|
-
#
|
|
180
|
-
rows = fetch_rates(["USD"], date(2025, 1, 15), date(2025, 1, 15), decimals=6)
|
|
181
|
-
|
|
182
|
-
# Strict mode — raises ECBError on weekends/holidays
|
|
173
|
+
# Strict mode — raises ECBError on weekends/holidays instead of substituting
|
|
183
174
|
rows = fetch_rates(["USD"], date(2025, 1, 13), date(2025, 1, 13), gap_fill=False)
|
|
184
|
-
rows = fetch_rates_for_pairs([(date(2025, 1, 13), "USD")], gap_fill=False)
|
|
185
175
|
|
|
186
176
|
for r in rows:
|
|
187
177
|
print(r["date"], r["currency"], r["convention"], r["rate"])
|
|
188
178
|
```
|
|
189
179
|
|
|
190
|
-
|
|
180
|
+
Both fetch functions accept `direct`, `decimals`, `gap_fill`, `timeout` and an
|
|
181
|
+
optional `session` for connection reuse.
|
|
182
|
+
|
|
183
|
+
### Behaviour worth knowing
|
|
184
|
+
|
|
185
|
+
- **`fetch_rates` is all-or-nothing across currencies.** A malformed code raises
|
|
186
|
+
before any request. A well-formed code the ECB will not serve raises partway
|
|
187
|
+
through: rows already fetched are discarded and later currencies are never
|
|
188
|
+
requested. Call once per currency if you need per-currency isolation.
|
|
189
|
+
- **Duplicate currencies collapse.** `["USD", "usd"]` is one HTTP call and one
|
|
190
|
+
row per date.
|
|
191
|
+
- **Output order differs between the two functions** — see the table below.
|
|
191
192
|
|
|
192
193
|
### Input validation
|
|
193
194
|
|
|
@@ -195,13 +196,14 @@ Each row is a dict: `{date, currency, rate, convention}`.
|
|
|
195
196
|
from ecbfx import validate_currency, ECBError
|
|
196
197
|
|
|
197
198
|
# Normalises and validates a currency code — raises ECBError if invalid
|
|
198
|
-
print(validate_currency("usd"))
|
|
199
|
-
print(validate_currency(" GBP "))
|
|
199
|
+
print(validate_currency("usd")) # → "USD"
|
|
200
|
+
print(validate_currency(" GBP ")) # → "GBP"
|
|
200
201
|
|
|
201
202
|
try:
|
|
202
203
|
validate_currency("US$")
|
|
203
204
|
except ECBError as e:
|
|
204
|
-
print(e)
|
|
205
|
+
print(e)
|
|
206
|
+
# Invalid currency code 'US$'. Expected a 3-letter ISO 4217 code, e.g. USD, GBP, CHF.
|
|
205
207
|
```
|
|
206
208
|
|
|
207
209
|
Use `ECBError` in `try/except` blocks when calling any `ecbfx` function
|
|
@@ -213,11 +215,15 @@ to handle API failures, network errors, or invalid inputs cleanly.
|
|
|
213
215
|
|---|---|---|
|
|
214
216
|
| Input | currency list + date range | list of `(date, currency)` tuples |
|
|
215
217
|
| Returns | every calendar day in range | exactly the requested dates |
|
|
218
|
+
| Order | sorted by `(date, currency)` | matches input order, repeats included |
|
|
216
219
|
| Best for | daily pipelines, backfill | transaction enrichment, broker CSVs |
|
|
217
|
-
| HTTP calls | one per currency | one per currency (full span, regardless of gaps) |
|
|
218
220
|
|
|
219
|
-
|
|
221
|
+
Both make one HTTP call per currency.
|
|
220
222
|
|
|
223
|
+
> **Note:** `fetch_rates_for_pairs` fetches the full span from the earliest to
|
|
224
|
+
> the latest date per currency in that single call. For very sparse data (e.g.
|
|
225
|
+
> two transactions 10 years apart) this pulls the entire intervening range; a
|
|
226
|
+
> warning is logged when the span exceeds one year.
|
|
221
227
|
|
|
222
228
|
---
|
|
223
229
|
|
|
@@ -15,12 +15,11 @@ rates directly from the [ECB SDMX API](https://data-api.ecb.europa.eu).
|
|
|
15
15
|
pip install ecbfx
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
Working on `ecbfx`:
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
21
|
git clone https://github.com/edvinassvedas-dev/ecbfx.git
|
|
22
|
-
cd ecbfx
|
|
23
|
-
pip install -e ".[dev]"
|
|
22
|
+
cd ecbfx && pip install -e ".[dev]"
|
|
24
23
|
pytest
|
|
25
24
|
```
|
|
26
25
|
|
|
@@ -32,10 +31,8 @@ pytest
|
|
|
32
31
|
# If today's rate isn't available yet, use --latest instead.
|
|
33
32
|
ecbfx USD
|
|
34
33
|
|
|
35
|
-
# Specific date
|
|
34
|
+
# Specific date, and multiple currencies at once
|
|
36
35
|
ecbfx USD 2025-01-15
|
|
37
|
-
|
|
38
|
-
# Multiple currencies, specific date
|
|
39
36
|
ecbfx USD GBP CHF 2025-01-15
|
|
40
37
|
|
|
41
38
|
# Date range
|
|
@@ -48,27 +45,14 @@ ecbfx USD 2025-01-15 --direct
|
|
|
48
45
|
ecbfx USD --latest
|
|
49
46
|
|
|
50
47
|
# Single value only — ideal for shell scripting
|
|
51
|
-
ecbfx USD --latest --quiet
|
|
52
48
|
RATE=$(ecbfx USD --quiet)
|
|
53
49
|
|
|
54
|
-
# Control decimal precision (default: 4)
|
|
55
|
-
ecbfx USD 2025-01-15 --decimal 6
|
|
56
|
-
|
|
57
|
-
# Strict mode — error instead of gap-filling on weekends/holidays
|
|
58
|
-
ecbfx USD 2025-01-15 --no-gap-fill
|
|
59
|
-
|
|
60
50
|
# CSV output (pipe-friendly)
|
|
61
|
-
ecbfx USD --from 2025-01-01 --to 2025-01-31 --csv
|
|
62
51
|
ecbfx USD --from 2025-01-01 --to 2025-01-31 --csv > rates.csv
|
|
63
52
|
|
|
64
|
-
# Read pairs from a file — one HTTP call per currency
|
|
53
|
+
# Read (date, currency) pairs from a file or stdin — one HTTP call per currency
|
|
65
54
|
ecbfx --pairs transactions.csv --direct --csv
|
|
66
|
-
|
|
67
|
-
# Read pairs from stdin
|
|
68
55
|
cat transactions.csv | ecbfx --pairs - --direct --csv
|
|
69
|
-
|
|
70
|
-
# Combine with date filter
|
|
71
|
-
ecbfx --pairs transactions.csv --from 2025-01-01 --to 2025-03-31 --csv
|
|
72
56
|
```
|
|
73
57
|
|
|
74
58
|
### Convention
|
|
@@ -86,13 +70,17 @@ direction explicit for downstream pipelines.
|
|
|
86
70
|
|
|
87
71
|
| Flag | Default | Description |
|
|
88
72
|
|---|---|---|
|
|
73
|
+
| `--from YYYY-MM-DD` | — | Start of date range (inclusive). Also filters `--pairs` input |
|
|
74
|
+
| `--to YYYY-MM-DD` | — | End of date range (inclusive). Requires `--from` in range mode |
|
|
89
75
|
| `--direct` | off | EUR per 1 foreign unit instead of ECB native |
|
|
90
76
|
| `--latest` | off | Most recent available rate, regardless of date |
|
|
91
77
|
| `--quiet` / `-q` | off | Print rate value(s) only — ideal for scripting |
|
|
92
|
-
| `--decimal N` | 4 | Decimal places
|
|
78
|
+
| `--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` |
|
|
79
|
+
| `--timeout N` | 30 | HTTP timeout in seconds, per attempt (3 attempts with backoff) |
|
|
93
80
|
| `--no-gap-fill` | off | Raise an error on weekends/holidays instead of substituting the nearest rate |
|
|
94
81
|
| `--csv` | off | CSV output instead of formatted table |
|
|
95
|
-
| `--pairs FILE\|-` | — | Read `date,currency` pairs from a file or stdin |
|
|
82
|
+
| `--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 |
|
|
83
|
+
| `--version` | — | Print the installed version and exit |
|
|
96
84
|
|
|
97
85
|
### Weekend and holiday gap-filling
|
|
98
86
|
|
|
@@ -101,11 +89,14 @@ uses the most recent prior trading day's rate (Last Observation Carried Forward)
|
|
|
101
89
|
when a requested date falls on a weekend or public holiday — including the first
|
|
102
90
|
date in a range that starts on a holiday such as January 1st.
|
|
103
91
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
92
|
+
A future date is never substituted, so a rate is never influenced by information
|
|
93
|
+
that did not exist on the date requested. If no trading day exists within 5
|
|
94
|
+
calendar days before the target — the widest real gap in the ECB calendar — the
|
|
95
|
+
lookup fails rather than reaching further back for a staler rate.
|
|
108
96
|
|
|
97
|
+
Use `--no-gap-fill` to disable substitution entirely and receive an explicit
|
|
98
|
+
error instead — useful in audit workflows where a substituted rate is not
|
|
99
|
+
acceptable.
|
|
109
100
|
|
|
110
101
|
### Exit codes
|
|
111
102
|
|
|
@@ -120,6 +111,8 @@ Useful for scripting:
|
|
|
120
111
|
ecbfx USD --quiet || echo "fetch failed, exit $?"
|
|
121
112
|
```
|
|
122
113
|
|
|
114
|
+
---
|
|
115
|
+
|
|
123
116
|
## Python API
|
|
124
117
|
|
|
125
118
|
```python
|
|
@@ -140,22 +133,28 @@ pairs = [
|
|
|
140
133
|
(date(2025, 1, 15), "USD"),
|
|
141
134
|
(date(2025, 1, 20), "GBP"),
|
|
142
135
|
(date(2025, 2, 3), "USD"),
|
|
143
|
-
(date(2025, 2, 3), "CHF"),
|
|
144
136
|
]
|
|
145
137
|
rows = fetch_rates_for_pairs(pairs, direct=True)
|
|
146
138
|
|
|
147
|
-
#
|
|
148
|
-
rows = fetch_rates(["USD"], date(2025, 1, 15), date(2025, 1, 15), decimals=6)
|
|
149
|
-
|
|
150
|
-
# Strict mode — raises ECBError on weekends/holidays
|
|
139
|
+
# Strict mode — raises ECBError on weekends/holidays instead of substituting
|
|
151
140
|
rows = fetch_rates(["USD"], date(2025, 1, 13), date(2025, 1, 13), gap_fill=False)
|
|
152
|
-
rows = fetch_rates_for_pairs([(date(2025, 1, 13), "USD")], gap_fill=False)
|
|
153
141
|
|
|
154
142
|
for r in rows:
|
|
155
143
|
print(r["date"], r["currency"], r["convention"], r["rate"])
|
|
156
144
|
```
|
|
157
145
|
|
|
158
|
-
|
|
146
|
+
Both fetch functions accept `direct`, `decimals`, `gap_fill`, `timeout` and an
|
|
147
|
+
optional `session` for connection reuse.
|
|
148
|
+
|
|
149
|
+
### Behaviour worth knowing
|
|
150
|
+
|
|
151
|
+
- **`fetch_rates` is all-or-nothing across currencies.** A malformed code raises
|
|
152
|
+
before any request. A well-formed code the ECB will not serve raises partway
|
|
153
|
+
through: rows already fetched are discarded and later currencies are never
|
|
154
|
+
requested. Call once per currency if you need per-currency isolation.
|
|
155
|
+
- **Duplicate currencies collapse.** `["USD", "usd"]` is one HTTP call and one
|
|
156
|
+
row per date.
|
|
157
|
+
- **Output order differs between the two functions** — see the table below.
|
|
159
158
|
|
|
160
159
|
### Input validation
|
|
161
160
|
|
|
@@ -163,13 +162,14 @@ Each row is a dict: `{date, currency, rate, convention}`.
|
|
|
163
162
|
from ecbfx import validate_currency, ECBError
|
|
164
163
|
|
|
165
164
|
# Normalises and validates a currency code — raises ECBError if invalid
|
|
166
|
-
print(validate_currency("usd"))
|
|
167
|
-
print(validate_currency(" GBP "))
|
|
165
|
+
print(validate_currency("usd")) # → "USD"
|
|
166
|
+
print(validate_currency(" GBP ")) # → "GBP"
|
|
168
167
|
|
|
169
168
|
try:
|
|
170
169
|
validate_currency("US$")
|
|
171
170
|
except ECBError as e:
|
|
172
|
-
print(e)
|
|
171
|
+
print(e)
|
|
172
|
+
# Invalid currency code 'US$'. Expected a 3-letter ISO 4217 code, e.g. USD, GBP, CHF.
|
|
173
173
|
```
|
|
174
174
|
|
|
175
175
|
Use `ECBError` in `try/except` blocks when calling any `ecbfx` function
|
|
@@ -181,11 +181,15 @@ to handle API failures, network errors, or invalid inputs cleanly.
|
|
|
181
181
|
|---|---|---|
|
|
182
182
|
| Input | currency list + date range | list of `(date, currency)` tuples |
|
|
183
183
|
| Returns | every calendar day in range | exactly the requested dates |
|
|
184
|
+
| Order | sorted by `(date, currency)` | matches input order, repeats included |
|
|
184
185
|
| Best for | daily pipelines, backfill | transaction enrichment, broker CSVs |
|
|
185
|
-
| HTTP calls | one per currency | one per currency (full span, regardless of gaps) |
|
|
186
186
|
|
|
187
|
-
|
|
187
|
+
Both make one HTTP call per currency.
|
|
188
188
|
|
|
189
|
+
> **Note:** `fetch_rates_for_pairs` fetches the full span from the earliest to
|
|
190
|
+
> the latest date per currency in that single call. For very sparse data (e.g.
|
|
191
|
+
> two transactions 10 years apart) this pulls the entire intervening range; a
|
|
192
|
+
> warning is logged when the span exceeds one year.
|
|
189
193
|
|
|
190
194
|
---
|
|
191
195
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ecbfx"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "ECB foreign exchange rate lookup — CLI and Python API with gap-fill, strict mode, and scripting support"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -27,6 +27,7 @@ classifiers = [
|
|
|
27
27
|
"Programming Language :: Python :: 3.11",
|
|
28
28
|
"Programming Language :: Python :: 3.12",
|
|
29
29
|
"Programming Language :: Python :: 3.13",
|
|
30
|
+
"Programming Language :: Python :: 3.14",
|
|
30
31
|
"Topic :: Office/Business :: Financial",
|
|
31
32
|
"Topic :: Utilities",
|
|
32
33
|
]
|
|
@@ -40,6 +41,7 @@ dev = [
|
|
|
40
41
|
"pytest>=7",
|
|
41
42
|
"pytest-cov",
|
|
42
43
|
"responses", # HTTP mocking for tests
|
|
44
|
+
"ruff", # linter — see [tool.ruff] below
|
|
43
45
|
]
|
|
44
46
|
|
|
45
47
|
[project.scripts]
|
|
@@ -53,6 +55,22 @@ Issues = "https://github.com/edvinassvedas-dev/ecbfx/issues"
|
|
|
53
55
|
[tool.hatch.build.targets.wheel]
|
|
54
56
|
packages = ["src/ecbfx"]
|
|
55
57
|
|
|
58
|
+
|
|
56
59
|
[tool.pytest.ini_options]
|
|
57
60
|
testpaths = ["tests"]
|
|
58
61
|
addopts = "-v --tb=short"
|
|
62
|
+
|
|
63
|
+
[tool.ruff]
|
|
64
|
+
# 88 is Black's default; this code is written to roughly 95, so the default
|
|
65
|
+
# would report ~9 pure-formatting hits that say nothing about correctness.
|
|
66
|
+
line-length = 100
|
|
67
|
+
|
|
68
|
+
[tool.ruff.lint]
|
|
69
|
+
# Bug-shaped rules only. The `UP`/`I` families were deliberately left out —
|
|
70
|
+
# they would flag ~37 "use `list` instead of `List`" style rewrites, and
|
|
71
|
+
# requires-python is >=3.9 where the typing imports are still needed.
|
|
72
|
+
select = ["E", "F", "B"]
|
|
73
|
+
ignore = [
|
|
74
|
+
"E731", # the DIM/BOLD/CYAN/... colour helpers read better as an aligned
|
|
75
|
+
# block of lambdas than as six separate defs
|
|
76
|
+
]
|
|
@@ -13,11 +13,9 @@ from .api import (
|
|
|
13
13
|
resolve_rate,
|
|
14
14
|
)
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
except Exception:
|
|
20
|
-
__version__ = "unknown"
|
|
16
|
+
from .api import package_version
|
|
17
|
+
|
|
18
|
+
__version__ = package_version()
|
|
21
19
|
|
|
22
20
|
__all__ = [
|
|
23
21
|
"ECBError",
|