cuenca-validations 2.0.0.dev9__tar.gz → 2.0.0.dev10__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. {cuenca_validations-2.0.0.dev9 → cuenca_validations-2.0.0.dev10}/PKG-INFO +2 -2
  2. {cuenca_validations-2.0.0.dev9 → cuenca_validations-2.0.0.dev10}/cuenca_validations/types/__init__.py +6 -3
  3. cuenca_validations-2.0.0.dev10/cuenca_validations/types/card.py +38 -0
  4. {cuenca_validations-2.0.0.dev9 → cuenca_validations-2.0.0.dev10}/cuenca_validations/types/files.py +3 -2
  5. {cuenca_validations-2.0.0.dev9 → cuenca_validations-2.0.0.dev10}/cuenca_validations/types/general.py +21 -8
  6. {cuenca_validations-2.0.0.dev9 → cuenca_validations-2.0.0.dev10}/cuenca_validations/types/identities.py +19 -36
  7. {cuenca_validations-2.0.0.dev9 → cuenca_validations-2.0.0.dev10}/cuenca_validations/types/morals.py +3 -3
  8. {cuenca_validations-2.0.0.dev9 → cuenca_validations-2.0.0.dev10}/cuenca_validations/types/queries.py +3 -4
  9. {cuenca_validations-2.0.0.dev9 → cuenca_validations-2.0.0.dev10}/cuenca_validations/types/requests.py +37 -52
  10. cuenca_validations-2.0.0.dev10/cuenca_validations/typing.py +5 -0
  11. {cuenca_validations-2.0.0.dev9 → cuenca_validations-2.0.0.dev10}/cuenca_validations/validators.py +2 -7
  12. cuenca_validations-2.0.0.dev10/cuenca_validations/version.py +1 -0
  13. {cuenca_validations-2.0.0.dev9 → cuenca_validations-2.0.0.dev10}/cuenca_validations.egg-info/PKG-INFO +2 -2
  14. {cuenca_validations-2.0.0.dev9 → cuenca_validations-2.0.0.dev10}/cuenca_validations.egg-info/requires.txt +1 -1
  15. {cuenca_validations-2.0.0.dev9 → cuenca_validations-2.0.0.dev10}/setup.py +1 -1
  16. {cuenca_validations-2.0.0.dev9 → cuenca_validations-2.0.0.dev10}/tests/test_card.py +12 -8
  17. {cuenca_validations-2.0.0.dev9 → cuenca_validations-2.0.0.dev10}/tests/test_errors.py +10 -7
  18. {cuenca_validations-2.0.0.dev9 → cuenca_validations-2.0.0.dev10}/tests/test_types.py +53 -61
  19. cuenca_validations-2.0.0.dev9/cuenca_validations/types/card.py +0 -42
  20. cuenca_validations-2.0.0.dev9/cuenca_validations/typing.py +0 -5
  21. cuenca_validations-2.0.0.dev9/cuenca_validations/version.py +0 -1
  22. {cuenca_validations-2.0.0.dev9 → cuenca_validations-2.0.0.dev10}/LICENSE +0 -0
  23. {cuenca_validations-2.0.0.dev9 → cuenca_validations-2.0.0.dev10}/README.md +0 -0
  24. {cuenca_validations-2.0.0.dev9 → cuenca_validations-2.0.0.dev10}/cuenca_validations/__init__.py +0 -0
  25. {cuenca_validations-2.0.0.dev9 → cuenca_validations-2.0.0.dev10}/cuenca_validations/card_bins.py +0 -0
  26. {cuenca_validations-2.0.0.dev9 → cuenca_validations-2.0.0.dev10}/cuenca_validations/errors.py +0 -0
  27. {cuenca_validations-2.0.0.dev9 → cuenca_validations-2.0.0.dev10}/cuenca_validations/py.typed +0 -0
  28. {cuenca_validations-2.0.0.dev9 → cuenca_validations-2.0.0.dev10}/cuenca_validations/types/enums.py +0 -0
  29. {cuenca_validations-2.0.0.dev9 → cuenca_validations-2.0.0.dev10}/cuenca_validations.egg-info/SOURCES.txt +0 -0
  30. {cuenca_validations-2.0.0.dev9 → cuenca_validations-2.0.0.dev10}/cuenca_validations.egg-info/dependency_links.txt +0 -0
  31. {cuenca_validations-2.0.0.dev9 → cuenca_validations-2.0.0.dev10}/cuenca_validations.egg-info/top_level.txt +0 -0
  32. {cuenca_validations-2.0.0.dev9 → cuenca_validations-2.0.0.dev10}/setup.cfg +0 -0
  33. {cuenca_validations-2.0.0.dev9 → cuenca_validations-2.0.0.dev10}/tests/__init__.py +0 -0
  34. {cuenca_validations-2.0.0.dev9 → cuenca_validations-2.0.0.dev10}/tests/test_statement.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cuenca_validations
3
- Version: 2.0.0.dev9
3
+ Version: 2.0.0.dev10
4
4
  Summary: Cuenca common validations
5
5
  Home-page: https://github.com/cuenca-mx/cuenca-validations
6
6
  Author: Cuenca
@@ -19,7 +19,7 @@ License-File: LICENSE
19
19
  Requires-Dist: clabe>=2.0.0
20
20
  Requires-Dist: pydantic[email]>=2.10.0
21
21
  Requires-Dist: pydantic-extra-types>=2.10.0
22
- Requires-Dist: python-dateutil>=2.8.0
22
+ Requires-Dist: python-dateutil>=2.9.0
23
23
 
24
24
  # Cuenca - validations
25
25
 
