Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions mpt_api_client/resources/exchange/exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
AsyncCurrenciesService,
CurrenciesService,
)
from mpt_api_client.resources.exchange.pairs import AsyncPairsService, PairsService


class Exchange:
Expand All @@ -16,6 +17,11 @@ def currencies(self) -> CurrenciesService:
"""Currencies service."""
return CurrenciesService(http_client=self.http_client)

@property
def pairs(self) -> PairsService:
"""Pairs service."""
return PairsService(http_client=self.http_client)


class AsyncExchange:
"""Exchange MPT API Module."""
Expand All @@ -27,3 +33,8 @@ def __init__(self, *, http_client: AsyncHTTPClient):
def currencies(self) -> AsyncCurrenciesService:
"""Currencies service."""
return AsyncCurrenciesService(http_client=self.http_client)

@property
def pairs(self) -> AsyncPairsService:
"""Pairs service."""
return AsyncPairsService(http_client=self.http_client)
69 changes: 69 additions & 0 deletions mpt_api_client/resources/exchange/pairs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from mpt_api_client.http import AsyncService, Service
from mpt_api_client.http.mixins import (
AsyncCollectionMixin,
AsyncGetMixin,
CollectionMixin,
GetMixin,
)
from mpt_api_client.models import Model
from mpt_api_client.models.model import BaseModel


class Pair(Model):
"""Exchange currency pair resource.

Attributes:
name: Pair name.
external_id: External identifier of the pair.
notes: Free-form notes for the pair.
primary: Whether the pair is the primary one for its currencies.
reverse: Reference to the pair holding the reverse conversion.
source_currency: Currency converted from.
destination_currency: Currency converted to.
latest_rate: Most recent rate recorded for the pair.
agreements: Number of agreements using the pair.
rates: Number of rates recorded for the pair.
status: Current status of the pair.
revision: Revision number.
audit: Audit information (created, updated events).
"""

name: str | None
external_id: str | None
notes: str | None
primary: bool | None
reverse: BaseModel | None
source_currency: BaseModel | None
destination_currency: BaseModel | None
latest_rate: BaseModel | None
agreements: int | None
rates: int | None
status: str | None
revision: int | None
audit: BaseModel | None


class PairsServiceConfig:
"""Exchange Pairs service configuration."""

_endpoint = "/public/v1/exchange/pairs"
_model_class = Pair
_collection_key = "data"


class PairsService(
GetMixin[Pair],
CollectionMixin[Pair],
Service[Pair],
PairsServiceConfig,
):
"""Exchange Pairs service."""


class AsyncPairsService(
AsyncGetMixin[Pair],
AsyncCollectionMixin[Pair],
AsyncService[Pair],
PairsServiceConfig,
):
"""Async Exchange Pairs service."""
15 changes: 15 additions & 0 deletions tests/unit/resources/exchange/test_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
CurrenciesService,
)
from mpt_api_client.resources.exchange.exchange import AsyncExchange, Exchange
from mpt_api_client.resources.exchange.pairs import AsyncPairsService, PairsService


@pytest.fixture
Expand Down Expand Up @@ -41,3 +42,17 @@ def test_async_exchange_currencies_property(async_exchange):

assert isinstance(result, AsyncCurrenciesService)
assert result.http_client is async_exchange.http_client


def test_exchange_pairs_property(exchange):
result = exchange.pairs

assert isinstance(result, PairsService)
assert result.http_client is exchange.http_client


def test_async_exchange_pairs_property(async_exchange):
result = async_exchange.pairs

assert isinstance(result, AsyncPairsService)
assert result.http_client is async_exchange.http_client
167 changes: 167 additions & 0 deletions tests/unit/resources/exchange/test_pairs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import httpx
import pytest
import respx

from mpt_api_client.models.model import BaseModel
from mpt_api_client.resources.exchange.pairs import AsyncPairsService, Pair, PairsService


@pytest.fixture
def pairs_service(http_client):
return PairsService(http_client=http_client)


@pytest.fixture
def async_pairs_service(async_http_client):
return AsyncPairsService(http_client=async_http_client)


