Skip to content

Commit 5d7ecbb

Browse files
authored
Merge pull request #387
MPT-24264 Add the exchange pairs service
2 parents a9aece1 + 3ab079d commit 5d7ecbb

4 files changed

Lines changed: 262 additions & 0 deletions

File tree

‎mpt_api_client/resources/exchange/exchange.py‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
AsyncCurrenciesService,
44
CurrenciesService,
55
)
6+
from mpt_api_client.resources.exchange.pairs import AsyncPairsService, PairsService
67

78

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

20+
@property
21+
def pairs(self) -> PairsService:
22+
"""Pairs service."""
23+
return PairsService(http_client=self.http_client)
24+
1925

2026
class AsyncExchange:
2127
"""Exchange MPT API Module."""
@@ -27,3 +33,8 @@ def __init__(self, *, http_client: AsyncHTTPClient):
2733
def currencies(self) -> AsyncCurrenciesService:
2834
"""Currencies service."""
2935
return AsyncCurrenciesService(http_client=self.http_client)
36+
37+
@property
38+
def pairs(self) -> AsyncPairsService:
39+
"""Pairs service."""
40+
return AsyncPairsService(http_client=self.http_client)
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
from mpt_api_client.http import AsyncService, Service
2+
from mpt_api_client.http.mixins import (
3+
AsyncCollectionMixin,
4+
AsyncGetMixin,
5+
CollectionMixin,
6+
GetMixin,
7+
)
8+
from mpt_api_client.models import Model
9+
from mpt_api_client.models.model import BaseModel
10+
11+
12+
class Pair(Model):
13+
"""Exchange currency pair resource.
14+
15+
Attributes:
16+
name: Pair name.
17+
external_id: External identifier of the pair.
18+
notes: Free-form notes for the pair.
19+
primary: Whether the pair is the primary one for its currencies.
20+
reverse: Reference to the pair holding the reverse conversion.
21+
source_currency: Currency converted from.
22+
destination_currency: Currency converted to.
23+
latest_rate: Most recent rate recorded for the pair.
24+
agreements: Number of agreements using the pair.
25+
rates: Number of rates recorded for the pair.
26+
status: Current status of the pair.
27+
revision: Revision number.
28+
audit: Audit information (created, updated events).
29+
"""
30+
31+
name: str | None
32+
external_id: str | None
33+
notes: str | None
34+
primary: bool | None
35+
reverse: BaseModel | None
36+
source_currency: BaseModel | None
37+
destination_currency: BaseModel | None
38+
latest_rate: BaseModel | None
39+
agreements: int | None
40+
rates: int | None
41+
status: str | None
42+
revision: int | None
43+
audit: BaseModel | None
44+
45+
46+
class PairsServiceConfig:
47+
"""Exchange Pairs service configuration."""
48+
49+
_endpoint = "/public/v1/exchange/pairs"
50+
_model_class = Pair
51+
_collection_key = "data"
52+
53+
54+
class PairsService(
55+
GetMixin[Pair],
56+
CollectionMixin[Pair],
57+
Service[Pair],
58+
PairsServiceConfig,
59+
):
60+
"""Exchange Pairs service."""
61+
62+
63+
class AsyncPairsService(
64+
AsyncGetMixin[Pair],
65+
AsyncCollectionMixin[Pair],
66+
AsyncService[Pair],
67+
PairsServiceConfig,
68+
):
69+
"""Async Exchange Pairs service."""

‎tests/unit/resources/exchange/test_exchange.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
CurrenciesService,
66
)
77
from mpt_api_client.resources.exchange.exchange import AsyncExchange, Exchange
8+
from mpt_api_client.resources.exchange.pairs import AsyncPairsService, PairsService
89

910

1011
@pytest.fixture
@@ -41,3 +42,17 @@ def test_async_exchange_currencies_property(async_exchange):
4142