@@ -49,7 +49,6 @@ __all__ = [
49
49
  'KYCVerificationUpdateRequest',
50
50
  'Language',
51
51
  'LimitedWalletRequest',
52
- 'PageSize',
53
52
  'PartnerRequest',
54
53
  'PartnerUpdateRequest',
55
54
  'PhoneNumber',
@@ -103,8 +102,10 @@ __all__ = [
103
102
  'WalletQuery',
104
103
  'WalletTransactionQuery',
105
104
  'WebhookEvent',
106
- 'digits',
105
+ 'Digits',
107
106
  'get_state_name',
107
+ 'HttpUrlString',
108
+ 'AnyUrlString',
108
109
  ]
109
110
 
110
111
  from .card import StrictPaymentCardNumber
@@ -152,11 +153,13 @@ from .enums import (
152
153
  )
153
154
  from .files import BatchFileMetadata
154
155
  from .general import (
156
+ AnyUrlString,
157
+ Digits,
158
+ HttpUrlString,
155
159
  JSONEncoder,
156
160
  SantizedDict,
157
161
  StrictPositiveFloat,
158
162
  StrictPositiveInt,
159
- digits,
160
163
  get_state_name,
161
164
  )
162
165
  from .identities import (
@@ -0,0 +1,38 @@
1
+ from pydantic_core import PydanticCustomError, core_schema
2
+ from pydantic_extra_types.payment import PaymentCardNumber
3
+
4
+ from ..card_bins import CARD_BINS
5
+
6
+
7
+ class StrictPaymentCardNumber(PaymentCardNumber):
8
+ """
9
+ Refactored `StrictPaymentCardNumber` to leverage Pydantic v2's
10
+ `PaymentCardNumber`, which now natively includes attributes such as
11
+ brand, bin, last4, and masked.
12
+
13
+ Previously, these attributes were manually computed in a custom
14
+ `PaymentCardNumber` class.
15
+
16
+ The `StrictPaymentCardNumber` class now wraps `PaymentCardNumber`
17
+ as a field, with additional validation to ensure the BIN is associated
18
+ with a known Mexican bank.
19
+ """
20
+
21
+ @classmethod
22
+ def validate(
23
+ cls, __input_value: str, _: core_schema.ValidationInfo
24
+ ) -> 'StrictPaymentCardNumber':
25
+ card = super().validate(__input_value, _)
26
+ if card.bin not in CARD_BINS:
27
+ raise PydanticCustomError(
28
+ 'payment_card_number.bin',
29
+ 'The card number contains a BIN (first six digits) '
30
+ 'that does not have a known association with a Mexican bank.'
31
+ 'To add the association, please file an issue:'
32
+ 'https://github.com/cuenca-mx/cuenca-validations/issues',
33
+ )
34
+ return cls(card)
35
+
36
+ @property
37
+ def bank_code(self) -> str:
38
+ return CARD_BINS[self.bin]
@@ -1,12 +1,13 @@
1
1
  from typing import Optional
2
2
 
3
- from pydantic import BaseModel, HttpUrl
3
+ from pydantic import BaseModel
4
4
 
5
5
  from .enums import KYCFileType
6
+ from .general import HttpUrlString
6
7
 
7
8
 
8
9
  class BatchFileMetadata(BaseModel):
9
10
  id: Optional[str] = None
10
11
  is_back: bool
11
12
  type: KYCFileType
12
- url: HttpUrl
13
+ url: HttpUrlString
@@ -1,12 +1,20 @@
1
1
  import json
2
- from typing import Any, Optional
2
+ from typing import Annotated, Any, Optional
3
3
 
4
- from pydantic import BeforeValidator, Field
5
- from typing_extensions import Annotated
4
+ from pydantic import AfterValidator, AnyUrl, BeforeValidator, Field, HttpUrl
6
5
 
7
6
  from ..validators import sanitize_dict, sanitize_item
8
7
  from .enums import State
9
8
 
9
+ # In Pydantic v2, URL fields like `HttpUrl` are stored as internal objects
10
+ # instead of `str`, which can break compatibility with code expecting str.
11
+ # Using `HttpUrlString` ensures the field is validated as a URL but stored as
12
+ # a `str` for compatibility.
13
+ # https://github.com/pydantic/pydantic/discussions/6395
14
+
15
+ HttpUrlString = Annotated[HttpUrl, AfterValidator(str)]
16
+ AnyUrlString = Annotated[AnyUrl, AfterValidator(str)]
17
+
10
18
 
11
19
  class SantizedDict(dict):
12
20
  def __init__(self, *args, **kwargs):
@@ -19,19 +27,24 @@ class JSONEncoder(json.JSONEncoder):
19
27
  return sanitize_item(o, default=super().default)
20
28
 
21
29
 
22
- StrictPositiveInt = Annotated[int, Field(strict=True, gt=0, le=21_474_836_47)]
30
+ MAX_VALUE_IN_DB = 21_474_836_47
31
+
32
+ StrictPositiveInt = Annotated[
33
+ int, Field(strict=True, gt=0, le=MAX_VALUE_IN_DB)
34
+ ]
23
35
 
24
36
 
25
37
  StrictPositiveFloat = Annotated[float, Field(strict=True, gt=0)]
26
38
 
27
39
 
28
- def validate_only_digits(value: str) -> str:
29
- if not value.isdigit():
40
+ def validate_only_digits(value: Any) -> str:
41
+ v_str = str(value)
42
+ if not v_str.isdigit():
30
43
  raise ValueError("Value must contain only digits")
31
- return value
44
+ return v_str
32
45
 
33
46
 
34
- def digits(
47
+ def Digits(
35
48
  min_length: Optional[int] = None, max_length: Optional[int] = None
36
49
  ) -> Any:
37
50
  return Annotated[
@@ -1,51 +1,34 @@
1
1
  import datetime as dt
2
- import re
3
- from typing import Any, Dict, List, Optional
2
+ from typing import Annotated, Any, Optional
4
3
 
5
4
  from pydantic import (
6
5
  BaseModel,
7
6
  ConfigDict,
8
7
  Field,
9
8
  IPvAnyAddress,
9
+ StringConstraints,
10
10
  model_validator,
11
11
  )
12
12
  from pydantic.types import StrictStr
13
13
 
14
14
  from .enums import Country, KYCFileType, State, VerificationStatus
15
15
 
16
+ PhoneNumber = Annotated[
17
+ str,
18
+ StringConstraints(
19
+ min_length=10, max_length=15, pattern=r'^\+?[0-9]{10,14}$'
20
+ ),
21
+ ]
16
22
 
17
- class PhoneNumber(StrictStr):
18
- min_length = 10
19
- max_length = 15
20
- regex = re.compile(r'^\+?[0-9]{10,14}$')
21
23
 
22
- @classmethod
23
- def __get_pydantic_core_schema__(
24
- cls, source_type: Any, handler: Any
25
- ) -> Dict[str, Any]:
26
- return {
27
- 'type': 'str',
28
- 'pattern': cls.regex.pattern,
29
- 'min_length': cls.min_length,
30
- 'max_length': cls.max_length,
31
- }
32
-
33
-
34
- class CurpField(StrictStr):
35
- min_length = 18
36
- max_length = 18
37
- regex = re.compile(r'^[A-Z]{4}[0-9]{6}[A-Z]{6}[A-Z|0-9][0-9]$')
38
-
39
- @classmethod
40
- def __get_pydantic_core_schema__(
41
- cls, source_type: Any, handler: Any
42
- ) -> Dict[str, Any]:
43
- return {
44
- 'type': 'str',
45
- 'pattern': cls.regex.pattern,
46
- 'min_length': cls.min_length,
47
- 'max_length': cls.max_length,
48
- }
24
+ CurpField = Annotated[
25
+ str,
26
+ StringConstraints(
27
+ min_length=18,
28
+ max_length=18,
29
+ pattern=r'^[A-Z]{4}[0-9]{6}[A-Z]{6}[A-Z|0-9][0-9]$',
30
+ ),
31
+ ]
49
32
 
50
33
 
51
34
  class Rfc(StrictStr):
@@ -61,7 +44,7 @@ class Rfc(StrictStr):
61
44
  @classmethod
62
45
  def __get_pydantic_core_schema__(
63
46
  cls, source_type: Any, handler: Any
64
- ) -> Dict[str, Any]:
47
+ ) -> dict[str, Any]:
65
48
  return {
66
49
  'type': 'str',
67
50
  'min_length': cls.min_length,
@@ -96,7 +79,7 @@ class Address(BaseModel):
96
79
 
97
80
  @model_validator(mode='before')
98
81
  @classmethod
99
- def full_name_complete(cls, values: Dict[str, Any]) -> Dict[str, Any]:
82
+ def full_name_complete(cls, values: dict[str, Any]) -> dict[str, Any]:
100
83
  if values.get('full_name'):
101
84
  return values
102
85
  if not values.get('street'):
@@ -162,7 +145,7 @@ class KYCFile(BaseModel):
162
145
  status: Optional[VerificationStatus] = Field(
163
146
  None, description='The status of the file depends on KYCValidation'
164
147
  )
165
- errors: Optional[List[VerificationErrors]] = Field(
148
+ errors: Optional[list[VerificationErrors]] = Field(
166
149
  None, description='List of document errors found during kyc validation'
167
150
  )
168
151
  verification_id: Optional[str] = Field(
@@ -1,5 +1,5 @@
1
1
  import datetime as dt
2
- from typing import List, Optional
2
+ from typing import Optional
3
3
 
4
4
  from pydantic import BaseModel, EmailStr
5
5
 
@@ -70,5 +70,5 @@ class ShareholderPhysical(PhysicalPerson):
70
70
  class Shareholder(BaseModel):
71
71
  name: str
72
72
  percentage: int
73
- shareholders: List[ShareholderPhysical]
74
- legal_representatives: List[LegalRepresentative]
73
+ shareholders: list[ShareholderPhysical]
74
+ legal_representatives: list[LegalRepresentative]
@@ -1,5 +1,5 @@
1
1
  import datetime as dt
2
- from typing import Optional
2
+ from typing import Annotated, Optional
3
3
 
4
4
  from pydantic import (
5
5
  BaseModel,
@@ -9,7 +9,6 @@ from pydantic import (
9
9
  PositiveInt,
10
10
  field_validator,
11
11
  )
12
- from typing_extensions import Annotated
13
12
 
14
13
  from ..typing import DictStrAny
15
14
  from ..validators import sanitize_dict
@@ -61,10 +60,10 @@ class QueryParams(BaseModel):
61
60
  },
62
61
  )
63
62
 
64
- def dict(self, *args, **kwargs) -> DictStrAny:
63
+ def model_dump(self, *args, **kwargs) -> DictStrAny:
65
64
  kwargs.setdefault('exclude_none', True)
66
65
  kwargs.setdefault('exclude_unset', True)
67
- d = super().dict(*args, **kwargs)
66
+ d = super().model_dump(*args, **kwargs)
68
67
  if self.count:
69
68
  d['count'] = 1
70
69
  sanitize_dict(d)
@@ -1,21 +1,18 @@
1
1
  import datetime as dt
2
- from typing import List, Optional, Union
2
+ from typing import Annotated, Optional, Union
3
3
 
4
4
  from clabe import Clabe
5
5
  from pydantic import (
6
- AnyUrl,
7
6
  BaseModel,
8
7
  ConfigDict,
9
8
  EmailStr,
10
9
  Field,
11
- HttpUrl,
12
10
  StrictStr,
13
11
  StringConstraints,
14
12
  field_validator,
15
13
  model_validator,
16
14
  )
17
15
  from pydantic.networks import IPvAnyAddress
18
- from typing_extensions import Annotated
19
16
 
20
17
  from ..types.enums import (
21
18
  AuthorizerTransaction,
@@ -51,7 +48,7 @@ from ..types.enums import (
51
48
  from ..typing import DictStrAny
52
49
  from ..validators import validate_age_requirement
53
50
  from .card import PaymentCardNumber, StrictPaymentCardNumber
54
- from .general import StrictPositiveInt
51
+ from .general import AnyUrlString, HttpUrlString, StrictPositiveInt
55
52
  from .identities import (
56
53
  Address,
57
54
  Beneficiary,
@@ -75,17 +72,14 @@ from .morals import (
75
72
  class BaseRequest(BaseModel):
76
73
  model_config = ConfigDict(extra="forbid")
77
74
 
78
- def dict(self, *args, **kwargs) -> DictStrAny:
75
+ def model_dump(self, *args, **kwargs) -> DictStrAny:
79
76
  kwargs.setdefault('exclude_none', True)
80
77
  kwargs.setdefault('exclude_unset', True)
81
- return super().dict(*args, **kwargs)
78
+ return super().model_dump(*args, **kwargs)
82
79
 
83
80
 
84
- class TransferRequest(BaseRequest):
81
+ class BaseTransferRequest(BaseRequest):
85
82
  recipient_name: StrictStr
86
- account_number: Union[Clabe, PaymentCardNumber] = Field(
87
- ..., description='Destination Clabe or Card number'
88
- )
89
83
  amount: StrictPositiveInt = Field(
90
84
  ..., description='Always in cents, not in MXN pesos'
91
85
  )
@@ -112,8 +106,16 @@ class TransferRequest(BaseRequest):
112
106
  )
113
107
 
114
108
 
115
- class StrictTransferRequest(TransferRequest):
116
- account_number: Union[Clabe, StrictPaymentCardNumber] # type: ignore
109
+ class TransferRequest(BaseTransferRequest):
110
+ account_number: Union[Clabe, PaymentCardNumber] = Field(
111
+ ..., description='Destination Clabe or Card number'
112
+ )
113
+
114
+
115
+ class StrictTransferRequest(BaseTransferRequest):
116
+ account_number: Union[Clabe, StrictPaymentCardNumber] = Field(
117
+ ..., description='Destination Clabe or Card number'
118
+ )
117
119
 
118
120
 
119
121
  class CardUpdateRequest(BaseRequest):
@@ -131,15 +133,7 @@ class CardRequest(BaseRequest):
131
133
 
132
134
 
133
135
  class CardActivationRequest(BaseModel):
134
- number: Annotated[
135
- str,
136
- StringConstraints(
137
- strip_whitespace=True,
138
- min_length=16,
139
- max_length=16,
140
- pattern=r'\d{16}',
141
- ),
142
- ]
136
+ number: PaymentCardNumber
143
137
  exp_month: Annotated[int, Field(strict=True, ge=1, le=12)]
144
138
  exp_year: Annotated[int, Field(strict=True, ge=18, le=99)]
145
139
  cvv2: Annotated[
@@ -170,9 +164,9 @@ class UserCredentialUpdateRequest(BaseRequest):
170
164
  ),
171
165
  )
172
166
 
173
- def dict(self, *args, **kwargs) -> DictStrAny:
167
+ def model_dump(self, *args, **kwargs) -> DictStrAny:
174
168
  # Password can be None but BaseRequest excludes None
175
- return BaseModel.dict(self, *args, **kwargs)
169
+ return BaseModel.model_dump(self, *args, **kwargs)
176
170
 
177
171
  @model_validator(mode="before")
178
172
  @classmethod
@@ -250,9 +244,7 @@ class ARPCRequest(BaseModel):
250
244
  arqc: StrictStr
251
245
  arpc_method: Annotated[
252
246
  str,
253
- StringConstraints( # type: ignore
254
- strict=True, min_length=1, max_length=1
255
- ),
247
+ StringConstraints(strict=True, min_length=1, max_length=1),
256
248
  ]
257
249
  transaction_data: StrictStr
258
250
  response_code: StrictStr
@@ -356,10 +348,10 @@ class FraudValidationRequest(BaseModel):
356
348
  None
357
349
  )
358
350
  ecommerce_indicator: Optional[EcommerceIndicator] = None
359
- card_id: Optional[str] = None # type: ignore
360
- user_id: Optional[str] = None # type: ignore
361
- card_type: Optional[CardType] = None # type: ignore
362
- card_status: Optional[CardStatus] = None # type: ignore
351
+ card_id: Optional[str] = None
352
+ user_id: Optional[str] = None
353
+ card_type: Optional[CardType] = None
354
+ card_status: Optional[CardStatus] = None
363
355
 
364
356
 
365
357
  class TransactionTokenValidationUpdateRequest(BaseRequest):
@@ -532,19 +524,19 @@ class UserUpdateRequest(BaseModel):
532
524
  email_verification_id: Optional[str] = None
533
525
  phone_verification_id: Optional[str] = None
534
526
  address: Optional[Address] = None
535
- beneficiaries: Optional[List[Beneficiary]] = None
527
+ beneficiaries: Optional[list[Beneficiary]] = None
536
528
  govt_id: Optional[KYCFile] = None
537
529
  proof_of_address: Optional[KYCFile] = None
538
530
  proof_of_life: Optional[KYCFile] = None
539
531
  status: Optional[UserStatus] = None
540
532
  terms_of_service: Optional[TOSRequest] = None
541
533
  platform_terms_of_service: Optional[TOSAgreement] = None
542
- curp_document_uri: Optional[HttpUrl] = None
534
+ curp_document_uri: Optional[HttpUrlString] = None
543
535
 
544
536
  @field_validator('beneficiaries')
545
537
  @classmethod
546
538
  def beneficiary_percentage(
547
- cls, beneficiaries: Optional[List[Beneficiary]] = None
539
+ cls, beneficiaries: Optional[list[Beneficiary]] = None
548
540
  ):
549
541
  if beneficiaries and sum(b.percentage for b in beneficiaries) > 100:
550
542
  raise ValueError('The total percentage is more than 100.')
@@ -569,8 +561,8 @@ class UserLoginRequest(BaseRequest):
569
561
  class SessionRequest(BaseRequest):
570
562
  user_id: str
571
563
  type: SessionType
572
- success_url: Optional[AnyUrl] = None
573
- failure_url: Optional[AnyUrl] = None
564
+ success_url: Optional[AnyUrlString] = None
565
+ failure_url: Optional[AnyUrlString] = None
574
566
  model_config = ConfigDict(
575
567
  json_schema_extra={
576
568
  'example': {
@@ -584,15 +576,15 @@ class SessionRequest(BaseRequest):
584
576
 
585
577
 
586
578
  class EndpointRequest(BaseRequest):
587
- url: HttpUrl
588
- events: Optional[List[WebhookEvent]] = None
579
+ url: HttpUrlString
580
+ events: Optional[list[WebhookEvent]] = None
589
581
  user_id: Optional[str] = None
590
582
 
591
583
 
592
584
  class EndpointUpdateRequest(BaseRequest):
593
- url: Optional[HttpUrl] = None
585
+ url: Optional[HttpUrlString] = None
594
586
  is_enable: Optional[bool] = None
595
- events: Optional[List[WebhookEvent]] = None
587
+ events: Optional[list[WebhookEvent]] = None
596
588
 
597
589
 
598
590
  class FileUploadRequest(BaseRequest):
@@ -605,12 +597,12 @@ class FileUploadRequest(BaseRequest):
605
597
 
606
598
  class FileRequest(BaseModel):
607
599
  is_back: Optional[bool] = False
608
- url: HttpUrl
600
+ url: HttpUrlString
609
601
  type: KYCFileType
610
602
 
611
603
 
612
604
  class FileBatchUploadRequest(BaseModel):
613
- files: List[FileRequest]
605
+ files: list[FileRequest]
614
606
  user_id: str
615
607
 
616
608
 
@@ -652,8 +644,6 @@ class VerificationAttemptRequest(BaseModel):
652
644
 
653
645
 
654
646
  class LimitedWalletRequest(BaseRequest):
655
- model_config = dict(arbitrary_types_allowed=True)
656
-
657
647
  allowed_curp: CurpField
658
648
  allowed_rfc: Optional[Rfc] = None
659
649
 
@@ -663,7 +653,6 @@ class KYCVerificationUpdateRequest(BaseRequest):
663
653
 
664
654
 
665
655
  class PlatformRequest(BaseModel):
666
- model_config = dict(arbitrary_types_allowed=True)
667
656
  name: str
668
657
  rfc: Optional[str] = None
669
658
  establishment_date: Optional[dt.date] = None
@@ -685,7 +674,7 @@ class WebhookRequest(BaseModel):
685
674
  class KYCValidationRequest(BaseRequest):
686
675
  user_id: str
687
676
  force: bool = False
688
- documents: List[KYCFile] = []
677
+ documents: list[KYCFile] = []
689
678
 
690
679
 
691
680
  class BankAccountValidationRequest(BaseModel):
@@ -739,8 +728,6 @@ class QuestionnairesRequest(BaseModel):
739
728
 
740
729
 
741
730
  class PartnerRequest(BaseRequest):
742
- model_config = dict(arbitrary_types_allowed=True)
743
-
744
731
  legal_name: str
745
732
  business_name: str
746
733
  nationality: Country
@@ -755,8 +742,6 @@ class PartnerRequest(BaseRequest):
755
742
 
756
743
 
757
744
  class PartnerUpdateRequest(BaseRequest):
758
- model_config = dict(arbitrary_types_allowed=True)
759
-
760
745
  legal_name: Optional[str] = None
761
746
  business_name: Optional[str] = None
762
747
  nationality: Optional[Country] = None
@@ -774,5 +759,5 @@ class PartnerUpdateRequest(BaseRequest):
774
759
  license: Optional[LicenseDetails] = None
775
760
  audit: Optional[AuditDetails] = None
776
761
  vulnerable_activity: Optional[VulnerableActivityDetails] = None
777
- legal_representatives: Optional[List[LegalRepresentative]] = None
778
- shareholders: Optional[List[Shareholder]] = None
762
+ legal_representatives: Optional[list[LegalRepresentative]] = None
763
+ shareholders: Optional[list[Shareholder]] = None
@@ -0,0 +1,5 @@
1
+ from typing import Any, MutableMapping, Optional, Union
2
+
3
+ ClientRequestParams = Union[None, bytes, MutableMapping[str, str]]
4
+ DictStrAny = dict[str, Any]
5
+ OptionalDict = Optional[dict[str, Union[int, str]]]
@@ -1,10 +1,9 @@
1
1
  import base64
2
2
  import datetime as dt
3
3
  from enum import Enum
4
- from typing import Any, Callable, List, Optional, Union
4
+ from typing import Any, Callable, Optional, Union
5
5
 
6
6
  from dateutil.relativedelta import relativedelta
7
- from pydantic import AnyUrl, HttpUrl
8
7
 
9
8
 
10
9
  def sanitize_dict(d: dict) -> dict:
@@ -21,16 +20,12 @@ def sanitize_item(
21
20
  :param default: Optional function to be used when there is no case
22
21
  for this type of item, default `None` it returns the item as is.
23
22
  """
24
- rv: Union[str, List[Any]]
23
+ rv: Union[str, list[Any]]
25
24
  if isinstance(item, dt.date):
26
25
  if isinstance(item, dt.datetime) and not item.tzinfo:
27
26
  rv = item.astimezone(dt.timezone.utc).isoformat()
28
27
  else:
29
28
  rv = item.isoformat()
30
- elif isinstance(item, HttpUrl):
31
- rv = str(item)
32
- elif isinstance(item, AnyUrl):
33
- rv = str(item)
34
29
  elif isinstance(item, list):
35
30
  rv = [
36
31
  sanitize_dict(e) if isinstance(e, dict) else sanitize_item(e)
@@ -0,0 +1 @@
1
+ __version__ = '2.0.0.dev10'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cuenca_validations
3
- Version: 2.0.0.dev9
3
+ Version: 2.0.0.dev10
4
4
  Summary: Cuenca common validations
5
5
  Home-page: https://github.com/cuenca-mx/cuenca-validations
6
6
  Author: Cuenca
@@ -19,7 +19,7 @@ License-File: LICENSE
19
19
  Requires-Dist: clabe>=2.0.0
20
20
  Requires-Dist: pydantic[email]>=2.10.0
21
21
  Requires-Dist: pydantic-extra-types>=2.10.0
22
- Requires-Dist: python-dateutil>=2.8.0
22
+ Requires-Dist: python-dateutil>=2.9.0
23
23
 
24
24
  # Cuenca - validations
25
25
 
@@ -1,4 +1,4 @@
1
1
  clabe>=2.0.0
2
2
  pydantic[email]>=2.10.0
3
3
  pydantic-extra-types>=2.10.0
4
- python-dateutil>=2.8.0
4
+ python-dateutil>=2.9.0
@@ -28,7 +28,7 @@ setup(
28
28
  'clabe>=2.0.0',
29
29
  'pydantic[email]>=2.10.0',
30
30
  'pydantic-extra-types>=2.10.0',
31
- 'python-dateutil>=2.8.0',
31
+ 'python-dateutil>=2.9.0',
32
32
  ],
33
33
  classifiers=[
34
34
  'Programming Language :: Python :: 3',
@@ -1,5 +1,5 @@
1
1
  import pytest
2
- from pydantic import ValidationError
2
+ from pydantic import BaseModel, ValidationError
3
3
  from pydantic_extra_types.payment import PaymentCardBrand
4
4
 
5
5
  from cuenca_validations.types import StrictPaymentCardNumber
@@ -8,9 +8,13 @@ VALID_BBVA = '4772130000000003'
8
8
  INVALID_BIN = '4050000000000001'
9
9
 
10
10
 
11
+ class CardModel(BaseModel):
12
+ card_number: StrictPaymentCardNumber
13
+
14
+
11
15
  def test_invalid_bin_strict_payment():
12
16
  with pytest.raises(ValidationError) as exc_info:
13
- StrictPaymentCardNumber(card_number=INVALID_BIN)
17
+ CardModel(card_number=INVALID_BIN)
14
18
  print(exc_info.value)
15
19
  assert 'payment_card_number.bin' in str(exc_info.value)
16
20
  assert 'The card number contains a BIN (first six digits) ' in str(
@@ -19,9 +23,9 @@ def test_invalid_bin_strict_payment():
19
23
 
20
24
 
21
25
  def test_valid_bin_strict_payment():
22
- card = StrictPaymentCardNumber(card_number=VALID_BBVA)
23
- assert card.brand == PaymentCardBrand.visa
24
- assert card.bin == '477213'
25
- assert card.last4 == '0003'
26
- assert card.masked == '477213******0003'
27
- assert card.bank_code == '40012'
26
+ card = CardModel(card_number=VALID_BBVA)
27
+ assert card.card_number.brand == PaymentCardBrand.visa
28
+ assert card.card_number.bin == '477213'
29
+ assert card.card_number.last4 == '0003'
30
+ assert card.card_number.masked == '477213******0003'
31
+ assert card.card_number.bank_code == '40012'
@@ -1,3 +1,5 @@
1
+ import pytest
2
+
1
3
  from cuenca_validations.errors import (
2
4
  ApiError,
3
5
  AuthMethodNotAllowedError,
@@ -16,8 +18,9 @@ def test_cuenca_error_base():
16
18
  assert issubclass(CuencaError, Exception)
17
19
 
18
20
 
19
- def test_error_codes_and_status():
20
- test_cases = [
21
+ @pytest.mark.parametrize(
22
+ "error_class, expected_code, expected_status",
23
+ [
21
24
  (WrongCredsError, 101, 401),
22
25
  (MissingAuthorizationHeaderError, 102, 401),
23
26
  (UserNotLoggedInError, 103, 401),
@@ -27,8 +30,8 @@ def test_error_codes_and_status():
27
30
  (UserLocationError, 108, 401),
28
31
  (InvalidOTPCodeError, 109, 401),
29
32
  (ApiError, 500, 500),
30
- ]
31
-
32
- for error_class, expected_code, expected_status in test_cases:
33
- assert error_class.code == expected_code
34
- assert error_class.status_code == expected_status
33
+ ],
34
+ )
35
+ def test_error_codes_and_status(error_class, expected_code, expected_status):
36
+ assert error_class.code == expected_code
37
+ assert error_class.status_code == expected_status
@@ -5,18 +5,18 @@ from enum import Enum
5
5
 
6
6
  import pytest
7
7
  from freezegun import freeze_time
8
- from pydantic import AnyUrl, BaseModel, HttpUrl, ValidationError
8
+ from pydantic import BaseModel, ValidationError
9
9
 
10
10
  from cuenca_validations.types import (
11
11
  Address,
12
12
  CardQuery,
13
+ Digits,
13
14
  JSONEncoder,
14
15
  QueryParams,
15
16
  Rfc,
16
17
  SantizedDict,
17
18
  SessionRequest,
18
19
  TransactionStatus,
19
- digits,
20
20
  get_state_name,
21
21
  )
22
22
  from cuenca_validations.types.enums import (
@@ -67,19 +67,21 @@ class TestClass:
67
67
 
68
68
  def test_dict():
69
69
  model = QueryParams(count=1, created_before=now)
70
- assert model.dict() == dict(count=1, created_before=utcnow.isoformat())
70
+ assert model.model_dump() == dict(
71
+ count=1, created_before=utcnow.isoformat()
72
+ )
71
73
 
72
74
 
73
75
  def test_dict_with_exclude():
74
76
  model = QueryParams(count=1, created_before=now, user_id='USXXXX')
75
- assert model.dict(exclude={'user_id'}) == dict(
77
+ assert model.model_dump(exclude={'user_id'}) == dict(
76
78
  count=1, created_before=utcnow.isoformat()
77
79
  )
78
80
 
79
81
 
80
82
  def test_dict_with_exclude_unset():
81
83
  model = QueryParams(count=1, created_before=now)
82
- assert model.dict(exclude_unset=False) == dict(
84
+ assert model.model_dump(exclude_unset=False) == dict(
83
85
  count=1, created_before=utcnow.isoformat(), page_size=100
84
86
  )
85
87
 
@@ -132,17 +134,6 @@ def test_json_encoder(value, result):
132
134
  assert decoded['value'] == result
133
135
 
134
136
 
135
- def test_sanitized_dict_with_urls():
136
- data = SantizedDict(
137
- api_url=HttpUrl('https://api.cuenca.com/v1'),
138
- ftp_url=AnyUrl('ftp://files.example.com/'),
139
- )
140
- assert data == {
141
- 'api_url': 'https://api.cuenca.com/v1',
142
- 'ftp_url': 'ftp://files.example.com/',
143
- }
144
-
145
-
146
137
  def test_invalid_class():
147
138
  """
148
139
  For a class that doesn't have a `to_dict` method and it is not a type of
@@ -158,12 +149,20 @@ def test_invalid_class():
158
149
 
159
150
 
160
151
  class Accounts(BaseModel):
161
- number: digits(5, 8) # type: ignore
152
+ number: Digits(5, 8) # type: ignore
162
153
 
163
154
 
164
- def test_only_digits():
165
- acc = Accounts(number='123456')
166
- assert acc.number == '123456'
155
+ @pytest.mark.parametrize(
156
+ "input_number, expected",
157
+ [
158
+ ('123456', '123456'),
159
+ (123456, '123456'),
160
+ ('0012312', '0012312'),
161
+ ],
162
+ )
163
+ def test_only_digits(input_number, expected):
164
+ acc = Accounts(number=input_number)
165
+ assert acc.number == expected
167
166
 
168
167
 
169
168
  @pytest.mark.parametrize(
@@ -204,7 +203,7 @@ def test_card_query_exp_cvv_if_number_not_set(input_value):
204
203
 
205
204
  def test_exclude_none_in_dict():
206
205
  request = ApiKeyUpdateRequest(user_id='US123')
207
- assert request.dict() == dict(user_id='US123')
206
+ assert request.model_dump() == dict(user_id='US123')
208
207
 
209
208
 
210
209
  def test_update_one_property_at_a_time_request():
@@ -228,7 +227,7 @@ def test_update_one_property_at_a_time_request():
228
227
  )
229
228
  def test_update_credential_update_request_dict(data, expected_dict):
230
229
  req = UserCredentialUpdateRequest(**data)
231
- assert req.dict() == expected_dict
230
+ assert req.model_dump() == expected_dict
232
231
 
233
232
 
234
233
  def test_card_transaction_requests():
@@ -355,7 +354,7 @@ def test_user_request():
355
354
  required_level=3,
356
355
  terms_of_service=None,
357
356
  )
358
- assert UserRequest(**request).dict() == request
357
+ assert UserRequest(**request).model_dump() == request
359
358
 
360
359
  # changing curp so user is underage
361
360
  request['curp'] = 'ABCD060604HDFSRN03'
@@ -392,7 +391,7 @@ def test_curp_validation_request():
392
391
  assert all(field in error_msg for field in required_fields)
393
392
 
394
393
  req_curp = CurpValidationRequest(**request)
395
- assert req_curp.dict() == request
394
+ assert req_curp.model_dump() == request
396
395
 
397
396
  request['date_of_birth'] = dt.date(2006, 5, 17)
398
397
 
@@ -430,9 +429,9 @@ def test_user_update_request():
430
429
  curp_document_uri='https://sandbox.cuenca.com/files/EF123',
431
430
  )
432
431
  update_req = UserUpdateRequest(**request)
433
- beneficiaries = [b.dict() for b in update_req.beneficiaries]
432
+ beneficiaries = [b.model_dump() for b in update_req.beneficiaries]
434
433
  assert beneficiaries == request['beneficiaries']
435
- assert str(update_req.curp_document_uri) == request['curp_document_uri']
434
+ assert update_req.curp_document_uri == request['curp_document_uri']
436
435
 
437
436
  request['beneficiaries'] = [
438
437
  dict(
@@ -585,49 +584,42 @@ class TestFloatModel(BaseModel):
585
584
  value: StrictPositiveFloat
586
585
 
587
586
 
588
- def test_strict_positive_float_valid():
589
- model = TestFloatModel(value=10.5)
590
- assert model.value == 10.5
591
- model = TestFloatModel(value=0.000001)
592
- assert model.value == 0.000001
587
+ @pytest.mark.parametrize("value", [10.5, 0.000001])
588
+ def test_strict_positive_float_valid(value):
589
+ model = TestFloatModel(value=value)
590
+ assert model.value == value
593
591
 
594
592
 
595
- def test_strict_positive_float_invalid():
593
+ @pytest.mark.parametrize("value", [0.0, -1.5])
594
+ def test_strict_positive_float_invalid(value):
596
595
  with pytest.raises(ValueError, match="Input should be greater than 0"):
597
- TestFloatModel(value=0.0)
598
- with pytest.raises(ValueError, match="Input should be greater than 0"):
599
- TestFloatModel(value=-1.5)
596
+ TestFloatModel(value=value)
600
597
 
601
598
 
602
599
  class TestIntModel(BaseModel):
603
600
  value: StrictPositiveInt
604
601
 
605
602
 
606
- def test_strict_positive_int_valid():
607
- model = TestIntModel(value=100)
608
- assert model.value == 100
609
-
610
- model = TestIntModel(value=1)
611
- assert model.value == 1
612
-
613
- model = TestIntModel(value=21_474_836_47)
614
- assert model.value == 21_474_836_47
603
+ @pytest.mark.parametrize("value", [100, 1, 21_474_836_47])
604
+ def test_strict_positive_int_valid(value):
605
+ model = TestIntModel(value=value)
606
+ assert model.value == value
615
607
 
616
608
 
617
- def test_strict_positive_int_invalid():
618
- with pytest.raises(ValueError, match="Input should be greater than 0"):
619
- TestIntModel(value=0)
620
-
621
- with pytest.raises(ValueError, match="Input should be greater than 0"):
622
- TestIntModel(value=-5)
623
-
624
- with pytest.raises(
625
- ValueError, match="Input should be less than or equal to 2147483647"
626
- ):
627
- TestIntModel(value=21_474_836_48)
628
-
629
- with pytest.raises(ValueError, match="Input should be a valid integer"):
630
- TestIntModel(value=5.5)
631
-
632
- with pytest.raises(ValueError, match="Input should be a valid integer"):
633
- TestIntModel(value="10")
609
+ @pytest.mark.parametrize(
610
+ "value, expected_error, expected_message",
611
+ [
612
+ (0, ValueError, "Input should be greater than 0"),
613
+ (-5, ValueError, "Input should be greater than 0"),
614
+ (
615
+ 21_474_836_48,
616
+ ValueError,
617
+ "Input should be less than or equal to 2147483647",
618
+ ),
619
+ (5.5, ValueError, "Input should be a valid integer"),
620
+ ("10", ValueError, "Input should be a valid integer"),
621
+ ],
622
+ )
623
+ def test_strict_positive_int_invalid(value, expected_error, expected_message):
624
+ with pytest.raises(expected_error, match=expected_message):
625
+ TestIntModel(value=value)
@@ -1,42 +0,0 @@
1
- from pydantic import BaseModel, field_validator
2
- from pydantic_core import PydanticCustomError
3
- from pydantic_extra_types.payment import PaymentCardBrand, PaymentCardNumber
4
-
5
- from ..card_bins import CARD_BINS
6
-
7
-
8
- class StrictPaymentCardNumber(BaseModel):
9
-
10
- card_number: PaymentCardNumber
11
-
12
- @field_validator('card_number')
13
- def validate_bin(cls, card_number: PaymentCardNumber) -> PaymentCardNumber:
14
- if card_number.bin not in CARD_BINS:
15
- raise PydanticCustomError(
16
- 'payment_card_number.bin',
17
- 'The card number contains a BIN (first six digits) '
18
- 'that does not have a known association with a Mexican bank.'
19
- 'To add the association, please file an issue:'
20
- 'https://github.com/cuenca-mx/cuenca-validations/issues',
21
- )
22
- return card_number
23
-
24
- @property
25
- def brand(self) -> PaymentCardBrand:
26
- return self.card_number.brand
27
-
28
- @property
29
- def last4(self) -> str:
30
- return self.card_number.last4
31
-
32
- @property
33
- def masked(self) -> str:
34
- return self.card_number.masked
35
-
36
- @property
37
- def bin(self) -> str:
38
- return self.card_number.bin
39
-
40
- @property
41
- def bank_code(self) -> str:
42
- return CARD_BINS[self.card_number.bin]
@@ -1,5 +0,0 @@
1
- from typing import Any, Dict, MutableMapping, Optional, Union
2
-
3
- ClientRequestParams = Union[None, bytes, MutableMapping[str, str]]
4
- DictStrAny = Dict[str, Any]
5
- OptionalDict = Optional[Dict[str, Union[int, str]]]
@@ -1 +0,0 @@
1
- __version__ = '2.0.0.dev9'