@pytest.fixture
def pair_data():
return {
"id": "PAI-001",
"name": "USD/EUR",
"externalId": "EXT-001",
"notes": "Primary conversion pair",
"primary": True,
"reverse": {"id": "PAI-002", "name": "EUR/USD"},
"sourceCurrency": {"id": "CUR-001", "code": "USD"},
"destinationCurrency": {"id": "CUR-002", "code": "EUR"},
"latestRate": {"id": "RAT-001", "value": 0.92},
"agreements": 3,
"rates": 12,
"status": "Active",
"revision": 1,
"audit": {"created": {"at": "2024-01-01T00:00:00Z"}},
}


@pytest.mark.parametrize("method", ["get", "fetch_page", "fetch_one", "iterate"])
def test_mixins_present(pairs_service, method):
result = hasattr(pairs_service, method)

assert result is True


@pytest.mark.parametrize("method", ["get", "fetch_page", "fetch_one", "iterate"])
def test_async_mixins_present(async_pairs_service, method):
result = hasattr(async_pairs_service, method)

assert result is True


def test_pairs_service_endpoint(pairs_service):
result = pairs_service.build_path()

assert result == "/public/v1/exchange/pairs"


def test_async_pairs_service_endpoint(async_pairs_service):
result = async_pairs_service.build_path()

assert result == "/public/v1/exchange/pairs"


def test_pair_primitive_fields(pair_data):
result = Pair(pair_data)

assert result.to_dict() == pair_data


@pytest.mark.parametrize("field", ["name", "external_id", "notes", "status"])
def test_pair_string_fields(pair_data, field):
result = Pair(pair_data)

assert isinstance(getattr(result, field), str)


@pytest.mark.parametrize("field", ["agreements", "rates", "revision"])
def test_pair_integer_fields(pair_data, field):
result = Pair(pair_data)

assert isinstance(getattr(result, field), int)


def test_pair_primary_field(pair_data):
result = Pair(pair_data)

assert result.primary is True


@pytest.mark.parametrize(
"field",
["reverse", "source_currency", "destination_currency", "latest_rate", "audit"],
)
def test_pair_nested_model_fields(pair_data, field):
result = Pair(pair_data)

assert isinstance(getattr(result, field), BaseModel)


@pytest.mark.parametrize("field", ["name", "external_id", "status", "audit"])
def test_pair_optional_fields_absent(field):
result = Pair({"id": "PAI-001"})

assert not hasattr(result, field)


def test_pair_id_present():
result = Pair({"id": "PAI-001"})

assert result.id == "PAI-001"


def test_get_pair(pairs_service):
pair_id = "PAI-001"
expected_response = {"id": pair_id, "name": "USD/EUR", "primary": True}
with respx.mock:
respx.get(f"https://api.example.com/public/v1/exchange/pairs/{pair_id}").mock(
return_value=httpx.Response(httpx.codes.OK, json=expected_response)
)

result = pairs_service.get(pair_id)

assert result.to_dict() == expected_response


async def test_async_get_pair(async_pairs_service):
pair_id = "PAI-001"
expected_response = {"id": pair_id, "name": "USD/EUR", "primary": True}
with respx.mock:
respx.get(f"https://api.example.com/public/v1/exchange/pairs/{pair_id}").mock(
return_value=httpx.Response(httpx.codes.OK, json=expected_response)
)

result = await async_pairs_service.get(pair_id)

assert result.to_dict() == expected_response


def test_fetch_page_pairs(pairs_service):
expected_pairs = [{"id": "PAI-001", "name": "USD/EUR"}, {"id": "PAI-002", "name": "EUR/USD"}]
with respx.mock:
respx.get("https://api.example.com/public/v1/exchange/pairs").mock(
return_value=httpx.Response(
httpx.codes.OK,
json={"data": expected_pairs, "$meta": {"pagination": {"total": 2}}},
)
)

result = pairs_service.fetch_page()

assert [pair.to_dict() for pair in result] == expected_pairs


async def test_async_fetch_page_pairs(async_pairs_service):
expected_pairs = [{"id": "PAI-001", "name": "USD/EUR"}, {"id": "PAI-002", "name": "EUR/USD"}]
with respx.mock:
respx.get("https://api.example.com/public/v1/exchange/pairs").mock(
return_value=httpx.Response(
httpx.codes.OK,
json={"data": expected_pairs, "$meta": {"pagination": {"total": 2}}},
)
)

result = await async_pairs_service.fetch_page()

assert [pair.to_dict() for pair in result] == expected_pairs