Skip to content

Commit 8c6d058

Browse files
matinrogelioLpz
andauthored
Transfers deposits (#15)
* transfers and deposits * created_at only for queryable * add commissions * Transfer.destination_uri * use cuenca-validaions lib. closes #10 * destination_uri in Transfers * make sure created_at is inherited * Tests and fixes * there are resources that are not cacheable * making Cacheable work creates too many edge cases that we can't properly test yet * cassette wasn't properly recorded * fix merge Co-authored-by: rogelioLpz <rogelio.lpz94@gmail.com>
1 parent 88772c8 commit 8c6d058

39 files changed

Lines changed: 1167 additions & 536 deletions

cuenca/__init__.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,24 @@
1-
__all__ = ['__version__', 'ApiKey', 'Transfer', 'configure']
1+
__all__ = [
2+
'__version__',
3+
'ApiKey',
4+
'Account',
5+
'BalanceEntry',
6+
'Deposit',
7+
'Transfer',
8+
'WhatsappTransfer',
9+
'configure',
10+
]
211

312

413
from .http import session
5-
from .resources import ApiKey, Transfer
14+
from .resources import (
15+
Account,
16+
ApiKey,
17+
BalanceEntry,
18+
Deposit,
19+
Transfer,
20+
WhatsappTransfer,
21+
)
622
from .version import __version__
723

824
configure = session.configure

cuenca/http/client.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import os
22
from typing import Optional, Tuple
3+
from urllib.parse import urljoin
34

45
import requests
6+
from cuenca_validations.typing import (
7+
ClientRequestParams,
8+
DictStrAny,
9+
OptionalDict,
10+
)
511
from requests import Response
612

713
from ..exc import CuencaResponseException
8-
from ..typing import ClientRequestParams, DictStrAny, OptionalDict
914
from ..version import API_VERSION, CLIENT_VERSION
1015

1116
API_URL = 'https://api.cuenca.com'
@@ -73,7 +78,7 @@ def request(
7378
) -> DictStrAny:
7479
resp = self.session.request(
7580
method=method,
76-
url=self.base_url + endpoint,
81+
url=self.base_url + urljoin('/', endpoint),
7782
auth=self.auth,
7883
json=data,
7984
params=params,

cuenca/resources/__init__.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,31 @@
1-
__all__ = ['ApiKey', 'Transfer']
1+
__all__ = [
2+
'ApiKey',
3+
'Account',
4+
'BalanceEntry',
5+
'Commission',
6+
'Deposit',
7+
'Transfer',
8+
'WhatsappTransfer',
9+
]
210

11+
from .accounts import Account
312
from .api_keys import ApiKey
13+
from .balance_entries import BalanceEntry
14+
from .commissions import Commission
15+
from .deposits import Deposit
16+
from .resources import RESOURCES
417
from .transfers import Transfer
18+
from .whatsapp_transfers import WhatsappTransfer
19+
20+
# avoid circular imports
21+
resource_classes = [
22+
ApiKey,
23+
Account,
24+
BalanceEntry,
25+
Commission,
26+
Deposit,
27+
Transfer,
28+
WhatsappTransfer,
29+
]
30+
for resource_cls in resource_classes:
31+
RESOURCES[resource_cls._resource] = resource_cls # type: ignore

cuenca/resources/accounts.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from typing import ClassVar
2+
3+
from pydantic.dataclasses import dataclass
4+
5+
from .base import Queryable, Retrievable
6+
7+
8+
@dataclass
9+
class Account(Retrievable, Queryable):
10+
_resource: ClassVar = 'accounts'
11+
12+
name: str # legal name provided by institution
13+
account_number: str
14+
institution_name: str

cuenca/resources/api_keys.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
11
import datetime as dt
22
from typing import ClassVar, Optional, cast
33

4+
from cuenca_validations import ApiKeyQuery
45
from pydantic.dataclasses import dataclass
56

67
from ..http import session
7-
from ..validators import ApiKeyQuery
88
from .base import Creatable, Queryable, Retrievable
99

1010

1111
@dataclass
1212
class ApiKey(Creatable, Queryable, Retrievable):
13-
_endpoint: ClassVar = '/api_keys'
13+
_resource: ClassVar = 'api_keys'
1414
_query_params: ClassVar = ApiKeyQuery
1515

16-
id: str
1716
secret: str
18-
created_at: dt.datetime
1917
deactivated_at: Optional[dt.datetime]
2018

2119
@property
@@ -38,6 +36,6 @@ def deactivate(cls, api_key_id: str, minutes: int = 0) -> 'ApiKey':
3836
locking you out. The deactivated key is returned so that you have the
3937
exact deactivated_at time.
4038
"""
41-
url = cls._endpoint + f'/{api_key_id}'
39+
url = cls._resource + f'/{api_key_id}'
4240
resp = session.delete(url, dict(minutes=minutes))
4341
return cast('ApiKey', cls._from_dict(resp))
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from typing import ClassVar
2+
3+
from pydantic.dataclasses import dataclass
4+
5+
from .base import Queryable, Retrievable
6+
from .resources import retrieve_uri
7+
8+
9+
@dataclass
10+
class BalanceEntry(Retrievable, Queryable):
11+
_resource: ClassVar = 'balance_entries'
12+
13+
amount: int # negative in the case of a debit
14+
descriptor: str
15+
rolling_balance: int
16+
transaction_uri: str
17+
18+
@property # type: ignore
19+
def transaction(self):
20+
return retrieve_uri(self.transaction_uri)

cuenca/resources/base.py

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,27 @@
1+
import datetime as dt
12
from dataclasses import asdict, dataclass, fields
23
from typing import ClassVar, Dict, Generator, Optional, Union
34
from urllib.parse import urlencode
45

6+
from cuenca_validations import (
7+
QueryParams,
8+
SantizedDict,
9+
Status,
10+
TransactionQuery,
11+
)
12+
513
from ..exc import MultipleResultsFound, NoResultFound
614
from ..http import session
7-
from ..types import SantizedDict
8-
from ..validators import QueryParams
915

1016

1117
@dataclass
1218
class Resource:
13-
_endpoint: ClassVar[str]
19+
_resource: ClassVar[str]
20+
21+
id: str
1422

15-
def __init__(self, **_): # pragma no cover
23+
# purely for MyPy
24+
def __init__(self, **_): # pragma: no cover
1625
...
1726

1827
@classmethod
@@ -38,7 +47,7 @@ def to_dict(self):
3847
class Retrievable(Resource):
3948
@classmethod
4049
def retrieve(cls, id: str) -> Resource:
41-
resp = session.get(f'{cls._endpoint}/{id}')
50+
resp = session.get(f'/{cls._resource}/{id}')
4251
return cls._from_dict(resp)
4352

4453
def refresh(self):
@@ -50,17 +59,20 @@ def refresh(self):
5059
class Creatable(Resource):
5160
@classmethod
5261
def _create(cls, **data) -> Resource:
53-
resp = session.post(cls._endpoint, data)
62+
resp = session.post(cls._resource, data)
5463
return cls._from_dict(resp)
5564

5665

66+
@dataclass
5767
class Queryable(Resource):
5868
_query_params: ClassVar = QueryParams
5969

70+
created_at: dt.datetime
71+
6072
@classmethod
6173
def one(cls, **query_params) -> Resource:
6274
q = cls._query_params(limit=2, **query_params)
63-
resp = session.get(cls._endpoint, q.dict())
75+
resp = session.get(cls._resource, q.dict())
6476
items = resp['items']
6577
len_items = len(items)
6678
if not len_items:
@@ -72,7 +84,7 @@ def one(cls, **query_params) -> Resource:
7284
@classmethod
7385
def first(cls, **query_params) -> Optional[Resource]:
7486
q = cls._query_params(limit=1, **query_params)
75-
resp = session.get(cls._endpoint, q.dict())
87+
resp = session.get(cls._resource, q.dict())
7688
try:
7789
item = resp['items'][0]
7890
except IndexError:
@@ -84,14 +96,23 @@ def first(cls, **query_params) -> Optional[Resource]:
8496
@classmethod
8597
def count(cls, **query_params) -> int:
8698
q = cls._query_params(count=True, **query_params)
87-
resp = session.get(cls._endpoint, q.dict())
99+
resp = session.get(cls._resource, q.dict())
88100
return resp['count']
89101

90102
@classmethod
91103
def all(cls, **query_params) -> Generator[Resource, None, None]:
92104
q = cls._query_params(**query_params)
93-
next_page_url = f'{cls._endpoint}?{urlencode(q.dict())}'
105+
next_page_url = f'{cls._resource}?{urlencode(q.dict())}'
94106
while next_page_url:
95107
page = session.get(next_page_url)
96108
yield from (cls._from_dict(item) for item in page['items'])
97109
next_page_url = page['next_page_url']
110+
111+
112+
@dataclass
113+
class Transaction(Retrievable, Queryable):
114+
_query_params: ClassVar = TransactionQuery
115+
116+
amount: int # in centavos
117+
status: Status
118+
descriptor: str # how it appears for the customer

cuenca/resources/commissions.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from typing import ClassVar, Optional
2+
3+
from pydantic.dataclasses import dataclass
4+
5+
from .base import Transaction
6+
7+
8+
@dataclass
9+
class Commission(Transaction):
10+
_resource: ClassVar = 'commissions'
11+
12+
related_transaction_uri: Optional[str]

cuenca/resources/deposits.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from dataclasses import dataclass
2+
from typing import ClassVar, Optional, cast
3+
4+
from cuenca_validations.types import DepositNetwork
5+
6+
from .accounts import Account
7+
from .base import Transaction
8+
from .resources import retrieve_uri
9+
10+
11+
@dataclass
12+
class Deposit(Transaction):
13+
_resource: ClassVar = 'deposits'
14+
15+
network: DepositNetwork
16+
source_uri: Optional[str]
17+
tracking_key: Optional[str] # clave rastreo if network is SPEI
18+
19+
@property # type: ignore
20+
def source(self) -> Optional[Account]:
21+
if self.source_uri is None: # cash deposit
22+
acct = None
23+
else:
24+
acct = cast(Account, retrieve_uri(self.source_uri))
25+
return acct

cuenca/resources/resources.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import re
2+
from typing import Dict, cast
3+
4+
from .base import Retrievable
5+
6+
ENDPOINT_RE = re.compile(r'.*/(?P<resource>[a-z]+)/(?P<id>.+)$')
7+
RESOURCES: Dict[str, Retrievable] = {} # set in ./__init__.py after imports
8+
9+
10+
def retrieve_uri(uri: str) -> Retrievable:
11+
m = ENDPOINT_RE.match(uri)
12+
if not m:
13+
raise ValueError(f'uri is not a valid format: {uri}')
14+
resource, id_ = m.groups()
15+
return cast(Retrievable, RESOURCES[resource].retrieve(id_))

0 commit comments

Comments
 (0)