Skip to content

Commit 9f8b2cd

Browse files
authored
Migration: deprecate borderless-accounts endpoint, and provide account-detail and balance-statement endpoints. (#25)
1 parent dccfe9f commit 9f8b2cd

8 files changed

+293
-48
lines changed

pywisetransfer/__init__.py

+6
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,17 @@
33

44
class Client:
55
def add_resources(self) -> None:
6+
from pywisetransfer.account_details import AccountDetails
7+
from pywisetransfer.balance_statements import BalanceStatements
8+
from pywisetransfer.balances import Balances
69
from pywisetransfer.borderless_account import BorderlessAccount
710
from pywisetransfer.profile import Profile
811
from pywisetransfer.subscription import Subscription
912
from pywisetransfer.user import User
1013

14+
self.account_details = AccountDetails(client=self)
15+
self.balance_statements = BalanceStatements(client=self)
16+
self.balances = Balances(client=self)
1117
self.borderless_accounts = BorderlessAccount(client=self)
1218
self.profiles = Profile(client=self)
1319
self.subscriptions = Subscription(client=self)

pywisetransfer/account_details.py

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from typing import Any
2+
3+
from apiron import JsonEndpoint
4+
from munch import munchify
5+
6+
from pywisetransfer import Client
7+
from pywisetransfer.base import Base
8+
9+
10+
class AccountDetailsService(Base):
11+
list = JsonEndpoint(path="/v1/profiles/{profile_id}/account-details")
12+
13+
14+
class AccountDetails:
15+
def __init__(self, client: Client):
16+
self.service = AccountDetailsService(client=client)
17+
18+
def list(self, profile_id: str) -> list[Any]:
19+
return munchify(self.service.list(profile_id=profile_id))

pywisetransfer/balance_statements.py

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from typing import Any
2+
3+
from munch import munchify
4+
5+
from pywisetransfer import Client
6+
from pywisetransfer.base import Base
7+
from pywisetransfer.endpoint import JsonEndpointWithSCA
8+
9+
10+
class BalanceStatementsService(Base):
11+
statement = JsonEndpointWithSCA(
12+
path="/v1/profiles/{profile_id}/balance-statements/{balance_id}/statement.json",
13+
required_params=["currency", "intervalStart", "intervalEnd"],
14+
)
15+
16+
17+
class BalanceStatements:
18+
def __init__(self, client: Client):
19+
self.service = BalanceStatementsService(client=client)
20+
21+
def statement(
22+
self,
23+
profile_id: str,
24+
balance_id: str,
25+
currency: str,
26+
interval_start: str,
27+
interval_end: str,
28+
type: str = "COMPACT",
29+
) -> Any:
30+
valid_types = ["COMPACT", "FLAT"]
31+
if type not in valid_types:
32+
raise ValueError(f"Invalid type '{type}'; value values are: {valid_types}")
33+
34+
return munchify(
35+
self.service.statement(
36+
profile_id=profile_id,
37+
balance_id=balance_id,
38+
params={
39+
"currency": currency,
40+
"intervalStart": interval_start,
41+
"intervalEnd": interval_end,
42+
"type": type,
43+
},
44+
)
45+
)

pywisetransfer/balances.py

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from typing import Any
2+
3+
from apiron import JsonEndpoint
4+
from munch import munchify
5+
6+
from pywisetransfer import Client
7+
from pywisetransfer.base import Base
8+
9+
10+
class BalancesService(Base):
11+
list = JsonEndpoint(path="/v4/profiles/{profile_id}/balances", required_params=["types"])
12+
get = JsonEndpoint(path="/v4/profiles/{profile_id}/balances/{balance_id}")
13+
14+
15+
class Balances:
16+
def __init__(self, client: Client):
17+
self.service = BalancesService(client=client)
18+
19+
def list(self, profile_id: str, types: str | list[str] = "STANDARD") -> Any:
20+
if not isinstance(types, list):
21+
assert isinstance(types, str)
22+
types = [types]
23+
24+
valid_types = ["STANDARD", "SAVINGS"]
25+
for value in types:
26+
assert isinstance(value, str)
27+
if value not in valid_types:
28+
raise ValueError(f"Invalid type '{type}'; value values are: {valid_types}")
29+
30+
params = {"types": ",".join(types)}
31+
return munchify(self.service.list(profile_id=profile_id, params=params))
32+
33+
def get(self, profile_id: str, balance_id: str) -> Any:
34+
return munchify(self.service.get(profile_id=profile_id, balance_id=balance_id))

pywisetransfer/borderless_account.py

+7
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from pywisetransfer import Client
77
from pywisetransfer.base import Base
8+
from pywisetransfer.deprecation import deprecated
89
from pywisetransfer.endpoint import JsonEndpointWithSCA
910

1011

@@ -20,10 +21,16 @@ class BorderlessAccount:
2021
def __init__(self, client: Client):
2122
self.service = BorderlessAccountService(client=client)
2223

24+
@deprecated(
25+
message="The borderless-accounts endpoint is deprecated; please use account-details instead"
26+
)
2327
def list(self, profile_id: str) -> list[Any]:
2428
accounts: list[Any] = self.service.list(params={"profileId": profile_id})
2529
return munchify(accounts)
2630

31+
@deprecated(
32+
message="The borderless-accounts statement endpoint is deprecated; please use balance-statements instead"
33+
)
2734
def statement(
2835
self,
2936
profile_id: str,

pywisetransfer/deprecation.py

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from __future__ import annotations
2+
3+
import warnings
4+
5+
6+
class deprecated:
7+
"""Decorator to indicate that a method is deprecated, with an optional
8+
message to emit in the warning.
9+
10+
Written following guidance from:
11+
https://blog.miguelgrinberg.com/post/the-ultimate-guide-to-python-decorators-part-iii-decorators-with-arguments
12+
"""
13+
14+
def __init__(self, *args, **kwargs):
15+
if len(args) == 1 and callable(args[0]):
16+
self.f = args[0]
17+
message = args[1] if len(args) > 1 else None
18+
else:
19+
self.f = None
20+
message = args[0] if len(args) == 1 else None
21+
self.message = kwargs.get("message", message)
22+
23+
def __call__(self, *args, **kwargs):
24+
if self.f is None and len(args) == 1 and callable(args[0]):
25+
self.f = args[0]
26+
return self
27+
28+
warnings.warn(self.message, DeprecationWarning, stacklevel=2)
29+
return self.f(*args, **kwargs)
30+
31+
def __repr__(self):
32+
return f"<deprecated {repr(self.f)}>"

test/test_deprecation.py

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
from warnings import catch_warnings
2+
3+
import pytest
4+
5+
from pywisetransfer.deprecation import deprecated
6+
7+
record_warnings = lambda: catch_warnings(record=True)
8+
9+
10+
def undecorated():
11+
return 1
12+
13+
14+
@deprecated
15+
def bare_decorator():
16+
return 1
17+
18+
19+
@deprecated()
20+
def zero_args_decorator():
21+
return 1
22+
23+
24+
@deprecated("positional")
25+
def posarg_decorator():
26+
return 1
27+
28+
29+
@deprecated(message="keyword")
30+
def kwarg_decorator():
31+
return 1
32+
33+
34+
@pytest.mark.parametrize(
35+
"func, name, deprecated, message",
36+
[
37+
(undecorated, "undecorated", False, None),
38+
(bare_decorator, "bare_decorator", True, None),
39+
(zero_args_decorator, "zero_args_decorator", True, None),
40+
(posarg_decorator, "posarg_decorator", True, "positional"),
41+
(kwarg_decorator, "kwarg_decorator", True, "keyword"),
42+
],
43+
)
44+
def test_no_decorator(func, name, deprecated, message):
45+
actual_repr = repr(func)
46+
with record_warnings() as ws:
47+
result = func()
48+
49+
# Check the Python repr of the function
50+
assert name in actual_repr
51+
actual_deprecated = "deprecated" in actual_repr
52+
assert deprecated == actual_deprecated
53+
54+
# Check the behaviour of the function
55+
assert result == 1
56+
57+
# Check the warnings emitted by the function
58+
assert ws if deprecated else not ws
59+
assert any([message in str(w.message) for w in ws] if message else [True])

0 commit comments

Comments
 (0)