Skip to content

Commit 7e4544e

Browse files
felipao-mxmatin
andauthored
Cards (#43)
* updatable * update package * updateable resource * iam * test pass * tests * correct domain * revert * corrected dependency * rebase * prod version + more consistent way of defining version * better way to manage IAM auth and basic auth side-by-side * minor cleanup * add type hiting to Client.auth * this makes more sense * give preference to basic auth * col79 * test deactivate * tests * sandbox.cuenca.com * creds tests * remove unused params * remove prints * revert * all() * no need to use multiple lines when it fits on one Co-authored-by: Matin Tamizi <matin@cuenca.com> Co-authored-by: Matin Tamizi <matin@users.noreply.github.com>
1 parent b84bc1c commit 7e4544e

21 files changed

+840
-31
lines changed

cuenca/http/client.py

Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import os
2-
from typing import Optional, Tuple
2+
from typing import Optional, Tuple, Union
33
from urllib.parse import urljoin
44

55
import requests
6+
from aws_requests_auth.aws_auth import AWSRequestsAuth
67
from cuenca_validations.typing import (
78
ClientRequestParams,
89
DictStrAny,
@@ -13,15 +14,17 @@
1314
from ..exc import CuencaResponseException
1415
from ..version import API_VERSION, CLIENT_VERSION
1516

16-
API_URL = 'https://api.cuenca.com'
17-
SANDBOX_URL = 'https://sandbox.cuenca.com'
17+
API_HOST = 'api.cuenca.com'
18+
SANDBOX_HOST = 'sandbox.cuenca.com'
19+
AWS_DEFAULT_REGION = 'us-east-1'
20+
AWS_SERVICE = 'execute-api'
1821

1922

2023
class Session:
2124

22-
base_url: str
23-
auth: Tuple[str, str]
24-
webhook_secret: Optional[str]
25+
host: str = API_HOST
26+
basic_auth: Tuple[str, str]
27+
iam_auth: Optional[AWSRequestsAuth] = None
2528
session: requests.Session
2629

2730
def __init__(self):
@@ -32,30 +35,72 @@ def __init__(self):
3235
'User-Agent': f'cuenca-python/{CLIENT_VERSION}',
3336
}
3437
)
35-
self.base_url = API_URL
38+
39+
# basic auth
3640
api_key = os.getenv('CUENCA_API_KEY', '')
3741
api_secret = os.getenv('CUENCA_API_SECRET', '')
38-
self.webhook_secret = os.getenv('CUENCA_WEBHOOK_SECRET')
39-
self.auth = (api_key, api_secret)
42+
self.basic_auth = (api_key, api_secret)
43+
44+
# IAM auth
45+
aws_access_key = os.getenv('AWS_ACCESS_KEY_ID', '')
46+
aws_secret_access_key = os.getenv('AWS_SECRET_ACCESS_KEY', '')
47+
aws_region = os.getenv('AWS_DEFAULT_REGION', AWS_DEFAULT_REGION)
48+
if aws_access_key and aws_secret_access_key:
49+
self.iam_auth = AWSRequestsAuth(
50+
aws_access_key=aws_access_key,
51+
aws_secret_access_key=aws_secret_access_key,
52+
aws_host=self.host,
53+
aws_region=aws_region,
54+
aws_service=AWS_SERVICE,
55+
)
56+
57+
@property
58+
def auth(self) -> Union[AWSRequestsAuth, Tuple[str, str]]:
59+
# preference to basic auth
60+
return self.basic_auth if all(self.basic_auth) else self.iam_auth
4061

4162
def configure(
4263
self,
4364
api_key: Optional[str] = None,
4465
api_secret: Optional[str] = None,
45-
webhook_secret: Optional[str] = None,
66+
aws_access_key: Optional[str] = None,
67+
aws_secret_access_key: Optional[str] = None,
68+
aws_region: str = AWS_DEFAULT_REGION,
4669
sandbox: Optional[bool] = None,
4770
):
4871
"""
4972
This allows us to instantiate the http client when importing the
5073
client library and configure it later. It's also useful when rolling
5174
the api key
5275
"""
53-
self.auth = (api_key or self.auth[0], api_secret or self.auth[1])
54-
self.webhook_secret = webhook_secret or self.webhook_secret
5576
if sandbox is False:
56-
self.base_url = API_URL
77+
self.host = API_HOST
5778
elif sandbox is True:
58-
self.base_url = SANDBOX_URL
79+
self.host = SANDBOX_HOST
80+
81+
# basic auth
82+
self.basic_auth = (
83+
api_key or self.basic_auth[0],
84+
api_secret or self.basic_auth[1],
85+
)
86+
87+
# IAM auth
88+
if self.iam_auth is not None:
89+
self.iam_auth.aws_access_key = (
90+
aws_access_key or self.iam_auth.aws_access_key
91+
)
92+
self.iam_auth.aws_secret_access_key = (
93+
aws_secret_access_key or self.iam_auth.aws_secret_access_key
94+
)
95+
self.iam_auth.aws_region = aws_region or self.iam_auth.aws_region
96+
elif aws_access_key and aws_secret_access_key:
97+
self.iam_auth = AWSRequestsAuth(
98+
aws_access_key=aws_access_key,
99+
aws_secret_access_key=aws_secret_access_key,
100+
aws_host=self.host,
101+
aws_region=aws_region,
102+
aws_service=AWS_SERVICE,
103+
)
59104

