ecb-rate 0.5.2__py3-none-any.whl
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/__init__.py +0 -0
- ecb_rate/cli.py +133 -0
- ecb_rate/client.py +93 -0
- ecb_rate/custom_types.py +111 -0
- ecb_rate/models.py +55 -0
- ecb_rate/service.py +89 -0
- ecb_rate-0.5.2.dist-info/METADATA +175 -0
- ecb_rate-0.5.2.dist-info/RECORD +12 -0
- ecb_rate-0.5.2.dist-info/WHEEL +5 -0
- ecb_rate-0.5.2.dist-info/entry_points.txt +2 -0
- ecb_rate-0.5.2.dist-info/licenses/LICENSE +21 -0
- ecb_rate-0.5.2.dist-info/top_level.txt +1 -0
ecb_rate/__init__.py
ADDED
|
File without changes
|
ecb_rate/cli.py
ADDED
|
@@ -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())
|
ecb_rate/client.py
ADDED
|
@@ -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
|
ecb_rate/custom_types.py
ADDED
|
@@ -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()
|
ecb_rate/models.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared models and exceptions for ecb_rate.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from datetime import date
|
|
6
|
+
from decimal import Decimal
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, field_validator
|
|
9
|
+
|
|
10
|
+
from ecb_rate.custom_types import CurrencyType
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EcbRateError(Exception):
|
|
14
|
+
"""Base exception for the application."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CliInputError(EcbRateError):
|
|
18
|
+
"""Raised when CLI input is invalid."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EcbApiError(EcbRateError):
|
|
22
|
+
"""Raised when the ECB API request or response is invalid."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class QueryParams(BaseModel):
|
|
26
|
+
"""
|
|
27
|
+
Query parameters for the currency conversion request.
|
|
28
|
+
|
|
29
|
+
EUR is always the base currency.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
target_currency: CurrencyType
|
|
33
|
+
specific_date: date
|
|
34
|
+
|
|
35
|
+
@field_validator("target_currency", mode="before")
|
|
36
|
+
@classmethod
|
|
37
|
+
def normalize_currency(cls, value: object) -> object:
|
|
38
|
+
"""
|
|
39
|
+
Normalize currency input to lowercase before enum validation.
|
|
40
|
+
"""
|
|
41
|
+
if isinstance(value, str) and not isinstance(value, CurrencyType):
|
|
42
|
+
return value.lower()
|
|
43
|
+
|
|
44
|
+
return value
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class RatePoint(BaseModel):
|
|
48
|
+
"""
|
|
49
|
+
A single exchange-rate result for a specific date.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
date: date
|
|
53
|
+
rate: Decimal
|
|
54
|
+
target_currency: CurrencyType
|
|
55
|
+
base_currency: CurrencyType = CurrencyType.EUR
|
ecb_rate/service.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Business logic for building EUR exchange rates from ECB JSON series.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from datetime import date
|
|
6
|
+
from decimal import Decimal
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ecb_rate.client import ECBJsonClient
|
|
10
|
+
from ecb_rate.custom_types import CurrencyType
|
|
11
|
+
from ecb_rate.models import EcbApiError, QueryParams, RatePoint
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EcbJsonParser:
|
|
15
|
+
"""
|
|
16
|
+
Parser for ECB JSON responses.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def extract_ecb_rate(
|
|
20
|
+
self,
|
|
21
|
+
payload: dict[str, Any],
|
|
22
|
+
) -> Decimal:
|
|
23
|
+
"""
|
|
24
|
+
Extract the currency rate from ECB JSON payload.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
result = list(payload["dataSets"][0]["series"].values())[0]["observations"][
|
|
29
|
+
"0"
|
|
30
|
+
][0]
|
|
31
|
+
|
|
32
|
+
if result is None:
|
|
33
|
+
raise EcbApiError("No exchange rate found.")
|
|
34
|
+
except (KeyError, IndexError, TypeError) as exc:
|
|
35
|
+
raise EcbApiError("Unexpected ECB JSON structure.") from exc
|
|
36
|
+
|
|
37
|
+
return Decimal(str(result))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ExchangeRateService:
|
|
41
|
+
"""
|
|
42
|
+
High-level service that fetches ECB series and computes:
|
|
43
|
+
|
|
44
|
+
1 EUR = X target_currency
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
client: ECBJsonClient,
|
|
50
|
+
parser: EcbJsonParser,
|
|
51
|
+
) -> None:
|
|
52
|
+
self._client = client
|
|
53
|
+
self._parser = parser
|
|
54
|
+
|
|
55
|
+
async def get_rate(self, query: QueryParams) -> RatePoint:
|
|
56
|
+
"""
|
|
57
|
+
Return the EUR -> target currency rate for the requested date.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
if query.target_currency == CurrencyType.EUR:
|
|
61
|
+
return RatePoint(
|
|
62
|
+
target_currency=query.target_currency,
|
|
63
|
+
date=query.specific_date,
|
|
64
|
+
rate=Decimal("1"),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
rate = await self._fetch_eur_to_currency(
|
|
68
|
+
currency=query.target_currency,
|
|
69
|
+
specific_date=query.specific_date,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return RatePoint(
|
|
73
|
+
target_currency=query.target_currency,
|
|
74
|
+
date=query.specific_date,
|
|
75
|
+
rate=rate,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
async def _fetch_eur_to_currency(
|
|
79
|
+
self,
|
|
80
|
+
currency: CurrencyType,
|
|
81
|
+
specific_date: date,
|
|
82
|
+
) -> Decimal:
|
|
83
|
+
payload = await self._client.fetch(
|
|
84
|
+
currency=currency,
|
|
85
|
+
specific_date=specific_date,
|
|
86
|
+
)
|
|
87
|
+
return self._parser.extract_ecb_rate(
|
|
88
|
+
payload=payload,
|
|
89
|
+
)
|
|
@@ -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
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
ecb_rate/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
ecb_rate/cli.py,sha256=1Ot79OjscUWuC-_u9Uu3ZYZe0oHQ68HkSPNbdhOJEsg,4148
|
|
3
|
+
ecb_rate/client.py,sha256=HZxy-OZjnLKfADKeNlOsOW21sQieErZNk2zfxWMZl1g,2888
|
|
4
|
+
ecb_rate/custom_types.py,sha256=Jrj73KOhaqDMUUPF6Q1O78pjtZiqwNPdYcz59bsHdGo,1991
|
|
5
|
+
ecb_rate/models.py,sha256=1p4cH3G3NNBqQfcPYviww0WWJy6WkUvVy_cuGjeJU9Y,1297
|
|
6
|
+
ecb_rate/service.py,sha256=Rg98iz3v6RpuNlAcyavSmjuthmtyDyQWoBvtFL0lLII,2390
|
|
7
|
+
ecb_rate-0.5.2.dist-info/licenses/LICENSE,sha256=ZYPOZtJBYetn0L8ZRA4enYpb1TPQfe1b8nJcqhAPU5I,1086
|
|
8
|
+
ecb_rate-0.5.2.dist-info/METADATA,sha256=hXQJL4OvelGlyIMb7MGAOQCgMFD6tenQ27QAzHxkntQ,4117
|
|
9
|
+
ecb_rate-0.5.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
10
|
+
ecb_rate-0.5.2.dist-info/entry_points.txt,sha256=gcVdhCrILbpRPW-mLQHaLBUQ_uV3e5p-Z4ZF5ki444Y,47
|
|
11
|
+
ecb_rate-0.5.2.dist-info/top_level.txt,sha256=VcYA2ySxOquKBOdsla5uzQNYZ9HIRQGhoYbRJw2zPF0,9
|
|
12
|
+
ecb_rate-0.5.2.dist-info/RECORD,,
|
|
@@ -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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ecb_rate
|