ecb-rate 0.5.2__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.
- ecb_rate-0.5.2/LICENSE +21 -0
- ecb_rate-0.5.2/MANIFEST.in +3 -0
- ecb_rate-0.5.2/PKG-INFO +175 -0
- ecb_rate-0.5.2/README.md +146 -0
- ecb_rate-0.5.2/ecb_rate/__init__.py +0 -0
- ecb_rate-0.5.2/ecb_rate/cli.py +133 -0
- ecb_rate-0.5.2/ecb_rate/client.py +93 -0
- ecb_rate-0.5.2/ecb_rate/custom_types.py +111 -0
- ecb_rate-0.5.2/ecb_rate/models.py +55 -0
- ecb_rate-0.5.2/ecb_rate/service.py +89 -0
- ecb_rate-0.5.2/ecb_rate.egg-info/PKG-INFO +175 -0
- ecb_rate-0.5.2/ecb_rate.egg-info/SOURCES.txt +21 -0
- ecb_rate-0.5.2/ecb_rate.egg-info/dependency_links.txt +1 -0
- ecb_rate-0.5.2/ecb_rate.egg-info/entry_points.txt +2 -0
- ecb_rate-0.5.2/ecb_rate.egg-info/requires.txt +2 -0
- ecb_rate-0.5.2/ecb_rate.egg-info/top_level.txt +1 -0
- ecb_rate-0.5.2/pyproject.toml +63 -0
- ecb_rate-0.5.2/setup.cfg +4 -0
- ecb_rate-0.5.2/tests/test_cli.py +262 -0
- ecb_rate-0.5.2/tests/test_client.py +225 -0
- ecb_rate-0.5.2/tests/test_custom_types.py +11 -0
- ecb_rate-0.5.2/tests/test_models.py +85 -0
- ecb_rate-0.5.2/tests/test_service.py +170 -0
ecb_rate-0.5.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PythBuster
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
ecb_rate-0.5.2/PKG-INFO
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ecb-rate
|
|
3
|
+
Version: 0.5.2
|
|
4
|
+
Summary: CLI for ECB JSON exchange rates.
|
|
5
|
+
Author: PythBuster
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/PythBuster/ecb-rate
|
|
8
|
+
Project-URL: Repository, https://github.com/PythBuster/ecb-rate
|
|
9
|
+
Project-URL: Issues, https://github.com/PythBuster/ecb-rate/issues
|
|
10
|
+
Keywords: ecb,exchange-rates,currency,cli,forex
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: <3.15,>=3.11
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: aiohttp>=3.13.5
|
|
27
|
+
Requires-Dist: pydantic>=2.12.5
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# ecb-rate
|
|
31
|
+
|
|
32
|
+
Simple CLI tool to fetch EUR-based exchange rates from the European Central Bank (ECB) API.
|
|
33
|
+
|
|
34
|
+
Note: ECB euro foreign exchange reference rates are published for information purposes only.
|
|
35
|
+
Using these rates for transaction purposes is strongly discouraged by the ECB.
|
|
36
|
+
|
|
37
|
+
Official ECB reference:
|
|
38
|
+
https://www.ecb.europa.eu/stats/policy_and_exchange_rates/euro_reference_exchange_rates/html/index.en.html
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
Using `uv` (recommended):
|
|
45
|
+
|
|
46
|
+
uv sync
|
|
47
|
+
|
|
48
|
+
Or with pip:
|
|
49
|
+
|
|
50
|
+
pip install .
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Usage
|
|
55
|
+
|
|
56
|
+
### Basic
|
|
57
|
+
|
|
58
|
+
ecb_rate TRY
|
|
59
|
+
|
|
60
|
+
Output:
|
|
61
|
+
|
|
62
|
+
51.2795
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
### With specific date
|
|
67
|
+
|
|
68
|
+
ecb_rate TRY --specific-date 2025-06-06
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
### Pretty output
|
|
73
|
+
|
|
74
|
+
ecb_rate TRY --pretty
|
|
75
|
+
|
|
76
|
+
Output:
|
|
77
|
+
|
|
78
|
+
Base currency: EUR
|
|
79
|
+
Target currency: TRY
|
|
80
|
+
|
|
81
|
+
2025-06-06: 1 EUR = 43.1234 TRY
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
### Version
|
|
86
|
+
|
|
87
|
+
ecb_rate --version
|
|
88
|
+
|
|
89
|
+
Output:
|
|
90
|
+
|
|
91
|
+
ecb_rate 0.4.1
|
|
92
|
+
|
|
93
|
+
Use this option to print the installed CLI version and exit immediately.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Supported currencies
|
|
98
|
+
|
|
99
|
+
The CLI supports all currencies defined by the ECB reference exchange rate dataset (EXR).
|
|
100
|
+
|
|
101
|
+
Currently implemented currencies:
|
|
102
|
+
|
|
103
|
+
- AUD – Australian dollar (Australia)
|
|
104
|
+
- BRL – Brazilian real (Brazil)
|
|
105
|
+
- CAD – Canadian dollar (Canada)
|
|
106
|
+
- CHF – Swiss franc (Switzerland)
|
|
107
|
+
- CNY – Chinese yuan (China)
|
|
108
|
+
- CZK – Czech koruna (Czech Republic)
|
|
109
|
+
- DKK – Danish krone (Denmark)
|
|
110
|
+
- EUR – Euro (Eurozone)
|
|
111
|
+
- GBP – Pound sterling (United Kingdom)
|
|
112
|
+
- HKD – Hong Kong dollar (Hong Kong)
|
|
113
|
+
- HUF – Hungarian forint (Hungary)
|
|
114
|
+
- IDR – Indonesian rupiah (Indonesia)
|
|
115
|
+
- ILS – Israeli shekel (Israel)
|
|
116
|
+
- INR – Indian rupee (India)
|
|
117
|
+
- ISK – Icelandic krona (Iceland)
|
|
118
|
+
- JPY – Japanese yen (Japan)
|
|
119
|
+
- KRW – South Korean won (South Korea)
|
|
120
|
+
- MXN – Mexican peso (Mexico)
|
|
121
|
+
- MYR – Malaysian ringgit (Malaysia)
|
|
122
|
+
- NOK – Norwegian krone (Norway)
|
|
123
|
+
- NZD – New Zealand dollar (New Zealand)
|
|
124
|
+
- PHP – Philippine peso (Philippines)
|
|
125
|
+
- PLN – Polish zloty (Poland)
|
|
126
|
+
- RON – Romanian leu (Romania)
|
|
127
|
+
- SEK – Swedish krona (Sweden)
|
|
128
|
+
- SGD – Singapore dollar (Singapore)
|
|
129
|
+
- THB – Thai baht (Thailand)
|
|
130
|
+
- TRY – Turkish lira (Turkey)
|
|
131
|
+
- USD – US dollar (United States)
|
|
132
|
+
- ZAR – South African rand (South Africa)
|
|
133
|
+
|
|
134
|
+
Source:
|
|
135
|
+
https://data-api.ecb.europa.eu/service/data/EXR
|
|
136
|
+
|
|
137
|
+
Currency support in this tool is defined via the `CurrencyType` enum.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## API
|
|
142
|
+
|
|
143
|
+
Uses the official ECB Data Portal:
|
|
144
|
+
|
|
145
|
+
https://data-api.ecb.europa.eu/service/data/EXR
|
|
146
|
+
|
|
147
|
+
Format:
|
|
148
|
+
|
|
149
|
+
- jsondata (SDMX JSON)
|
|
150
|
+
|
|
151
|
+
Reference rates are intended for informational use and should not be treated as executable market prices.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Project structure
|
|
156
|
+
|
|
157
|
+
ecb_rate/
|
|
158
|
+
├─ cli.py
|
|
159
|
+
├─ client.py
|
|
160
|
+
├─ custom_types.py
|
|
161
|
+
├─ models.py
|
|
162
|
+
├─ service.py
|
|
163
|
+
└─ utils.py
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Development
|
|
168
|
+
|
|
169
|
+
Install dev dependencies:
|
|
170
|
+
|
|
171
|
+
uv sync --dev
|
|
172
|
+
|
|
173
|
+
Run tests:
|
|
174
|
+
|
|
175
|
+
pytest
|
ecb_rate-0.5.2/README.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# ecb-rate
|
|
2
|
+
|
|
3
|
+
Simple CLI tool to fetch EUR-based exchange rates from the European Central Bank (ECB) API.
|
|
4
|
+
|
|
5
|
+
Note: ECB euro foreign exchange reference rates are published for information purposes only.
|
|
6
|
+
Using these rates for transaction purposes is strongly discouraged by the ECB.
|
|
7
|
+
|
|
8
|
+
Official ECB reference:
|
|
9
|
+
https://www.ecb.europa.eu/stats/policy_and_exchange_rates/euro_reference_exchange_rates/html/index.en.html
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
Using `uv` (recommended):
|
|
16
|
+
|
|
17
|
+
uv sync
|
|
18
|
+
|
|
19
|
+
Or with pip:
|
|
20
|
+
|
|
21
|
+
pip install .
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
### Basic
|
|
28
|
+
|
|
29
|
+
ecb_rate TRY
|
|
30
|
+
|
|
31
|
+
Output:
|
|
32
|
+
|
|
33
|
+
51.2795
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
### With specific date
|
|
38
|
+
|
|
39
|
+
ecb_rate TRY --specific-date 2025-06-06
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
### Pretty output
|
|
44
|
+
|
|
45
|
+
ecb_rate TRY --pretty
|
|
46
|
+
|
|
47
|
+
Output:
|
|
48
|
+
|
|
49
|
+
Base currency: EUR
|
|
50
|
+
Target currency: TRY
|
|
51
|
+
|
|
52
|
+
2025-06-06: 1 EUR = 43.1234 TRY
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
### Version
|
|
57
|
+
|
|
58
|
+
ecb_rate --version
|
|
59
|
+
|
|
60
|
+
Output:
|
|
61
|
+
|
|
62
|
+
ecb_rate 0.4.1
|
|
63
|
+
|
|
64
|
+
Use this option to print the installed CLI version and exit immediately.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Supported currencies
|
|
69
|
+
|
|
70
|
+
The CLI supports all currencies defined by the ECB reference exchange rate dataset (EXR).
|
|
71
|
+
|
|
72
|
+
Currently implemented currencies:
|
|
73
|
+
|
|
74
|
+
- AUD – Australian dollar (Australia)
|
|
75
|
+
- BRL – Brazilian real (Brazil)
|
|
76
|
+
- CAD – Canadian dollar (Canada)
|
|
77
|
+
- CHF – Swiss franc (Switzerland)
|
|
78
|
+
- CNY – Chinese yuan (China)
|
|
79
|
+
- CZK – Czech koruna (Czech Republic)
|
|
80
|
+
- DKK – Danish krone (Denmark)
|
|
81
|
+
- EUR – Euro (Eurozone)
|
|
82
|
+
- GBP – Pound sterling (United Kingdom)
|
|
83
|
+
- HKD – Hong Kong dollar (Hong Kong)
|
|
84
|
+
- HUF – Hungarian forint (Hungary)
|
|
85
|
+
- IDR – Indonesian rupiah (Indonesia)
|
|
86
|
+
- ILS – Israeli shekel (Israel)
|
|
87
|
+
- INR – Indian rupee (India)
|
|
88
|
+
- ISK – Icelandic krona (Iceland)
|
|
89
|
+
- JPY – Japanese yen (Japan)
|
|
90
|
+
- KRW – South Korean won (South Korea)
|
|
91
|
+
- MXN – Mexican peso (Mexico)
|
|
92
|
+
- MYR – Malaysian ringgit (Malaysia)
|
|
93
|
+
- NOK – Norwegian krone (Norway)
|
|
94
|
+
- NZD – New Zealand dollar (New Zealand)
|
|
95
|
+
- PHP – Philippine peso (Philippines)
|
|
96
|
+
- PLN – Polish zloty (Poland)
|
|
97
|
+
- RON – Romanian leu (Romania)
|
|
98
|
+
- SEK – Swedish krona (Sweden)
|
|
99
|
+
- SGD – Singapore dollar (Singapore)
|
|
100
|
+
- THB – Thai baht (Thailand)
|
|
101
|
+
- TRY – Turkish lira (Turkey)
|
|
102
|
+
- USD – US dollar (United States)
|
|
103
|
+
- ZAR – South African rand (South Africa)
|
|
104
|
+
|
|
105
|
+
Source:
|
|
106
|
+
https://data-api.ecb.europa.eu/service/data/EXR
|
|
107
|
+
|
|
108
|
+
Currency support in this tool is defined via the `CurrencyType` enum.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## API
|
|
113
|
+
|
|
114
|
+
Uses the official ECB Data Portal:
|
|
115
|
+
|
|
116
|
+
https://data-api.ecb.europa.eu/service/data/EXR
|
|
117
|
+
|
|
118
|
+
Format:
|
|
119
|
+
|
|
120
|
+
- jsondata (SDMX JSON)
|
|
121
|
+
|
|
122
|
+
Reference rates are intended for informational use and should not be treated as executable market prices.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Project structure
|
|
127
|
+
|
|
128
|
+
ecb_rate/
|
|
129
|
+
├─ cli.py
|
|
130
|
+
├─ client.py
|
|
131
|
+
├─ custom_types.py
|
|
132
|
+
├─ models.py
|
|
133
|
+
├─ service.py
|
|
134
|
+
└─ utils.py
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Development
|
|
139
|
+
|
|
140
|
+
Install dev dependencies:
|
|
141
|
+
|
|
142
|
+
uv sync --dev
|
|
143
|
+
|
|
144
|
+
Run tests:
|
|
145
|
+
|
|
146
|
+
pytest
|
|
File without changes
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI entry point for ecb_rate.
|
|
3
|
+
|
|
4
|
+
Examples:
|
|
5
|
+
ecb_rate TRY
|
|
6
|
+
ecb_rate try
|
|
7
|
+
ecb_rate TRY --specific-date 2025-06-06
|
|
8
|
+
ecb_rate TRY --pretty
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import asyncio
|
|
13
|
+
import sys
|
|
14
|
+
from datetime import date
|
|
15
|
+
|
|
16
|
+
from pydantic import ValidationError
|
|
17
|
+
from importlib.metadata import metadata
|
|
18
|
+
|
|
19
|
+
from ecb_rate.client import ECBJsonClient
|
|
20
|
+
from ecb_rate.models import CliInputError, EcbRateError, QueryParams, RatePoint
|
|
21
|
+
from ecb_rate.service import EcbJsonParser, ExchangeRateService
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CliApplication:
|
|
25
|
+
"""
|
|
26
|
+
Application entry point coordinating argparse and the service layer.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self) -> None:
|
|
30
|
+
client = ECBJsonClient(timeout_seconds=10)
|
|
31
|
+
parser = EcbJsonParser()
|
|
32
|
+
self._service = ExchangeRateService(
|
|
33
|
+
client=client,
|
|
34
|
+
parser=parser,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def run(self, argv: list[str] | None = None) -> int:
|
|
38
|
+
try:
|
|
39
|
+
args = self._parse_args(argv)
|
|
40
|
+
query = self._build_query(args)
|
|
41
|
+
result = asyncio.run(self._service.get_rate(query))
|
|
42
|
+
self._print_result(result, pretty=args.pretty)
|
|
43
|
+
return 0
|
|
44
|
+
except (CliInputError, EcbRateError) as exc:
|
|
45
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
46
|
+
return 1
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def _parse_args(argv: list[str] | None) -> argparse.Namespace:
|
|
50
|
+
package_metadata = metadata("ecb-rate")
|
|
51
|
+
|
|
52
|
+
project_name = package_metadata["Name"].replace("-", "_")
|
|
53
|
+
project_version = package_metadata["Version"]
|
|
54
|
+
project_description = package_metadata["Summary"]
|
|
55
|
+
|
|
56
|
+
parser = argparse.ArgumentParser(
|
|
57
|
+
prog=project_name,
|
|
58
|
+
description=project_description,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
parser.add_argument(
|
|
62
|
+
"--version",
|
|
63
|
+
action="version",
|
|
64
|
+
version=f"%(prog)s {project_version}",
|
|
65
|
+
help="Show the installed CLI version and exit.",
|
|
66
|
+
)
|
|
67
|
+
parser.add_argument(
|
|
68
|
+
"target_currency",
|
|
69
|
+
nargs="?",
|
|
70
|
+
help="Target currency code, for example TRY or USD.",
|
|
71
|
+
)
|
|
72
|
+
parser.add_argument(
|
|
73
|
+
"--specific-date",
|
|
74
|
+
dest="specific_date",
|
|
75
|
+
default=None,
|
|
76
|
+
help="Specific date in YYYY-MM-DD format. Defaults to today.",
|
|
77
|
+
)
|
|
78
|
+
parser.add_argument(
|
|
79
|
+
"--pretty",
|
|
80
|
+
action="store_true",
|
|
81
|
+
help="Print formatted output with base currency, target currency, and rate date.",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
args = parser.parse_args(argv)
|
|
85
|
+
|
|
86
|
+
if args.target_currency is None:
|
|
87
|
+
parser.error("the following arguments are required: target_currency")
|
|
88
|
+
|
|
89
|
+
return args
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def _build_query(args: argparse.Namespace) -> QueryParams:
|
|
93
|
+
if args.specific_date is None:
|
|
94
|
+
specific_date = date.today()
|
|
95
|
+
else:
|
|
96
|
+
try:
|
|
97
|
+
specific_date = date.fromisoformat(args.specific_date)
|
|
98
|
+
except ValueError as exc:
|
|
99
|
+
raise CliInputError(
|
|
100
|
+
f"Invalid --specific-date: {args.specific_date}. Expected YYYY-MM-DD."
|
|
101
|
+
) from exc
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
return QueryParams(
|
|
105
|
+
target_currency=args.target_currency,
|
|
106
|
+
specific_date=specific_date,
|
|
107
|
+
)
|
|
108
|
+
except (ValidationError, ValueError) as exc:
|
|
109
|
+
raise CliInputError(str(exc)) from exc
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def _print_result(rate_point: RatePoint, pretty: bool = False) -> None:
|
|
113
|
+
if not pretty:
|
|
114
|
+
print(f"{rate_point.rate:.4f}")
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
print(f"Base currency: {rate_point.base_currency.code}")
|
|
118
|
+
print(f"Target currency: {rate_point.target_currency.code}")
|
|
119
|
+
print()
|
|
120
|
+
|
|
121
|
+
print(
|
|
122
|
+
f"{rate_point.date.isoformat()}: "
|
|
123
|
+
f"1 {rate_point.base_currency.code} = {rate_point.rate:.4f} {rate_point.target_currency.code}"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def main() -> int:
|
|
128
|
+
app = CliApplication()
|
|
129
|
+
return app.run()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
if __name__ == "__main__": # pragma: no cover
|
|
133
|
+
sys.exit(main())
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ECB-specific async JSON API client.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from datetime import date
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import aiohttp
|
|
9
|
+
|
|
10
|
+
from ecb_rate.custom_types import CurrencyType
|
|
11
|
+
from ecb_rate.models import EcbApiError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ECBJsonClient:
|
|
15
|
+
"""
|
|
16
|
+
Async HTTP client for the ECB Data Portal API.
|
|
17
|
+
|
|
18
|
+
This client is responsible for:
|
|
19
|
+
- building ECB-specific URLs
|
|
20
|
+
- executing HTTP requests
|
|
21
|
+
- returning parsed JSON responses
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
BASE_URL = "https://data-api.ecb.europa.eu/service/data/EXR"
|
|
25
|
+
|
|
26
|
+
def __init__(self, timeout_seconds: int = 10) -> None:
|
|
27
|
+
self._timeout = aiohttp.ClientTimeout(total=timeout_seconds)
|
|
28
|
+
|
|
29
|
+
async def fetch(
|
|
30
|
+
self,
|
|
31
|
+
currency: CurrencyType,
|
|
32
|
+
specific_date: date,
|
|
33
|
+
) -> dict[str, Any]:
|
|
34
|
+
"""
|
|
35
|
+
Fetch ECB time series data in jsondata format for a specific date.
|
|
36
|
+
|
|
37
|
+
Uses:
|
|
38
|
+
D.<CURRENCY>.EUR.SP00.A
|
|
39
|
+
format=jsondata
|
|
40
|
+
lastNObservations=1
|
|
41
|
+
endPeriod=<date>
|
|
42
|
+
details=dataonly
|
|
43
|
+
|
|
44
|
+
This returns the latest available observation up to the given date.
|
|
45
|
+
"""
|
|
46
|
+
series_key = f"D.{currency.code}.EUR.SP00.A"
|
|
47
|
+
|
|
48
|
+
iso_formatted_date = specific_date.isoformat()
|
|
49
|
+
params = {
|
|
50
|
+
"format": "jsondata",
|
|
51
|
+
"lastNObservations": 1,
|
|
52
|
+
"endPeriod": iso_formatted_date,
|
|
53
|
+
"details": "dataonly",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
headers = {
|
|
57
|
+
"Accept": "application/json",
|
|
58
|
+
"User-Agent": "ecb_rate/0.1.0",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
url = f"{self.BASE_URL}/{series_key}"
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
async with aiohttp.ClientSession(
|
|
65
|
+
timeout=self._timeout,
|
|
66
|
+
headers=headers,
|
|
67
|
+
) as session:
|
|
68
|
+
async with session.get(url, params=params) as response:
|
|
69
|
+
if response.status >= 400:
|
|
70
|
+
text = await response.text()
|
|
71
|
+
raise EcbApiError(
|
|
72
|
+
f"HTTP error: {response.status} {response.reason}. "
|
|
73
|
+
f"Response: {text}"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
data = await response.json()
|
|
78
|
+
|
|
79
|
+
except aiohttp.ContentTypeError as exc:
|
|
80
|
+
text = await response.text()
|
|
81
|
+
raise EcbApiError(
|
|
82
|
+
f"Expected JSON but got different content: {text}"
|
|
83
|
+
) from exc
|
|
84
|
+
|
|
85
|
+
if not isinstance(data, dict):
|
|
86
|
+
raise EcbApiError("Expected JSON object as top-level response.")
|
|
87
|
+
|
|
88
|
+
return data
|
|
89
|
+
|
|
90
|
+
except aiohttp.ClientError as exc:
|
|
91
|
+
raise EcbApiError(f"Network error: {exc}") from exc
|
|
92
|
+
except TimeoutError as exc:
|
|
93
|
+
raise EcbApiError("Request timeout.") from exc
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom shared types for ecb_rate.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CurrencyType(StrEnum):
|
|
9
|
+
"""
|
|
10
|
+
Supported currencies for the CLI.
|
|
11
|
+
|
|
12
|
+
Values are stored in lowercase. Use ``code`` for the ECB-compatible
|
|
13
|
+
uppercase representation.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
AUD = "aud"
|
|
17
|
+
# Australian dollar (Australia)
|
|
18
|
+
|
|
19
|
+
BRL = "brl"
|
|
20
|
+
# Brazilian real (Brazil)
|
|
21
|
+
|
|
22
|
+
CAD = "cad"
|
|
23
|
+
# Canadian dollar (Canada)
|
|
24
|
+
|
|
25
|
+
CHF = "chf"
|
|
26
|
+
# Swiss franc (Switzerland)
|
|
27
|
+
|
|
28
|
+
CNY = "cny"
|
|
29
|
+
# Chinese yuan (China)
|
|
30
|
+
|
|
31
|
+
CZK = "czk"
|
|
32
|
+
# Czech koruna (Czech Republic)
|
|
33
|
+
|
|
34
|
+
DKK = "dkk"
|
|
35
|
+
# Danish krone (Denmark)
|
|
36
|
+
|
|
37
|
+
EUR = "eur"
|
|
38
|
+
# Euro (Eurozone)
|
|
39
|
+
|
|
40
|
+
GBP = "gbp"
|
|
41
|
+
# Pound sterling (United Kingdom)
|
|
42
|
+
|
|
43
|
+
HKD = "hkd"
|
|
44
|
+
# Hong Kong dollar (Hong Kong)
|
|
45
|
+
|
|
46
|
+
HUF = "huf"
|
|
47
|
+
# Hungarian forint (Hungary)
|
|
48
|
+
|
|
49
|
+
IDR = "idr"
|
|
50
|
+
# Indonesian rupiah (Indonesia)
|
|
51
|
+
|
|
52
|
+
ILS = "ils"
|
|
53
|
+
# Israeli shekel (Israel)
|
|
54
|
+
|
|
55
|
+
INR = "inr"
|
|
56
|
+
# Indian rupee (India)
|
|
57
|
+
|
|
58
|
+
ISK = "isk"
|
|
59
|
+
# Icelandic krona (Iceland)
|
|
60
|
+
|
|
61
|
+
JPY = "jpy"
|
|
62
|
+
# Japanese yen (Japan)
|
|
63
|
+
|
|
64
|
+
KRW = "krw"
|
|
65
|
+
# South Korean won (South Korea)
|
|
66
|
+
|
|
67
|
+
MXN = "mxn"
|
|
68
|
+
# Mexican peso (Mexico)
|
|
69
|
+
|
|
70
|
+
MYR = "myr"
|
|
71
|
+
# Malaysian ringgit (Malaysia)
|
|
72
|
+
|
|
73
|
+
NOK = "nok"
|
|
74
|
+
# Norwegian krone (Norway)
|
|
75
|
+
|
|
76
|
+
NZD = "nzd"
|
|
77
|
+
# New Zealand dollar (New Zealand)
|
|
78
|
+
|
|
79
|
+
PHP = "php"
|
|
80
|
+
# Philippine peso (Philippines)
|
|
81
|
+
|
|
82
|
+
PLN = "pln"
|
|
83
|
+
# Polish zloty (Poland)
|
|
84
|
+
|
|
85
|
+
RON = "ron"
|
|
86
|
+
# Romanian leu (Romania)
|
|
87
|
+
|
|
88
|
+
SEK = "sek"
|
|
89
|
+
# Swedish krona (Sweden)
|
|
90
|
+
|
|
91
|
+
SGD = "sgd"
|
|
92
|
+
# Singapore dollar (Singapore)
|
|
93
|
+
|
|
94
|
+
THB = "thb"
|
|
95
|
+
# Thai baht (Thailand)
|
|
96
|
+
|
|
97
|
+
TRY = "try"
|
|
98
|
+
# Turkish lira (Turkey)
|
|
99
|
+
|
|
100
|
+
USD = "usd"
|
|
101
|
+
# US dollar (United States)
|
|
102
|
+
|
|
103
|
+
ZAR = "zar"
|
|
104
|
+
# South African rand (South Africa)
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def code(self) -> str:
|
|
108
|
+
"""
|
|
109
|
+
Return the ECB-compatible uppercase currency code.
|
|
110
|
+
"""
|
|
111
|
+
return self.value.upper()
|