Repository: cuenca-mx/cuenca-python

Issue:
Customized session in each request
There are cases where you might have to use temporary credentials in order to perform an operation. For instance, when you need to authenticate a user for certain request.

Right now there's already a way to override the current header by doing:
```python
import cuenca

# Configure user credentials
cuenca.session.configure(
    api_key=USER_API_KEY,
    api_secret=USER_SECRET_KEY
)

# Perform operation
cuenca.Card.all(user_id='USlskdjflskdjf')

# Return to default credentials
cuenca.session.configure(
    api_key =CUENCA_API_KEY,
    api_secret=CUENCA_SECRET_KEY
)
```

But it would be clearer and safer doing something like:
```python
import cuenca

with cuenca.configure(
        api_key=USER_API_KEY,
        api_secret=USER_SECRET_KEY
    ) as cuenca_user:
    cuenca_user.Card.all(user_id='USlskdjflskdjf')
```

This is specially useful in `core` which has to make requests on behalf of users to authenticate with their credentials.

Comments:

Patch:
diff --git a/cuenca/__init__.py b/cuenca/__init__.py
index b7a01ba..441c121 100644
--- a/cuenca/__init__.py
+++ b/cuenca/__init__.py
@@ -4,6 +4,7 @@ __all__ = [
     'Account',
     'BalanceEntry',
     'BillPayment',
+    'Card',
     'CardTransaction',
     'Commission',
     'Deposit',
@@ -20,6 +21,7 @@ from .resources import (
     ApiKey,
     BalanceEntry,
     BillPayment,
+    Card,
     CardTransaction,
     Commission,
     Deposit,
diff --git a/cuenca/resources/base.py b/cuenca/resources/base.py
index ae28f77..d477d24 100644
--- a/cuenca/resources/base.py
+++ b/cuenca/resources/base.py
@@ -11,7 +11,7 @@ from cuenca_validations.types import (
 )
 
 from ..exc import MultipleResultsFound, NoResultFound
-from ..http import session
+from ..http import Session, session as global_session
 
 
 @dataclass
@@ -46,19 +46,21 @@ class Resource:
 
 class Retrievable(Resource):
     @classmethod
-    def retrieve(cls, id: str) -> Resource:
+    def retrieve(
+        cls, id: str, *, session: Session = global_session
+    ) -> Resource:
         resp = session.get(f'/{cls._resource}/{id}')
         return cls._from_dict(resp)
 
-    def refresh(self):
-        new = self.retrieve(self.id)
+    def refresh(self, *, session: Session = global_session):
+        new = self.retrieve(self.id, session=session)
         for attr, value in new.__dict__.items():
             setattr(self, attr, value)
 
 
 class Creatable(Resource):
     @classmethod
-    def _create(cls, **data) -> Resource:
+    def _create(cls, *, session: Session = global_session, **data) -> Resource:
         resp = session.post(cls._resource, data)
         return cls._from_dict(resp)
 
@@ -68,7 +70,9 @@ class Updateable(Resource):
     updated_at: dt.datetime
 
     @classmethod
-    def _update(cls, id: str, **data) -> Resource:
+    def _update(
+        cls, id: str, *, session: Session = global_session, **data
+    ) -> Resource:
         resp = session.patch(f'/{cls._resource}/{id}', data)
         return cls._from_dict(resp)
 
@@ -80,7 +84,9 @@ class Queryable(Resource):
     created_at: dt.datetime
 
     @classmethod
-    def one(cls, **query_params) -> Resource:
+    def one(
+        cls, *, session: Session = global_session, **query_params
+    ) -> Resource:
         q = cls._query_params(limit=2, **query_params)
         resp = session.get(cls._resource, q.dict())
         items = resp['items']
@@ -92,7 +98,9 @@ class Queryable(Resource):
         return cls._from_dict(items[0])
 
     @classmethod
-    def first(cls, **query_params) -> Optional[Resource]:
+    def first(
+        cls, *, session: Session = global_session, **query_params
+    ) -> Optional[Resource]:
         q = cls._query_params(limit=1, **query_params)
         resp = session.get(cls._resource, q.dict())
         try:
@@ -104,13 +112,18 @@ class Queryable(Resource):
         return rv
 
     @classmethod
-    def count(cls, **query_params) -> int:
+    def count(
+        cls, *, session: Session = global_session, **query_params
+    ) -> int:
         q = cls._query_params(count=True, **query_params)
         resp = session.get(cls._resource, q.dict())
         return resp['count']
 
     @classmethod
-    def all(cls, **query_params) -> Generator[Resource, None, None]:
+    def all(
+        cls, *, session: Session = global_session, **query_params
+    ) -> Generator[Resource, None, None]:
+        session = session or global_session
         q = cls._query_params(**query_params)
         next_page_uri = f'{cls._resource}?{urlencode(q.dict())}'
         while next_page_uri:
diff --git a/cuenca/resources/cards.py b/cuenca/resources/cards.py
index f20d35d..2c4ea77 100644
--- a/cuenca/resources/cards.py
+++ b/cuenca/resources/cards.py
@@ -7,7 +7,7 @@ from pydantic.dataclasses import dataclass
 
 from cuenca.resources.base import Creatable, Queryable, Retrievable, Updateable
 
-from ..http import session
+from ..http import Session, session as global_session
 
 
 @dataclass
@@ -25,7 +25,13 @@ class Card(Retrievable, Queryable, Creatable, Updateable):
     status: CardStatus
 
     @classmethod
-    def create(cls, ledger_account_id: str, user_id: str) -> 'Card':
+    def create(
+        cls,
+        ledger_account_id: str,
+        user_id: str,
+        *,
+        session: Session = global_session,
+    ) -> 'Card':
         """
         Assigns user_id and ledger_account_id to a existing card
 
@@ -34,7 +40,7 @@ class Card(Retrievable, Queryable, Creatable, Updateable):
         :return: New assigned card
         """
         req = CardRequest(ledger_account_id=ledger_account_id, user_id=user_id)
-        return cast('Card', cls._create(**req.dict()))
+        return cast('Card', cls._create(session=session, **req.dict()))
 
     @classmethod
     def update(
@@ -43,6 +49,8 @@ class Card(Retrievable, Queryable, Creatable, Updateable):
         user_id: Optional[str] = None,
         ledger_account_id: Optional[str] = None,
         status: Optional[CardStatus] = None,
+        *,
+        session: Session = global_session,
     ):
         """
         Updates card properties that are not sensitive or fixed data. It allows
@@ -57,11 +65,15 @@ class Card(Retrievable, Queryable, Creatable, Updateable):
         req = CardUpdateRequest(
             user_id=user_id, ledger_account_id=ledger_account_id, status=status
         )
-        resp = cls._update(card_id, **req.dict(exclude_none=True))
+        resp = cls._update(
+            card_id, session=session, **req.dict(exclude_none=True)
+        )
         return cast('Card', resp)
 
     @classmethod
-    def deactivate(cls, card_id: str) -> 'Card':
+    def deactivate(
+        cls, card_id: str, *, session: Session = global_session
+    ) -> 'Card':
         """
         Deactivates a card
         """
diff --git a/cuenca/version.py b/cuenca/version.py
index ef205ef..6603bec 100644
--- a/cuenca/version.py
+++ b/cuenca/version.py
@@ -1,3 +1,3 @@
-__version__ = '0.4.1'
+__version__ = '0.4.2'
 CLIENT_VERSION = __version__
 API_VERSION = '2020-03-19'


Test patch:
diff --git a/tests/http/test_client.py b/tests/http/test_client.py
index aa630b3..867e41a 100644
--- a/tests/http/test_client.py
+++ b/tests/http/test_client.py
@@ -1,7 +1,10 @@
+from unittest.mock import MagicMock, patch
+
 import pytest
 
 from cuenca.exc import CuencaResponseException
 from cuenca.http.client import Session
+from cuenca.resources import Card
 
 
 @pytest.mark.vcr
@@ -56,3 +59,18 @@ def test_overrides_aws_creds():
     assert session.auth.aws_secret_access_key == 'new_aws_secret'
     assert session.auth.aws_access_key == 'new_aws_key'
     assert session.auth.aws_region == 'us-east-2'
+
+
+@patch('cuenca.http.client.requests.Session.request')
+def test_overrides_session(mock_request):
+    magic_mock = MagicMock()
+    magic_mock.json.return_value = dict(items=[])
+    mock_request.return_value = magic_mock
+    session = Session()
+    session.configure(
+        api_key='USER_API_KEY', api_secret='USER_SECRET', sandbox=True
+    )
+    Card.first(user_id='USER_ID', session=session)
+    mock_request.assert_called_once()
+    _, kwargs = mock_request.call_args_list[0]
+    assert kwargs['auth'] == session.auth
diff --git a/tests/resources/cassettes/test_card_all.yaml b/tests/resources/cassettes/test_card_all.yaml
new file mode 100644
index 0000000..c92f2a8
--- /dev/null
+++ b/tests/resources/cassettes/test_card_all.yaml
@@ -0,0 +1,50 @@
+interactions:
+- request:
+    body: null
+    headers:
+      Accept:
+      - '*/*'
+      Accept-Encoding:
+      - gzip, deflate
+      Authorization:
+      - DUMMY
+      Connection:
+      - keep-alive
+      User-Agent:
+      - cuenca-python/0.4.3.dev1
+      X-Cuenca-Api-Version:
+      - '2020-03-19'
+    method: GET
+    uri: https://sandbox.cuenca.com/cards?user_id=US1237
+  response:
+    body:
+      string: '{"items":[{"id":"CACs_Itr72QfaS6NSPhwanNA","created_at":"2020-09-18T22:05:53.233000","updated_at":"2020-09-18T22:05:53.394000","user_id":"US1237","ledger_account_id":"LAnbstynwl3","number":"4231450000000012","exp_month":9,"exp_year":2025,"cvv2":"123","type":"virtual","status":"active","batch":null,"manufacturer":null,"cvv":null,"icvv":null,"pin":null}],"next_page_uri":null}'
+    headers:
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '394'
+      Content-Type:
+      - application/json
+      Date:
+      - Wed, 06 Jan 2021 22:19:47 GMT
+      X-Amzn-Trace-Id:
+      - Root=1-5ff63783-637d9bf30645c2f614f3cd99;Sampled=0
+      x-amz-apigw-id:
+      - Yv2cgGttCYcFuSw=
+      x-amzn-Remapped-Connection:
+      - keep-alive
+      x-amzn-Remapped-Content-Length:
+      - '394'
+      x-amzn-Remapped-Date:
+      - Wed, 06 Jan 2021 22:19:47 GMT
+      x-amzn-Remapped-Server:
+      - nginx/1.18.0
+      x-amzn-Remapped-x-amzn-RequestId:
+      - c3a609d7-4e21-4221-a088-0339e5eb2b1b
+      x-amzn-RequestId:
+      - 37f91dcf-3e56-4039-8401-5f693c9915af
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/resources/test_cards.py b/tests/resources/test_cards.py
index 53b9625..d240096 100644
--- a/tests/resources/test_cards.py
+++ b/tests/resources/test_cards.py
@@ -59,7 +59,7 @@ def test_card_one_errors():
 @pytest.mark.vcr
 def test_card_all():
     cards = Card.all(user_id=user_id)
-    assert len([cards]) == 1
+    assert len(list(cards)) == 1
 
 
 @pytest.mark.vcr