60105
def get(
61106
self, endpoint: str, params: ClientRequestParams = None,
@@ -65,6 +110,9 @@ def get(
65110
def post(self, endpoint: str, data: DictStrAny) -> DictStrAny:
66111
return self.request('post', endpoint, data=data)
67112

113+
def patch(self, endpoint: str, data: DictStrAny) -> DictStrAny:
114+
return self.request('patch', endpoint, data=data)
115+
68116
def delete(self, endpoint: str, data: OptionalDict = None) -> DictStrAny:
69117
return self.request('delete', endpoint, data=data)
70118

@@ -78,7 +126,7 @@ def request(
78126
) -> DictStrAny:
79127
resp = self.session.request(
80128
method=method,
81-
url=self.base_url + urljoin('/', endpoint),
129+
url='https://' + self.host + urljoin('/', endpoint),
82130
auth=self.auth,
83131
json=data,
84132
params=params,

cuenca/resources/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
'ApiKey',
33
'Account',
44
'BalanceEntry',
5+
'Card',
56
'Commission',
67
'Deposit',
78
'Transfer',
@@ -11,6 +12,7 @@
1112
from .accounts import Account
1213
from .api_keys import ApiKey
1314
from .balance_entries import BalanceEntry
15+
from .cards import Card
1416
from .commissions import Commission
1517
from .deposits import Deposit
1618
from .resources import RESOURCES
@@ -22,6 +24,7 @@
2224
ApiKey,
2325
Account,
2426
BalanceEntry,
27+
Card,
2528
Commission,
2629
Deposit,
2730
Transfer,

cuenca/resources/base.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
from cuenca_validations.types import (
77
QueryParams,
88
SantizedDict,
9-
Status,
109
TransactionQuery,
10+
TransactionStatus,
1111
)
1212

1313
from ..exc import MultipleResultsFound, NoResultFound
@@ -63,6 +63,16 @@ def _create(cls, **data) -> Resource:
6363
return cls._from_dict(resp)
6464

6565

66+
@dataclass
67+
class Updateable(Resource):
68+
updated_at: dt.datetime
69+
70+
@classmethod
71+
def _update(cls, id: str, **data) -> Resource:
72+
resp = session.patch(f'/{cls._resource}/{id}', data)
73+
return cls._from_dict(resp)
74+
75+
6676
@dataclass
6777
class Queryable(Resource):
6878
_query_params: ClassVar = QueryParams
@@ -114,5 +124,5 @@ class Transaction(Retrievable, Queryable):
114124
_query_params: ClassVar = TransactionQuery
115125

116126
amount: int # in centavos
117-
status: Status
127+
status: TransactionStatus
118128
descriptor: str # how it appears for the customer

cuenca/resources/cards.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
from typing import ClassVar, Optional, cast
2+
3+
from cuenca_validations.types import CardStatus, CardType
4+
from cuenca_validations.types.queries import CardQuery
5+
from cuenca_validations.types.requests import CardRequest, CardUpdateRequest
6+
from pydantic.dataclasses import dataclass
7+
8+
from cuenca.resources.base import Creatable, Queryable, Retrievable, Updateable
9+
10+
from ..http import session
11+
12+
13+
@dataclass
14+
class Card(Retrievable, Queryable, Creatable, Updateable):
15+
_resource: ClassVar = 'cards'
16+
_query_params: ClassVar = CardQuery
17+
18+
user_id: str
19+
ledger_account_id: str
20+
number: str
21+
exp_month: int
22+
exp_year: int
23+
cvv2: str
24+
type: CardType
25+
status: CardStatus
26+
27+
@classmethod
28+
def create(cls, ledger_account_id: str, user_id: str) -> 'Card':
29+
"""
30+
Assigns user_id and ledger_account_id to a existing card
31+
32+
:param ledger_account_id: associated ledger account id
33+
:param user_id: associated user id
34+
:return: New assigned card
35+
"""
36+
req = CardRequest(ledger_account_id=ledger_account_id, user_id=user_id)
37+
return cast('Card', cls._create(**req.dict()))
38+
39+
@classmethod
40+
def update(
41+
cls,
42+
card_id: str,
43+
user_id: Optional[str] = None,
44+
ledger_account_id: Optional[str] = None,
45+
status: Optional[CardStatus] = None,
46+
):
47+
"""
48+
Updates card properties that are not sensitive or fixed data. It allows
49+
reconfigure properties like status, and manufacturer.
50+
51+
:param card_id: existing card_id
52+
:param user_id: owner user id
53+
:param ledger_account_id: owner ledger account
54+
:param status:
55+
:return: Updated card object
56+
"""
57+
req = CardUpdateRequest(
58+
user_id=user_id, ledger_account_id=ledger_account_id, status=status
59+
)
60+
resp = cls._update(card_id, **req.dict(exclude_none=True))
61+
return cast('Card', resp)
62+
63+
@classmethod
64+
def deactivate(cls, card_id: str) -> 'Card':
65+
"""
66+
Deactivates a card
67+
"""
68+
url = f'{cls._resource}/{card_id}'
69+
resp = session.delete(url)
70+
return cast('Card', cls._from_dict(resp))

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
requests==2.24.0
2-
cuenca-validations==0.4.3
2+
cuenca-validations==0.5.0
33
dataclasses>=0.7;python_version<"3.7"
4+
aws-requests-auth==0.4.3

setup.cfg

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,6 @@ combine_as_imports=True
1616
1717
[mypy-pytest]
1818
ignore_missing_imports = True
19+
20+
[mypy-aws_requests_auth.*]
21+
ignore_missing_imports = True

setup.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
setup(
1313
name='cuenca',
14-
version=version.CLIENT_VERSION,
14+
version=version.__version__,
1515
author='Cuenca',
1616
author_email='dev@cuenca.com',
1717
description='Cuenca API Client',
@@ -23,8 +23,9 @@
2323
package_data=dict(cuenca=['py.typed']),
2424
python_requires='>=3.6',
2525
install_requires=[
26+
'aws-requests-auth==0.4.3',
2627
'requests>=2.24,<2.25',
27-
'cuenca-validations>=0.4,<0.5',
28+
'cuenca-validations>=0.5.0,<0.6.0',
2829
'dataclasses>=0.7;python_version<"3.7"',
2930
],
3031
classifiers=[

tests/http/conftest.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import pytest
2+
3+
4+
@pytest.fixture
5+
def cuenca_creds(monkeypatch) -> None:
6+
monkeypatch.setenv('CUENCA_API_KEY', 'api_key')
7+
monkeypatch.setenv('CUENCA_API_SECRET', 'secret')
8+
9+
10+
@pytest.fixture
11+
def aws_creds(monkeypatch) -> None:
12+
monkeypatch.setenv('AWS_ACCESS_KEY_ID', 'aws_key')
13+
monkeypatch.setenv('AWS_SECRET_ACCESS_KEY', 'aws_secret')
14+
monkeypatch.setenv('AWS_DEFAULT_REGION', 'us-east-1')

tests/http/test_client.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,47 @@ def test_invalid_auth():
1212
session.post('/api_keys', dict())
1313
assert e.value.status_code == 401
1414
assert str(e.value)
15+
16+
17+
@pytest.mark.usefixtures('cuenca_creds')
18+
def test_basic_auth_configuration():
19+
session = Session()
20+
assert session.auth == session.basic_auth
21+
assert session.auth == ('api_key', 'secret')
22+
assert not session.iam_auth
23+
24+
25+
@pytest.mark.usefixtures('cuenca_creds', 'aws_creds')
26+
def test_gives_preference_to_basic_auth_configuration():
27+
session = Session()
28+
assert session.auth == session.basic_auth
29+
assert session.iam_auth
30+
31+
32+
@pytest.mark.usefixtures('aws_creds')
33+
def test_aws_iam_auth_configuration():
34+
session = Session()
35+
assert session.auth == session.iam_auth
36+
37+
38+
def test_configures_new_aws_creds():
39+
session = Session()
40+
session.configure(
41+
aws_access_key='new_aws_key', aws_secret_access_key='new_aws_secret'
42+
)
43+
assert session.auth.aws_secret_access_key == 'new_aws_secret'
44+
assert session.auth.aws_access_key == 'new_aws_key'
45+
assert session.auth.aws_region == 'us-east-1'
46+
47+
48+
@pytest.mark.usefixtures('aws_creds')
49+
def test_overrides_aws_creds():
50+
session = Session()
51+
session.configure(
52+
aws_access_key='new_aws_key',
53+
aws_secret_access_key='new_aws_secret',
54+
aws_region='us-east-2',
55+
)
56+
assert session.auth.aws_secret_access_key == 'new_aws_secret'
57+
assert session.auth.aws_access_key == 'new_aws_key'
58+
assert session.auth.aws_region == 'us-east-2'

0 commit comments

Comments
 (0)