Skip to content

Commit

Permalink
Update/pydantic v2 (#411)
Browse files Browse the repository at this point in the history
* Update Python version and dependencies in Makefile and requirements files

* Update Python version requirement and dependencies in setup.py

* Update GitHub Actions workflows to use Python 3.13

* update syntax for Pydantic v2 compatibility

* re-record card activation with valid card number

* Remove unused test for valid card creation in test_cards.py

* Refactor type checks in tests to use isinstance

* Fix endpoint URL assertion and clean up session test assertions

* Update error message in test_invalid_params to reflect new validation response

* Update test_user_beneficiaries_update.yaml to correct phone number format in response body

* Resolve linting errors

* Replace built-in type hints (Dict, List) with dict and list

* Update pydantic-extra-types version to 2.10.2 in requirements.txt

* Update version to 2.0.0 in version.py

* Add mypy configuration for Pydantic plugin

* Refactor Card model to use string for card number instead of PaymentCardNumber type

* Update cuenca-validations version to 2.0.0 in requirements.txt

* Refactor CURP handling across multiple resources to use the updated Curp type instead of CurpField

* Refactor optional parameters in resource classes

* Update version to 2.0.0.dev7 in version.py for development release

---------

Co-authored-by: gabino <gabino@cuenca.com>
gmorales96 and gabino authored Jan 18, 2025
1 parent b308816 commit a42283c
Showing 53 changed files with 393 additions and 412 deletions.
10 changes: 5 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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 }}
18 changes: 9 additions & 9 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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
13 changes: 7 additions & 6 deletions cuenca/resources/api_keys.py
Original file line number Diff line number Diff line change
@@ -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)
6 changes: 3 additions & 3 deletions cuenca/resources/arpc.py
Original file line number Diff line number Diff line change
@@ -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())
6 changes: 2 additions & 4 deletions cuenca/resources/balance_entries.py
Original file line number Diff line number Diff line change
@@ -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):
19 changes: 10 additions & 9 deletions cuenca/resources/base.py
Original file line number Diff line number Diff line change
@@ -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'])
4 changes: 2 additions & 2 deletions cuenca/resources/card_activations.py
Original file line number Diff line number Diff line change
@@ -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]:
10 changes: 5 additions & 5 deletions cuenca/resources/card_transactions.py
Original file line number Diff line number Diff line change
@@ -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),
)

