-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 238b93c
Showing
8 changed files
with
397 additions
and
0 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
# oxr | ||
|
||
`oxr` is a thin-wrapper over the [Open Exchange Rates API](https://openexchangerates.org/). It provides a type-safe interface to the API, allowing you to easily fetch exchange rates and convert between currencies. | ||
|
||
|
||
## Installation | ||
|
||
```bash | ||
pip install oxr | ||
``` | ||
|
||
|
||
## Usage | ||
|
||
```python | ||
import oxr | ||
|
||
import datetime as dt | ||
|
||
# Base default to USD | ||
client = oxr.Client(app_id='your_app_id') | ||
|
||
# Fetch the latest exchange rates | ||
rates = client.latest(symbols=['EUR', 'JPY']) | ||
|
||
# Convert 100 USD to EUR | ||
converted = client.convert(amount, 'USD', 'EUR') | ||
|
||
# Get time series data | ||
timeseries = client.timeseries(start_date=dt.date(2020, 1, 1), end_date=dt.date(2020, 1, 31), symbols=['EUR', 'JPY']) | ||
|
||
# Get open, high, low, close data | ||
olhc = client.olhc(start_time=dt.datetime(2020, 1, 1), period="1m", symbols=['EUR', 'JPY']) | ||
``` | ||
|
||
## License | ||
|
||
MIT | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
"""Client for Open Exchange Rates API. | ||
Examples: | ||
```python | ||
import openexchangerates as oxr | ||
client = oxr.Client("YOUR_APP_ID") | ||
# Get the latest exchange rates. | ||
latest = client.latest("USD") | ||
# Get historical exchange rates. | ||
historical = client.historical("2021-01-01") | ||
# Get time series exchange rates. | ||
time_series = client.time_series(dt.date(2021, 1, 1), dt.date(2021, 1, 31)) | ||
# Get OHLC exchange rates. | ||
ohlc = client.ohlc(dt.date(2021, 1, 1), dt.date(2021, 1, 31)) | ||
# Convert currency. | ||
conversion = client.convert("USD", "EUR", 100) | ||
``` | ||
""" | ||
|
||
from openexchangerates.core import Client | ||
|
||
__all__ = ["Client"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
from typing import Final | ||
from openexchangerates.exceptions import ( | ||
Error, | ||
InvalidAppID, | ||
InvalidCurrency, | ||
InvalidDate, | ||
InvalidDateRange, | ||
) | ||
|
||
_EXCEPTION_MAP: Final[dict[tuple[int, str], type[Error]]] = { | ||
(401, "invalid_app_id"): InvalidAppID, | ||
(400, "invalid_currency"): InvalidCurrency, | ||
(400, "invalid_date"): InvalidDate, | ||
(400, "invalid_date_range"): InvalidDateRange, | ||
} | ||
|
||
|
||
def get(code: int, message: str) -> type[Error] | None: | ||
"""Get the error class for the given code and message.""" | ||
return _EXCEPTION_MAP.get((code, message), None) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
from __future__ import annotations | ||
|
||
from oxr import responses, exceptions, _exceptions | ||
from typing import Any, Final, Iterable, Literal, TypeAlias, cast | ||
import datetime as dt | ||
import requests | ||
|
||
|
||
_Endpoint: TypeAlias = Literal[ | ||
"latest", | ||
"historical", | ||
"convert", | ||
"time-series", | ||
"ohlc", | ||
"usage", | ||
] | ||
|
||
_BASE_URL: Final = "https://openexchangerates.org/api" | ||
|
||
|
||
class Client: | ||
"""A client for the Open Exchange Rates API.""" | ||
|
||
def __init__( | ||
self, | ||
app_id: str, | ||
*, | ||
base: responses.Currency = "USD", | ||
base_url: str = _BASE_URL, | ||
) -> None: | ||
self.app_id = app_id | ||
self._base = base | ||
self._base_url = base_url | ||
|
||
def _get( | ||
self, | ||
endpoint: _Endpoint, | ||
params: dict[str, Any], | ||
) -> dict[str, Any]: | ||
"""Make a GET request to the API.""" | ||
url = f"{self._base_url}/{endpoint}.json" | ||
response = requests.get(url, params={**params, "app_id": self.app_id}) | ||
try: | ||
response.raise_for_status() | ||
except requests.HTTPError as error: | ||
msg = response.json().get("message", "") | ||
exc = _exceptions.get(response.status_code, msg) | ||
if exc is not None: | ||
raise exc from error | ||
raise exceptions.Error from error | ||
|
||
return response.json() | ||
|
||
def latest( | ||
self, | ||
base: str | None = None, | ||
symbols: Iterable[responses.Currency] | None = None, | ||
show_alternative: bool = False, | ||
) -> responses.Rates: | ||
"""Get the latest exchange rates. | ||
Args: | ||
base: The base currency. | ||
symbols: The target currencies. | ||
show_alternative: Whether to show alternative currencies. | ||
Such as black market and digital currency rates. | ||
""" | ||
params = {"base": base or self._base, "show_alternative": show_alternative} | ||
if symbols is not None: | ||
params["symbols"] = ",".join(symbols) | ||
return cast(responses.Rates, self._get("latest", params)) | ||
|
||
def historical( | ||
self, | ||
date: dt.date, | ||
base: str | None = None, | ||
symbols: Iterable[responses.Currency] | None = None, | ||
show_alternative: bool = False, | ||
) -> responses.Rates: | ||
"""Get historical exchange rates. | ||
Args: | ||
date: The date of the rates. | ||
base: The base currency. | ||
symbols: The target currencies. | ||
show_alternative: Whether to show alternative currencies. | ||
Such as black market and digital currency rates. | ||
""" | ||
params = { | ||
"base": base or self._base, | ||
"date": date.isoformat(), | ||
"show_alternative": show_alternative, | ||
} | ||
|
||
if symbols is not None: | ||
params["symbols"] = ",".join(symbols) | ||
return cast(responses.Rates, self._get("historical", params)) | ||
|
||
def convert( | ||
self, | ||
amount: float, | ||
from_: str, | ||
to: str, | ||
) -> responses.Conversion: | ||
"""Convert an amount between two currencies. | ||
Args: | ||
amount: The amount to convert. | ||
from_: The source currency. | ||
to: The target currency. | ||
date: The date of the rates to use. | ||
""" | ||
params = {"from": from_, "to": to, "amount": amount} | ||
return cast(responses.Conversion, self._get("convert", params)) | ||
|
||
def time_series( | ||
self, | ||
start: dt.date, | ||
end: dt.date, | ||
symbols: Iterable[responses.Currency] | None = None, | ||
base: str | None = None, | ||
show_alternative: bool = False, | ||
) -> responses.TimeSeries: | ||
"""Get historical exchange rates for a range of dates. | ||
Args: | ||
start: The start date of the range. | ||
end: The end date of the range. | ||
symbols: The target currencies. | ||
base: The base currency. | ||
show_alternative: Whether to show alternative currencies. | ||
Such as black market and digital currency rates. | ||
""" | ||
params = { | ||
"start": start.isoformat(), | ||
"end": end.isoformat(), | ||
"show_alternative": show_alternative, | ||
} | ||
params["base"] = base or self._base | ||
if symbols is not None: | ||
params["symbols"] = ",".join(symbols) | ||
return cast(responses.TimeSeries, self._get("time-series", params)) | ||
|
||
def olhc( | ||
self, | ||
start_time: dt.datetime, | ||
period: Literal["1m", "5m", "15m", "30m", "1h", "12", "1d", "1w", "1mo"], | ||
base: str | None = None, | ||
symbols: Iterable[responses.Currency] | None = None, | ||
show_alternative: bool = False, | ||
) -> responses.OHLC: | ||
"""Get the latest open, low, high, and close rates for a currency. | ||
Args: | ||
base: The base currency. | ||
symbols: The target currencies. | ||
show_alternative: Whether to show alternative currencies. | ||
Such as black market and digital currency rates. | ||
""" | ||
params = { | ||
"start_time": start_time.isoformat(), | ||
"period": period, | ||
"show_alternative": show_alternative, | ||
} | ||
params["base"] = base or self._base | ||
if symbols is not None: | ||
params["symbols"] = ",".join(symbols) | ||
return cast(responses.OHLC, self._get("ohlc", params)) | ||
|
||
def usage(self) -> dict[str, Any]: | ||
"""Get the usage statistics for the API key.""" | ||
return self._get("usage", {}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
from typing import ClassVar, Final | ||
|
||
|
||
class Error(Exception): | ||
"""Base exception class for `openexchangerates`.""" | ||
|
||
code: ClassVar[int | None] = None | ||
|
||
|
||
class InvalidCurrency(Error): | ||
"""Raised when an invalid currency is provided.""" | ||
|
||
code = 400 | ||
|
||
|
||
class InvalidDate(Error): | ||
"""Raised when an invalid date is provided.""" | ||
|
||
code = 400 | ||
|
||
|
||
class InvalidDateRange(Error): | ||
"""Raised when an invalid date range is provided.""" | ||
|
||
code = 400 | ||
|
||
|
||
class InvalidAppID(Error): | ||
"""Raised when an invalid App ID is provided.""" | ||
|
||
code = 401 | ||
|
||
|
||
from typing import Final | ||
from openexchangerates.exceptions import ( | ||
Error, | ||
InvalidAppID, | ||
InvalidCurrency, | ||
InvalidDate, | ||
InvalidDateRange, | ||
) | ||
|
||
_MAPPING: Final[dict[tuple[int, str], type[Error]]] = { | ||
(401, "invalid_app_id"): InvalidAppID, | ||
(400, "invalid_currency"): InvalidCurrency, | ||
(400, "invalid_date"): InvalidDate, | ||
(400, "invalid_date_range"): InvalidDateRange, | ||
} | ||
|
||
|
||
def get(code: int, message: str) -> type[Error] | None: | ||
"""Get the error class for the given code and message.""" | ||
return MAPPING.get((code, message), None) |
Oops, something went wrong.