From 6abe75cd8fee0bb963647a3e402232e992f9a9dd Mon Sep 17 00:00:00 2001 From: gabino Date: Sun, 22 Dec 2024 08:52:47 -0600 Subject: [PATCH 01/29] Update dependencies to support Pydantic v2 --- cuenca/resources/api_keys.py | 11 +-- cuenca/resources/arpc.py | 6 +- cuenca/resources/base.py | 4 +- cuenca/resources/card_activations.py | 4 +- cuenca/resources/card_transactions.py | 2 +- cuenca/resources/card_validations.py | 12 +-- cuenca/resources/cards.py | 4 +- cuenca/resources/curp_validations.py | 81 +++++++++++---------- cuenca/resources/deposits.py | 2 +- cuenca/resources/endpoints.py | 45 ++++++------ cuenca/resources/file_batches.py | 4 +- cuenca/resources/identities.py | 26 +++---- cuenca/resources/kyc_validations.py | 13 ++-- cuenca/resources/kyc_verifications.py | 11 +-- cuenca/resources/limited_wallets.py | 2 +- cuenca/resources/login_tokens.py | 8 +- cuenca/resources/otps.py | 8 +- cuenca/resources/platforms.py | 57 +++++++-------- cuenca/resources/questionnaires.py | 9 ++- cuenca/resources/savings.py | 4 +- cuenca/resources/sessions.py | 14 ++-- cuenca/resources/transfers.py | 2 +- cuenca/resources/user_events.py | 8 +- cuenca/resources/user_lists_validation.py | 2 +- cuenca/resources/user_logins.py | 9 ++- cuenca/resources/users.py | 89 +++++++++++------------ cuenca/resources/verifications.py | 23 +++--- cuenca/resources/webhooks.py | 15 ++-- cuenca/resources/whatsapp_transfers.py | 8 +- requirements-test.txt | 4 +- requirements.txt | 5 +- tests/resources/test_endpoints.py | 2 +- tests/resources/test_sessions.py | 4 +- tests/resources/test_transfers.py | 2 +- 34 files changed, 253 insertions(+), 247 deletions(-) diff --git a/cuenca/resources/api_keys.py b/cuenca/resources/api_keys.py index 1675b78c..0fbe4530 100644 --- a/cuenca/resources/api_keys.py +++ b/cuenca/resources/api_keys.py @@ -2,6 +2,7 @@ from typing import ClassVar, Optional, cast from cuenca_validations.types import ApiKeyQuery, ApiKeyUpdateRequest +from pydantic import ConfigDict from ..http import Session, session as global_session from .base import Creatable, Queryable, Retrievable, Updateable @@ -12,11 +13,10 @@ class ApiKey(Creatable, Queryable, Retrievable, Updateable): _query_params: ClassVar = ApiKeyQuery secret: str - deactivated_at: Optional[dt.datetime] - user_id: Optional[str] - - class Config: - schema_extra = { + deactivated_at: Optional[dt.datetime] = None + user_id: Optional[str] = None + model_config = ConfigDict( + json_schema_extra={ 'example': { 'id': 'AKNEUInh69SuKXXmK95sROwQ', 'updated_at': '2021-08-24T14:15:22Z', @@ -26,6 +26,7 @@ class Config: 'user_id': 'USWqY5cvkISJOxHyEKjAKf8w', } } + ) @property def active(self) -> bool: diff --git a/cuenca/resources/arpc.py b/cuenca/resources/arpc.py index 78efd9c5..773d6a99 100644 --- a/cuenca/resources/arpc.py +++ b/cuenca/resources/arpc.py @@ -23,8 +23,8 @@ class Arpc(Creatable): created_at: dt.datetime card_uri: str - is_valid_arqc: Optional[bool] - arpc: Optional[str] + is_valid_arqc: Optional[bool] = None + arpc: Optional[str] = None @classmethod def create( @@ -52,4 +52,4 @@ def create( unique_number=unique_number, track_data_method=track_data_method, ) - return cast('Arpc', cls._create(session=session, **req.dict())) + return cast('Arpc', cls._create(session=session, **req.model_dump())) diff --git a/cuenca/resources/base.py b/cuenca/resources/base.py index 7a34dece..d8745a31 100644 --- a/cuenca/resources/base.py +++ b/cuenca/resources/base.py @@ -42,7 +42,7 @@ def _filter_excess_fields(cls, obj_dict): del obj_dict[f] def to_dict(self): - return SantizedDict(self.dict()) + return SantizedDict(self.model_dump()) class Retrievable(Resource): @@ -79,7 +79,7 @@ def _update( class Deactivable(Resource): - deactivated_at: Optional[dt.datetime] + deactivated_at: Optional[dt.datetime] = None @classmethod def deactivate( diff --git a/cuenca/resources/card_activations.py b/cuenca/resources/card_activations.py index 6523e253..66890d64 100644 --- a/cuenca/resources/card_activations.py +++ b/cuenca/resources/card_activations.py @@ -15,7 +15,7 @@ class CardActivation(Creatable): created_at: dt.datetime user_id: str ip_address: str - card_uri: Optional[str] + card_uri: Optional[str] = None success: bool @classmethod @@ -43,7 +43,7 @@ def create( cvv2=cvv2, ) return cast( - 'CardActivation', cls._create(session=session, **req.dict()) + 'CardActivation', cls._create(session=session, **req.model_dump()) ) @property diff --git a/cuenca/resources/card_transactions.py b/cuenca/resources/card_transactions.py index dae0a674..529ffd22 100644 --- a/cuenca/resources/card_transactions.py +++ b/cuenca/resources/card_transactions.py @@ -24,7 +24,7 @@ class CardTransaction(Transaction): card_last4: str card_type: CardType metadata: dict - error_type: Optional[CardErrorType] + error_type: Optional[CardErrorType] = None @property # type: ignore def related_card_transactions(self) -> Optional[List['CardTransaction']]: diff --git a/cuenca/resources/card_validations.py b/cuenca/resources/card_validations.py index 858febd9..5e03b895 100644 --- a/cuenca/resources/card_validations.py +++ b/cuenca/resources/card_validations.py @@ -18,11 +18,11 @@ class CardValidation(Creatable): user_id: str card_status: CardStatus card_type: CardType - is_valid_cvv: Optional[bool] - is_valid_cvv2: Optional[bool] - is_valid_icvv: Optional[bool] - is_valid_pin_block: Optional[bool] - is_valid_exp_date: Optional[bool] + is_valid_cvv: Optional[bool] = None + is_valid_cvv2: Optional[bool] = None + is_valid_icvv: Optional[bool] = None + is_valid_pin_block: Optional[bool] = None + is_valid_exp_date: Optional[bool] = None is_pin_attempts_exceeded: bool is_expired: bool platform_id: Optional[str] = None @@ -52,7 +52,7 @@ def create( pin_attempts_exceeded=pin_attempts_exceeded, ) return cast( - 'CardValidation', cls._create(session=session, **req.dict()) + 'CardValidation', cls._create(session=session, **req.model_dump()) ) @property diff --git a/cuenca/resources/cards.py b/cuenca/resources/cards.py index ff739615..505158c0 100644 --- a/cuenca/resources/cards.py +++ b/cuenca/resources/cards.py @@ -21,12 +21,12 @@ class Card(Retrievable, Queryable, Creatable, Updateable): _resource: ClassVar = 'cards' _query_params: ClassVar = CardQuery - user_id: Optional[str] + user_id: Optional[str] = None number: str exp_month: int exp_year: int cvv2: str - pin: Optional[str] + pin: Optional[str] = None type: CardType status: CardStatus issuer: CardIssuer diff --git a/cuenca/resources/curp_validations.py b/cuenca/resources/curp_validations.py index 5c466503..49cb20f6 100644 --- a/cuenca/resources/curp_validations.py +++ b/cuenca/resources/curp_validations.py @@ -8,6 +8,7 @@ State, ) from cuenca_validations.types.identities import CurpField +from pydantic import ConfigDict, Field from ..http import Session, session as global_session from .base import Creatable, Retrievable @@ -17,44 +18,43 @@ class CurpValidation(Creatable, Retrievable): _resource: ClassVar = 'curp_validations' created_at: dt.datetime - names: Optional[str] = None - first_surname: Optional[str] = None - second_surname: Optional[str] = None - date_of_birth: Optional[dt.date] = None - country_of_birth: Optional[Country] = None - state_of_birth: Optional[State] = None - gender: Optional[Gender] = None - nationality: Optional[Country] = None - manual_curp: Optional[CurpField] = None - calculated_curp: CurpField - validated_curp: Optional[CurpField] = None - renapo_curp_match: bool - renapo_full_match: bool - - class Config: - fields = { - 'names': {'description': 'Official name from Renapo'}, - 'first_surname': {'description': 'Official surname from Renapo'}, - 'second_surname': {'description': 'Official surname from Renapo'}, - 'country_of_birth': {'description': 'In format ISO 3166 Alpha-2'}, - 'state_of_birth': {'description': 'In format ISO 3166 Alpha-2'}, - 'nationality': {'description': 'In format ISO 3166 Alpha-2'}, - 'manual_curp': {'description': 'curp provided in request'}, - 'calculated_curp': { - 'description': 'Calculated CURP based on request data' - }, - 'validated_curp': { - 'description': 'CURP validated in Renapo, null if not exists' - }, - 'renapo_curp_match': { - 'description': 'True if CURP exists and is valid' - }, - 'renapo_full_match': { - 'description': 'True if all fields provided match the response' - ' from RENAPO. Accents in names are ignored' - }, - } - schema_extra = { + names: Optional[str] = Field(None, description='Official name from Renapo') + first_surname: Optional[str] = Field( + None, description='Official surname from Renapo' + ) + second_surname: Optional[str] = Field( + None, description='Official surname from Renapo' + ) + date_of_birth: Optional[dt.date] = Field( + None, description='In format ISO 3166 Alpha-2' + ) + country_of_birth: Optional[Country] = Field( + None, description='In format ISO 3166 Alpha-2' + ) + state_of_birth: Optional[State] = Field(None, description='State of birth') + gender: Optional[Gender] = Field(None, description='Gender') + nationality: Optional[Country] = Field( + None, description='In format ISO 3166 Alpha-2' + ) + manual_curp: Optional[CurpField] = Field( + None, description='curp provided in request' + ) + calculated_curp: CurpField = Field( + ..., description='Calculated CURP based on request data' + ) + validated_curp: Optional[CurpField] = Field( + None, description='CURP validated in Renapo, null if not exists' + ) + renapo_curp_match: bool = Field( + ..., description='True if CURP exists and is valid' + ) + renapo_full_match: bool = Field( + ..., + description='True if all fields provided match the response from ' + 'RENAPO. Accents in names are ignored', + ) + model_config = ConfigDict( + json_schema_extra={ 'example': { 'id': 'CVNEUInh69SuKXXmK95sROwQ', 'created_at': '2019-08-24T14:15:22Z', @@ -72,7 +72,8 @@ class Config: 'renapo_curp_match': True, 'renapo_full_match': True, } - } + }, + ) @classmethod def create( @@ -100,5 +101,5 @@ def create( ) return cast( 'CurpValidation', - cls._create(session=session, **req.dict()), + cls._create(session=session, **req.model_dump()), ) diff --git a/cuenca/resources/deposits.py b/cuenca/resources/deposits.py index eccdce8a..5e8f9192 100644 --- a/cuenca/resources/deposits.py +++ b/cuenca/resources/deposits.py @@ -13,7 +13,7 @@ class Deposit(Transaction): network: DepositNetwork source_uri: str - tracking_key: Optional[str] # clave rastreo if network is SPEI + tracking_key: Optional[str] = None # clave rastreo if network is SPEI @property # type: ignore def source(self) -> Account: diff --git a/cuenca/resources/endpoints.py b/cuenca/resources/endpoints.py index 43fb79c9..adafd16d 100644 --- a/cuenca/resources/endpoints.py +++ b/cuenca/resources/endpoints.py @@ -5,7 +5,7 @@ EndpointRequest, EndpointUpdateRequest, ) -from pydantic import HttpUrl +from pydantic import ConfigDict, Field, HttpUrl from ..http import Session, session as global_session from .base import Creatable, Deactivable, Queryable, Retrievable, Updateable @@ -14,28 +14,24 @@ class Endpoint(Creatable, Deactivable, Retrievable, Queryable, Updateable): _resource: ClassVar = 'endpoints' - url: HttpUrl - secret: str - is_enable: bool - events: List[WebhookEvent] - - class Config: - fields = { - 'url': {'description': 'HTTPS url to send webhooks'}, - 'secret': { - 'description': 'token to verify the webhook is sent by Cuenca ' - 'using HMAC algorithm' - }, - 'is_enable': { - 'description': 'Allows user to turn-off the endpoint ' - 'without the need of deleting it' - }, - 'events': { - 'description': 'list of enabled events. If None, ' - 'all events will be enabled for this Endpoint' - }, - } - schema_extra = { + url: HttpUrl = Field(..., description='HTTPS url to send webhooks') + secret: str = Field( + ..., + description='token to verify the webhook is sent by Cuenca ' + 'using HMAC algorithm', + ) + is_enable: bool = Field( + ..., + description='Allows user to turn-off the endpoint without the ' + 'need of deleting it', + ) + events: List[WebhookEvent] = Field( + ..., + description='list of enabled events. If None, all events will ' + 'be enabled for this Endpoint', + ) + model_config = ConfigDict( + json_schema_extra={ 'example': { '_id': 'ENxxne2Z5VSTKZm_w8Hzffcw', 'platform_id': 'PTZoPrrPT6Ts-9myamq5h1bA', @@ -52,7 +48,8 @@ class Config: ], 'is_enable': True, } - } + }, + ) @classmethod def create( diff --git a/cuenca/resources/file_batches.py b/cuenca/resources/file_batches.py index 68123898..201c7648 100644 --- a/cuenca/resources/file_batches.py +++ b/cuenca/resources/file_batches.py @@ -22,4 +22,6 @@ def create( session: Session = global_session, ) -> 'FileBatch': req = FileBatchUploadRequest(files=files, user_id=user_id) - return cast('FileBatch', cls._create(session=session, **req.dict())) + return cast( + 'FileBatch', cls._create(session=session, **req.model_dump()) + ) diff --git a/cuenca/resources/identities.py b/cuenca/resources/identities.py index d59f1f6d..6cb4738a 100644 --- a/cuenca/resources/identities.py +++ b/cuenca/resources/identities.py @@ -23,17 +23,17 @@ class Identity(Retrievable, Queryable): created_at: dt.datetime names: str first_surname: str - second_surname: Optional[str] - curp: Optional[CurpField] - rfc: Optional[str] + second_surname: Optional[str] = None + curp: Optional[CurpField] = None + rfc: Optional[str] = None gender: Gender - date_of_birth: Optional[dt.date] - state_of_birth: Optional[State] - country_of_birth: Optional[str] - status: Optional[UserStatus] - tos_agreement: Optional[TOSAgreement] - blacklist_validation_status: Optional[VerificationStatus] - address: Optional[Address] - govt_id: Optional[KYCFile] - proof_of_address: Optional[KYCFile] - proof_of_life: Optional[KYCFile] + date_of_birth: Optional[dt.date] = None + state_of_birth: Optional[State] = None + country_of_birth: Optional[str] = None + status: Optional[UserStatus] = None + tos_agreement: Optional[TOSAgreement] = None + blacklist_validation_status: Optional[VerificationStatus] = None + address: Optional[Address] = None + govt_id: Optional[KYCFile] = None + proof_of_address: Optional[KYCFile] = None + proof_of_life: Optional[KYCFile] = None diff --git a/cuenca/resources/kyc_validations.py b/cuenca/resources/kyc_validations.py index 1ab0bc47..76373466 100644 --- a/cuenca/resources/kyc_validations.py +++ b/cuenca/resources/kyc_validations.py @@ -1,6 +1,7 @@ from typing import ClassVar, List, Optional, cast from cuenca_validations.types import KYCFile, KYCValidationRequest +from pydantic import ConfigDict from ..http import Session, session as global_session from .base import Creatable, Queryable, Retrievable @@ -9,12 +10,11 @@ class KYCValidation(Creatable, Retrievable, Queryable): _resource: ClassVar = 'kyc_validations' platform_id: str - attemps: Optional[int] - verification_id: Optional[str] - files_uri: Optional[List[str]] - - class Config: - schema_extra = { + attemps: Optional[int] = None + verification_id: Optional[str] = None + files_uri: Optional[List[str]] = None + model_config = ConfigDict( + json_schema_extra={ 'example': { 'id': 'KVNEUInh69SuKXXmK95sROwQ', 'platform_id': 'PT8UEv02zBTcymd4Kd3MO6pg', @@ -24,6 +24,7 @@ class Config: 'attemps': '1', } } + ) @classmethod def create( diff --git a/cuenca/resources/kyc_verifications.py b/cuenca/resources/kyc_verifications.py index 106b77b3..b4a378cf 100644 --- a/cuenca/resources/kyc_verifications.py +++ b/cuenca/resources/kyc_verifications.py @@ -7,6 +7,7 @@ KYCVerificationUpdateRequest, Rfc, ) +from pydantic import ConfigDict from ..http import Session, session as global_session from .base import Creatable, Retrievable, Updateable @@ -17,14 +18,13 @@ class KYCVerification(Creatable, Retrievable, Updateable): platform_id: str created_at: dt.datetime - deactivated_at: Optional[dt.datetime] - verification_id: Optional[str] + deactivated_at: Optional[dt.datetime] = None + verification_id: Optional[str] = None curp: Optional[CurpField] = None rfc: Optional[Rfc] = None address: Optional[Address] = None - - class Config: - schema_extra = { + model_config = ConfigDict( + json_schema_extra={ 'example': { 'id': 'KVNEUInh69SuKXXmK95sROwQ', 'updated_at': '2020-05-24T14:15:22Z', @@ -36,6 +36,7 @@ class Config: 'address': Address.schema().get('example'), } } + ) @classmethod def create(cls, session: Session = global_session) -> 'KYCVerification': diff --git a/cuenca/resources/limited_wallets.py b/cuenca/resources/limited_wallets.py index 8ea03865..dbeddb17 100644 --- a/cuenca/resources/limited_wallets.py +++ b/cuenca/resources/limited_wallets.py @@ -15,7 +15,7 @@ class LimitedWallet(Wallet): _resource: ClassVar = 'limited_wallets' _query_params: ClassVar = AccountQuery account_number: Clabe - allowed_rfc: Optional[Rfc] + allowed_rfc: Optional[Rfc] = None allowed_curp: CurpField @classmethod diff --git a/cuenca/resources/login_tokens.py b/cuenca/resources/login_tokens.py index 7bb3828b..009b3dca 100644 --- a/cuenca/resources/login_tokens.py +++ b/cuenca/resources/login_tokens.py @@ -1,14 +1,16 @@ from typing import ClassVar, cast +from pydantic import ConfigDict + from ..http import Session, session as global_session from .base import Creatable class LoginToken(Creatable): _resource: ClassVar = 'login_tokens' - - class Config: - schema_extra = {'example': {'id': 'LTNEUInh69SuKXXmK95sROwQ'}} + model_config = ConfigDict( + json_schema_extra={'example': {'id': 'LTNEUInh69SuKXXmK95sROwQ'}} + ) @classmethod def create(cls, session: Session = global_session) -> 'LoginToken': diff --git a/cuenca/resources/otps.py b/cuenca/resources/otps.py index 03450c1b..182622dd 100644 --- a/cuenca/resources/otps.py +++ b/cuenca/resources/otps.py @@ -1,5 +1,7 @@ from typing import ClassVar, cast +from pydantic import ConfigDict + from ..http import Session, session as global_session from .base import Creatable @@ -7,14 +9,14 @@ class Otp(Creatable): _resource: ClassVar = 'otps' secret: str - - class Config: - schema_extra = { + model_config = ConfigDict( + json_schema_extra={ 'example': { 'id': 'OTNEUInh69SuKXXmK95sROwQ', 'secret': 'somesecret', } } + ) @classmethod def create(cls, session: Session = global_session) -> 'Otp': diff --git a/cuenca/resources/platforms.py b/cuenca/resources/platforms.py index e867fcdb..641e5fc2 100644 --- a/cuenca/resources/platforms.py +++ b/cuenca/resources/platforms.py @@ -2,6 +2,7 @@ from typing import ClassVar, Optional, cast from cuenca_validations.types import Country, PlatformRequest, State +from pydantic import ConfigDict, Field from ..http import Session, session as global_session from .base import Creatable @@ -11,33 +12,28 @@ class Platform(Creatable): _resource: ClassVar = 'platforms' created_at: dt.datetime - name: str - rfc: Optional[str] = None - establishment_date: Optional[dt.date] = None - country: Optional[Country] = None - state: Optional[State] = None - economic_activity: Optional[str] = None - email_address: Optional[str] = None - phone_number: Optional[str] = None - - class Config: - fields = { - 'name': {'description': 'name of the platform being created'}, - 'rfc': {'description': 'RFC or CURP of the platform'}, - 'establishment_date': { - 'description': 'when the platform was established' - }, - 'country': {'description': 'country where the platform resides'}, - 'state': {'description': 'state where the platform resides'}, - 'economic_activity': {'description': 'what the platform does'}, - 'phone_number': { - 'description': 'phone number to contact the platform' - }, - 'email_address': { - 'description': 'email address to contact the platform' - }, - } - schema_extra = { + name: str = Field(..., description='name of the platform being created') + rfc: Optional[str] = Field(None, description='RFC or CURP of the platform') + establishment_date: Optional[dt.date] = Field( + None, description='when the platform was established' + ) + country: Optional[Country] = Field( + None, description='country where the platform resides' + ) + state: Optional[State] = Field( + None, description='state where the platform resides' + ) + economic_activity: Optional[str] = Field( + None, description='what the platform does' + ) + email_address: Optional[str] = Field( + None, description='email address to contact the platform' + ) + phone_number: Optional[str] = Field( + None, description='phone number to contact the platform' + ) + model_config = ConfigDict( + json_schema_extra={ 'example': { 'id': 'PT0123456789', 'name': 'Arteria', @@ -50,7 +46,8 @@ class Config: 'phone_number': '+525555555555', 'email_address': 'art@eria.com', } - } + }, + ) @classmethod def create( @@ -76,4 +73,6 @@ def create( phone_number=phone_number, email_address=email_address, ) - return cast('Platform', cls._create(session=session, **req.dict())) + return cast( + 'Platform', cls._create(session=session, **req.model_dump()) + ) diff --git a/cuenca/resources/questionnaires.py b/cuenca/resources/questionnaires.py index 5e19c9b6..906c4f40 100644 --- a/cuenca/resources/questionnaires.py +++ b/cuenca/resources/questionnaires.py @@ -2,6 +2,7 @@ from typing import ClassVar, cast from cuenca_validations.types import QuestionnairesRequest +from pydantic import ConfigDict from ..http import Session, session as global_session from .base import Creatable, Retrievable @@ -14,15 +15,15 @@ class Questionnaires(Creatable, Retrievable): token: str form_id: str user_id: str - - class Config: - schema_extra = { + model_config = ConfigDict( + json_schema_extra={ 'example': { 'user_id': 'US234i23jh23h4h23', 'token': '3223j23ij23ij3', 'alert_id': 'ALewifjwiejf', } } + ) @classmethod def create( @@ -39,5 +40,5 @@ def create( form_id=form_id, ) return cast( - 'Questionnaires', cls._create(session=session, **req.dict()) + 'Questionnaires', cls._create(session=session, **req.model_dump()) ) diff --git a/cuenca/resources/savings.py b/cuenca/resources/savings.py index 2a857097..25a76c7c 100644 --- a/cuenca/resources/savings.py +++ b/cuenca/resources/savings.py @@ -17,8 +17,8 @@ class Saving(Wallet, Updateable): _query_params: ClassVar = WalletQuery name: str category: SavingCategory - goal_amount: Optional[StrictPositiveInt] - goal_date: Optional[dt.datetime] + goal_amount: Optional[StrictPositiveInt] = None + goal_date: Optional[dt.datetime] = None @classmethod def create( diff --git a/cuenca/resources/sessions.py b/cuenca/resources/sessions.py index 227e3b15..7639698c 100644 --- a/cuenca/resources/sessions.py +++ b/cuenca/resources/sessions.py @@ -2,7 +2,7 @@ from typing import ClassVar, Optional, cast from cuenca_validations.types import SessionRequest, SessionType -from pydantic import AnyUrl +from pydantic import AnyUrl, ConfigDict from .. import http from .base import Creatable, Queryable, Retrievable @@ -16,12 +16,11 @@ class Session(Creatable, Retrievable, Queryable): user_id: str platform_id: str expires_at: dt.datetime - success_url: Optional[AnyUrl] - failure_url: Optional[AnyUrl] - type: Optional[SessionType] - - class Config: - schema_extra = { + success_url: Optional[AnyUrl] = None + failure_url: Optional[AnyUrl] = None + type: Optional[SessionType] = None + model_config = ConfigDict( + json_schema_extra={ 'example': { 'id': 'SENEUInh69SuKXXmK95sROwQ', 'created_at': '2022-08-24T14:15:22Z', @@ -33,6 +32,7 @@ class Config: 'type': 'session.registration', } } + ) @classmethod def create( diff --git a/cuenca/resources/transfers.py b/cuenca/resources/transfers.py index 97b0cb9e..3e407c1d 100644 --- a/cuenca/resources/transfers.py +++ b/cuenca/resources/transfers.py @@ -25,7 +25,7 @@ class Transfer(Transaction, Creatable): idempotency_key: str network: TransferNetwork destination_uri: str - tracking_key: Optional[str] # clave rastreo if network is SPEI + tracking_key: Optional[str] = None # clave rastreo if network is SPEI @property # type: ignore def destination(self) -> Account: diff --git a/cuenca/resources/user_events.py b/cuenca/resources/user_events.py index 8a6de5fa..ba565d9b 100644 --- a/cuenca/resources/user_events.py +++ b/cuenca/resources/user_events.py @@ -1,5 +1,7 @@ from typing import ClassVar +from pydantic import ConfigDict + from .identity_events import IdentityEvent from .users import User @@ -9,9 +11,8 @@ class UserEvent(IdentityEvent): user_id: str platform_id: str - - class Config: - schema_extra = { + model_config = ConfigDict( + json_schema_extra={ 'example': { 'id': 'UEYE4qnWs3Sm68tbgqkx_d5Q', 'created_at': '2022-05-24T14:15:22Z', @@ -22,3 +23,4 @@ class Config: 'new_model': User.schema().get('example'), } } + ) diff --git a/cuenca/resources/user_lists_validation.py b/cuenca/resources/user_lists_validation.py index 8578071b..f475e7d3 100644 --- a/cuenca/resources/user_lists_validation.py +++ b/cuenca/resources/user_lists_validation.py @@ -38,5 +38,5 @@ def create( ) return cast( 'UserListsValidation', - cls._create(session=session, **req.dict()), + cls._create(session=session, **req.model_dump()), ) diff --git a/cuenca/resources/user_logins.py b/cuenca/resources/user_logins.py index 703f1d81..fa420705 100644 --- a/cuenca/resources/user_logins.py +++ b/cuenca/resources/user_logins.py @@ -2,6 +2,7 @@ from typing import ClassVar, Optional, cast from cuenca_validations.types.requests import UserLoginRequest +from pydantic import ConfigDict from ..http import Session, session as global_session from .base import Creatable @@ -10,17 +11,17 @@ class UserLogin(Creatable): _resource: ClassVar = 'user_logins' - last_login_at: Optional[dt.datetime] + last_login_at: Optional[dt.datetime] = None success: bool - - class Config: - schema_extra = { + model_config = ConfigDict( + json_schema_extra={ 'example': { 'id': 'ULNEUInh69SuKXXmK95sROwQ', 'last_login_at': '2022-01-01T14:15:22Z', 'success': True, } } + ) @classmethod def create( diff --git a/cuenca/resources/users.py b/cuenca/resources/users.py index 0a0d5816..b616ac10 100644 --- a/cuenca/resources/users.py +++ b/cuenca/resources/users.py @@ -16,7 +16,7 @@ ) from cuenca_validations.types.enums import Country, Gender, State from cuenca_validations.types.identities import CurpField -from pydantic import EmailStr, HttpUrl +from pydantic import ConfigDict, EmailStr, Field, HttpUrl from ..http import Session, session as global_session from .balance_entries import BalanceEntry @@ -30,60 +30,52 @@ class User(Creatable, Retrievable, Updateable, Queryable): _query_params: ClassVar = UserQuery identity_uri: str - level: int - required_level: int + level: int = Field( + ..., description='Account level according to KYC information' + ) + required_level: int = Field( + ..., description='Maximum level User can reach. Set by platform' + ) created_at: dt.datetime - phone_number: Optional[PhoneNumber] - email_address: Optional[EmailStr] - profession: Optional[str] - terms_of_service: Optional[TOSAgreement] - status: Optional[UserStatus] - address: Optional[Address] - govt_id: Optional[KYCFile] - proof_of_address: Optional[KYCFile] - proof_of_life: Optional[KYCFile] - beneficiaries: Optional[List[Beneficiary]] + phone_number: Optional[PhoneNumber] = None + email_address: Optional[EmailStr] = None + profession: Optional[str] = None + terms_of_service: Optional[TOSAgreement] = None + status: Optional[UserStatus] = None + address: Optional[Address] = None + govt_id: Optional[KYCFile] = Field( + None, description='Government ID document validation' + ) + proof_of_address: Optional[KYCFile] = Field( + None, description='Detail of proof of address document validation' + ) + proof_of_life: Optional[KYCFile] = Field( + None, description='Detail of selfie video validation' + ) + beneficiaries: Optional[List[Beneficiary]] = Field( + None, description='Beneficiaries of account in case of death' + ) platform_id: Optional[str] = None clabe: Optional[Clabe] = None # These fields are added by identify when retrieving a User: - names: Optional[str] - first_surname: Optional[str] - second_surname: Optional[str] - curp: Optional[str] - rfc: Optional[str] - gender: Optional[Gender] - date_of_birth: Optional[dt.date] - state_of_birth: Optional[State] - nationality: Optional[Country] - country_of_birth: Optional[Country] + names: Optional[str] = None + first_surname: Optional[str] = None + second_surname: Optional[str] = None + curp: Optional[str] = None + rfc: Optional[str] = None + gender: Optional[Gender] = None + date_of_birth: Optional[dt.date] = None + state_of_birth: Optional[State] = None + nationality: Optional[Country] = None + country_of_birth: Optional[Country] = None @property def balance(self) -> int: be = cast(BalanceEntry, BalanceEntry.first(user_id=self.id)) return be.rolling_balance if be else 0 - class Config: - fields = { - 'level': { - 'description': 'Account level according to KYC information' - }, - 'required_level': { - 'description': 'Maximum level User can reach. Set by platform' - }, - 'govt_id': { - 'description': 'Detail of government id document validation' - }, - 'proof_of_address': { - 'description': 'Detail of proof of address document validation' - }, - 'proof_of_life': { - 'description': 'Detail of selfie video validation' - }, - 'beneficiaries': { - 'description': 'Beneficiaries of account in case of death' - }, - } - schema_extra = { + model_config = ConfigDict( + json_schema_extra={ 'example': { 'id': 'USWqY5cvkISJOxHyEKjAKf8w', 'created_at': '2019-08-24T14:15:22Z', @@ -105,7 +97,8 @@ class Config: ], 'platform_id': 'PT8UEv02zBTcymd4Kd3MO6pg', } - } + }, + ) @classmethod def create( @@ -135,7 +128,7 @@ def create( status=status, terms_of_service=terms_of_service, ) - return cast('User', cls._create(session=session, **req.dict())) + return cast('User', cls._create(session=session, **req.model_dump())) @classmethod def update( @@ -176,7 +169,7 @@ def update( ) return cast( 'User', - cls._update(id=user_id, **request.dict(), session=session), + cls._update(id=user_id, **request.model_dump(), session=session), ) @property diff --git a/cuenca/resources/verifications.py b/cuenca/resources/verifications.py index bf59b1f7..cd141cd7 100644 --- a/cuenca/resources/verifications.py +++ b/cuenca/resources/verifications.py @@ -7,7 +7,7 @@ VerificationType, ) from cuenca_validations.types.identities import PhoneNumber -from pydantic import EmailStr +from pydantic import ConfigDict, EmailStr, Field from ..http import Session, session as global_session from .base import Creatable, Updateable @@ -16,14 +16,14 @@ class Verification(Creatable, Updateable): _resource: ClassVar = 'verifications' - recipient: Union[EmailStr, PhoneNumber] + recipient: Union[EmailStr, PhoneNumber] = Field( + ..., description='Phone or email to validate' + ) type: VerificationType created_at: dt.datetime - deactivated_at: Optional[dt.datetime] - - class Config: - fields = {'recipient': {'description': 'Phone or email to validate'}} - schema_extra = { + deactivated_at: Optional[dt.datetime] = None + model_config = ConfigDict( + json_schema_extra={ 'example': { 'id': 'VENEUInh69SuKXXmK95sROwQ', 'recipient': 'user@example.com', @@ -31,7 +31,8 @@ class Config: 'created_at': '2022-05-24T14:15:22Z', 'deactivated_at': None, } - } + }, + ) @classmethod def create( @@ -44,7 +45,9 @@ def create( req = VerificationRequest( recipient=recipient, type=type, platform_id=platform_id ) - return cast('Verification', cls._create(**req.dict(), session=session)) + return cast( + 'Verification', cls._create(**req.model_dump(), session=session) + ) @classmethod def verify( @@ -56,5 +59,5 @@ def verify( req = VerificationAttemptRequest(code=code) return cast( 'Verification', - cls._update(id=id, **req.dict(), session=session), + cls._update(id=id, **req.model_dump(), session=session), ) diff --git a/cuenca/resources/webhooks.py b/cuenca/resources/webhooks.py index 257e8b68..dd7a3cce 100644 --- a/cuenca/resources/webhooks.py +++ b/cuenca/resources/webhooks.py @@ -1,6 +1,7 @@ from typing import Any, ClassVar, Dict from cuenca_validations.types.enums import WebhookEvent +from pydantic import Field from .base import Queryable, Retrievable @@ -8,11 +9,9 @@ class Webhook(Retrievable, Queryable): _resource: ClassVar = 'webhooks' - payload: Dict[str, Any] - event: WebhookEvent - - class Config: - fields = { - 'payload': {'description': 'object sent by the webhook'}, - 'event': {'description': 'type of event being reported'}, - } + payload: Dict[str, Any] = Field( + ..., description='object sent by the webhook' + ) + event: WebhookEvent = Field( + ..., description='type of event being reported' + ) diff --git a/cuenca/resources/whatsapp_transfers.py b/cuenca/resources/whatsapp_transfers.py index 7a067599..5be24966 100644 --- a/cuenca/resources/whatsapp_transfers.py +++ b/cuenca/resources/whatsapp_transfers.py @@ -14,12 +14,12 @@ class WhatsappTransfer(Transaction): updated_at: dt.datetime recipient_name: str phone_number: str - claim_url: Optional[str] + claim_url: Optional[str] = None expires_at: dt.datetime # defined after the transfer has been claimed - destination_uri: Optional[str] - network: Optional[TransferNetwork] - tracking_key: Optional[str] # clave rastreo if network is SPEI + destination_uri: Optional[str] = None + network: Optional[TransferNetwork] = None + tracking_key: Optional[str] = None # clave rastreo if network is SPEI @property # type: ignore def destination(self) -> Optional[Account]: diff --git a/requirements-test.txt b/requirements-test.txt index 22487320..ffdac3d6 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -2,11 +2,11 @@ black==24.3.0 flake8==4.0.* freezegun==1.1.* isort==5.10.* -mypy==0.931 +mypy==1.13.0 pytest==6.2.* pytest-cov==3.0.* pytest-vcr==1.0.* requests-mock==1.9.* types-freezegun types-requests -vcrpy==4.3.1 +vcrpy==6.0.2 diff --git a/requirements.txt b/requirements.txt index 7cd483b3..e8f11747 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ -requests==2.31.0 -cuenca-validations==0.11.30 +requests==2.32.3 +cuenca-validations==2.0.0.dev5 dataclasses>=0.7;python_version<"3.7" +pydantic-extra-types==2.10.1 \ No newline at end of file diff --git a/tests/resources/test_endpoints.py b/tests/resources/test_endpoints.py index 4317b9ff..1ae1c51f 100644 --- a/tests/resources/test_endpoints.py +++ b/tests/resources/test_endpoints.py @@ -43,7 +43,7 @@ def test_endpoint_update(): ) assert endpoint.id == id_endpoint assert len(endpoint.events) == 2 - assert endpoint.url == 'https://url.io' + assert str(endpoint.url) == 'https://url.io/' assert not endpoint.is_enable assert endpoint.is_active diff --git a/tests/resources/test_sessions.py b/tests/resources/test_sessions.py index 30366772..60dba2bf 100644 --- a/tests/resources/test_sessions.py +++ b/tests/resources/test_sessions.py @@ -34,8 +34,8 @@ def test_session_create(curp_validation_request: Dict, user_request: Dict): assert user_session.user_id == user.id assert user_session.type == SessionType.registration - assert user_session.success_url == success_url - assert user_session.failure_url == failure_url + assert str(user_session.success_url) == success_url + assert str(user_session.failure_url) == failure_url ephimeral_cuenca_session = cuenca.http.Session() ephimeral_cuenca_session.configure(session_token=user_session.id) diff --git a/tests/resources/test_transfers.py b/tests/resources/test_transfers.py index 631d691e..327b645d 100644 --- a/tests/resources/test_transfers.py +++ b/tests/resources/test_transfers.py @@ -129,4 +129,4 @@ def test_transfers_count_vs_all(): def test_invalid_params(): with pytest.raises(ValidationError) as e: Transfer.one(invalid_param='invalid_param') - assert 'extra fields not permitted' in str(e) + assert 'Extra inputs are not permitted' in str(e) From 12a855684083ac50c8a7499826163acf9695799d Mon Sep 17 00:00:00 2001 From: gabino Date: Sun, 22 Dec 2024 10:24:54 -0600 Subject: [PATCH 02/29] Refactor type hints and casting in resource files --- cuenca/resources/arpc.py | 3 ++- cuenca/resources/balance_entries.py | 6 ++---- cuenca/resources/base.py | 4 ++-- cuenca/resources/card_validations.py | 2 +- cuenca/resources/curp_validations.py | 2 +- cuenca/resources/file_batches.py | 10 ++++++++-- cuenca/resources/files.py | 9 +++++++-- cuenca/resources/kyc_verifications.py | 2 +- cuenca/resources/limited_wallets.py | 2 +- cuenca/resources/platforms.py | 2 +- cuenca/resources/sessions.py | 4 ++-- cuenca/resources/transfers.py | 3 ++- cuenca/resources/user_lists_validation.py | 3 ++- cuenca/resources/users.py | 6 ++++-- 14 files changed, 36 insertions(+), 22 deletions(-) diff --git a/cuenca/resources/arpc.py b/cuenca/resources/arpc.py index 773d6a99..77e663a8 100644 --- a/cuenca/resources/arpc.py +++ b/cuenca/resources/arpc.py @@ -1,6 +1,7 @@ import datetime as dt from typing import ClassVar, Optional, cast +from cuenca_validations.types.enums import TrackDataMethod from cuenca_validations.types.requests import ARPCRequest from ..http import Session, session as global_session @@ -50,6 +51,6 @@ def create( transaction_counter=transaction_counter, pan_sequence=pan_sequence, unique_number=unique_number, - track_data_method=track_data_method, + track_data_method=cast(TrackDataMethod, track_data_method), ) return cast('Arpc', cls._create(session=session, **req.model_dump())) diff --git a/cuenca/resources/balance_entries.py b/cuenca/resources/balance_entries.py index 9104f38d..e160f6bb 100644 --- a/cuenca/resources/balance_entries.py +++ b/cuenca/resources/balance_entries.py @@ -1,4 +1,4 @@ -from typing import ClassVar, TypeVar, cast +from typing import ClassVar, Union, cast from cuenca_validations.types import BalanceEntryQuery, EntryType @@ -8,9 +8,7 @@ from .resources import retrieve_uri from .service_providers import ServiceProvider -FundingInstrument = TypeVar( - 'FundingInstrument', Account, ServiceProvider, Card -) +FundingInstrument = Union[Account, ServiceProvider, Card] class BalanceEntry(Retrievable, Queryable): diff --git a/cuenca/resources/base.py b/cuenca/resources/base.py index d8745a31..c8da1294 100644 --- a/cuenca/resources/base.py +++ b/cuenca/resources/base.py @@ -2,7 +2,7 @@ import datetime as dt import json from io import BytesIO -from typing import ClassVar, Dict, Generator, Optional, Union +from typing import Any, ClassVar, Dict, Generator, Optional from urllib.parse import urlencode from cuenca_validations.types import ( @@ -24,7 +24,7 @@ class Resource(BaseModel): id: str @classmethod - def _from_dict(cls, obj_dict: Dict[str, Union[str, int]]) -> 'Resource': + def _from_dict(cls, obj_dict: Dict[str, Any]) -> 'Resource': cls._filter_excess_fields(obj_dict) return cls(**obj_dict) diff --git a/cuenca/resources/card_validations.py b/cuenca/resources/card_validations.py index 5e03b895..59870a48 100644 --- a/cuenca/resources/card_validations.py +++ b/cuenca/resources/card_validations.py @@ -37,7 +37,7 @@ def create( exp_month: Optional[int] = None, exp_year: Optional[int] = None, pin_block: Optional[str] = None, - pin_attempts_exceeded: Optional[str] = None, + pin_attempts_exceeded: Optional[bool] = None, *, session: Session = global_session, ) -> 'CardValidation': diff --git a/cuenca/resources/curp_validations.py b/cuenca/resources/curp_validations.py index 49cb20f6..85643d5d 100644 --- a/cuenca/resources/curp_validations.py +++ b/cuenca/resources/curp_validations.py @@ -95,7 +95,7 @@ def create( second_surname=second_surname, date_of_birth=date_of_birth, state_of_birth=state_of_birth, - country_of_birth=country_of_birth, + country_of_birth=cast(Country, country_of_birth), gender=gender, manual_curp=manual_curp, ) diff --git a/cuenca/resources/file_batches.py b/cuenca/resources/file_batches.py index 201c7648..e658e435 100644 --- a/cuenca/resources/file_batches.py +++ b/cuenca/resources/file_batches.py @@ -1,6 +1,10 @@ from typing import ClassVar, Dict, List, cast -from cuenca_validations.types import BatchFileMetadata, FileBatchUploadRequest +from cuenca_validations.types import ( + BatchFileMetadata, + FileBatchUploadRequest, + FileRequest, +) from ..http import Session, session as global_session from .base import Creatable, Queryable @@ -21,7 +25,9 @@ def create( *, session: Session = global_session, ) -> 'FileBatch': - req = FileBatchUploadRequest(files=files, user_id=user_id) + req = FileBatchUploadRequest( + files=cast(List[FileRequest], files), user_id=user_id + ) return cast( 'FileBatch', cls._create(session=session, **req.model_dump()) ) diff --git a/cuenca/resources/files.py b/cuenca/resources/files.py index b56bb1a5..a60df8f2 100644 --- a/cuenca/resources/files.py +++ b/cuenca/resources/files.py @@ -1,7 +1,12 @@ from io import BytesIO from typing import ClassVar, Optional, cast -from cuenca_validations.types import FileQuery, FileUploadRequest, KYCFileType +from cuenca_validations.types import ( + FileExtension, + FileQuery, + FileUploadRequest, + KYCFileType, +) from pydantic import HttpUrl from ..http import Session, session as global_session @@ -40,7 +45,7 @@ def upload( req = FileUploadRequest( file=file.read(), type=file_type, - extension=extension, + extension=cast(FileExtension, extension), is_back=is_back, user_id=user_id, ) diff --git a/cuenca/resources/kyc_verifications.py b/cuenca/resources/kyc_verifications.py index b4a378cf..b579b7ef 100644 --- a/cuenca/resources/kyc_verifications.py +++ b/cuenca/resources/kyc_verifications.py @@ -46,7 +46,7 @@ def create(cls, session: Session = global_session) -> 'KYCVerification': def update( cls, kyc_id: str, - curp: Optional[CurpField] = None, + curp: CurpField, ) -> 'KYCVerification': req = KYCVerificationUpdateRequest(curp=curp) return cast('KYCVerification', cls._update(id=kyc_id, **req.dict())) diff --git a/cuenca/resources/limited_wallets.py b/cuenca/resources/limited_wallets.py index dbeddb17..48dc015c 100644 --- a/cuenca/resources/limited_wallets.py +++ b/cuenca/resources/limited_wallets.py @@ -21,7 +21,7 @@ class LimitedWallet(Wallet): @classmethod def create( cls, - allowed_curp: Optional[CurpField] = None, + allowed_curp: CurpField, allowed_rfc: Optional[Rfc] = None, ) -> 'LimitedWallet': """ diff --git a/cuenca/resources/platforms.py b/cuenca/resources/platforms.py index 641e5fc2..f7055153 100644 --- a/cuenca/resources/platforms.py +++ b/cuenca/resources/platforms.py @@ -54,7 +54,7 @@ def create( cls, name: str, rfc: Optional[str] = None, - establishment_date: Optional[str] = None, + establishment_date: Optional[dt.date] = None, country: Optional[Country] = None, state: Optional[State] = None, economic_activity: Optional[str] = None, diff --git a/cuenca/resources/sessions.py b/cuenca/resources/sessions.py index 7639698c..94eef9af 100644 --- a/cuenca/resources/sessions.py +++ b/cuenca/resources/sessions.py @@ -39,8 +39,8 @@ def create( cls, user_id: str, type: SessionType, - success_url: Optional[str] = None, - failure_url: Optional[str] = None, + success_url=cast(Optional[AnyUrl], success_url), + failure_url=cast(Optional[AnyUrl], failure_url), *, session: http.Session = http.session, ) -> 'Session': diff --git a/cuenca/resources/transfers.py b/cuenca/resources/transfers.py index 3e407c1d..2ec50188 100644 --- a/cuenca/resources/transfers.py +++ b/cuenca/resources/transfers.py @@ -1,6 +1,7 @@ import datetime as dt from typing import ClassVar, List, Optional, cast +from clabe import Clabe from cuenca_validations.types import ( TransferNetwork, TransferQuery, @@ -62,7 +63,7 @@ def create( if not idempotency_key: idempotency_key = cls._gen_idempotency_key(account_number, amount) req = TransferRequest( - account_number=account_number, + account_number=cast(Clabe, account_number), amount=amount, descriptor=descriptor, recipient_name=recipient_name, diff --git a/cuenca/resources/user_lists_validation.py b/cuenca/resources/user_lists_validation.py index f475e7d3..072e3420 100644 --- a/cuenca/resources/user_lists_validation.py +++ b/cuenca/resources/user_lists_validation.py @@ -1,6 +1,7 @@ import datetime as dt from typing import ClassVar, Optional, cast +from clabe import Clabe from cuenca_validations.types import UserListsRequest, VerificationStatus from cuenca_validations.types.identities import CurpField @@ -34,7 +35,7 @@ def create( first_surname=first_surname, second_surname=second_surname, curp=curp, - account_number=account_number, + account_number=cast(Clabe, account_number), ) return cast( 'UserListsValidation', diff --git a/cuenca/resources/users.py b/cuenca/resources/users.py index b616ac10..abdcef22 100644 --- a/cuenca/resources/users.py +++ b/cuenca/resources/users.py @@ -104,6 +104,7 @@ def balance(self) -> int: def create( cls, curp: CurpField, + id: Optional[str] = None, phone_number: Optional[PhoneNumber] = None, email_address: Optional[EmailStr] = None, profession: Optional[str] = None, @@ -117,6 +118,7 @@ def create( session: Session = global_session, ) -> 'User': req = UserRequest( + id=id, curp=curp, phone_number=phone_number, email_address=email_address, @@ -147,7 +149,7 @@ def update( status: Optional[UserStatus] = None, email_verification_id: Optional[str] = None, phone_verification_id: Optional[str] = None, - curp_document: Optional[HttpUrl] = None, + curp_document_uri: Optional[HttpUrl] = None, *, session: Session = global_session, ): @@ -164,7 +166,7 @@ def update( verification_id=verification_id, email_verification_id=email_verification_id, phone_verification_id=phone_verification_id, - curp_document=curp_document, + curp_document_uri=curp_document_uri, status=status, ) return cast( From ff94458e76865da3e3c13a7490bc41121b400717 Mon Sep 17 00:00:00 2001 From: gabino Date: Sun, 22 Dec 2024 10:33:04 -0600 Subject: [PATCH 03/29] Remove Python 3.10 from GitHub Actions workflow matrix for test --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b4a75f5f..9b379416 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.8', '3.9', '3.10'] + python-version: ['3.8', '3.9'] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} From e96cfb697ec9bc45d9321c4db98976d75d245778 Mon Sep 17 00:00:00 2001 From: gabino Date: Sun, 22 Dec 2024 10:51:29 -0600 Subject: [PATCH 04/29] Update version to 2.0.0.dev1 in version.py --- cuenca/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cuenca/version.py b/cuenca/version.py index 2d56c88c..0b341e1d 100644 --- a/cuenca/version.py +++ b/cuenca/version.py @@ -1,3 +1,3 @@ -__version__ = '1.0.2' +__version__ = '2.0.0.dev1' CLIENT_VERSION = __version__ API_VERSION = '2020-03-19' From 19c58382daef5003522e6274d66451567f8eb933 Mon Sep 17 00:00:00 2001 From: gabino Date: Mon, 30 Dec 2024 16:36:01 -0600 Subject: [PATCH 05/29] Update dependencies and GitHub Actions workflow --- .github/workflows/test.yml | 2 +- requirements-test.txt | 16 ++++++++-------- requirements.txt | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9b379416..121089b3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.8', '3.9'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} diff --git a/requirements-test.txt b/requirements-test.txt index ffdac3d6..c39e277b 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,12 +1,12 @@ -black==24.3.0 -flake8==4.0.* -freezegun==1.1.* -isort==5.10.* -mypy==1.13.0 -pytest==6.2.* -pytest-cov==3.0.* +black==22.8.0 +flake8==5.0.4 +freezegun==1.5.1 +isort==5.11.5 +mypy==1.4.1 +pytest==7.4.4 +pytest-cov==4.1.0 pytest-vcr==1.0.* requests-mock==1.9.* -types-freezegun +types-freezegun==1.1.7 types-requests vcrpy==6.0.2 diff --git a/requirements.txt b/requirements.txt index e8f11747..f329c548 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ requests==2.32.3 -cuenca-validations==2.0.0.dev5 +cuenca-validations==2.0.0.dev8 dataclasses>=0.7;python_version<"3.7" -pydantic-extra-types==2.10.1 \ No newline at end of file +pydantic-extra-types==2.10.* \ No newline at end of file From 622e6cf1d7c17fa43d6b16445a29596abd81a7b9 Mon Sep 17 00:00:00 2001 From: gabino Date: Mon, 30 Dec 2024 17:29:22 -0600 Subject: [PATCH 06/29] Fix VCRHTTPResponse version_string AttributeError in tests --- tests/conftest.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index d61b02cc..ec82faa8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,6 +9,20 @@ cuenca.configure(sandbox=True) +# Temporary patch for the VCRHTTPResponse object to avoid +# AttributeError: 'VCRHTTPResponse' object has no attribute 'version_string' +# Occurs with urllib3>=2.3.0 and is being addressed in the following PR: +# https://github.com/kevin1024/vcrpy/pull/889 + + +@pytest.fixture(autouse=True) +def patch_VCRHTTPResponse_version_string(): + from vcr.stubs import VCRHTTPResponse # type: ignore + + if not hasattr(VCRHTTPResponse, 'version_string'): + VCRHTTPResponse.version_string = None + yield + @pytest.fixture(scope='module') def vcr_config(): From 9c105908bda06b33959c94ecb6a99cb57015c17a Mon Sep 17 00:00:00 2001 From: gabino Date: Mon, 30 Dec 2024 17:55:03 -0600 Subject: [PATCH 07/29] Removed temporary patch for VCRHTTPResponse version_string from conftest.py --- cuenca/resources/card_transactions.py | 3 +++ requirements-test.txt | 3 ++- tests/conftest.py | 14 -------------- 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/cuenca/resources/card_transactions.py b/cuenca/resources/card_transactions.py index 529ffd22..74adb2ff 100644 --- a/cuenca/resources/card_transactions.py +++ b/cuenca/resources/card_transactions.py @@ -14,6 +14,9 @@ class CardTransaction(Transaction): + def __init__(self, *args, **kwargs): + super(CardTransaction, self).__init__(*args, **kwargs) + _resource: ClassVar = 'card_transactions' _query_params: ClassVar = CardTransactionQuery diff --git a/requirements-test.txt b/requirements-test.txt index c39e277b..9d96fdb3 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -9,4 +9,5 @@ pytest-vcr==1.0.* requests-mock==1.9.* types-freezegun==1.1.7 types-requests -vcrpy==6.0.2 +#vcrpy==6.0.2 +git+https://github.com/kevin1024/vcrpy.git@master diff --git a/tests/conftest.py b/tests/conftest.py index ec82faa8..d61b02cc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,20 +9,6 @@ cuenca.configure(sandbox=True) -# Temporary patch for the VCRHTTPResponse object to avoid -# AttributeError: 'VCRHTTPResponse' object has no attribute 'version_string' -# Occurs with urllib3>=2.3.0 and is being addressed in the following PR: -# https://github.com/kevin1024/vcrpy/pull/889 - - -@pytest.fixture(autouse=True) -def patch_VCRHTTPResponse_version_string(): - from vcr.stubs import VCRHTTPResponse # type: ignore - - if not hasattr(VCRHTTPResponse, 'version_string'): - VCRHTTPResponse.version_string = None - yield - @pytest.fixture(scope='module') def vcr_config(): From aeca28ded04c4d6b1dd73d211c51bb7bc051774b Mon Sep 17 00:00:00 2001 From: gabino Date: Mon, 30 Dec 2024 18:56:15 -0600 Subject: [PATCH 08/29] Update Python version to 3.13, adjust dependencies --- .github/workflows/release.yml | 4 ++-- .github/workflows/test.yml | 6 +++--- Makefile | 4 ++-- cuenca/exc.py | 9 ++++----- cuenca/resources/transfers.py | 2 +- examples/batch_transfers.py | 3 +-- requirements-test.txt | 5 ++--- requirements.txt | 1 - setup.py | 14 +++++++++----- 9 files changed, 24 insertions(+), 24 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8d232cd7..f7111646 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,10 +7,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2.4.0 - - name: Set up Python 3.8 + - name: Set up Python 3.13 uses: actions/setup-python@v2.3.1 with: - python-version: 3.8 + python-version: 3.13 - name: Install dependencies run: pip install -qU setuptools wheel twine - name: Generating distribution archives diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 121089b3..dacda387 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5.1.0 with: - python-version: 3.8 + python-version: 3.13 - name: Install dependencies run: make install-test - name: Lint @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} @@ -39,7 +39,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v5.1.0 with: - python-version: 3.8 + python-version: 3.13 - name: Install dependencies run: make install-test - name: Generate coverage report diff --git a/Makefile b/Makefile index f62d6cf1..2bc05db5 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,9 @@ SHELL := bash PATH := ./venv/bin:${PATH} -PYTHON = python3.8 +PYTHON = python3.13 PROJECT = cuenca isort = isort $(PROJECT) tests setup.py examples -black = black -S -l 79 --target-version py38 $(PROJECT) tests setup.py examples +black = black -S -l 79 --target-version py313 $(PROJECT) tests setup.py examples all: test diff --git a/cuenca/exc.py b/cuenca/exc.py index e73a93c8..9c2d5c7d 100644 --- a/cuenca/exc.py +++ b/cuenca/exc.py @@ -1,5 +1,3 @@ -from dataclasses import dataclass - from cuenca_validations.typing import DictStrAny @@ -19,10 +17,11 @@ class MultipleResultsFound(CuencaException): """One result was expected but multiple were returned""" -@dataclass class CuencaResponseException(CuencaException): - json: DictStrAny - status_code: int + def __init__(self, json: DictStrAny, status_code: int) -> None: + self.json = json + self.status_code = status_code + super().__init__() def __str__(self) -> str: return repr(self) diff --git a/cuenca/resources/transfers.py b/cuenca/resources/transfers.py index 2ec50188..01a2e40b 100644 --- a/cuenca/resources/transfers.py +++ b/cuenca/resources/transfers.py @@ -91,4 +91,4 @@ def _gen_idempotency_key(account_number: str, amount: int) -> str: idempotency_key, but this provides some level of protection against submitting duplicate transfers """ - return f'{dt.datetime.utcnow().date()}:{account_number}:{amount}' + return f'{dt.datetime.utcnow().date()}: {account_number}: {amount}' diff --git a/examples/batch_transfers.py b/examples/batch_transfers.py index b2a33c5c..8730f015 100644 --- a/examples/batch_transfers.py +++ b/examples/batch_transfers.py @@ -1,7 +1,6 @@ import argparse import csv import logging -from dataclasses import fields from cuenca.resources.transfers import Transfer, TransferRequest @@ -25,7 +24,7 @@ def main(): transfer_requests = [TransferRequest(**line) for line in reader] transfers = Transfer.create_many(transfer_requests) with open(args.output, 'w') as f: - fieldnames = [field.name for field in fields(Transfer)] + fieldnames = list(transfers['submitted'][0].to_dict().keys()) writer = csv.DictWriter(f, fieldnames) writer.writeheader() for tr in transfers['submitted']: diff --git a/requirements-test.txt b/requirements-test.txt index 9d96fdb3..1953f3c0 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,4 +1,4 @@ -black==22.8.0 +black==24.10.0 flake8==5.0.4 freezegun==1.5.1 isort==5.11.5 @@ -9,5 +9,4 @@ pytest-vcr==1.0.* requests-mock==1.9.* types-freezegun==1.1.7 types-requests -#vcrpy==6.0.2 -git+https://github.com/kevin1024/vcrpy.git@master +vcrpy==7.0.0 diff --git a/requirements.txt b/requirements.txt index f329c548..0670b41a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ requests==2.32.3 cuenca-validations==2.0.0.dev8 -dataclasses>=0.7;python_version<"3.7" pydantic-extra-types==2.10.* \ No newline at end of file diff --git a/setup.py b/setup.py index 36c728e9..657b1cc9 100644 --- a/setup.py +++ b/setup.py @@ -21,14 +21,18 @@ packages=find_packages(), include_package_data=True, package_data=dict(cuenca=['py.typed']), - python_requires='>=3.8', + python_requires='>=3.9', install_requires=[ - 'requests>=2.24,<28', - 'dataclasses>=0.7;python_version<"3.8"', - 'cuenca-validations>= 0.11.3,<0.12.0', + 'requests>=2.32.3', + 'cuenca-validations>=2.0.0', + 'pydantic-extra-types>=2.10.1', ], classifiers=[ - 'Programming Language :: Python :: 3.8', + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', ], From 63823a5516c3ae53e95e7934cd8d711fbf3e7f45 Mon Sep 17 00:00:00 2001 From: gabino Date: Mon, 30 Dec 2024 19:04:46 -0600 Subject: [PATCH 09/29] Update version to 2.0.0.dev2 --- cuenca/version.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cuenca/version.py b/cuenca/version.py index 0b341e1d..71010709 100644 --- a/cuenca/version.py +++ b/cuenca/version.py @@ -1,3 +1,3 @@ -__version__ = '2.0.0.dev1' +__version__ = '2.0.0.dev2' CLIENT_VERSION = __version__ API_VERSION = '2020-03-19' diff --git a/setup.py b/setup.py index 657b1cc9..aef9aeb1 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ python_requires='>=3.9', install_requires=[ 'requests>=2.32.3', - 'cuenca-validations>=2.0.0', + 'cuenca-validations==2.0.0.dev8', 'pydantic-extra-types>=2.10.1', ], classifiers=[ From 11bb9639b98b14695f0b35f395f65353a323c3ff Mon Sep 17 00:00:00 2001 From: gabino Date: Thu, 2 Jan 2025 11:09:41 -0600 Subject: [PATCH 10/29] Update cuenca-validations dependency to version 2.0.0.dev9 in setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index aef9aeb1..ed3caadf 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ python_requires='>=3.9', install_requires=[ 'requests>=2.32.3', - 'cuenca-validations==2.0.0.dev8', + 'cuenca-validations==2.0.0.dev9', 'pydantic-extra-types>=2.10.1', ], classifiers=[ From 8bf739a1d1d0afb2926580447061c2aea909bd07 Mon Sep 17 00:00:00 2001 From: gabino Date: Thu, 2 Jan 2025 11:11:55 -0600 Subject: [PATCH 11/29] Increment version to 2.0.0.dev3 in version.py --- cuenca/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cuenca/version.py b/cuenca/version.py index 71010709..50d53cee 100644 --- a/cuenca/version.py +++ b/cuenca/version.py @@ -1,3 +1,3 @@ -__version__ = '2.0.0.dev2' +__version__ = '2.0.0.dev3' CLIENT_VERSION = __version__ API_VERSION = '2020-03-19' From 4bc3a54009be7486c8a3acc2dacc86b915e8adf6 Mon Sep 17 00:00:00 2001 From: gabino Date: Mon, 6 Jan 2025 13:32:12 -0600 Subject: [PATCH 12/29] Refactor CuencaResponseException to use dataclass and update fieldnames extraction in batch_transfers.py --- cuenca/exc.py | 9 +++++---- examples/batch_transfers.py | 3 ++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/cuenca/exc.py b/cuenca/exc.py index 9c2d5c7d..e73a93c8 100644 --- a/cuenca/exc.py +++ b/cuenca/exc.py @@ -1,3 +1,5 @@ +from dataclasses import dataclass + from cuenca_validations.typing import DictStrAny @@ -17,11 +19,10 @@ class MultipleResultsFound(CuencaException): """One result was expected but multiple were returned""" +@dataclass class CuencaResponseException(CuencaException): - def __init__(self, json: DictStrAny, status_code: int) -> None: - self.json = json - self.status_code = status_code - super().__init__() + json: DictStrAny + status_code: int def __str__(self) -> str: return repr(self) diff --git a/examples/batch_transfers.py b/examples/batch_transfers.py index 8730f015..b2a33c5c 100644 --- a/examples/batch_transfers.py +++ b/examples/batch_transfers.py @@ -1,6 +1,7 @@ import argparse import csv import logging +from dataclasses import fields from cuenca.resources.transfers import Transfer, TransferRequest @@ -24,7 +25,7 @@ def main(): transfer_requests = [TransferRequest(**line) for line in reader] transfers = Transfer.create_many(transfer_requests) with open(args.output, 'w') as f: - fieldnames = list(transfers['submitted'][0].to_dict().keys()) + fieldnames = [field.name for field in fields(Transfer)] writer = csv.DictWriter(f, fieldnames) writer.writeheader() for tr in transfers['submitted']: From ceb1d4184eccc7e2c34fefe22a770d5e9e156981 Mon Sep 17 00:00:00 2001 From: gabino Date: Mon, 6 Jan 2025 13:40:56 -0600 Subject: [PATCH 13/29] Refactor URL fields in endpoints and sessions to use annotated types for compatibility with Pydantic v2 --- cuenca/resources/endpoints.py | 14 +++++++++++--- cuenca/resources/sessions.py | 16 ++++++++++++---- tests/resources/test_endpoints.py | 2 +- tests/resources/test_sessions.py | 4 ++-- 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/cuenca/resources/endpoints.py b/cuenca/resources/endpoints.py index adafd16d..665238ea 100644 --- a/cuenca/resources/endpoints.py +++ b/cuenca/resources/endpoints.py @@ -1,20 +1,28 @@ -from typing import ClassVar, List, Optional, cast +from typing import Annotated, ClassVar, List, Optional, cast from cuenca_validations.types.enums import WebhookEvent from cuenca_validations.types.requests import ( EndpointRequest, EndpointUpdateRequest, ) -from pydantic import ConfigDict, Field, HttpUrl +from pydantic import AfterValidator, ConfigDict, Field, HttpUrl from ..http import Session, session as global_session from .base import Creatable, Deactivable, Queryable, Retrievable, Updateable +# In Pydantic v2, URL fields like `HttpUrl` are stored as internal objects +# instead of `str`, which can break compatibility with code expecting str. +# Using `HttpUrlString` ensures the field is validated as a URL but stored as +# a `str` for compatibility. +# https://github.com/pydantic/pydantic/discussions/6395 + +HttpUrlString = Annotated[HttpUrl, AfterValidator(str)] + class Endpoint(Creatable, Deactivable, Retrievable, Queryable, Updateable): _resource: ClassVar = 'endpoints' - url: HttpUrl = Field(..., description='HTTPS url to send webhooks') + url: HttpUrlString = Field(..., description='HTTPS url to send webhooks') secret: str = Field( ..., description='token to verify the webhook is sent by Cuenca ' diff --git a/cuenca/resources/sessions.py b/cuenca/resources/sessions.py index 94eef9af..e223e525 100644 --- a/cuenca/resources/sessions.py +++ b/cuenca/resources/sessions.py @@ -1,12 +1,20 @@ import datetime as dt -from typing import ClassVar, Optional, cast +from typing import Annotated, ClassVar, Optional, cast from cuenca_validations.types import SessionRequest, SessionType -from pydantic import AnyUrl, ConfigDict +from pydantic import AfterValidator, AnyUrl, ConfigDict from .. import http from .base import Creatable, Queryable, Retrievable +# In Pydantic v2, URL fields like `AnyUrl` are stored as internal objects +# instead of `str`, which can break compatibility with code expecting str. +# Using `AnyUrlString` ensures the field is validated as a URL but stored as +# a `str` for compatibility. +# https://github.com/pydantic/pydantic/discussions/6395 + +AnyUrlString = Annotated[AnyUrl, AfterValidator(str)] + class Session(Creatable, Retrievable, Queryable): _resource: ClassVar = 'sessions' @@ -16,8 +24,8 @@ class Session(Creatable, Retrievable, Queryable): user_id: str platform_id: str expires_at: dt.datetime - success_url: Optional[AnyUrl] = None - failure_url: Optional[AnyUrl] = None + success_url: Optional[AnyUrlString] = None + failure_url: Optional[AnyUrlString] = None type: Optional[SessionType] = None model_config = ConfigDict( json_schema_extra={ diff --git a/tests/resources/test_endpoints.py b/tests/resources/test_endpoints.py index 1ae1c51f..2a748a55 100644 --- a/tests/resources/test_endpoints.py +++ b/tests/resources/test_endpoints.py @@ -43,7 +43,7 @@ def test_endpoint_update(): ) assert endpoint.id == id_endpoint assert len(endpoint.events) == 2 - assert str(endpoint.url) == 'https://url.io/' + assert endpoint.url == 'https://url.io/' assert not endpoint.is_enable assert endpoint.is_active diff --git a/tests/resources/test_sessions.py b/tests/resources/test_sessions.py index 60dba2bf..30366772 100644 --- a/tests/resources/test_sessions.py +++ b/tests/resources/test_sessions.py @@ -34,8 +34,8 @@ def test_session_create(curp_validation_request: Dict, user_request: Dict): assert user_session.user_id == user.id assert user_session.type == SessionType.registration - assert str(user_session.success_url) == success_url - assert str(user_session.failure_url) == failure_url + assert user_session.success_url == success_url + assert user_session.failure_url == failure_url ephimeral_cuenca_session = cuenca.http.Session() ephimeral_cuenca_session.configure(session_token=user_session.id) From 3c0f14f3c510603ba34229bab70d2c28b1da3a97 Mon Sep 17 00:00:00 2001 From: gabino Date: Mon, 6 Jan 2025 13:47:24 -0600 Subject: [PATCH 14/29] Refactor retrieve_uris function to use a simple for loop instead of ThreadPoolExecutor --- cuenca/resources/resources.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/cuenca/resources/resources.py b/cuenca/resources/resources.py index eeac433b..6003aa2a 100644 --- a/cuenca/resources/resources.py +++ b/cuenca/resources/resources.py @@ -1,5 +1,4 @@ import re -from concurrent.futures import ThreadPoolExecutor from typing import Dict, List, cast from .base import Retrievable @@ -17,5 +16,11 @@ def retrieve_uri(uri: str) -> Retrievable: def retrieve_uris(uris: List[str]) -> List[Retrievable]: - with ThreadPoolExecutor(max_workers=len(uris)) as executor: - return [obj for obj in executor.map(retrieve_uri, uris)] + # Changed the implementation to use a simple for loop instead of + # ThreadPoolExecutor. The list of URIs is small, so the performance + # difference is negligible. Additionally, using ThreadPoolExecutor + # caused issues with VCR tests, as the recordings were not retrieved + # in the correct order, leading to unexpected HTTP calls instead of + # using the mocked recordings. + + return [retrieve_uri(uri) for uri in uris] From 53add20f2beb8f3d6510b6dff1d0580e1e1cb3a9 Mon Sep 17 00:00:00 2001 From: gabino Date: Wed, 8 Jan 2025 12:59:02 -0600 Subject: [PATCH 15/29] Update test and main dependencies to latest versions --- requirements-test.txt | 18 +++++++++--------- requirements.txt | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/requirements-test.txt b/requirements-test.txt index 1953f3c0..558100da 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,12 +1,12 @@ black==24.10.0 -flake8==5.0.4 +flake8==7.1.1 freezegun==1.5.1 -isort==5.11.5 -mypy==1.4.1 -pytest==7.4.4 -pytest-cov==4.1.0 -pytest-vcr==1.0.* -requests-mock==1.9.* -types-freezegun==1.1.7 -types-requests +isort==5.13.2 +mypy==1.14.1 +pytest==8.3.4 +pytest-cov==6.0.0 +pytest-vcr==1.0.2 +requests-mock==1.12.1 +types-freezegun==1.1.10 +types-requests==2.32.* vcrpy==7.0.0 diff --git a/requirements.txt b/requirements.txt index 0670b41a..22e44ebc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ requests==2.32.3 -cuenca-validations==2.0.0.dev8 +cuenca-validations==2.0.0.dev10 pydantic-extra-types==2.10.* \ No newline at end of file From e0d54d30876fca953239370b3fd31579c7358d32 Mon Sep 17 00:00:00 2001 From: gabino Date: Wed, 8 Jan 2025 13:00:22 -0600 Subject: [PATCH 16/29] Refactor API resource methods to use model_dump() instead of dict() --- cuenca/resources/api_keys.py | 2 +- cuenca/resources/base.py | 8 ++++---- cuenca/resources/cards.py | 4 ++-- cuenca/resources/endpoints.py | 4 ++-- cuenca/resources/files.py | 2 +- cuenca/resources/kyc_validations.py | 2 +- cuenca/resources/kyc_verifications.py | 2 +- cuenca/resources/limited_wallets.py | 2 +- cuenca/resources/savings.py | 4 ++-- cuenca/resources/sessions.py | 2 +- cuenca/resources/transfers.py | 4 ++-- cuenca/resources/user_credentials.py | 4 ++-- cuenca/resources/user_logins.py | 2 +- cuenca/resources/wallet_transactions.py | 2 +- 14 files changed, 22 insertions(+), 22 deletions(-) diff --git a/cuenca/resources/api_keys.py b/cuenca/resources/api_keys.py index 0fbe4530..6c85beea 100644 --- a/cuenca/resources/api_keys.py +++ b/cuenca/resources/api_keys.py @@ -75,5 +75,5 @@ def update( req = ApiKeyUpdateRequest( metadata=metadata, user_id=user_id, platform_id=platform_id ) - resp = cls._update(api_key_id, **req.dict(), session=session) + resp = cls._update(api_key_id, **req.model_dump(), session=session) return cast('ApiKey', resp) diff --git a/cuenca/resources/base.py b/cuenca/resources/base.py index c8da1294..839e83f7 100644 --- a/cuenca/resources/base.py +++ b/cuenca/resources/base.py @@ -151,7 +151,7 @@ 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()) + resp = session.get(cls._resource, q.model_dump()) items = resp['items'] len_items = len(items) if not len_items: @@ -165,7 +165,7 @@ 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()) + resp = session.get(cls._resource, q.model_dump()) try: item = resp['items'][0] except IndexError: @@ -179,7 +179,7 @@ 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()) + resp = session.get(cls._resource, q.model_dump()) return resp['count'] @classmethod @@ -188,7 +188,7 @@ def all( ) -> Generator[Resource, None, None]: session = session or global_session q = cls._query_params(**query_params) - next_page_uri = f'{cls._resource}?{urlencode(q.dict())}' + next_page_uri = f'{cls._resource}?{urlencode(q.model_dump())}' while next_page_uri: page = session.get(next_page_uri) yield from (cls._from_dict(item) for item in page['items']) diff --git a/cuenca/resources/cards.py b/cuenca/resources/cards.py index 505158c0..d239375d 100644 --- a/cuenca/resources/cards.py +++ b/cuenca/resources/cards.py @@ -81,7 +81,7 @@ def create( card_holder_user_id=card_holder_user_id, is_dynamic_cvv=is_dynamic_cvv, ) - return cast('Card', cls._create(session=session, **req.dict())) + return cast('Card', cls._create(session=session, **req.model_dump())) @classmethod def update( @@ -106,7 +106,7 @@ def update( req = CardUpdateRequest( status=status, pin_block=pin_block, is_dynamic_cvv=is_dynamic_cvv ) - resp = cls._update(card_id, session=session, **req.dict()) + resp = cls._update(card_id, session=session, **req.model_dump()) return cast('Card', resp) @classmethod diff --git a/cuenca/resources/endpoints.py b/cuenca/resources/endpoints.py index 665238ea..e1537951 100644 --- a/cuenca/resources/endpoints.py +++ b/cuenca/resources/endpoints.py @@ -77,7 +77,7 @@ def create( :return: New active endpoint """ req = EndpointRequest(url=url, events=events) - return cast('Endpoint', cls._create(session=session, **req.dict())) + return cast('Endpoint', cls._create(session=session, **req.model_dump())) @classmethod def update( @@ -101,5 +101,5 @@ def update( req = EndpointUpdateRequest( url=url, is_enable=is_enable, events=events ) - resp = cls._update(endpoint_id, session=session, **req.dict()) + resp = cls._update(endpoint_id, session=session, **req.model_dump()) return cast('Endpoint', resp) diff --git a/cuenca/resources/files.py b/cuenca/resources/files.py index a60df8f2..38c0f14c 100644 --- a/cuenca/resources/files.py +++ b/cuenca/resources/files.py @@ -53,7 +53,7 @@ def upload( 'File', cls._upload( session=session, - **req.dict(), + **req.model_dump(), ), ) diff --git a/cuenca/resources/kyc_validations.py b/cuenca/resources/kyc_validations.py index 76373466..7b04d38c 100644 --- a/cuenca/resources/kyc_validations.py +++ b/cuenca/resources/kyc_validations.py @@ -40,5 +40,5 @@ def create( documents=documents, ) return cast( - 'KYCValidation', cls._create(**req.dict(), session=session) + 'KYCValidation', cls._create(**req.model_dump(), session=session) ) diff --git a/cuenca/resources/kyc_verifications.py b/cuenca/resources/kyc_verifications.py index b579b7ef..88f8ef7a 100644 --- a/cuenca/resources/kyc_verifications.py +++ b/cuenca/resources/kyc_verifications.py @@ -49,4 +49,4 @@ def update( curp: CurpField, ) -> 'KYCVerification': req = KYCVerificationUpdateRequest(curp=curp) - return cast('KYCVerification', cls._update(id=kyc_id, **req.dict())) + return cast('KYCVerification', cls._update(id=kyc_id, **req.model_dump())) diff --git a/cuenca/resources/limited_wallets.py b/cuenca/resources/limited_wallets.py index 48dc015c..5180781e 100644 --- a/cuenca/resources/limited_wallets.py +++ b/cuenca/resources/limited_wallets.py @@ -37,4 +37,4 @@ def create( allowed_curp=allowed_curp, allowed_rfc=allowed_rfc, ) - return cast('LimitedWallet', cls._create(**request.dict())) + return cast('LimitedWallet', cls._create(**request.model_dump())) diff --git a/cuenca/resources/savings.py b/cuenca/resources/savings.py index 25a76c7c..54e7754b 100644 --- a/cuenca/resources/savings.py +++ b/cuenca/resources/savings.py @@ -34,7 +34,7 @@ def create( goal_amount=goal_amount, goal_date=goal_date, ) - return cast('Saving', cls._create(**request.dict())) + return cast('Saving', cls._create(**request.model_dump())) @classmethod def update( @@ -51,4 +51,4 @@ def update( goal_amount=goal_amount, goal_date=goal_date, ) - return cast('Saving', cls._update(id=saving_id, **request.dict())) + return cast('Saving', cls._update(id=saving_id, **request.model_dump())) diff --git a/cuenca/resources/sessions.py b/cuenca/resources/sessions.py index e223e525..99957531 100644 --- a/cuenca/resources/sessions.py +++ b/cuenca/resources/sessions.py @@ -58,4 +58,4 @@ def create( success_url=success_url, failure_url=failure_url, ) - return cast('Session', cls._create(session=session, **req.dict())) + return cast('Session', cls._create(session=session, **req.model_dump())) diff --git a/cuenca/resources/transfers.py b/cuenca/resources/transfers.py index 01a2e40b..8843324f 100644 --- a/cuenca/resources/transfers.py +++ b/cuenca/resources/transfers.py @@ -70,14 +70,14 @@ def create( idempotency_key=idempotency_key, user_id=user_id, ) - return cast('Transfer', cls._create(**req.dict())) + return cast('Transfer', cls._create(**req.model_dump())) @classmethod def create_many(cls, requests: List[TransferRequest]) -> DictStrAny: transfers: DictStrAny = dict(submitted=[], errors=[]) for req in requests: try: - transfer = cls._create(**req.dict()) + transfer = cls._create(**req.model_dump()) except (CuencaException, HTTPError) as e: transfers['errors'].append(dict(request=req, error=e)) else: diff --git a/cuenca/resources/user_credentials.py b/cuenca/resources/user_credentials.py index 097dd6ff..005a0c4c 100644 --- a/cuenca/resources/user_credentials.py +++ b/cuenca/resources/user_credentials.py @@ -26,7 +26,7 @@ def create( ) -> 'UserCredential': req = UserCredentialRequest(password=password, user_id=user_id) return cast( - 'UserCredential', cls._create(**req.dict(), session=session) + 'UserCredential', cls._create(**req.model_dump(), session=session) ) @classmethod @@ -44,5 +44,5 @@ def update( ) return cast( 'UserCredential', - cls._update(id=user_id, **req.dict(), session=session), + cls._update(id=user_id, **req.model_dump(), session=session), ) diff --git a/cuenca/resources/user_logins.py b/cuenca/resources/user_logins.py index fa420705..3b63390b 100644 --- a/cuenca/resources/user_logins.py +++ b/cuenca/resources/user_logins.py @@ -32,7 +32,7 @@ def create( session: Session = global_session, ) -> 'UserLogin': req = UserLoginRequest(password=password, user_id=user_id) - login = cast('UserLogin', cls._create(session=session, **req.dict())) + login = cast('UserLogin', cls._create(session=session, **req.model_dump())) if login.success: session.headers['X-Cuenca-LoginId'] = login.id return login diff --git a/cuenca/resources/wallet_transactions.py b/cuenca/resources/wallet_transactions.py index c745d007..ed9e8d72 100644 --- a/cuenca/resources/wallet_transactions.py +++ b/cuenca/resources/wallet_transactions.py @@ -35,4 +35,4 @@ def create( transaction_type=transaction_type, amount=amount, ) - return cast('WalletTransaction', cls._create(**request.dict())) + return cast('WalletTransaction', cls._create(**request.model_dump())) From 6a41ff9e381eb1cb83571dfb3c499d64d52825d0 Mon Sep 17 00:00:00 2001 From: gabino Date: Wed, 8 Jan 2025 16:47:42 -0600 Subject: [PATCH 17/29] Update card activation test values and cassette data --- .../cassettes/test_card_activation.yaml | 75 +++++++------------ tests/resources/test_card_activations.py | 8 +- 2 files changed, 31 insertions(+), 52 deletions(-) diff --git a/tests/resources/cassettes/test_card_activation.yaml b/tests/resources/cassettes/test_card_activation.yaml index 268c2a3b..1861b4e6 100644 --- a/tests/resources/cassettes/test_card_activation.yaml +++ b/tests/resources/cassettes/test_card_activation.yaml @@ -1,111 +1,90 @@ interactions: - request: - body: '{"number": "4122943400023502", "exp_month": 11, "exp_year": 24, "cvv2": - "123"}' + body: '{"number": "5448750001621241", "exp_month": 11, "exp_year": 24, "cvv2": + "111"}' headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate Authorization: - DUMMY - Connection: - - keep-alive Content-Length: - '78' Content-Type: - application/json User-Agent: - - cuenca-python/0.7.1 + - cuenca-python/2.0.0.dev3 X-Cuenca-Api-Version: - '2020-03-19' - X-Cuenca-LoginId: - - ULj4BvmWfnRkGk62g321ikiQ + X-Cuenca-LoginToken: + - LTtwn7EyHrRyG7dBe927XWew method: POST uri: https://sandbox.cuenca.com/card_activations response: body: - string: '{"id":"CAAmnPW-msFSUK0V0fpjnC_UQ","created_at":"2021-03-26T19:25:36.504000","user_id":"US1237","ip_address":"200.56.74.39, - 10.5.52.181","card_uri":"/cards/CA2XIurUccQIcqvPGNpdTm7k","success":true}' + string: '{"id":"CAAEkTme9YJRQGmZgTJhSpBdg","created_at":"2025-01-08T22:40:46.834250","user_id":"US1w9BJ0DZ9kSdac39ur14Nf","ip_address":"10.0.2.68","card_uri":"/cards/CAPpdShtSGR0m__EmAmH7dWg","success":true,"bin":"544875","deactivated_at":null}' headers: Connection: - keep-alive Content-Length: - - '214' + - '235' Content-Type: - application/json Date: - - Fri, 26 Mar 2021 19:25:36 GMT - X-Amzn-Trace-Id: - - Root=1-605e352f-728c710217fdaeb646ffeb5a;Sampled=0 + - Wed, 08 Jan 2025 22:40:47 GMT X-Request-Time: - - 'value: 1.241' + - 'value: 1.156' x-amz-apigw-id: - - cz0_aGfEiYcFkHw= + - EFzFPFpRCYcEk8Q= x-amzn-Remapped-Connection: - keep-alive x-amzn-Remapped-Content-Length: - - '214' + - '235' x-amzn-Remapped-Date: - - Fri, 26 Mar 2021 19:25:36 GMT + - Wed, 08 Jan 2025 22:40:47 GMT x-amzn-Remapped-Server: - - nginx/1.18.0 - x-amzn-Remapped-x-amzn-RequestId: - - d825e840-021f-4ea3-885b-47136dc4b951 + - nginx/1.26.2 x-amzn-RequestId: - - b7e4b2a6-e501-44be-adfe-ab8d1437915b + - 0dfcf94e-9d46-447c-96d7-4fbaf1668ce0 status: code: 201 message: Created - request: body: null headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate Authorization: - DUMMY - Connection: - - keep-alive User-Agent: - - cuenca-python/0.7.1 + - cuenca-python/2.0.0.dev3 X-Cuenca-Api-Version: - '2020-03-19' - X-Cuenca-LoginId: - - ULj4BvmWfnRkGk62g321ikiQ + X-Cuenca-LoginToken: + - LTtwn7EyHrRyG7dBe927XWew method: GET - uri: https://sandbox.cuenca.com/cards/CA2XIurUccQIcqvPGNpdTm7k + uri: https://sandbox.cuenca.com/cards/CAPpdShtSGR0m__EmAmH7dWg response: body: - string: '{"id":"CA2XIurUccQIcqvPGNpdTm7k","created_at":"2019-11-13T21:15:44.877000","updated_at":"2019-11-13T21:15:44.877000","user_id":"US1237","number":"4122943400023502","exp_month":11,"exp_year":24,"cvv2":"123","type":"physical","status":"active","pin":"1234","issuer":"accendo","funding_type":"debit","pin_block":""}' + string: '{"id":"CAPpdShtSGR0m__EmAmH7dWg","created_at":"2021-02-26T06:56:36.285000","updated_at":"2025-01-08T22:40:46.233000","user_id":"US1w9BJ0DZ9kSdac39ur14Nf","platform_id":"PTZbBlk__kQt-wfwzP5nwA9A","number":"5448750001621241","exp_month":11,"exp_year":24,"cvv2":"111","type":"physical","status":"active","pin":"1111","issuer":"cuenca","funding_type":"credit","card_holder_user_id":null,"deactivated_at":null,"manufacturer":"manufacturer","cvv":"111","icvv":"111","is_dynamic_cvv":false,"first_validation":null,"pin_block":null,"pin_block_switch":null,"pin_attempts_failed":0}' headers: Connection: - keep-alive Content-Length: - - '330' + - '572' Content-Type: - application/json Date: - - Fri, 26 Mar 2021 19:25:37 GMT - X-Amzn-Trace-Id: - - Root=1-605e3530-41588b232c2acbdb55635076;Sampled=0 + - Wed, 08 Jan 2025 22:40:48 GMT X-Request-Time: - - 'value: 0.423' + - 'value: 0.716' x-amz-apigw-id: - - cz0_pGfNCYcFVlA= + - EFzFdFIeCYcEs9g= x-amzn-Remapped-Connection: - keep-alive x-amzn-Remapped-Content-Length: - - '330' + - '572' x-amzn-Remapped-Date: - - Fri, 26 Mar 2021 19:25:37 GMT + - Wed, 08 Jan 2025 22:40:48 GMT x-amzn-Remapped-Server: - - nginx/1.18.0 - x-amzn-Remapped-x-amzn-RequestId: - - 5e9c4391-baff-4bc8-a71a-d515803b6c99 + - nginx/1.26.2 x-amzn-RequestId: - - 52340184-a7f4-4ecf-b905-51e7a1a38805 + - 48813814-edea-47dc-b7f8-5ef6069da98b status: code: 200 message: OK diff --git a/tests/resources/test_card_activations.py b/tests/resources/test_card_activations.py index eb14e0db..f7dbb10b 100644 --- a/tests/resources/test_card_activations.py +++ b/tests/resources/test_card_activations.py @@ -8,17 +8,17 @@ @pytest.mark.vcr def test_card_activation(): values = dict( - number='4122943400023502', + number='5448750001621241', exp_month=11, exp_year=24, - cvv2='123', + cvv2='111', ) card_activation = CardActivation.create(**values) assert card_activation.success - assert card_activation.user_id == 'US1237' + assert card_activation.user_id == 'US1w9BJ0DZ9kSdac39ur14Nf' card = card_activation.card assert all(getattr(card, key) == value for key, value in values.items()) - assert card.user_id == 'US1237' + assert card.user_id == 'US1w9BJ0DZ9kSdac39ur14Nf' assert card.status is CardStatus.active From f7c54f8fe4d07fce27bb6ddee522a700cf347d92 Mon Sep 17 00:00:00 2001 From: gabino Date: Wed, 8 Jan 2025 16:54:38 -0600 Subject: [PATCH 18/29] style: fix formatting according to lint rules --- cuenca/resources/endpoints.py | 4 +++- cuenca/resources/kyc_verifications.py | 4 +++- cuenca/resources/savings.py | 4 +++- cuenca/resources/sessions.py | 4 +++- cuenca/resources/user_logins.py | 4 +++- 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/cuenca/resources/endpoints.py b/cuenca/resources/endpoints.py index e1537951..fd1e30a3 100644 --- a/cuenca/resources/endpoints.py +++ b/cuenca/resources/endpoints.py @@ -77,7 +77,9 @@ def create( :return: New active endpoint """ req = EndpointRequest(url=url, events=events) - return cast('Endpoint', cls._create(session=session, **req.model_dump())) + return cast( + 'Endpoint', cls._create(session=session, **req.model_dump()) + ) @classmethod def update( diff --git a/cuenca/resources/kyc_verifications.py b/cuenca/resources/kyc_verifications.py index 88f8ef7a..acccd46f 100644 --- a/cuenca/resources/kyc_verifications.py +++ b/cuenca/resources/kyc_verifications.py @@ -49,4 +49,6 @@ def update( curp: CurpField, ) -> 'KYCVerification': req = KYCVerificationUpdateRequest(curp=curp) - return cast('KYCVerification', cls._update(id=kyc_id, **req.model_dump())) + return cast( + 'KYCVerification', cls._update(id=kyc_id, **req.model_dump()) + ) diff --git a/cuenca/resources/savings.py b/cuenca/resources/savings.py index 54e7754b..10320046 100644 --- a/cuenca/resources/savings.py +++ b/cuenca/resources/savings.py @@ -51,4 +51,6 @@ def update( goal_amount=goal_amount, goal_date=goal_date, ) - return cast('Saving', cls._update(id=saving_id, **request.model_dump())) + return cast( + 'Saving', cls._update(id=saving_id, **request.model_dump()) + ) diff --git a/cuenca/resources/sessions.py b/cuenca/resources/sessions.py index 99957531..b909b1b6 100644 --- a/cuenca/resources/sessions.py +++ b/cuenca/resources/sessions.py @@ -58,4 +58,6 @@ def create( success_url=success_url, failure_url=failure_url, ) - return cast('Session', cls._create(session=session, **req.model_dump())) + return cast( + 'Session', cls._create(session=session, **req.model_dump()) + ) diff --git a/cuenca/resources/user_logins.py b/cuenca/resources/user_logins.py index 3b63390b..1111ce95 100644 --- a/cuenca/resources/user_logins.py +++ b/cuenca/resources/user_logins.py @@ -32,7 +32,9 @@ def create( session: Session = global_session, ) -> 'UserLogin': req = UserLoginRequest(password=password, user_id=user_id) - login = cast('UserLogin', cls._create(session=session, **req.model_dump())) + login = cast( + 'UserLogin', cls._create(session=session, **req.model_dump()) + ) if login.success: session.headers['X-Cuenca-LoginId'] = login.id return login From e5070d415abbf805075abaee479d8dcf163ac034 Mon Sep 17 00:00:00 2001 From: gabino Date: Wed, 8 Jan 2025 16:55:02 -0600 Subject: [PATCH 19/29] refactor: change card number type to PaymentCardNumber for improved validation --- cuenca/resources/card_activations.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cuenca/resources/card_activations.py b/cuenca/resources/card_activations.py index 66890d64..853c9c1b 100644 --- a/cuenca/resources/card_activations.py +++ b/cuenca/resources/card_activations.py @@ -2,6 +2,7 @@ from typing import ClassVar, Optional, cast from cuenca_validations.types.requests import CardActivationRequest +from pydantic_extra_types.payment import PaymentCardNumber from ..http import Session, session as global_session from .base import Creatable @@ -21,7 +22,7 @@ class CardActivation(Creatable): @classmethod def create( cls, - number: str, + number: PaymentCardNumber, exp_month: int, exp_year: int, cvv2: str, From 7ea8e3d19d4a02a9cdd8f062d7f79ec9822d23b2 Mon Sep 17 00:00:00 2001 From: gabino Date: Wed, 8 Jan 2025 16:55:18 -0600 Subject: [PATCH 20/29] refactor: replace type checks with isinstance --- tests/resources/test_commissions.py | 4 ++-- tests/resources/test_otps.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/resources/test_commissions.py b/tests/resources/test_commissions.py index 3acd764a..0ee85938 100644 --- a/tests/resources/test_commissions.py +++ b/tests/resources/test_commissions.py @@ -17,7 +17,7 @@ def test_commission_retrieve_with_cash_deposit(): assert commission.id == id_commission related_transaction = commission.related_transaction assert related_transaction - assert type(related_transaction) == Deposit + assert isinstance(related_transaction, Deposit) assert related_transaction.network == 'cash' @@ -28,5 +28,5 @@ def test_commission_retrieve_with_cash_transfer(): assert commission.id == id_commission related_transaction = commission.related_transaction assert related_transaction - assert type(related_transaction) == Transfer + assert isinstance(related_transaction, Transfer) assert related_transaction.network == 'spei' diff --git a/tests/resources/test_otps.py b/tests/resources/test_otps.py index 74152ae0..102d294f 100644 --- a/tests/resources/test_otps.py +++ b/tests/resources/test_otps.py @@ -25,4 +25,4 @@ def test_otps(session): session.configure(login_token=login_token.id) otp = Otp.create() assert otp - assert type(otp.secret) == str + assert isinstance(otp.secret, str) From 304c4055d06f4e3f1536b2f2b2ba23d9a29ff574 Mon Sep 17 00:00:00 2001 From: gabino Date: Wed, 8 Jan 2025 17:51:00 -0600 Subject: [PATCH 21/29] chore: update GitHub Actions workflows to use latest versions of setup-python and codecov-action --- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f7111646..60a54393 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,7 +8,7 @@ jobs: steps: - uses: actions/checkout@v2.4.0 - name: Set up Python 3.13 - uses: actions/setup-python@v2.3.1 + uses: actions/setup-python@v5.1.0 with: python-version: 3.13 - name: Install dependencies diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dacda387..9b1fbd15 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,7 +45,7 @@ jobs: - name: Generate coverage report run: pytest --cov-report=xml - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4.1.1 + uses: codecov/codecov-action@v5 with: file: ./coverage.xml flags: unittests From 42d9677b90b1953cdb836443bd001315360d223d Mon Sep 17 00:00:00 2001 From: gabino Date: Wed, 8 Jan 2025 17:51:07 -0600 Subject: [PATCH 22/29] refactor: remove unnecessary constructor in CardTransaction class --- cuenca/resources/card_transactions.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/cuenca/resources/card_transactions.py b/cuenca/resources/card_transactions.py index 74adb2ff..6ef40a65 100644 --- a/cuenca/resources/card_transactions.py +++ b/cuenca/resources/card_transactions.py @@ -14,8 +14,6 @@ class CardTransaction(Transaction): - def __init__(self, *args, **kwargs): - super(CardTransaction, self).__init__(*args, **kwargs) _resource: ClassVar = 'card_transactions' _query_params: ClassVar = CardTransactionQuery From 5e2c585c9f5cfcc4aaa3700272aed08ad7ac2c09 Mon Sep 17 00:00:00 2001 From: gabino Date: Wed, 8 Jan 2025 18:22:26 -0600 Subject: [PATCH 23/29] refactor: update type hints to use built-in `list` and `dict` --- cuenca/resources/base.py | 4 ++-- cuenca/resources/card_transactions.py | 8 ++++---- cuenca/resources/file_batches.py | 10 +++++----- cuenca/resources/resources.py | 6 +++--- cuenca/resources/service_providers.py | 4 ++-- cuenca/resources/users.py | 6 +++--- cuenca/resources/webhooks.py | 4 ++-- tests/conftest.py | 7 +++---- tests/resources/test_sessions.py | 4 +--- 9 files changed, 25 insertions(+), 28 deletions(-) diff --git a/cuenca/resources/base.py b/cuenca/resources/base.py index 839e83f7..f4d87138 100644 --- a/cuenca/resources/base.py +++ b/cuenca/resources/base.py @@ -2,7 +2,7 @@ import datetime as dt import json from io import BytesIO -from typing import Any, ClassVar, Dict, Generator, Optional +from typing import Any, ClassVar, Generator, Optional from urllib.parse import urlencode from cuenca_validations.types import ( @@ -24,7 +24,7 @@ class Resource(BaseModel): id: str @classmethod - def _from_dict(cls, obj_dict: Dict[str, Any]) -> 'Resource': + def _from_dict(cls, obj_dict: dict[str, Any]) -> 'Resource': cls._filter_excess_fields(obj_dict) return cls(**obj_dict) diff --git a/cuenca/resources/card_transactions.py b/cuenca/resources/card_transactions.py index 6ef40a65..cfcbfe09 100644 --- a/cuenca/resources/card_transactions.py +++ b/cuenca/resources/card_transactions.py @@ -1,4 +1,4 @@ -from typing import ClassVar, List, Optional, cast +from typing import ClassVar, Optional, cast from cuenca_validations.types import ( CardErrorType, @@ -20,7 +20,7 @@ class CardTransaction(Transaction): type: CardTransactionType network: CardNetwork - related_card_transaction_uris: List[str] + related_card_transaction_uris: list[str] card_uri: str card_last4: str card_type: CardType @@ -28,11 +28,11 @@ class CardTransaction(Transaction): error_type: Optional[CardErrorType] = None @property # type: ignore - def related_card_transactions(self) -> Optional[List['CardTransaction']]: + def related_card_transactions(self) -> Optional[list['CardTransaction']]: if not self.related_card_transaction_uris: return [] return cast( - List['CardTransaction'], + list['CardTransaction'], retrieve_uris(self.related_card_transaction_uris), ) diff --git a/cuenca/resources/file_batches.py b/cuenca/resources/file_batches.py index e658e435..b846d308 100644 --- a/cuenca/resources/file_batches.py +++ b/cuenca/resources/file_batches.py @@ -1,4 +1,4 @@ -from typing import ClassVar, Dict, List, cast +from typing import ClassVar, cast from cuenca_validations.types import ( BatchFileMetadata, @@ -13,20 +13,20 @@ class FileBatch(Creatable, Queryable): _resource: ClassVar = 'file_batches' - received_files: List[BatchFileMetadata] - uploaded_files: List[BatchFileMetadata] + received_files: list[BatchFileMetadata] + uploaded_files: list[BatchFileMetadata] user_id: str @classmethod def create( cls, - files: List[Dict], + files: list[dict], user_id: str, *, session: Session = global_session, ) -> 'FileBatch': req = FileBatchUploadRequest( - files=cast(List[FileRequest], files), user_id=user_id + files=cast(list[FileRequest], files), user_id=user_id ) return cast( 'FileBatch', cls._create(session=session, **req.model_dump()) diff --git a/cuenca/resources/resources.py b/cuenca/resources/resources.py index 6003aa2a..82388a59 100644 --- a/cuenca/resources/resources.py +++ b/cuenca/resources/resources.py @@ -1,10 +1,10 @@ import re -from typing import Dict, List, cast +from typing import cast from .base import Retrievable ENDPOINT_RE = re.compile(r'.*/(?P[a-z_]+)/(?P.+)$') -RESOURCES: Dict[str, Retrievable] = {} # set in ./__init__.py after imports +RESOURCES: dict[str, Retrievable] = {} # set in ./__init__.py after imports def retrieve_uri(uri: str) -> Retrievable: @@ -15,7 +15,7 @@ def retrieve_uri(uri: str) -> Retrievable: return cast(Retrievable, RESOURCES[resource].retrieve(id_)) -def retrieve_uris(uris: List[str]) -> List[Retrievable]: +def retrieve_uris(uris: list[str]) -> list[Retrievable]: # Changed the implementation to use a simple for loop instead of # ThreadPoolExecutor. The list of URIs is small, so the performance # difference is negligible. Additionally, using ThreadPoolExecutor diff --git a/cuenca/resources/service_providers.py b/cuenca/resources/service_providers.py index 9935ed63..4401d6a0 100644 --- a/cuenca/resources/service_providers.py +++ b/cuenca/resources/service_providers.py @@ -1,4 +1,4 @@ -from typing import ClassVar, List +from typing import ClassVar from cuenca_validations.types import ServiceProviderCategory @@ -10,4 +10,4 @@ class ServiceProvider(Retrievable, Queryable): name: str provider_key: str - categories: List[ServiceProviderCategory] + categories: list[ServiceProviderCategory] diff --git a/cuenca/resources/users.py b/cuenca/resources/users.py index abdcef22..283b3485 100644 --- a/cuenca/resources/users.py +++ b/cuenca/resources/users.py @@ -1,5 +1,5 @@ import datetime as dt -from typing import ClassVar, List, Optional, cast +from typing import ClassVar, Optional, cast from clabe import Clabe from cuenca_validations.types import ( @@ -52,7 +52,7 @@ class User(Creatable, Retrievable, Updateable, Queryable): proof_of_life: Optional[KYCFile] = Field( None, description='Detail of selfie video validation' ) - beneficiaries: Optional[List[Beneficiary]] = Field( + beneficiaries: Optional[list[Beneficiary]] = Field( None, description='Beneficiaries of account in case of death' ) platform_id: Optional[str] = None @@ -140,7 +140,7 @@ def update( email_address: Optional[str] = None, profession: Optional[str] = None, address: Optional[Address] = None, - beneficiaries: Optional[List[Beneficiary]] = None, + beneficiaries: Optional[list[Beneficiary]] = None, govt_id: Optional[KYCFile] = None, proof_of_address: Optional[KYCFile] = None, proof_of_life: Optional[KYCFile] = None, diff --git a/cuenca/resources/webhooks.py b/cuenca/resources/webhooks.py index dd7a3cce..af168696 100644 --- a/cuenca/resources/webhooks.py +++ b/cuenca/resources/webhooks.py @@ -1,4 +1,4 @@ -from typing import Any, ClassVar, Dict +from typing import Any, ClassVar from cuenca_validations.types.enums import WebhookEvent from pydantic import Field @@ -9,7 +9,7 @@ class Webhook(Retrievable, Queryable): _resource: ClassVar = 'webhooks' - payload: Dict[str, Any] = Field( + payload: dict[str, Any] = Field( ..., description='object sent by the webhook' ) event: WebhookEvent = Field( diff --git a/tests/conftest.py b/tests/conftest.py index d61b02cc..c659e32e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,5 @@ import datetime as dt from io import BytesIO -from typing import Dict import pytest from cuenca_validations.types import Country, Gender, State @@ -31,7 +30,7 @@ def transfer(): @pytest.fixture -def curp_validation_request() -> Dict: +def curp_validation_request() -> dict: curp_validation = dict( names='José', first_surname='López', @@ -45,7 +44,7 @@ def curp_validation_request() -> Dict: @pytest.fixture -def user_request() -> Dict: +def user_request() -> dict: user_dict = dict( curp='LOHJ660606HDFPRS02', phone_number='+525511223344', @@ -64,7 +63,7 @@ def user_request() -> Dict: @pytest.fixture -def user_lists_request() -> Dict: +def user_lists_request() -> dict: user_dict = dict( curp='LOHJ660606HDFPRS02', names='Alejandro', diff --git a/tests/resources/test_sessions.py b/tests/resources/test_sessions.py index 30366772..d371a0a4 100644 --- a/tests/resources/test_sessions.py +++ b/tests/resources/test_sessions.py @@ -1,5 +1,3 @@ -from typing import Dict - import pytest from cuenca_validations.types import SessionType from pydantic import ValidationError @@ -9,7 +7,7 @@ @pytest.mark.vcr -def test_session_create(curp_validation_request: Dict, user_request: Dict): +def test_session_create(curp_validation_request: dict, user_request: dict): curp_valdation = CurpValidation.create(**curp_validation_request) user_request['curp'] = curp_valdation.validated_curp user = User.create(**user_request) From 2c6ce311da818d22617e9ad06d701eee727a3236 Mon Sep 17 00:00:00 2001 From: gabino Date: Wed, 8 Jan 2025 18:23:17 -0600 Subject: [PATCH 24/29] refactor: replace HttpUrl and AnyUrl with cuenca_validations.types --- cuenca/resources/endpoints.py | 13 +++---------- cuenca/resources/sessions.py | 14 +++----------- 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/cuenca/resources/endpoints.py b/cuenca/resources/endpoints.py index fd1e30a3..1f64fbfc 100644 --- a/cuenca/resources/endpoints.py +++ b/cuenca/resources/endpoints.py @@ -1,23 +1,16 @@ -from typing import Annotated, ClassVar, List, Optional, cast +from typing import ClassVar, List, Optional, cast +from cuenca_validations.types import HttpUrlString from cuenca_validations.types.enums import WebhookEvent from cuenca_validations.types.requests import ( EndpointRequest, EndpointUpdateRequest, ) -from pydantic import AfterValidator, ConfigDict, Field, HttpUrl +from pydantic import ConfigDict, Field, HttpUrl from ..http import Session, session as global_session from .base import Creatable, Deactivable, Queryable, Retrievable, Updateable -# In Pydantic v2, URL fields like `HttpUrl` are stored as internal objects -# instead of `str`, which can break compatibility with code expecting str. -# Using `HttpUrlString` ensures the field is validated as a URL but stored as -# a `str` for compatibility. -# https://github.com/pydantic/pydantic/discussions/6395 - -HttpUrlString = Annotated[HttpUrl, AfterValidator(str)] - class Endpoint(Creatable, Deactivable, Retrievable, Queryable, Updateable): _resource: ClassVar = 'endpoints' diff --git a/cuenca/resources/sessions.py b/cuenca/resources/sessions.py index b909b1b6..eb7f700d 100644 --- a/cuenca/resources/sessions.py +++ b/cuenca/resources/sessions.py @@ -1,20 +1,12 @@ import datetime as dt -from typing import Annotated, ClassVar, Optional, cast +from typing import ClassVar, Optional, cast -from cuenca_validations.types import SessionRequest, SessionType -from pydantic import AfterValidator, AnyUrl, ConfigDict +from cuenca_validations.types import AnyUrlString, SessionRequest, SessionType +from pydantic import AnyUrl, ConfigDict from .. import http from .base import Creatable, Queryable, Retrievable -# In Pydantic v2, URL fields like `AnyUrl` are stored as internal objects -# instead of `str`, which can break compatibility with code expecting str. -# Using `AnyUrlString` ensures the field is validated as a URL but stored as -# a `str` for compatibility. -# https://github.com/pydantic/pydantic/discussions/6395 - -AnyUrlString = Annotated[AnyUrl, AfterValidator(str)] - class Session(Creatable, Retrievable, Queryable): _resource: ClassVar = 'sessions' From 19081ee3c6b299f0a5b9148698346eb088e95ca3 Mon Sep 17 00:00:00 2001 From: gabino Date: Wed, 8 Jan 2025 18:48:01 -0600 Subject: [PATCH 25/29] chore: update types-requests dependency version --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 558100da..01d92536 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -8,5 +8,5 @@ pytest-cov==6.0.0 pytest-vcr==1.0.2 requests-mock==1.12.1 types-freezegun==1.1.10 -types-requests==2.32.* +types-requests==2.31.0.6 vcrpy==7.0.0 From 1712d333dbf7660f3c4c4f24ac31e99ed339fd15 Mon Sep 17 00:00:00 2001 From: gabino Date: Wed, 8 Jan 2025 18:59:28 -0600 Subject: [PATCH 26/29] refactor: update type hints to use built-in `list` --- cuenca/resources/endpoints.py | 8 ++++---- cuenca/resources/kyc_validations.py | 6 +++--- cuenca/resources/transfers.py | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/cuenca/resources/endpoints.py b/cuenca/resources/endpoints.py index 1f64fbfc..e83326a5 100644 --- a/cuenca/resources/endpoints.py +++ b/cuenca/resources/endpoints.py @@ -1,4 +1,4 @@ -from typing import ClassVar, List, Optional, cast +from typing import ClassVar, Optional, cast from cuenca_validations.types import HttpUrlString from cuenca_validations.types.enums import WebhookEvent @@ -26,7 +26,7 @@ class Endpoint(Creatable, Deactivable, Retrievable, Queryable, Updateable): description='Allows user to turn-off the endpoint without the ' 'need of deleting it', ) - events: List[WebhookEvent] = Field( + events: list[WebhookEvent] = Field( ..., description='list of enabled events. If None, all events will ' 'be enabled for this Endpoint', @@ -56,7 +56,7 @@ class Endpoint(Creatable, Deactivable, Retrievable, Queryable, Updateable): def create( cls, url: HttpUrl, - events: Optional[List[WebhookEvent]] = None, + events: Optional[list[WebhookEvent]] = None, *, session: Session = global_session, ) -> 'Endpoint': @@ -79,7 +79,7 @@ def update( cls, endpoint_id: str, url: Optional[HttpUrl] = None, - events: Optional[List[WebhookEvent]] = None, + events: Optional[list[WebhookEvent]] = None, is_enable: Optional[bool] = None, *, session: Session = global_session, diff --git a/cuenca/resources/kyc_validations.py b/cuenca/resources/kyc_validations.py index 7b04d38c..2f215a9b 100644 --- a/cuenca/resources/kyc_validations.py +++ b/cuenca/resources/kyc_validations.py @@ -1,4 +1,4 @@ -from typing import ClassVar, List, Optional, cast +from typing import ClassVar, Optional, cast from cuenca_validations.types import KYCFile, KYCValidationRequest from pydantic import ConfigDict @@ -12,7 +12,7 @@ class KYCValidation(Creatable, Retrievable, Queryable): platform_id: str attemps: Optional[int] = None verification_id: Optional[str] = None - files_uri: Optional[List[str]] = None + files_uri: Optional[list[str]] = None model_config = ConfigDict( json_schema_extra={ 'example': { @@ -31,7 +31,7 @@ def create( cls, user_id: str, force: bool = False, - documents: List[KYCFile] = [], + documents: list[KYCFile] = [], session: Session = global_session, ) -> 'KYCValidation': req = KYCValidationRequest( diff --git a/cuenca/resources/transfers.py b/cuenca/resources/transfers.py index 8843324f..780d3cfb 100644 --- a/cuenca/resources/transfers.py +++ b/cuenca/resources/transfers.py @@ -1,5 +1,5 @@ import datetime as dt -from typing import ClassVar, List, Optional, cast +from typing import ClassVar, Optional, cast from clabe import Clabe from cuenca_validations.types import ( @@ -73,7 +73,7 @@ def create( return cast('Transfer', cls._create(**req.model_dump())) @classmethod - def create_many(cls, requests: List[TransferRequest]) -> DictStrAny: + def create_many(cls, requests: list[TransferRequest]) -> DictStrAny: transfers: DictStrAny = dict(submitted=[], errors=[]) for req in requests: try: From f295c735627063c13f1614d3907d17b58fc6ebc6 Mon Sep 17 00:00:00 2001 From: gabino Date: Wed, 8 Jan 2025 19:06:15 -0600 Subject: [PATCH 27/29] refactor: simplify success_url and failure_url type hints in Session class --- cuenca/resources/sessions.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cuenca/resources/sessions.py b/cuenca/resources/sessions.py index eb7f700d..55f3004c 100644 --- a/cuenca/resources/sessions.py +++ b/cuenca/resources/sessions.py @@ -2,7 +2,7 @@ from typing import ClassVar, Optional, cast from cuenca_validations.types import AnyUrlString, SessionRequest, SessionType -from pydantic import AnyUrl, ConfigDict +from pydantic import ConfigDict from .. import http from .base import Creatable, Queryable, Retrievable @@ -39,16 +39,16 @@ def create( cls, user_id: str, type: SessionType, - success_url=cast(Optional[AnyUrl], success_url), - failure_url=cast(Optional[AnyUrl], failure_url), + success_url: Optional[str] = None, + failure_url: Optional[str] = None, *, session: http.Session = http.session, ) -> 'Session': req = SessionRequest( user_id=user_id, type=type, - success_url=success_url, - failure_url=failure_url, + success_url=success_url, # type: ignore + failure_url=failure_url, # type: ignore ) return cast( 'Session', cls._create(session=session, **req.model_dump()) From ebe078b058b0403fc9e995e67c00c1259800fef1 Mon Sep 17 00:00:00 2001 From: gabino Date: Thu, 9 Jan 2025 12:13:39 -0600 Subject: [PATCH 28/29] chore: update cuenca-validations dependency to version 2.0.0.dev12 --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 22e44ebc..5016027a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ requests==2.32.3 -cuenca-validations==2.0.0.dev10 +cuenca-validations==2.0.0.dev12 pydantic-extra-types==2.10.* \ No newline at end of file diff --git a/setup.py b/setup.py index ed3caadf..e4085a21 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ python_requires='>=3.9', install_requires=[ 'requests>=2.32.3', - 'cuenca-validations==2.0.0.dev9', + 'cuenca-validations==2.0.0.dev12', 'pydantic-extra-types>=2.10.1', ], classifiers=[ From f81c8bfde29f843cabd1dbf933401d861f74cd65 Mon Sep 17 00:00:00 2001 From: gabino Date: Thu, 9 Jan 2025 12:13:47 -0600 Subject: [PATCH 29/29] chore: bump version to 2.0.0.dev4 --- cuenca/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cuenca/version.py b/cuenca/version.py index 50d53cee..69c9c9b9 100644 --- a/cuenca/version.py +++ b/cuenca/version.py @@ -1,3 +1,3 @@ -__version__ = '2.0.0.dev3' +__version__ = '2.0.0.dev4' CLIENT_VERSION = __version__ API_VERSION = '2020-03-19'