4243
assert isinstance(result, AsyncCurrenciesService)
4344
assert result.http_client is async_exchange.http_client
45+
46+
47+
def test_exchange_pairs_property(exchange):
48+
result = exchange.pairs
49+
50+
assert isinstance(result, PairsService)
51+
assert result.http_client is exchange.http_client
52+
53+
54+
def test_async_exchange_pairs_property(async_exchange):
55+
result = async_exchange.pairs
56+
57+
assert isinstance(result, AsyncPairsService)
58+
assert result.http_client is async_exchange.http_client
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import httpx
2+
import pytest
3+
import respx
4+
5+
from mpt_api_client.models.model import BaseModel
6+
from mpt_api_client.resources.exchange.pairs import AsyncPairsService, Pair, PairsService
7+
8+
9+
@pytest.fixture
10+
def pairs_service(http_client):
11+
return PairsService(http_client=http_client)
12+
13+
14+
@pytest.fixture
15+
def async_pairs_service(async_http_client):
16+
return AsyncPairsService(http_client=async_http_client)
17+
18+
19+
@pytest.fixture
20+
def pair_data():
21+
return {
22+
"id": "PAI-001",
23+
"name": "USD/EUR",
24+
"externalId": "EXT-001",
25+
"notes": "Primary conversion pair",
26+
"primary": True,
27+
"reverse": {"id": "PAI-002", "name": "EUR/USD"},
28+
"sourceCurrency": {"id": "CUR-001", "code": "USD"},
29+
"destinationCurrency": {"id": "CUR-002", "code": "EUR"},
30+
"latestRate": {"id": "RAT-001", "value": 0.92},
31+
"agreements": 3,
32+
"rates": 12,
33+
"status": "Active",
34+
"revision": 1,
35+
"audit": {"created": {"at": "2024-01-01T00:00:00Z"}},
36+
}
37+
38+
39+
@pytest.mark.parametrize("method", ["get", "fetch_page", "fetch_one", "iterate"])
40+
def test_mixins_present(pairs_service, method):
41+
result = hasattr(pairs_service, method)
42+
43+
assert result is True
44+
45+
46+
@pytest.mark.parametrize("method", ["get", "fetch_page", "fetch_one", "iterate"])
47+
def test_async_mixins_present(async_pairs_service, method):
48+
result = hasattr(async_pairs_service, method)
49+
50+
assert result is True
51+
52+
53+
def test_pairs_service_endpoint(pairs_service):
54+
result = pairs_service.build_path()
55+
56+
assert result == "/public/v1/exchange/pairs"
57+
58+
59+
def test_async_pairs_service_endpoint(async_pairs_service):
60+
result = async_pairs_service.build_path()
61+
62+
assert result == "/public/v1/exchange/pairs"
63+
64+
65+
def test_pair_primitive_fields(pair_data):
66+
result = Pair(pair_data)
67+
68+
assert result.to_dict() == pair_data
69+
70+
71+
@pytest.mark.parametrize("field", ["name", "external_id", "notes", "status"])
72+
def test_pair_string_fields(pair_data, field):
73+
result = Pair(pair_data)
74+
75+
assert isinstance(getattr(result, field), str)
76+
77+
78+
@pytest.mark.parametrize("field", ["agreements", "rates", "revision"])
79+
def test_pair_integer_fields(pair_data, field):
80+
result = Pair(pair_data)
81+
82+
assert isinstance(getattr(result, field), int)
83+
84+
85+
def test_pair_primary_field(pair_data):
86+
result = Pair(pair_data)
87+
88+
assert result.primary is True
89+
90+
91+
@pytest.mark.parametrize(
92+
"field",
93+
["reverse", "source_currency", "destination_currency", "latest_rate", "audit"],
94+
)
95+
def test_pair_nested_model_fields(pair_data, field):
96+
result = Pair(pair_data)
97+
98+
assert isinstance(getattr(result, field), BaseModel)
99+
100+
101+
@pytest.mark.parametrize("field", ["name", "external_id", "status", "audit"])
102+
def test_pair_optional_fields_absent(field):
103+
result = Pair({"id": "PAI-001"})
104+
105+
assert not hasattr(result, field)
106+
107+
108+
def test_pair_id_present():
109+
result = Pair({"id": "PAI-001"})
110+
111+
assert result.id == "PAI-001"
112+
113+
114+
def test_get_pair(pairs_service):
115+
pair_id = "PAI-001"
116+
expected_response = {"id": pair_id, "name": "USD/EUR", "primary": True}
117+
with respx.mock:
118+
respx.get(f"https://api.example.com/public/v1/exchange/pairs/{pair_id}").mock(
119+
return_value=httpx.Response(httpx.codes.OK, json=expected_response)
120+
)
121+
122+
result = pairs_service.get(pair_id)
123+
124+
assert result.to_dict() == expected_response
125+
126+
127+
async def test_async_get_pair(async_pairs_service):
128+
pair_id = "PAI-001"
129+
expected_response = {"id": pair_id, "name": "USD/EUR", "primary": True}
130+
with respx.mock:
131+
respx.get(f"https://api.example.com/public/v1/exchange/pairs/{pair_id}").mock(
132+
return_value=httpx.Response(httpx.codes.OK, json=expected_response)
133+
)
134+
135+
result = await async_pairs_service.get(pair_id)
136+
137+
assert result.to_dict() == expected_response
138+
139+
140+
def test_fetch_page_pairs(pairs_service):
141+
expected_pairs = [{"id": "PAI-001", "name": "USD/EUR"}, {"id": "PAI-002", "name": "EUR/USD"}]
142+
with respx.mock:
143+
respx.get("https://api.example.com/public/v1/exchange/pairs").mock(
144+
return_value=httpx.Response(
145+
httpx.codes.OK,
146+
json={"data": expected_pairs, "$meta": {"pagination": {"total": 2}}},
147+
)
148+
)
149+
150+
result = pairs_service.fetch_page()
151+
152+
assert [pair.to_dict() for pair in result] == expected_pairs
153+
154+
155+
async def test_async_fetch_page_pairs(async_pairs_service):
156+
expected_pairs = [{"id": "PAI-001", "name": "USD/EUR"}, {"id": "PAI-002", "name": "EUR/USD"}]
157+
with respx.mock:
158+
respx.get("https://api.example.com/public/v1/exchange/pairs").mock(
159+
return_value=httpx.Response(
160+
httpx.codes.OK,
161+
json={"data": expected_pairs, "$meta": {"pagination": {"total": 2}}},
162+
)
163+
)
164+
165+
result = await async_pairs_service.fetch_page()
166+
167+
assert [pair.to_dict() for pair in result] == expected_pairs

0 commit comments

Comments
 (0)