Skip to content

Commit

Permalink
Basic async client
Browse files Browse the repository at this point in the history
  • Loading branch information
max-muoto committed Jun 15, 2024
1 parent 7929b9e commit 73e3b8c
Show file tree
Hide file tree
Showing 9 changed files with 728 additions and 77 deletions.
2 changes: 1 addition & 1 deletion oxr/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@

from __future__ import annotations

from oxr.core import Client
from oxr.client import Client

__all__ = ["Client"]
69 changes: 69 additions & 0 deletions oxr/_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from __future__ import annotations

import abc
import datetime as dt
from collections.abc import Awaitable
from typing import Final, Iterable

from oxr import responses
from oxr._types import Currency, Period

_BASE_URL: Final = "https://openexchangerates.org/api"


class BaseClient(abc.ABC):
def __init__(
self,
app_id: str,
*,
base: Currency = "USD",
base_url: str = _BASE_URL,
) -> None:
self._app_id = app_id
self._base = base
self._base_url = base_url

@abc.abstractmethod
def latest(
self,
base: str | None = None,
symbols: Iterable[Currency] | None = None,
show_alternative: bool = False,
) -> responses.Rates | Awaitable[responses.Rates]: ...

@abc.abstractmethod
def historical(
self,
date: dt.date,
base: str | None = None,
symbols: Iterable[Currency] | None = None,
show_alternative: bool = False,
) -> responses.Rates | Awaitable[responses.Rates]: ...

@abc.abstractmethod
def convert(
self,
amount: float,
from_: str,
to: str,
) -> responses.Conversion | Awaitable[responses.Conversion]: ...

@abc.abstractmethod
def time_series(
self,
start: dt.date,
end: dt.date,
symbols: Iterable[Currency] | None = None,
base: str | None = None,
show_alternative: bool = False,
) -> responses.TimeSeries | Awaitable[responses.TimeSeries]: ...

@abc.abstractmethod
def olhc(
self,
start_time: dt.datetime,
period: Period,
base: str | None = None,
symbols: Iterable[Currency] | None = None,
show_alternative: bool = False,
) -> responses.OHLC | Awaitable[responses.OHLC]: ...
41 changes: 41 additions & 0 deletions oxr/_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from __future__ import annotations

from typing import Literal

from typing_extensions import TypeAlias

# Valid currency codes.
# fmt: off
Currency: TypeAlias = Literal[
"AED", "AFN", "ALL", "AMD", "ANG", "AOA", "ARS", "AUD", "AWG", "AZN", "BAM", "BBD",
"BDT", "BGN", "BHD", "BIF", "BMD", "BND", "BOB", "BRL", "BSD", "BTC", "BTN", "BWP",
"BYN", "BZD", "CAD", "CDF", "CHF", "CLF", "CLP", "CNH", "CNY", "COP", "CRC", "CUC",
"CUP", "CVE", "CZK", "DJF", "DKK", "DOP", "DZD", "EGP", "ERN", "ETB", "EUR", "FJD",
"FKP", "GBP", "GEL", "GGP", "GHS", "GIP", "GMD", "GNF", "GTQ", "GYD", "HKD", "HNL",
"HRK", "HTG", "HUF", "IDR", "ILS", "IMP", "INR", "IQD", "IRR", "ISK", "JEP", "JMD",
"JOD", "JPY", "KES", "KGS", "KHR", "KMF", "KPW", "KRW", "KWD", "KYD", "KZT", "LAK",
"LBP", "LKR", "LRD", "LSL", "LYD", "MAD", "MDL", "MGA", "MKD", "MMK", "MNT", "MOP",
"MRU", "MUR", "MVR", "MWK", "MXN", "MYR", "MZN", "NAD", "NGN", "NIO", "NOK", "NPR",
"NZD", "OMR", "PAB", "PEN", "PGK", "PHP", "PKR", "PLN", "PYG", "QAR", "RON", "RSD",
"RUB", "RWF", "SAR", "SBD", "SCR", "SDG", "SEK", "SGD", "SHP", "SLL", "SOS", "SRD",
"SSP", "STD", "STN", "SVC", "SYP", "SZL", "THB", "TJS", "TMT", "TND", "TOP", "TRY",
"TTD", "TWD", "TZS", "UAH", "UGX", "USD", "UYU", "UZS", "VEF", "VES", "VND", "VUV",
"WST", "XAF", "XAG", "XAU", "XCD", "XDR", "XOF", "XPD", "XPF", "XPT", "YER", "ZAR",
"ZMW", "ZWL"
]
# fmt: on


# Valid API endpoints.
Endpoint: TypeAlias = Literal[
"latest",
"historical",
"convert",
"time-series",
"ohlc",
"usage",
]


