diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8d232cd7..ae833749 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,18 +6,18 @@ jobs: publish-pypi: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2.4.0 - - name: Set up Python 3.8 - uses: actions/setup-python@v2.3.1 + - uses: actions/checkout@v4 + - name: Set up Python 3.13 + uses: actions/setup-python@v5 with: - python-version: 3.8 + python-version: 3.13 - name: Install dependencies run: pip install -qU setuptools wheel twine - name: Generating distribution archives run: python setup.py sdist bdist_wheel - name: Publish distribution 📦 to PyPI if: startsWith(github.event.ref, 'refs/tags') - uses: pypa/gh-action-pypi-publish@master + uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ password: ${{ secrets.pypi_password }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b4a75f5f..3d56589d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,10 +7,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5.1.0 + - name: Set up Python 13 + uses: actions/setup-python@v5 with: - python-version: 3.8 + python-version: 3.13 - name: Install dependencies run: make install-test - name: Lint @@ -20,11 +20,11 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.8', '3.9', '3.10'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5.1.0 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -36,16 +36,16 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Setup Python - uses: actions/setup-python@v5.1.0 + - name: Setup Python 13 + uses: actions/setup-python@v5 with: - python-version: 3.8 + python-version: 3.13 - name: Install dependencies run: make install-test - 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 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/resources/api_keys.py b/cuenca/resources/api_keys.py index c7db6c75..f211a5fc 100644 --- a/cuenca/resources/api_keys.py +++ b/cuenca/resources/api_keys.py @@ -2,6 +2,7 @@ from typing import ClassVar, Optional 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: @@ -74,4 +75,4 @@ def update( req = ApiKeyUpdateRequest( metadata=metadata, user_id=user_id, platform_id=platform_id ) - return cls._update(api_key_id, **req.dict(), session=session) + return cls._update(api_key_id, **req.model_dump(), session=session) diff --git a/cuenca/resources/arpc.py b/cuenca/resources/arpc.py index f045f69b..111a5924 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 cls._create(session=session, **req.dict()) + return 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 1d300fd2..cc9c95ea 100644 --- a/cuenca/resources/base.py +++ b/cuenca/resources/base.py @@ -12,7 +12,7 @@ TransactionQuery, TransactionStatus, ) -from pydantic import BaseModel, Extra +from pydantic import BaseModel, ConfigDict from ..exc import MultipleResultsFound, NoResultFound from ..http import Session, session as global_session @@ -25,11 +25,12 @@ class Resource(BaseModel): id: str - class Config: - extra = Extra.ignore + model_config = ConfigDict( + extra="ignore", + ) def to_dict(self): - return SantizedDict(self.dict()) + return SantizedDict(self.model_dump()) class Retrievable(Resource): @@ -78,7 +79,7 @@ def _update( class Deactivable(Resource): - deactivated_at: Optional[dt.datetime] + deactivated_at: Optional[dt.datetime] = None @classmethod def deactivate( @@ -157,7 +158,7 @@ def one( **query_params: Any, ) -> R_co: q = cast(Queryable, 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: @@ -174,7 +175,7 @@ def first( **query_params: Any, ) -> Optional[R_co]: q = cast(Queryable, 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: @@ -191,7 +192,7 @@ def count( **query_params: Any, ) -> int: q = cast(Queryable, 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 @@ -203,7 +204,7 @@ def all( ) -> Generator[R_co, None, None]: session = session or global_session q = cast(Queryable, 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(**item) for item in page['items']) diff --git a/cuenca/resources/card_activations.py b/cuenca/resources/card_activations.py index 7743c9b1..f7d72520 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 @@ -42,7 +42,7 @@ def create( exp_year=exp_year, cvv2=cvv2, ) - return cls._create(session=session, **req.dict()) + return cls._create(session=session, **req.model_dump()) @property def card(self) -> Optional[Card]: diff --git a/cuenca/resources/card_transactions.py b/cuenca/resources/card_transactions.py index dae0a674..9cab9ce9 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, @@ -19,19 +19,19 @@ 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 metadata: dict - error_type: Optional[CardErrorType] + 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/card_validations.py b/cuenca/resources/card_validations.py index 0bcd3770..8253ad25 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 @@ -51,7 +51,7 @@ def create( pin_block=pin_block, pin_attempts_exceeded=pin_attempts_exceeded, ) - return cls._create(session=session, **req.dict()) + return cls._create(session=session, **req.model_dump()) @property def card(self) -> Card: diff --git a/cuenca/resources/cards.py b/cuenca/resources/cards.py index ea9460f6..5cd24564 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 @@ -81,7 +81,7 @@ def create( card_holder_user_id=card_holder_user_id, is_dynamic_cvv=is_dynamic_cvv, ) - return cls._create(session=session, **req.dict()) + return 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 ) - return cls._update(card_id, session=session, **req.dict()) + return cls._update(card_id, session=session, **req.model_dump()) @classmethod def deactivate( diff --git a/cuenca/resources/curp_validations.py b/cuenca/resources/curp_validations.py index 144ea332..1e5b7974 100644 --- a/cuenca/resources/curp_validations.py +++ b/cuenca/resources/curp_validations.py @@ -7,7 +7,8 @@ Gender, State, ) -from cuenca_validations.types.identities import CurpField +from cuenca_validations.types.identities import Curp +from pydantic import ConfigDict, Field from ..http import Session, session as global_session from .base import Creatable, Retrievable @@ -17,44 +18,42 @@ 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[Curp] = Field( + None, description='curp provided in request' + ) + calculated_curp: Curp = Field( + description='Calculated CURP based on request data' + ) + validated_curp: Optional[Curp] = 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 +71,8 @@ class Config: 'renapo_curp_match': True, 'renapo_full_match': True, } - } + }, + ) @classmethod def create( @@ -84,7 +84,7 @@ def create( state_of_birth: Optional[State] = None, gender: Optional[Gender] = None, second_surname: Optional[str] = None, - manual_curp: Optional[CurpField] = None, + manual_curp: Optional[Curp] = None, *, session: Session = global_session, ) -> 'CurpValidation': @@ -98,4 +98,4 @@ def create( gender=gender, manual_curp=manual_curp, ) - return cls._create(session=session, **req.dict()) + return 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 79aa4750..7d6b1b69 100644 --- a/cuenca/resources/endpoints.py +++ b/cuenca/resources/endpoints.py @@ -1,11 +1,11 @@ -from typing import ClassVar, List, Optional +from typing import ClassVar, Optional from cuenca_validations.types.enums import WebhookEvent from cuenca_validations.types.requests import ( 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,21 @@ 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,13 +45,14 @@ class Config: ], 'is_enable': True, } - } + }, + ) @classmethod def create( cls, url: HttpUrl, - events: Optional[List[WebhookEvent]] = None, + events: Optional[list[WebhookEvent]] = None, *, session: Session = global_session, ) -> 'Endpoint': @@ -72,14 +66,14 @@ def create( :return: New active endpoint """ req = EndpointRequest(url=url, events=events) - return cls._create(session=session, **req.dict()) + return cls._create(session=session, **req.model_dump()) @classmethod 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, @@ -97,4 +91,4 @@ def update( req = EndpointUpdateRequest( url=url, is_enable=is_enable, events=events ) - return cls._update(endpoint_id, session=session, **req.dict()) + return cls._update(endpoint_id, session=session, **req.model_dump()) diff --git a/cuenca/resources/file_batches.py b/cuenca/resources/file_batches.py index 137ca6a2..185bf1cc 100644 --- a/cuenca/resources/file_batches.py +++ b/cuenca/resources/file_batches.py @@ -1,6 +1,10 @@ -from typing import ClassVar, Dict, List +from typing import ClassVar -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 @@ -9,17 +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=files, user_id=user_id) - return cls._create(session=session, **req.dict()) + req = FileBatchUploadRequest( + files=[FileRequest(**f) for f in files], + user_id=user_id, + ) + return cls._create(session=session, **req.model_dump()) diff --git a/cuenca/resources/files.py b/cuenca/resources/files.py index 30fcecf9..f97fbf93 100644 --- a/cuenca/resources/files.py +++ b/cuenca/resources/files.py @@ -44,7 +44,7 @@ def upload( is_back=is_back, user_id=user_id, ) - return cls._upload(session=session, **req.dict()) + return cls._upload(session=session, **req.model_dump()) @property def file(self) -> bytes: diff --git a/cuenca/resources/identities.py b/cuenca/resources/identities.py index d59f1f6d..fa9d1294 100644 --- a/cuenca/resources/identities.py +++ b/cuenca/resources/identities.py @@ -11,7 +11,7 @@ UserStatus, VerificationStatus, ) -from cuenca_validations.types.identities import CurpField +from cuenca_validations.types.identities import Curp from .base import Queryable, Retrievable @@ -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[Curp] = 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 bb5fa6f1..c9ad95ea 100644 --- a/cuenca/resources/kyc_validations.py +++ b/cuenca/resources/kyc_validations.py @@ -1,6 +1,7 @@ -from typing import ClassVar, List, Optional +from typing import ClassVar, Optional 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,12 @@ class KYCValidation(Creatable, Retrievable, Queryable): _resource: ClassVar = 'kyc_validations' platform_id: str - attemps: Optional[int] - verification_id: Optional[str] - files_uri: Optional[List[str]] + attemps: Optional[int] = None + verification_id: Optional[str] = None + files_uri: Optional[list[str]] = None - class Config: - schema_extra = { + model_config = ConfigDict( + json_schema_extra={ 'example': { 'id': 'KVNEUInh69SuKXXmK95sROwQ', 'platform_id': 'PT8UEv02zBTcymd4Kd3MO6pg', @@ -24,13 +25,14 @@ class Config: 'attemps': '1', } } + ) @classmethod def create( cls, user_id: str, force: bool = False, - documents: List[KYCFile] = [], + documents: list[KYCFile] = [], session: Session = global_session, ) -> 'KYCValidation': req = KYCValidationRequest( @@ -38,4 +40,4 @@ def create( force=force, documents=documents, ) - return cls._create(**req.dict(), session=session) + return cls._create(**req.model_dump(), session=session) diff --git a/cuenca/resources/kyc_verifications.py b/cuenca/resources/kyc_verifications.py index d95d21ad..13694135 100644 --- a/cuenca/resources/kyc_verifications.py +++ b/cuenca/resources/kyc_verifications.py @@ -3,10 +3,11 @@ from cuenca_validations.types import ( Address, - CurpField, + Curp, KYCVerificationUpdateRequest, Rfc, ) +from pydantic import ConfigDict from ..http import Session, session as global_session from .base import Creatable, Retrievable, Updateable @@ -17,14 +18,14 @@ class KYCVerification(Creatable, Retrievable, Updateable): platform_id: str created_at: dt.datetime - deactivated_at: Optional[dt.datetime] - verification_id: Optional[str] - curp: Optional[CurpField] = None + deactivated_at: Optional[dt.datetime] = None + verification_id: Optional[str] = None + curp: Optional[Curp] = 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 +37,7 @@ class Config: 'address': Address.schema().get('example'), } } + ) @classmethod def create(cls, session: Session = global_session) -> 'KYCVerification': @@ -45,7 +47,7 @@ def create(cls, session: Session = global_session) -> 'KYCVerification': def update( cls, kyc_id: str, - curp: Optional[CurpField] = None, + curp: Optional[Curp] = None, ) -> 'KYCVerification': req = KYCVerificationUpdateRequest(curp=curp) - return cls._update(id=kyc_id, **req.dict()) + return cls._update(id=kyc_id, **req.model_dump()) diff --git a/cuenca/resources/limited_wallets.py b/cuenca/resources/limited_wallets.py index e9b91255..0407a98a 100644 --- a/cuenca/resources/limited_wallets.py +++ b/cuenca/resources/limited_wallets.py @@ -3,7 +3,7 @@ from clabe import Clabe from cuenca_validations.types import ( AccountQuery, - CurpField, + Curp, LimitedWalletRequest, Rfc, ) @@ -15,13 +15,13 @@ class LimitedWallet(Wallet): _resource: ClassVar = 'limited_wallets' _query_params: ClassVar = AccountQuery account_number: Clabe - allowed_rfc: Optional[Rfc] - allowed_curp: CurpField + allowed_rfc: Optional[Rfc] = None + allowed_curp: Curp @classmethod def create( cls, - allowed_curp: Optional[CurpField] = None, + allowed_curp: Optional[Curp] = None, allowed_rfc: Optional[Rfc] = None, ) -> 'LimitedWallet': """ @@ -37,4 +37,4 @@ def create( allowed_curp=allowed_curp, allowed_rfc=allowed_rfc, ) - return cls._create(**request.dict()) + return cls._create(**request.model_dump()) diff --git a/cuenca/resources/login_tokens.py b/cuenca/resources/login_tokens.py index d5f36c4b..2be84eac 100644 --- a/cuenca/resources/login_tokens.py +++ b/cuenca/resources/login_tokens.py @@ -1,5 +1,7 @@ from typing import ClassVar +from pydantic import ConfigDict + from ..http import Session, session as global_session from .base import Creatable @@ -7,8 +9,9 @@ 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 b3534131..f6e2ba3c 100644 --- a/cuenca/resources/otps.py +++ b/cuenca/resources/otps.py @@ -1,5 +1,7 @@ from typing import ClassVar +from pydantic import ConfigDict + from ..http import Session, session as global_session from .base import Creatable @@ -8,13 +10,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 ad9320f9..d5290b63 100644 --- a/cuenca/resources/platforms.py +++ b/cuenca/resources/platforms.py @@ -2,6 +2,7 @@ from typing import ClassVar, Optional 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', @@ -51,6 +47,7 @@ class Config: 'email_address': 'art@eria.com', } } + ) @classmethod def create( @@ -76,4 +73,4 @@ def create( phone_number=phone_number, email_address=email_address, ) - return cls._create(session=session, **req.dict()) + return cls._create(session=session, **req.model_dump()) diff --git a/cuenca/resources/questionnaires.py b/cuenca/resources/questionnaires.py index 0d23beac..5fa3e303 100644 --- a/cuenca/resources/questionnaires.py +++ b/cuenca/resources/questionnaires.py @@ -2,6 +2,7 @@ from typing import ClassVar from cuenca_validations.types import QuestionnairesRequest +from pydantic import ConfigDict from ..http import Session, session as global_session from .base import Creatable, Retrievable @@ -15,14 +16,15 @@ class Questionnaires(Creatable, Retrievable): 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( @@ -38,4 +40,4 @@ def create( token=token, form_id=form_id, ) - return cls._create(session=session, **req.dict()) + return cls._create(session=session, **req.model_dump()) diff --git a/cuenca/resources/resources.py b/cuenca/resources/resources.py index 2633e04a..352a92cf 100644 --- a/cuenca/resources/resources.py +++ b/cuenca/resources/resources.py @@ -1,11 +1,9 @@ import re -from concurrent.futures import ThreadPoolExecutor -from typing import Dict, List 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: @@ -16,6 +14,12 @@ def retrieve_uri(uri: str) -> Retrievable: return RESOURCES[resource].retrieve(id_) -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)] +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 + # 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] diff --git a/cuenca/resources/savings.py b/cuenca/resources/savings.py index d5722ea9..984f0c10 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( @@ -34,7 +34,7 @@ def create( goal_amount=goal_amount, goal_date=goal_date, ) - return cls._create(**request.dict()) + return cls._create(**request.model_dump()) @classmethod def update( @@ -51,4 +51,4 @@ def update( goal_amount=goal_amount, goal_date=goal_date, ) - return cls._update(id=saving_id, **request.dict()) + return cls._update(id=saving_id, **request.model_dump()) 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/sessions.py b/cuenca/resources/sessions.py index 58adaff6..4c8eff0d 100644 --- a/cuenca/resources/sessions.py +++ b/cuenca/resources/sessions.py @@ -2,7 +2,7 @@ from typing import ClassVar, Optional 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,12 @@ 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] + success_url: Optional[AnyUrl] = None + failure_url: Optional[AnyUrl] = None + type: Optional[SessionType] = None - class Config: - schema_extra = { + model_config = ConfigDict( + json_schema_extra={ 'example': { 'id': 'SENEUInh69SuKXXmK95sROwQ', 'created_at': '2022-08-24T14:15:22Z', @@ -33,6 +33,7 @@ class Config: 'type': 'session.registration', } } + ) @classmethod def create( @@ -50,4 +51,4 @@ def create( success_url=success_url, failure_url=failure_url, ) - return cls._create(session=session, **req.dict()) + return cls._create(session=session, **req.model_dump()) diff --git a/cuenca/resources/transfers.py b/cuenca/resources/transfers.py index 6cdf5fde..bde76ac3 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 cuenca_validations.types import ( TransferNetwork, @@ -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: @@ -69,14 +69,14 @@ def create( idempotency_key=idempotency_key, user_id=user_id, ) - return cls._create(**req.dict()) + return 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: - 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 b5a05aec..cde0bd39 100644 --- a/cuenca/resources/user_credentials.py +++ b/cuenca/resources/user_credentials.py @@ -25,7 +25,7 @@ def create( session: Session = global_session, ) -> 'UserCredential': req = UserCredentialRequest(password=password, user_id=user_id) - return cls._create(**req.dict(), session=session) + return cls._create(**req.model_dump(), session=session) @classmethod def update( @@ -40,4 +40,4 @@ def update( is_active=is_active, password=password, ) - return cls._update(id=user_id, **req.dict(), session=session) + return cls._update(id=user_id, **req.model_dump(), session=session) diff --git a/cuenca/resources/user_events.py b/cuenca/resources/user_events.py index 8a6de5fa..279f13ab 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 @@ -10,8 +12,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 +24,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 c248588f..e69266c9 100644 --- a/cuenca/resources/user_lists_validation.py +++ b/cuenca/resources/user_lists_validation.py @@ -2,7 +2,7 @@ from typing import ClassVar, Optional from cuenca_validations.types import UserListsRequest, VerificationStatus -from cuenca_validations.types.identities import CurpField +from cuenca_validations.types.identities import Curp from ..http import Session, session as global_session from .base import Creatable, Retrievable @@ -14,7 +14,7 @@ class UserListsValidation(Creatable, Retrievable): names: Optional[str] = None first_surname: Optional[str] = None second_surname: Optional[str] = None - curp: Optional[CurpField] = None + curp: Optional[Curp] = None account_number: Optional[str] = None status: Optional[VerificationStatus] = None @@ -24,7 +24,7 @@ def create( names: Optional[str] = None, first_surname: Optional[str] = None, second_surname: Optional[str] = None, - curp: Optional[CurpField] = None, + curp: Optional[Curp] = None, account_number: Optional[str] = None, *, session: Session = global_session, @@ -36,4 +36,4 @@ def create( curp=curp, account_number=account_number, ) - return cls._create(session=session, **req.dict()) + return cls._create(session=session, **req.model_dump()) diff --git a/cuenca/resources/user_logins.py b/cuenca/resources/user_logins.py index 33b0f2eb..bfe22745 100644 --- a/cuenca/resources/user_logins.py +++ b/cuenca/resources/user_logins.py @@ -2,6 +2,7 @@ from typing import ClassVar, Optional 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,18 @@ 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( @@ -31,7 +33,7 @@ def create( session: Session = global_session, ) -> 'UserLogin': req = UserLoginRequest(password=password, user_id=user_id) - login = cls._create(session=session, **req.dict()) + login = cls._create(session=session, **req.model_dump()) if login.success: session.headers['X-Cuenca-LoginId'] = login.id return login diff --git a/cuenca/resources/users.py b/cuenca/resources/users.py index ffed5c3a..19798589 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 ( @@ -15,8 +15,8 @@ UserUpdateRequest, ) from cuenca_validations.types.enums import Country, Gender, State -from cuenca_validations.types.identities import CurpField -from pydantic import EmailStr, HttpUrl +from cuenca_validations.types.identities import Curp +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 = 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', @@ -106,11 +98,12 @@ class Config: 'platform_id': 'PT8UEv02zBTcymd4Kd3MO6pg', } } + ) @classmethod def create( cls, - curp: CurpField, + curp: Curp, phone_number: Optional[PhoneNumber] = None, email_address: Optional[EmailStr] = None, profession: Optional[str] = None, @@ -135,7 +128,7 @@ def create( status=status, terms_of_service=terms_of_service, ) - return cls._create(session=session, **req.dict()) + return cls._create(session=session, **req.model_dump()) @classmethod def update( @@ -145,7 +138,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, @@ -174,7 +167,7 @@ def update( curp_document=curp_document, status=status, ) - return cls._update(id=user_id, **request.dict(), session=session) + return cls._update(id=user_id, **request.model_dump(), session=session) @property def identity(self) -> Identity: diff --git a/cuenca/resources/verifications.py b/cuenca/resources/verifications.py index a70fc487..ca34707d 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', @@ -32,6 +32,7 @@ class Config: 'deactivated_at': None, } } + ) @classmethod def create( @@ -44,7 +45,7 @@ def create( req = VerificationRequest( recipient=recipient, type=type, platform_id=platform_id ) - return cls._create(**req.dict(), session=session) + return cls._create(**req.model_dump(), session=session) @classmethod def verify( @@ -54,4 +55,4 @@ def verify( session: Session = global_session, ) -> 'Verification': req = VerificationAttemptRequest(code=code) - return cls._update(id=id, **req.dict(), session=session) + return cls._update(id=id, **req.model_dump(), session=session) diff --git a/cuenca/resources/wallet_transactions.py b/cuenca/resources/wallet_transactions.py index 5204383d..c1dc65a5 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 cls._create(**request.dict()) + return cls._create(**request.model_dump()) diff --git a/cuenca/resources/webhooks.py b/cuenca/resources/webhooks.py index 257e8b68..580ebc29 100644 --- a/cuenca/resources/webhooks.py +++ b/cuenca/resources/webhooks.py @@ -1,6 +1,7 @@ -from typing import Any, ClassVar, Dict +from typing import Any, ClassVar from cuenca_validations.types.enums import WebhookEvent +from pydantic import Field from .base import Queryable, Retrievable @@ -8,11 +9,5 @@ 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/cuenca/version.py b/cuenca/version.py index e2e3271b..89da15cb 100644 --- a/cuenca/version.py +++ b/cuenca/version.py @@ -1,3 +1,3 @@ -__version__ = '1.0.3' +__version__ = '2.0.0.dev7' CLIENT_VERSION = __version__ API_VERSION = '2020-03-19' diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..895701cc --- /dev/null +++ b/mypy.ini @@ -0,0 +1,2 @@ +[mypy] +plugins = pydantic.mypy \ No newline at end of file diff --git a/requirements-test.txt b/requirements-test.txt index 22487320..01d92536 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==0.931 -pytest==6.2.* -pytest-cov==3.0.* -pytest-vcr==1.0.* -requests-mock==1.9.* -types-freezegun -types-requests -vcrpy==4.3.1 +black==24.10.0 +flake8==7.1.1 +freezegun==1.5.1 +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.31.0.6 +vcrpy==7.0.0 diff --git a/requirements.txt b/requirements.txt index 7cd483b3..372eb38d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -requests==2.31.0 -cuenca-validations==0.11.30 -dataclasses>=0.7;python_version<"3.7" +requests==2.32.3 +cuenca-validations==2.0.0 +pydantic-extra-types==2.10.2 diff --git a/setup.py b/setup.py index 36c728e9..38e57211 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.0', + 'cuenca-validations>=2.0.0', + 'pydantic-extra-types>=2.10.0', ], 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', ], 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/cassettes/test_card_activation.yaml b/tests/resources/cassettes/test_card_activation.yaml index 268c2a3b..6cf3c45e 100644 --- a/tests/resources/cassettes/test_card_activation.yaml +++ b/tests/resources/cassettes/test_card_activation.yaml @@ -1,112 +1,91 @@ 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 -version: 1 +version: 1 \ No newline at end of file diff --git a/tests/resources/cassettes/test_user_beneficiaries_update.yaml b/tests/resources/cassettes/test_user_beneficiaries_update.yaml index 3e4cb2e0..8ecaf994 100644 --- a/tests/resources/cassettes/test_user_beneficiaries_update.yaml +++ b/tests/resources/cassettes/test_user_beneficiaries_update.yaml @@ -29,7 +29,7 @@ interactions: uri: https://sandbox.cuenca.com/users/USw182B9fVTxK3J1A2ElKV7g response: body: - string: "{\"id\":\"USw182B9fVTxK3J1A2ElKV7g\",\"identity_uri\":\"/identities/IDYsENkazRRuuZ46KnlN0x1Q\",\"created_at\":\"2022-07-04T20:36:35.007000\",\"updated_at\":\"2022-07-05T18:32:38.470895\",\"platform_id\":\"PTk5UC6RWyQjmDR74oiHlFng\",\"level\":0,\"required_level\":4,\"phone_number\":\"+5299887766\",\"email_address\":\"danisan@mail.com\",\"profession\":null,\"clabe\":null,\"status\":\"active\",\"terms_of_service\":null,\"blacklist_validation_status\":\"not_verified\",\"address\":null,\"govt_id\":null,\"proof_of_address\":null,\"proof_of_life\":null,\"beneficiaries\":[{\"name\":\"Pedro + string: "{\"id\":\"USw182B9fVTxK3J1A2ElKV7g\",\"identity_uri\":\"/identities/IDYsENkazRRuuZ46KnlN0x1Q\",\"created_at\":\"2022-07-04T20:36:35.007000\",\"updated_at\":\"2022-07-05T18:32:38.470895\",\"platform_id\":\"PTk5UC6RWyQjmDR74oiHlFng\",\"level\":0,\"required_level\":4,\"phone_number\":\"+529988776666\",\"email_address\":\"danisan@mail.com\",\"profession\":null,\"clabe\":null,\"status\":\"active\",\"terms_of_service\":null,\"blacklist_validation_status\":\"not_verified\",\"address\":null,\"govt_id\":null,\"proof_of_address\":null,\"proof_of_life\":null,\"beneficiaries\":[{\"name\":\"Pedro P\xE9rez\",\"birth_date\":\"2020-01-01\",\"phone_number\":\"+525555555555\",\"user_relationship\":\"brother\",\"percentage\":50,\"created_at\":\"2022-07-05T18:32:38.470632\"},{\"name\":\"Jos\xE9 P\xE9rez\",\"birth_date\":\"2020-01-02\",\"phone_number\":\"+525544444444\",\"user_relationship\":\"brother\",\"percentage\":50,\"created_at\":\"2022-07-05T18:32:38.470703\"}],\"names\":\"Daniel\",\"first_surname\":\"Sanchez\",\"second_surname\":\"Chavez\",\"curp\":\"LOHJ660606HDFPRS02\",\"rfc\":\"LOHJ660606HDF\"}" headers: 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 diff --git a/tests/resources/test_cards.py b/tests/resources/test_cards.py index ada1c5e9..8d51eede 100644 --- a/tests/resources/test_cards.py +++ b/tests/resources/test_cards.py @@ -79,14 +79,6 @@ def test_card_not_found(): assert exc.value.json['Code'] == 'NotFoundError' -@pytest.mark.vcr -def test_card_one(): - card = Card.one( - number='5448750078699849', exp_month=2, exp_year=2026, cvv2='353' - ) - assert card.id - - @pytest.mark.vcr def test_card_one_errors(): with pytest.raises(NoResultFound): 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_endpoints.py b/tests/resources/test_endpoints.py index 4317b9ff..5509c56b 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 endpoint.url.unicode_string() == 'https://url.io/' assert not endpoint.is_enable assert endpoint.is_active 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) diff --git a/tests/resources/test_sessions.py b/tests/resources/test_sessions.py index 30366772..9ef92644 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) @@ -34,8 +32,6 @@ 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 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)