12 changes: 6 additions & 6 deletions cuenca/resources/card_validations.py
Original file line number Diff line number Diff line change
@@ -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:
8 changes: 4 additions & 4 deletions cuenca/resources/cards.py
Original file line number Diff line number Diff line change
@@ -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(
84 changes: 42 additions & 42 deletions cuenca/resources/curp_validations.py
Original file line number Diff line number Diff line change
@@ -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())
2 changes: 1 addition & 1 deletion cuenca/resources/deposits.py
Original file line number Diff line number Diff line change
@@ -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:
52 changes: 23 additions & 29 deletions cuenca/resources/endpoints.py
Original file line number Diff line number Diff line change
@@ -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())
21 changes: 14 additions & 7 deletions cuenca/resources/file_batches.py
Original file line number Diff line number Diff line change
@@ -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())
2 changes: 1 addition & 1 deletion cuenca/resources/files.py
Original file line number Diff line number Diff line change
@@ -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:
28 changes: 14 additions & 14 deletions cuenca/resources/identities.py
Original file line number Diff line number Diff line change
@@ -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
18 changes: 10 additions & 8 deletions cuenca/resources/kyc_validations.py
Original file line number Diff line number Diff line change
@@ -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,18 +25,19 @@ 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(
user_id=user_id,
force=force,
documents=documents,
)
return cls._create(**req.dict(), session=session)
return cls._create(**req.model_dump(), session=session)
18 changes: 10 additions & 8 deletions cuenca/resources/kyc_verifications.py
Original file line number Diff line number Diff line change
@@ -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())
10 changes: 5 additions & 5 deletions cuenca/resources/limited_wallets.py
Original file line number Diff line number Diff line change
@@ -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())
7 changes: 5 additions & 2 deletions cuenca/resources/login_tokens.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
from typing import ClassVar

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':
7 changes: 5 additions & 2 deletions cuenca/resources/otps.py
Original file line number Diff line number Diff line change
@@ -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':
53 changes: 25 additions & 28 deletions cuenca/resources/platforms.py
Original file line number Diff line number Diff line change
@@ -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())
8 changes: 5 additions & 3 deletions cuenca/resources/questionnaires.py
Original file line number Diff line number Diff line change
@@ -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())
16 changes: 10 additions & 6 deletions cuenca/resources/resources.py
Original file line number Diff line number Diff line change
@@ -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<resource>[a-z_]+)/(?P<id>.+)$')
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]
8 changes: 4 additions & 4 deletions cuenca/resources/savings.py
Original file line number Diff line number Diff line change
@@ -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())
4 changes: 2 additions & 2 deletions cuenca/resources/service_providers.py
Original file line number Diff line number Diff line change
@@ -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]
15 changes: 8 additions & 7 deletions cuenca/resources/sessions.py
Original file line number Diff line number Diff line change
@@ -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())
10 changes: 5 additions & 5 deletions cuenca/resources/transfers.py
Original file line number Diff line number Diff line change
@@ -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:
4 changes: 2 additions & 2 deletions cuenca/resources/user_credentials.py
Original file line number Diff line number Diff line change
@@ -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)
7 changes: 5 additions & 2 deletions cuenca/resources/user_events.py
Original file line number Diff line number Diff line change
@@ -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'),
}
}
)
8 changes: 4 additions & 4 deletions cuenca/resources/user_lists_validation.py
Original file line number Diff line number Diff line change
@@ -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())
10 changes: 6 additions & 4 deletions cuenca/resources/user_logins.py
Original file line number Diff line number Diff line change
@@ -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
95 changes: 44 additions & 51 deletions cuenca/resources/users.py
Original file line number Diff line number Diff line change
@@ -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:
19 changes: 10 additions & 9 deletions cuenca/resources/verifications.py
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 1 addition & 1 deletion cuenca/resources/wallet_transactions.py
Original file line number Diff line number Diff line change
@@ -35,4 +35,4 @@ def create(
transaction_type=transaction_type,
amount=amount,
)
return cls._create(**request.dict())
return cls._create(**request.model_dump())
13 changes: 4 additions & 9 deletions cuenca/resources/webhooks.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
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


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')
8 changes: 4 additions & 4 deletions cuenca/resources/whatsapp_transfers.py
Original file line number Diff line number Diff line change
@@ -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]:
2 changes: 1 addition & 1 deletion cuenca/version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
__version__ = '1.0.3'
__version__ = '2.0.0.dev7'
CLIENT_VERSION = __version__
API_VERSION = '2020-03-19'
2 changes: 2 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[mypy]
plugins = pydantic.mypy
24 changes: 12 additions & 12 deletions requirements-test.txt
Original file line number Diff line number Diff line change
@@ -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
6 changes: 3 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -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
14 changes: 9 additions & 5 deletions setup.py
Original file line number Diff line number Diff line change
@@ -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',
],
7 changes: 3 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -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',
77 changes: 28 additions & 49 deletions tests/resources/cassettes/test_card_activation.yaml
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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:
8 changes: 4 additions & 4 deletions tests/resources/test_card_activations.py
Original file line number Diff line number Diff line change
@@ -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


8 changes: 0 additions & 8 deletions tests/resources/test_cards.py
Original file line number Diff line number Diff line change
@@ -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):
4 changes: 2 additions & 2 deletions tests/resources/test_commissions.py
Original file line number Diff line number Diff line change
@@ -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'
2 changes: 1 addition & 1 deletion tests/resources/test_endpoints.py
Original file line number Diff line number Diff line change
@@ -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

2 changes: 1 addition & 1 deletion tests/resources/test_otps.py
Original file line number Diff line number Diff line change
@@ -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)
6 changes: 1 addition & 5 deletions tests/resources/test_sessions.py
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 1 addition & 1 deletion tests/resources/test_transfers.py
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit a42283c

Please sign in to comment.