# Valid periods for the OHLC endpoint.
Period: TypeAlias = Literal["1m", "5m", "15m", "30m", "1h", "12h", "1d", "1w", "1mo"]
7 changes: 7 additions & 0 deletions oxr/asynchronous/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""Asynchronous client for Open Exchange Rates API."""

from __future__ import annotations

from oxr.asynchronous.client import Client

__all__ = ["Client"]
144 changes: 144 additions & 0 deletions oxr/asynchronous/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
from __future__ import annotations

import datetime as dt
from collections.abc import Iterable
from typing import Any, cast

import aiohttp

from oxr import responses
from oxr._base import BaseClient
from oxr._types import Currency, Endpoint, Period


class Client(BaseClient):
"""A asynchronous client for the Open Exchange Rates API."""

async def _get(self, endpoint: Endpoint, params: dict[str, Any]) -> dict[str, Any]:
url = f"{self._base_url}/{endpoint}.json"
async with aiohttp.ClientSession() as session, session.get(url, params=params) as response:
response.raise_for_status()
return await response.json()

async def latest(
self,
base: str | None = None,
symbols: Iterable[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 = {
"app_id": self._app_id,
"base": base or self._base,
"show_alternative": show_alternative,
}
if symbols is not None:
params["symbols"] = ",".join(symbols)
return cast(responses.Rates, await self._get("latest", params))

async def historical(
self,
date: dt.date,
base: str | None = None,
symbols: Iterable[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, await self._get("historical", params))

async 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, await self._get("convert", params))

async def time_series(
self,
start: dt.date,
end: dt.date,
symbols: Iterable[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, await self._get("time-series", params))

async def olhc(
self,
start_time: dt.datetime,
period: Period,
base: str | None = None,
symbols: Iterable[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, await self._get("ohlc", params))

async def usage(self) -> dict[str, Any]:
"""Get the usage statistics for the API key."""
return await self._get("usage", {})
41 changes: 10 additions & 31 deletions oxr/core.py → oxr/client.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,21 @@
from __future__ import annotations

import datetime as dt
from typing import Any, Final, Iterable, Literal, cast
from typing import Any, Iterable, cast

import requests
from typing_extensions import TypeAlias

from oxr import _exceptions, exceptions, responses
from oxr._base import BaseClient
from oxr._types import Currency, Endpoint, Period

_Endpoint: TypeAlias = Literal[
"latest",
"historical",
"convert",
"time-series",
"ohlc",
"usage",
]

_BASE_URL: Final = "https://openexchangerates.org/api"


class Client:
class Client(BaseClient):
"""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,
endpoint: Endpoint,
params: dict[str, Any],
) -> dict[str, Any]:
"""Make a GET request to the API."""
Expand All @@ -56,7 +35,7 @@ def _get(
def latest(
self,
base: str | None = None,
symbols: Iterable[responses.Currency] | None = None,
symbols: Iterable[Currency] | None = None,
show_alternative: bool = False,
) -> responses.Rates:
"""Get the latest exchange rates.
Expand All @@ -76,7 +55,7 @@ def historical(
self,
date: dt.date,
base: str | None = None,
symbols: Iterable[responses.Currency] | None = None,
symbols: Iterable[Currency] | None = None,
show_alternative: bool = False,
) -> responses.Rates:
"""Get historical exchange rates.
Expand Down Expand Up @@ -118,7 +97,7 @@ def time_series(
self,
start: dt.date,
end: dt.date,
symbols: Iterable[responses.Currency] | None = None,
symbols: Iterable[Currency] | None = None,
base: str | None = None,
show_alternative: bool = False,
) -> responses.TimeSeries:
Expand All @@ -145,9 +124,9 @@ def time_series(
def olhc(
self,
start_time: dt.datetime,
period: Literal["1m", "5m", "15m", "30m", "1h", "12", "1d", "1w", "1mo"],
period: Period,
base: str | None = None,
symbols: Iterable[responses.Currency] | None = None,
symbols: Iterable[Currency] | None = None,
show_alternative: bool = False,
) -> responses.OHLC:
"""Get the latest open, low, high, and close rates for a currency.
Expand Down
26 changes: 3 additions & 23 deletions oxr/responses.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,8 @@
from __future__ import annotations

from typing import Literal, TypedDict

from typing_extensions import TypeAlias

# fmt: off
Currency: TypeAlias = Literal[
"AED", "AFN", "ALL", "AMD", "ANG", "AOA", "ARS", "AUD", "AWG", "AZN", "BAM", "BBD",
"BDT", "BGN", "BHD", "BIF", "BMD", "BND", "BOB", "BRL", "BSD", "BTC", "BTN", "BWP",
"BYN", "BZD", "CAD", "CDF", "CHF", "CLF", "CLP", "CNH", "CNY", "COP", "CRC", "CUC",
"CUP", "CVE", "CZK", "DJF", "DKK", "DOP", "DZD", "EGP", "ERN", "ETB", "EUR", "FJD",
"FKP", "GBP", "GEL", "GGP", "GHS", "GIP", "GMD", "GNF", "GTQ", "GYD", "HKD", "HNL",
"HRK", "HTG", "HUF", "IDR", "ILS", "IMP", "INR", "IQD", "IRR", "ISK", "JEP", "JMD",
"JOD", "JPY", "KES", "KGS", "KHR", "KMF", "KPW", "KRW", "KWD", "KYD", "KZT", "LAK",
"LBP", "LKR", "LRD", "LSL", "LYD", "MAD", "MDL", "MGA", "MKD", "MMK", "MNT", "MOP",
"MRU", "MUR", "MVR", "MWK", "MXN", "MYR", "MZN", "NAD", "NGN", "NIO", "NOK", "NPR",
"NZD", "OMR", "PAB", "PEN", "PGK", "PHP", "PKR", "PLN", "PYG", "QAR", "RON", "RSD",
"RUB", "RWF", "SAR", "SBD", "SCR", "SDG", "SEK", "SGD", "SHP", "SLL", "SOS", "SRD",
"SSP", "STD", "STN", "SVC", "SYP", "SZL", "THB", "TJS", "TMT", "TND", "TOP", "TRY",
"TTD", "TWD", "TZS", "UAH", "UGX", "USD", "UYU", "UZS", "VEF", "VES", "VND", "VUV",
"WST", "XAF", "XAG", "XAU", "XCD", "XDR", "XOF", "XPD", "XPF", "XPT", "YER", "ZAR",
"ZMW", "ZWL"
]
# fmt: on
from typing import TypedDict

from oxr._types import Currency


class _Base(TypedDict):
Expand Down
Loading

0 comments on commit 73e3b8c

Please sign in to comment.