diff --git a/cuenca/__init__.py b/cuenca/__init__.py index 32bce1f4..32950045 100644 --- a/cuenca/__init__.py +++ b/cuenca/__init__.py @@ -42,6 +42,7 @@ 'WhatsappTransfer', 'configure', 'get_balance', + 'JwtToken', ] from . import http @@ -65,6 +66,7 @@ FileBatch, Identity, IdentityEvent, + JwtToken, KYCValidation, KYCVerification, LimitedWallet, diff --git a/cuenca/resources/__init__.py b/cuenca/resources/__init__.py index f161cee3..8daf3c7d 100644 --- a/cuenca/resources/__init__.py +++ b/cuenca/resources/__init__.py @@ -38,6 +38,7 @@ 'WalletTransaction', 'Webhook', 'WhatsappTransfer', + 'JwtToken', ] from .accounts import Account @@ -59,6 +60,7 @@ from .files import File from .identities import Identity from .identity_events import IdentityEvent +from .jwt_tokens import JwtToken from .kyc_validations import KYCValidation from .kyc_verifications import KYCVerification from .limited_wallets import LimitedWallet @@ -123,6 +125,7 @@ WhatsappTransfer, Webhook, Platform, + JwtToken, ] for resource_cls in resource_classes: RESOURCES[resource_cls._resource] = resource_cls # type: ignore diff --git a/cuenca/resources/api_keys.py b/cuenca/resources/api_keys.py index f211a5fc..de62e7ad 100644 --- a/cuenca/resources/api_keys.py +++ b/cuenca/resources/api_keys.py @@ -1,7 +1,11 @@ import datetime as dt -from typing import ClassVar, Optional +from typing import Annotated, ClassVar, Optional -from cuenca_validations.types import ApiKeyQuery, ApiKeyUpdateRequest +from cuenca_validations.types import ( + ApiKeyQuery, + ApiKeyUpdateRequest, + LogConfig, +) from pydantic import ConfigDict from ..http import Session, session as global_session @@ -12,7 +16,7 @@ class ApiKey(Creatable, Queryable, Retrievable, Updateable): _resource: ClassVar = 'api_keys' _query_params: ClassVar = ApiKeyQuery - secret: str + secret: Annotated[str, LogConfig(masked=True)] deactivated_at: Optional[dt.datetime] = None user_id: Optional[str] = None model_config = ConfigDict( diff --git a/cuenca/resources/cards.py b/cuenca/resources/cards.py index 5cd24564..bf706578 100644 --- a/cuenca/resources/cards.py +++ b/cuenca/resources/cards.py @@ -1,5 +1,5 @@ import datetime as dt -from typing import ClassVar, Optional +from typing import Annotated, ClassVar, Optional from cuenca_validations.types import ( CardFundingType, @@ -7,6 +7,7 @@ CardStatus, CardType, ) +from cuenca_validations.types.general import LogConfig from cuenca_validations.types.queries import CardQuery from cuenca_validations.types.requests import CardRequest, CardUpdateRequest @@ -22,7 +23,7 @@ class Card(Retrievable, Queryable, Creatable, Updateable): _query_params: ClassVar = CardQuery user_id: Optional[str] = None - number: str + number: Annotated[str, LogConfig(masked=True, unmasked_chars_length=4)] exp_month: int exp_year: int cvv2: str diff --git a/cuenca/resources/jwt_tokens.py b/cuenca/resources/jwt_tokens.py new file mode 100644 index 00000000..e8cf4625 --- /dev/null +++ b/cuenca/resources/jwt_tokens.py @@ -0,0 +1,32 @@ +import datetime as dt +from typing import Annotated, ClassVar + +from cuenca_validations.types import LogConfig +from pydantic import ConfigDict + +from ..http import Session, session as global_session +from .base import Creatable + + +class JwtToken(Creatable): + _resource: ClassVar = 'token' + + id: Annotated[str, LogConfig(masked=True, unmasked_chars_length=4)] + token: Annotated[str, LogConfig(masked=True, unmasked_chars_length=4)] + created_at: dt.datetime + api_key_uri: str + + model_config = ConfigDict( + json_schema_extra={ + 'example': { + 'id': 'jwt_XXXX...redacted', + 'token': 'jwt_XXXX...redacted', + 'created_at': '2025-01-24T22:34:37.659667', + 'api_key_uri': '/api_key/AKsczy7tsiRd2IbjL_nYFolQ', + } + } + ) + + @classmethod + def create(cls, session: Session = global_session) -> 'JwtToken': + return cls._create(session=session) diff --git a/cuenca/resources/login_tokens.py b/cuenca/resources/login_tokens.py index 2be84eac..79ba20b1 100644 --- a/cuenca/resources/login_tokens.py +++ b/cuenca/resources/login_tokens.py @@ -1,5 +1,6 @@ -from typing import ClassVar +from typing import Annotated, ClassVar +from cuenca_validations.types import LogConfig from pydantic import ConfigDict from ..http import Session, session as global_session @@ -9,6 +10,8 @@ class LoginToken(Creatable): _resource: ClassVar = 'login_tokens' + id: Annotated[str, LogConfig(masked=True, unmasked_chars_length=4)] + model_config = ConfigDict( json_schema_extra={'example': {'id': 'LTNEUInh69SuKXXmK95sROwQ'}} ) diff --git a/cuenca/resources/otps.py b/cuenca/resources/otps.py index f6e2ba3c..6b65bc40 100644 --- a/cuenca/resources/otps.py +++ b/cuenca/resources/otps.py @@ -1,5 +1,6 @@ -from typing import ClassVar +from typing import Annotated, ClassVar +from cuenca_validations.types import LogConfig from pydantic import ConfigDict from ..http import Session, session as global_session @@ -8,7 +9,7 @@ class Otp(Creatable): _resource: ClassVar = 'otps' - secret: str + secret: Annotated[str, LogConfig(masked=True)] model_config = ConfigDict( json_schema_extra={ diff --git a/cuenca/resources/sessions.py b/cuenca/resources/sessions.py index f08caa88..ba51be64 100644 --- a/cuenca/resources/sessions.py +++ b/cuenca/resources/sessions.py @@ -1,7 +1,7 @@ import datetime as dt -from typing import ClassVar, Optional +from typing import Annotated, ClassVar, Optional -from cuenca_validations.types import SessionRequest, SessionType +from cuenca_validations.types import LogConfig, SessionRequest, SessionType from cuenca_validations.types.general import SerializableAnyUrl from pydantic import ConfigDict @@ -12,7 +12,7 @@ class Session(Creatable, Retrievable, Queryable): _resource: ClassVar = 'sessions' - id: str + id: Annotated[str, LogConfig(masked=True, unmasked_chars_length=4)] created_at: dt.datetime user_id: str platform_id: str diff --git a/cuenca/resources/user_logins.py b/cuenca/resources/user_logins.py index bfe22745..b23768f1 100644 --- a/cuenca/resources/user_logins.py +++ b/cuenca/resources/user_logins.py @@ -1,6 +1,7 @@ import datetime as dt -from typing import ClassVar, Optional +from typing import Annotated, ClassVar, Optional +from cuenca_validations.types import LogConfig from cuenca_validations.types.requests import UserLoginRequest from pydantic import ConfigDict @@ -11,6 +12,8 @@ class UserLogin(Creatable): _resource: ClassVar = 'user_logins' + id: Annotated[str, LogConfig(masked=True, unmasked_chars_length=4)] + last_login_at: Optional[dt.datetime] = None success: bool diff --git a/cuenca/version.py b/cuenca/version.py index 68b04b6a..04511fde 100644 --- a/cuenca/version.py +++ b/cuenca/version.py @@ -1,3 +1,3 @@ -__version__ = '2.0.1' +__version__ = '2.1.0' CLIENT_VERSION = __version__ API_VERSION = '2020-03-19' diff --git a/requirements.txt b/requirements.txt index 3568110f..190cc68d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ requests==2.32.3 -cuenca-validations==2.0.4 +cuenca-validations==2.1.0 pydantic-extra-types==2.10.2 diff --git a/setup.py b/setup.py index 2d847811..4ba3904e 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ python_requires='>=3.9', install_requires=[ 'requests>=2.32.0', - 'cuenca-validations>=2.0.4', + 'cuenca-validations>=2.1.0', 'pydantic-extra-types>=2.10.0', ], classifiers=[ diff --git a/tests/resources/cassettes/test_jwt_tokens.yaml b/tests/resources/cassettes/test_jwt_tokens.yaml new file mode 100644 index 00000000..d921f1f7 --- /dev/null +++ b/tests/resources/cassettes/test_jwt_tokens.yaml @@ -0,0 +1,138 @@ +interactions: +- request: + body: '{"password": "111111"}' + headers: + Authorization: + - DUMMY + Content-Length: + - '26' + Content-Type: + - application/json + User-Agent: + - cuenca-python/2.0.1 + X-Cuenca-Api-Version: + - '2020-03-19' + method: POST + uri: https://sandbox.cuenca.com/user_logins + response: + body: + string: '{"success":true,"id":"ULm7x8e0hfQYyUWwM3ysqgTw","last_login_at":"2025-01-27T22:50:04.745000Z"}' + headers: + Connection: + - keep-alive + Content-Length: + - '94' + Content-Type: + - application/json + Date: + - Mon, 27 Jan 2025 23:08:55 GMT + X-Request-Time: + - 'value: 0.421' + x-amz-apigw-id: + - FEfBLEMtCYcEKuQ= + x-amzn-Remapped-Connection: + - keep-alive + x-amzn-Remapped-Content-Length: + - '94' + x-amzn-Remapped-Date: + - Mon, 27 Jan 2025 23:08:55 GMT + x-amzn-Remapped-Server: + - nginx/1.26.2 + x-amzn-RequestId: + - 855b6c10-2e41-4b1f-b0fe-6030bd4e0032 + status: + code: 201 + message: Created +- request: + body: '{}' + headers: + Authorization: + - DUMMY + Content-Length: + - '2' + Content-Type: + - application/json + User-Agent: + - cuenca-python/2.0.1 + X-Cuenca-Api-Version: + - '2020-03-19' + X-Cuenca-LoginId: + - ULm7x8e0hfQYyUWwM3ysqgTw + method: POST + uri: https://sandbox.cuenca.com/login_tokens + response: + body: + string: '{"id":"LTqXrT9Z64S329W0D-uJKwmQ"}' + headers: + Connection: + - keep-alive + Content-Length: + - '33' + Content-Type: + - application/json + Date: + - Mon, 27 Jan 2025 23:08:56 GMT + X-Request-Time: + - 'value: 0.202' + x-amz-apigw-id: + - FEfBSG6ZCYcEPMg= + x-amzn-Remapped-Connection: + - keep-alive + x-amzn-Remapped-Content-Length: + - '33' + x-amzn-Remapped-Date: + - Mon, 27 Jan 2025 23:08:56 GMT + x-amzn-Remapped-Server: + - nginx/1.26.2 + x-amzn-RequestId: + - 7876a2df-cbdc-40f5-b2b8-c4dcfcdc2d23 + status: + code: 201 + message: Created +- request: + body: '{}' + headers: + Authorization: + - DUMMY + Content-Length: + - '2' + Content-Type: + - application/json + User-Agent: + - cuenca-python/2.0.1 + X-Cuenca-Api-Version: + - '2020-03-19' + X-Cuenca-LoginToken: + - LTqXrT9Z64S329W0D-uJKwmQ + method: POST + uri: https://sandbox.cuenca.com/token + response: + body: + string: '{"id":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3Mzg2MjQxMzYsImlhdCI6MTczODAxOTMzNiwic3ViIjoiQUsyTDh6Y0x2YlRJR1ZsQXdndm9LX1dnIiwidWlkIjoiYWYxM2JmZDgtZGQwMy0xMWVmLThkN2MtMGE1OGE5ZmVhYzAyIn0.tpCxBTcKGWvuRUsKmo7a7IcofgrhVmxIIEiaDBRbAhY","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3Mzg2MjQxMzYsImlhdCI6MTczODAxOTMzNiwic3ViIjoiQUsyTDh6Y0x2YlRJR1ZsQXdndm9LX1dnIiwidWlkIjoiYWYxM2JmZDgtZGQwMy0xMWVmLThkN2MtMGE1OGE5ZmVhYzAyIn0.tpCxBTcKGWvuRUsKmo7a7IcofgrhVmxIIEiaDBRbAhY","created_at":"2025-01-27T23:08:56.372428","api_key_uri":"/api_key/AK2L8zcLvbTIGVlAwgvoK_Wg"}' + headers: + Connection: + - keep-alive + Content-Length: + - '576' + Content-Type: + - application/json + Date: + - Mon, 27 Jan 2025 23:08:56 GMT + X-Request-Time: + - 'value: 0.104' + x-amz-apigw-id: + - FEfBWHrtiYcEAJA= + x-amzn-Remapped-Connection: + - keep-alive + x-amzn-Remapped-Content-Length: + - '576' + x-amzn-Remapped-Date: + - Mon, 27 Jan 2025 23:08:56 GMT + x-amzn-Remapped-Server: + - nginx/1.26.2 + x-amzn-RequestId: + - 79544974-46c4-4003-a548-48731d96f703 + status: + code: 201 + message: Created +version: 1 diff --git a/tests/resources/test_jwt_tokens.py b/tests/resources/test_jwt_tokens.py new file mode 100644 index 00000000..7ade20e0 --- /dev/null +++ b/tests/resources/test_jwt_tokens.py @@ -0,0 +1,10 @@ +import pytest + +from cuenca import JwtToken + + +@pytest.mark.vcr +def test_jwt_tokens(): + jwt_token = JwtToken.create() + assert jwt_token + assert isinstance(jwt_token.token, str)