cuenca-validations 2.0.0.dev13__tar.gz → 2.0.1__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 (33) hide show
  1. {cuenca_validations-2.0.0.dev13 → cuenca_validations-2.0.1}/PKG-INFO +2 -1
  2. {cuenca_validations-2.0.0.dev13 → cuenca_validations-2.0.1}/cuenca_validations/__init__.py +4 -0
  3. {cuenca_validations-2.0.0.dev13 → cuenca_validations-2.0.1}/cuenca_validations/types/__init__.py +4 -8
  4. cuenca_validations-2.0.1/cuenca_validations/types/card.py +41 -0
  5. {cuenca_validations-2.0.0.dev13 → cuenca_validations-2.0.1}/cuenca_validations/types/files.py +2 -3
  6. {cuenca_validations-2.0.0.dev13 → cuenca_validations-2.0.1}/cuenca_validations/types/general.py +9 -21
  7. {cuenca_validations-2.0.0.dev13 → cuenca_validations-2.0.1}/cuenca_validations/types/identities.py +21 -32
  8. {cuenca_validations-2.0.0.dev13 → cuenca_validations-2.0.1}/cuenca_validations/types/morals.py +2 -2
  9. {cuenca_validations-2.0.0.dev13 → cuenca_validations-2.0.1}/cuenca_validations/types/queries.py +2 -14
  10. {cuenca_validations-2.0.0.dev13 → cuenca_validations-2.0.1}/cuenca_validations/types/requests.py +43 -104
  11. {cuenca_validations-2.0.0.dev13 → cuenca_validations-2.0.1}/cuenca_validations/validators.py +0 -3
  12. cuenca_validations-2.0.1/cuenca_validations/version.py +1 -0
  13. {cuenca_validations-2.0.0.dev13 → cuenca_validations-2.0.1}/cuenca_validations.egg-info/PKG-INFO +2 -1
  14. {cuenca_validations-2.0.0.dev13 → cuenca_validations-2.0.1}/cuenca_validations.egg-info/requires.txt +1 -0
  15. {cuenca_validations-2.0.0.dev13 → cuenca_validations-2.0.1}/setup.py +1 -0
  16. {cuenca_validations-2.0.0.dev13 → cuenca_validations-2.0.1}/tests/test_card.py +8 -4
  17. {cuenca_validations-2.0.0.dev13 → cuenca_validations-2.0.1}/tests/test_errors.py +0 -5
  18. {cuenca_validations-2.0.0.dev13 → cuenca_validations-2.0.1}/tests/test_types.py +21 -41
  19. cuenca_validations-2.0.0.dev13/cuenca_validations/types/card.py +0 -38
  20. cuenca_validations-2.0.0.dev13/cuenca_validations/version.py +0 -1
  21. {cuenca_validations-2.0.0.dev13 → cuenca_validations-2.0.1}/LICENSE +0 -0
  22. {cuenca_validations-2.0.0.dev13 → cuenca_validations-2.0.1}/README.md +0 -0
  23. {cuenca_validations-2.0.0.dev13 → cuenca_validations-2.0.1}/cuenca_validations/card_bins.py +0 -0
  24. {cuenca_validations-2.0.0.dev13 → cuenca_validations-2.0.1}/cuenca_validations/errors.py +0 -0
  25. {cuenca_validations-2.0.0.dev13 → cuenca_validations-2.0.1}/cuenca_validations/py.typed +0 -0
  26. {cuenca_validations-2.0.0.dev13 → cuenca_validations-2.0.1}/cuenca_validations/types/enums.py +0 -0
  27. {cuenca_validations-2.0.0.dev13 → cuenca_validations-2.0.1}/cuenca_validations/typing.py +0 -0
  28. {cuenca_validations-2.0.0.dev13 → cuenca_validations-2.0.1}/cuenca_validations.egg-info/SOURCES.txt +0 -0
  29. {cuenca_validations-2.0.0.dev13 → cuenca_validations-2.0.1}/cuenca_validations.egg-info/dependency_links.txt +0 -0
  30. {cuenca_validations-2.0.0.dev13 → cuenca_validations-2.0.1}/cuenca_validations.egg-info/top_level.txt +0 -0
  31. {cuenca_validations-2.0.0.dev13 → cuenca_validations-2.0.1}/setup.cfg +0 -0
  32. {cuenca_validations-2.0.0.dev13 → cuenca_validations-2.0.1}/tests/__init__.py +0 -0
  33. {cuenca_validations-2.0.0.dev13 → cuenca_validations-2.0.1}/tests/test_statement.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: cuenca_validations
3
- Version: 2.0.0.dev13
3
+ Version: 2.0.1
4
4
  Summary: Cuenca common validations
5
5
  Home-page: https://github.com/cuenca-mx/cuenca-validations
6
6
  Author: Cuenca
@@ -20,6 +20,7 @@ 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
22
  Requires-Dist: python-dateutil>=2.9.0
23
+ Requires-Dist: phonenumbers>=8.13.0
23
24
  Dynamic: author
24
25
  Dynamic: author-email
25
26
  Dynamic: classifier
@@ -5,5 +5,9 @@ __all__ = [
5
5
  'validators',
6
6
  ]
7
7
 
8
+ from pydantic_extra_types.phone_numbers import PhoneNumber
9
+
8
10
  from . import types, typing, validators
9
11
  from .version import __version__
12
+
13
+ PhoneNumber.phone_format = 'E164'
@@ -22,7 +22,7 @@ __all__ = [
22
22
  'CardTransactionType',
23
23
  'CardType',
24
24
  'Country',
25
- 'CurpField',
25
+ 'Curp',
26
26
  'CurpValidationRequest',
27
27
  'CommissionType',
28
28
  'DepositNetwork',
@@ -101,10 +101,8 @@ __all__ = [
101
101
  'WalletQuery',
102
102
  'WalletTransactionQuery',
103
103
  'WebhookEvent',
104
- 'Digits',
104
+ 'digits',
105
105
  'get_state_name',
106
- 'HttpUrlString',
107
- 'AnyUrlString',
108
106
  ]
109
107
 
110
108
  from .card import StrictPaymentCardNumber
@@ -152,18 +150,16 @@ from .enums import (
152
150
  )
153
151
  from .files import BatchFileMetadata
154
152
  from .general import (
155
- AnyUrlString,
156
- Digits,
157
- HttpUrlString,
158
153
  JSONEncoder,
159
154
  SantizedDict,
160
155
  StrictPositiveInt,
156
+ digits,
161
157
  get_state_name,
162
158
  )
163
159
  from .identities import (
164
160
  Address,
165
161
  Beneficiary,
166
- CurpField,
162
+ Curp,
167
163
  KYCFile,
168
164
  PhoneNumber,
169
165
  Rfc,
@@ -0,0 +1,41 @@
1
+ from typing import Annotated
2
+
3
+ from pydantic import Field, StringConstraints
4
+ from pydantic_core import PydanticCustomError, core_schema
5
+ from pydantic_extra_types.payment import PaymentCardNumber
6
+
7
+ from ..card_bins import CARD_BINS
8
+
9
+ ExpMonth = Annotated[int, Field(strict=True, ge=1, le=12)]
10
+ ExpYear = Annotated[int, Field(strict=True, ge=1, le=99)]
11
+ Cvv = Annotated[
12
+ str,
13
+ StringConstraints(
14
+ strip_whitespace=True,
15
+ min_length=3,
16
+ max_length=3,
17
+ pattern=r'\d{3}',
18
+ ),
19
+ ]
20
+
21
+
22
+ class StrictPaymentCardNumber(PaymentCardNumber):
23
+
24
+ @classmethod
25
+ def validate(
26
+ cls, card_number: str, validation_info: core_schema.ValidationInfo
27
+ ) -> 'StrictPaymentCardNumber':
28
+ card = super().validate(card_number, validation_info)
29
+ if card.bin not in CARD_BINS:
30
+ raise PydanticCustomError(
31
+ 'payment_card_number.bin',
32
+ 'The card number contains a BIN (first six digits) that '
33
+ 'does not have a known association with a Mexican bank. '
34
+ 'To add the association, please file an issue: '
35
+ 'https://github.com/cuenca-mx/cuenca-validations/issues',
36
+ )
37
+ return cls(card)
38
+
39
+ @property
40
+ def bank_code(self) -> str:
41
+ return CARD_BINS[self.bin]
@@ -1,13 +1,12 @@
1
1
  from typing import Optional
2
2
 
3
- from pydantic import BaseModel
3
+ from pydantic import BaseModel, HttpUrl
4
4
 
5
5
  from .enums import KYCFileType
6
- from .general import HttpUrlString
7
6
 
8
7
 
9
8
  class BatchFileMetadata(BaseModel):
10
9
  id: Optional[str] = None
11
10
  is_back: bool
12
11
  type: KYCFileType
13
- url: HttpUrlString
12
+ url: HttpUrl
@@ -1,20 +1,11 @@
1
1
  import json
2
2
  from typing import Annotated, Any, Optional
3
3
 
4
- from pydantic import AfterValidator, AnyUrl, BeforeValidator, Field, HttpUrl
4
+ from pydantic import Field, StringConstraints
5
5
 
6
6
  from ..validators import sanitize_dict, sanitize_item
7
7
  from .enums import State
8
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
-
18
9
 
19
10
  class SantizedDict(dict):
20
11
  def __init__(self, *args, **kwargs):
@@ -34,20 +25,17 @@ StrictPositiveInt = Annotated[
34
25
  ]
35
26
 
36
27
 
37
- def validate_only_digits(value: Any) -> str:
38
- v_str = str(value)
39
- if not v_str.isdigit():
40
- raise ValueError("Value must contain only digits")
41
- return v_str
42
-
43
-
44
- def Digits(
28
+ def digits(
45
29
  min_length: Optional[int] = None, max_length: Optional[int] = None
46
- ) -> Any:
30
+ ) -> Annotated[Any, StringConstraints]:
47
31
  return Annotated[
48
32
  str,
49
- BeforeValidator(validate_only_digits),
50
- Field(min_length=min_length, max_length=max_length),
33
+ StringConstraints(
34
+ strip_whitespace=True,
35
+ min_length=min_length,
36
+ max_length=max_length,
37
+ pattern=r'^\d+$',
38
+ ),
51
39
  ]
52
40
 
53
41
 
@@ -6,22 +6,26 @@ from pydantic import (
6
6
  ConfigDict,
7
7
  Field,
8
8
  IPvAnyAddress,
9
+ SecretStr,
9
10
  StringConstraints,
10
11
  model_validator,
11
12
  )
12
- from pydantic.types import StrictStr
13
+ from pydantic_extra_types.phone_numbers import PhoneNumber
13
14
 
14
15
  from .enums import Country, KYCFileType, State, VerificationStatus
15
16
 
16
- PhoneNumber = Annotated[
17
- str,
18
- StringConstraints(
19
- min_length=10, max_length=15, pattern=r'^\+?[0-9]{10,14}$'
17
+ Password = Annotated[
18
+ SecretStr,
19
+ Field(
20
+ min_length=6,
21
+ max_length=128,
22
+ description=(
23
+ 'Any str with at least 6 characters, maximum 128 characters'
24
+ ),
20
25
  ),
21
26
  ]
22
27
 
23
-
24
- CurpField = Annotated[
28
+ Curp = Annotated[
25
29
  str,
26
30
  StringConstraints(
27
31
  min_length=18,
@@ -31,25 +35,13 @@ CurpField = Annotated[
31
35
  ]
32
36
 
33
37
 
34
- class Rfc(StrictStr):
35
- min_length = 12
36
- max_length = 13
37
-
38
- @classmethod
39
- def validate(cls, rfc: str):
40
- if len(rfc) < cls.min_length or len(rfc) > cls.max_length:
41
- raise ValueError('Not a valid RFC.')
42
- return cls(rfc)
43
-
44
- @classmethod
45
- def __get_pydantic_core_schema__(
46
- cls, source_type: Any, handler: Any
47
- ) -> dict[str, Any]:
48
- return {
49
- 'type': 'str',
50
- 'min_length': cls.min_length,
51
- 'max_length': cls.max_length,
52
- }
38
+ Rfc = Annotated[
39
+ str,
40
+ StringConstraints(
41
+ min_length=12,
42
+ max_length=13,
43
+ ),
44
+ ]
53
45
 
54
46
 
55
47
  class Address(BaseModel):
@@ -110,17 +102,14 @@ class Beneficiary(BaseModel):
110
102
 
111
103
  class VerificationErrors(BaseModel):
112
104
  identifier: str = Field(
113
- ..., description='Unique identifier for the step validation'
105
+ description='Unique identifier for the step validation'
114
106
  )
115
107
  error: str = Field(
116
- ...,
117
108
  description='Error throwed on validation,'
118
109
  ' can be StepError or SystemError in case of '
119
110
  'KYCProvider intermittence',
120
111
  )
121
- code: str = Field(
122
- ..., description='Specific code of the failure in the step.'
123
- )
112
+ code: str = Field(description='Specific code of the failure in the step.')
124
113
  message: Optional[str] = Field(None, description='Error description')
125
114
  model_config = ConfigDict(
126
115
  json_schema_extra={
@@ -136,7 +125,7 @@ class VerificationErrors(BaseModel):
136
125
 
137
126
  class KYCFile(BaseModel):
138
127
  type: KYCFileType
139
- uri_front: str = Field(..., description='API uri to fetch the file')
128
+ uri_front: str = Field(description='API uri to fetch the file')
140
129
  uri_back: Optional[str] = Field(
141
130
  None, description='API uri to fetch the file'
142
131
  )
@@ -3,7 +3,7 @@ from typing import Optional
3
3
 
4
4
  from pydantic import BaseModel, EmailStr
5
5
 
6
- from cuenca_validations.types import Address, CurpField, PhoneNumber, Rfc
6
+ from cuenca_validations.types import Address, Curp, PhoneNumber, Rfc
7
7
 
8
8
 
9
9
  class BusinessDetails(BaseModel):
@@ -52,7 +52,7 @@ class PhysicalPerson(BaseModel):
52
52
  names: str
53
53
  first_surname: str
54
54
  second_surname: Optional[str] = None
55
- curp: Optional[CurpField] = None
55
+ curp: Optional[Curp] = None
56
56
  rfc: Optional[Rfc] = None
57
57
 
58
58
 
@@ -24,7 +24,7 @@ from .enums import (
24
24
  TransferNetwork,
25
25
  UserStatus,
26
26
  )
27
- from .identities import CurpField
27
+ from .identities import Curp
28
28
 
29
29
  MAX_PAGE_SIZE = 100
30
30
 
@@ -112,23 +112,11 @@ class ApiKeyQuery(QueryParams):
112
112
 
113
113
  class CardQuery(QueryParams):
114
114
  number: Optional[str] = None
115
- exp_month: Optional[int] = None
116
- exp_year: Optional[int] = None
117
- cvv: Optional[str] = None
118
- cvv2: Optional[str] = None
119
- icvv: Optional[str] = None
120
- pin_block: Optional[str] = None
121
115
  issuer: Optional[CardIssuer] = None
122
116
  funding_type: Optional[CardFundingType] = None
123
117
  status: Optional[CardStatus] = None
124
118
  type: Optional[CardType] = None
125
119
 
126
- @field_validator('exp_month', 'exp_year', 'cvv2', 'cvv')
127
- def query_by_exp_cvv_if_number_set(cls, v, values):
128
- if not values.data.get('number'):
129
- raise ValueError('Number must be set to query by exp or cvv')
130
- return v
131
-
132
120
 
133
121
  class StatementQuery(QueryParams):
134
122
  year: int
@@ -170,7 +158,7 @@ class UserQuery(QueryParams):
170
158
 
171
159
 
172
160
  class IdentityQuery(QueryParams):
173
- curp: Optional[CurpField] = None
161
+ curp: Optional[Curp] = None
174
162
  rfc: Optional[str] = None
175
163
  status: Optional[UserStatus] = None
176
164
 
@@ -3,10 +3,12 @@ from typing import Annotated, Optional, Union
3
3
 
4
4
  from clabe import Clabe
5
5
  from pydantic import (
6
+ AnyUrl,
6
7
  BaseModel,
7
8
  ConfigDict,
8
9
  EmailStr,
9
10
  Field,
11
+ HttpUrl,
10
12
  StrictStr,
11
13
  StringConstraints,
12
14
  field_validator,
@@ -47,13 +49,20 @@ from ..types.enums import (
47
49
  )
48
50
  from ..typing import DictStrAny
49
51
  from ..validators import validate_age_requirement
50
- from .card import PaymentCardNumber, StrictPaymentCardNumber
51
- from .general import AnyUrlString, HttpUrlString, StrictPositiveInt
52
+ from .card import (
53
+ Cvv,
54
+ ExpMonth,
55
+ ExpYear,
56
+ PaymentCardNumber,
57
+ StrictPaymentCardNumber,
58
+ )
59
+ from .general import StrictPositiveInt
52
60
  from .identities import (
53
61
  Address,
54
62
  Beneficiary,
55
- CurpField,
63
+ Curp,
56
64
  KYCFile,
65
+ Password,
57
66
  PhoneNumber,
58
67
  Rfc,
59
68
  TOSAgreement,
@@ -81,13 +90,13 @@ class BaseRequest(BaseModel):
81
90
  class BaseTransferRequest(BaseRequest):
82
91
  recipient_name: StrictStr
83
92
  amount: StrictPositiveInt = Field(
84
- ..., description='Always in cents, not in MXN pesos'
93
+ description='Always in cents, not in MXN pesos'
85
94
  )
86
95
  descriptor: StrictStr = Field(
87
- ..., description='Short description for the recipient'
96
+ description='Short description for the recipient'
88
97
  )
89
98
  idempotency_key: str = Field(
90
- ..., description='Custom Id, must be unique for each transfer'
99
+ description='Custom Id, must be unique for each transfer'
91
100
  )
92
101
  user_id: Optional[str] = Field(
93
102
  None, description='Source user to take the funds'
@@ -108,13 +117,13 @@ class BaseTransferRequest(BaseRequest):
108
117
 
109
118
  class TransferRequest(BaseTransferRequest):
110
119
  account_number: Union[Clabe, PaymentCardNumber] = Field(
111
- ..., description='Destination Clabe or Card number'
120
+ description='Destination Clabe or Card number'
112
121
  )
113
122
 
114
123
 
115
124
  class StrictTransferRequest(BaseTransferRequest):
116
125
  account_number: Union[Clabe, StrictPaymentCardNumber] = Field(
117
- ..., description='Destination Clabe or Card number'
126
+ description='Destination Clabe or Card number'
118
127
  )
119
128
 
120
129
 
@@ -134,17 +143,9 @@ class CardRequest(BaseRequest):
134
143
 
135
144
  class CardActivationRequest(BaseModel):
136
145
  number: PaymentCardNumber
137
- exp_month: Annotated[int, Field(strict=True, ge=1, le=12)]
138
- exp_year: Annotated[int, Field(strict=True, ge=18, le=99)]
139
- cvv2: Annotated[
140
- str,
141
- StringConstraints(
142
- strip_whitespace=True,
143
- min_length=3,
144
- max_length=3,
145
- pattern=r'\d{3}',
146
- ),
147
- ]
146
+ exp_month: ExpMonth
147
+ exp_year: ExpYear
148
+ cvv2: Cvv
148
149
 
149
150
 
150
151
  class ApiKeyUpdateRequest(BaseRequest):
@@ -155,14 +156,7 @@ class ApiKeyUpdateRequest(BaseRequest):
155
156
 
156
157
  class UserCredentialUpdateRequest(BaseRequest):
157
158
  is_active: Optional[bool] = None
158
- password: Optional[str] = Field(
159
- None,
160
- min_length=6,
161
- max_length=128,
162
- description=(
163
- 'Any str with at least 6 characters, maximum 128 characters'
164
- ),
165
- )
159
+ password: Optional[Password] = None
166
160
 
167
161
  def model_dump(self, *args, **kwargs) -> DictStrAny:
168
162
  # Password can be None but BaseRequest excludes None
@@ -178,53 +172,17 @@ class UserCredentialUpdateRequest(BaseRequest):
178
172
 
179
173
 
180
174
  class UserCredentialRequest(BaseRequest):
181
- password: str = Field(
182
- ...,
183
- min_length=6,
184
- max_length=128,
185
- description=(
186
- 'Any str with at least 6 characters, maximum 128 characters'
187
- ),
188
- )
175
+ password: Password
189
176
  user_id: Optional[str] = None
190
177
 
191
178
 
192
179
  class CardValidationRequest(BaseModel):
193
- number: Annotated[
194
- str,
195
- StringConstraints(
196
- min_length=16,
197
- max_length=16,
198
- pattern=r'\d{16}',
199
- strip_whitespace=True,
200
- ),
201
- ]
202
- exp_month: Optional[Annotated[int, Field(strict=True, ge=1, le=12)]] = None
203
- exp_year: Optional[Annotated[int, Field(strict=True, ge=18, le=99)]] = None
204
- cvv: Optional[
205
- Annotated[
206
- str,
207
- StringConstraints(
208
- strip_whitespace=True, strict=True, min_length=3, max_length=3
209
- ),
210
- ]
211
- ] = None
212
- cvv2: Optional[
213
- Annotated[
214
- str,
215
- StringConstraints(
216
- strip_whitespace=True, strict=True, min_length=3, max_length=3
217
- ),
218
- ]
219
- ] = None
220
- icvv: Optional[
221
- Annotated[
222
- str,
223
- StringConstraints(
224
- strip_whitespace=True, strict=True, min_length=3, max_length=3
225
- ),
226
- ]
227
- ] = None
180
+ number: PaymentCardNumber
181
+ exp_month: Optional[ExpMonth] = None
182
+ exp_year: Optional[ExpYear] = None
183
+ cvv: Optional[Cvv] = None
184
+ cvv2: Optional[Cvv] = None
185
+ icvv: Optional[Cvv] = None
228
186
  pin_block: Optional[
229
187
  Annotated[str, StringConstraints(strip_whitespace=True)]
230
188
  ] = None
@@ -232,15 +190,7 @@ class CardValidationRequest(BaseModel):
232
190
 
233
191
 
234
192
  class ARPCRequest(BaseModel):
235
- number: Annotated[
236
- str,
237
- StringConstraints(
238
- min_length=16,
239
- max_length=16,
240
- pattern=r'\d{16}',
241
- strip_whitespace=True,
242
- ),
243
- ]
193
+ number: PaymentCardNumber
244
194
  arqc: StrictStr
245
195
  arpc_method: Annotated[
246
196
  str,
@@ -377,7 +327,7 @@ class CurpValidationRequest(BaseModel):
377
327
  None, description='In format ISO 3166 Alpha-2'
378
328
  )
379
329
  gender: Optional[Gender] = None
380
- manual_curp: Optional[CurpField] = Field(
330
+ manual_curp: Optional[Curp] = Field(
381
331
  None,
382
332
  description='Force to validate this curp instead of use '
383
333
  'the one we calculate',
@@ -452,8 +402,8 @@ class UserRequest(BaseModel):
452
402
  id: Optional[str] = Field(
453
403
  None, description='if you want to create with specific `id`'
454
404
  )
455
- curp: CurpField = Field(
456
- ..., description='Previously validated in `curp_validations`'
405
+ curp: Curp = Field(
406
+ description='Previously validated in `curp_validations`'
457
407
  )
458
408
  phone_number: Optional[PhoneNumber] = Field(
459
409
  None, description='Only if you validated previously on your side'
@@ -497,9 +447,7 @@ class UserRequest(BaseModel):
497
447
 
498
448
  @field_validator('curp')
499
449
  @classmethod
500
- def validate_birth_date(
501
- cls, curp: Optional[CurpField]
502
- ) -> Optional[CurpField]:
450
+ def validate_birth_date(cls, curp: Optional[Curp]) -> Optional[Curp]:
503
451
  if curp:
504
452
  current_date = dt.datetime.utcnow()
505
453
  curp_date = curp[4:10]
@@ -531,7 +479,7 @@ class UserUpdateRequest(BaseModel):
531
479
  status: Optional[UserStatus] = None
532
480
  terms_of_service: Optional[TOSRequest] = None
533
481
  platform_terms_of_service: Optional[TOSAgreement] = None
534
- curp_document_uri: Optional[HttpUrlString] = None
482
+ curp_document_uri: Optional[HttpUrl] = None
535
483
 
536
484
  @field_validator('beneficiaries')
537
485
  @classmethod
@@ -544,14 +492,7 @@ class UserUpdateRequest(BaseModel):
544
492
 
545
493
 
546
494
  class UserLoginRequest(BaseRequest):
547
- password: str = Field(
548
- ...,
549
- min_length=6,
550
- max_length=128,
551
- description=(
552
- 'Any str with at least 6 characters, maximum 128 characters'
553
- ),
554
- )
495
+ password: Password
555
496
  user_id: Optional[str] = Field(None, description='Deprecated field')
556
497
  model_config = ConfigDict(
557
498
  json_schema_extra={'example': {'password': 'supersecret'}},
@@ -561,8 +502,8 @@ class UserLoginRequest(BaseRequest):
561
502
  class SessionRequest(BaseRequest):
562
503
  user_id: str
563
504
  type: SessionType
564
- success_url: Optional[AnyUrlString] = None
565
- failure_url: Optional[AnyUrlString] = None
505
+ success_url: Optional[AnyUrl] = None
506
+ failure_url: Optional[AnyUrl] = None
566
507
  model_config = ConfigDict(
567
508
  json_schema_extra={
568
509
  'example': {
@@ -576,13 +517,13 @@ class SessionRequest(BaseRequest):
576
517
 
577
518
 
578
519
  class EndpointRequest(BaseRequest):
579
- url: HttpUrlString
520
+ url: HttpUrl
580
521
  events: Optional[list[WebhookEvent]] = None
581
522
  user_id: Optional[str] = None
582
523
 
583
524
 
584
525
  class EndpointUpdateRequest(BaseRequest):
585
- url: Optional[HttpUrlString] = None
526
+ url: Optional[HttpUrl] = None
586
527
  is_enable: Optional[bool] = None
587
528
  events: Optional[list[WebhookEvent]] = None
588
529
 
@@ -597,7 +538,7 @@ class FileUploadRequest(BaseRequest):
597
538
 
598
539
  class FileRequest(BaseModel):
599
540
  is_back: Optional[bool] = False
600
- url: HttpUrlString
541
+ url: HttpUrl
601
542
  type: KYCFileType
602
543
 
603
544
 
@@ -644,12 +585,12 @@ class VerificationAttemptRequest(BaseModel):
644
585
 
645
586
 
646
587
  class LimitedWalletRequest(BaseRequest):
647
- allowed_curp: CurpField
588
+ allowed_curp: Curp
648
589
  allowed_rfc: Optional[Rfc] = None
649
590
 
650
591
 
651
592
  class KYCVerificationUpdateRequest(BaseRequest):
652
- curp: CurpField
593
+ curp: Curp
653
594
 
654
595
 
655
596
  class PlatformRequest(BaseModel):
@@ -682,9 +623,7 @@ class BankAccountValidationRequest(BaseModel):
682
623
 
683
624
 
684
625
  class UserListsRequest(BaseModel):
685
- curp: Optional[CurpField] = Field(
686
- None, description='Curp to review on lists'
687
- )
626
+ curp: Optional[Curp] = Field(None, description='Curp to review on lists')
688
627
  account_number: Optional[Union[Clabe, PaymentCardNumber]] = Field(
689
628
  None, description='Account to review on lists'
690
629
  )
@@ -4,7 +4,6 @@ from enum import Enum
4
4
  from typing import Any, Callable, Optional, Union
5
5
 
6
6
  from dateutil.relativedelta import relativedelta
7
- from pydantic import SecretStr
8
7
 
9
8
 
10
9
  def sanitize_dict(d: dict) -> dict:
@@ -36,8 +35,6 @@ def sanitize_item(
36
35
  rv = base64.b64encode(item).decode('utf-8')
37
36
  elif isinstance(item, Enum):
38
37
  rv = item.value
39
- elif isinstance(item, SecretStr):
40
- rv = item.get_secret_value()
41
38
  elif hasattr(item, 'to_dict'):
42
39
  rv = item.to_dict()
43
40
  elif default:
@@ -0,0 +1 @@
1
+ __version__ = '2.0.1'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: cuenca_validations
3
- Version: 2.0.0.dev13
3
+ Version: 2.0.1
4
4
  Summary: Cuenca common validations
5
5
  Home-page: https://github.com/cuenca-mx/cuenca-validations
6
6
  Author: Cuenca
@@ -20,6 +20,7 @@ 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
22
  Requires-Dist: python-dateutil>=2.9.0
23
+ Requires-Dist: phonenumbers>=8.13.0
23
24
  Dynamic: author
24
25
  Dynamic: author-email
25
26
  Dynamic: classifier
@@ -2,3 +2,4 @@ clabe>=2.0.0
2
2
  pydantic[email]>=2.10.0
3
3
  pydantic-extra-types>=2.10.0
4
4
  python-dateutil>=2.9.0
5
+ phonenumbers>=8.13.0
@@ -29,6 +29,7 @@ setup(
29
29
  'pydantic[email]>=2.10.0',
30
30
  'pydantic-extra-types>=2.10.0',
31
31
  'python-dateutil>=2.9.0',
32
+ 'phonenumbers>=8.13.0',
32
33
  ],
33
34
  classifiers=[
34
35
  'Programming Language :: Python :: 3',
@@ -15,10 +15,14 @@ class CardModel(BaseModel):
15
15
  def test_invalid_bin_strict_payment():
16
16
  with pytest.raises(ValidationError) as exc_info:
17
17
  CardModel(card_number=INVALID_BIN)
18
- print(exc_info.value)
19
- assert 'payment_card_number.bin' in str(exc_info.value)
20
- assert 'The card number contains a BIN (first six digits) ' in str(
21
- exc_info.value
18
+ assert exc_info.value.errors()[0] == dict(
19
+ loc=('card_number',),
20
+ type='payment_card_number.bin',
21
+ msg='The card number contains a BIN (first six digits) that does '
22
+ 'not have a known association with a Mexican bank. To add the '
23
+ 'association, please file an issue: '
24
+ 'https://github.com/cuenca-mx/cuenca-validations/issues',
25
+ input=INVALID_BIN,
22
26
  )
23
27
 
24
28
 
@@ -3,7 +3,6 @@ import pytest
3
3
  from cuenca_validations.errors import (
4
4
  ApiError,
5
5
  AuthMethodNotAllowedError,
6
- CuencaError,
7
6
  InvalidOTPCodeError,
8
7
  MissingAuthorizationHeaderError,
9
8
  NoPasswordFoundError,
@@ -14,10 +13,6 @@ from cuenca_validations.errors import (
14
13
  )
15
14
 
16
15
 
17
- def test_cuenca_error_base():
18
- assert issubclass(CuencaError, Exception)
19
-
20
-
21
16
  @pytest.mark.parametrize(
22
17
  "error_class, expected_code, expected_status",
23
18
  [
@@ -10,13 +10,12 @@ from pydantic import BaseModel, SecretStr, ValidationError
10
10
  from cuenca_validations.types import (
11
11
  Address,
12
12
  CardQuery,
13
- Digits,
14
13
  JSONEncoder,
15
14
  QueryParams,
16
- Rfc,
17
15
  SantizedDict,
18
16
  SessionRequest,
19
17
  TransactionStatus,
18
+ digits,
20
19
  get_state_name,
21
20
  )
22
21
  from cuenca_validations.types.enums import (
@@ -50,12 +49,12 @@ now = dt.datetime.now()
50
49
  utcnow = now.astimezone(dt.timezone.utc)
51
50
 
52
51
 
53
- class TestEnum(Enum):
52
+ class EnumModel(Enum):
54
53
  zero = 0
55
54
 
56
55
 
57
56
  @dataclass
58
- class TestClass:
57
+ class DictModel:
59
58
  uno: str
60
59
 
61
60
  def to_dict(self):
@@ -89,13 +88,11 @@ def test_sanitized_dict():
89
88
  time=now,
90
89
  hello='there',
91
90
  dates=[now],
92
- secret=SecretStr('secret'),
93
91
  ) == dict(
94
92
  status='succeeded',
95
93
  time=utcnow.isoformat(),
96
94
  hello='there',
97
95
  dates=[utcnow.isoformat()],
98
- secret='secret',
99
96
  )
100
97
 
101
98
 
@@ -118,10 +115,10 @@ def test_count(count, truth):
118
115
  @pytest.mark.parametrize(
119
116
  'value, result',
120
117
  [
121
- (TestEnum.zero, 0),
118
+ (EnumModel.zero, 0),
122
119
  (today, today.isoformat()),
123
120
  (now, utcnow.isoformat()),
124
- (TestClass(uno='uno'), dict(uno='uno', dos='dos')),
121
+ (DictModel(uno='uno'), dict(uno='uno', dos='dos')),
125
122
  (b'test', 'dGVzdA=='), # b64 encode
126
123
  ],
127
124
  )
@@ -148,14 +145,13 @@ def test_invalid_class():
148
145
 
149
146
 
150
147
  class Accounts(BaseModel):
151
- number: Digits(5, 8) # type: ignore
148
+ number: digits(5, 8) # type: ignore
152
149
 
153
150
 
154
151
  @pytest.mark.parametrize(
155
152
  "input_number, expected",
156
153
  [
157
154
  ('123456', '123456'),
158
- (123456, '123456'),
159
155
  ('0012312', '0012312'),
160
156
  ],
161
157
  )
@@ -167,9 +163,10 @@ def test_only_digits(input_number, expected):
167
163
  @pytest.mark.parametrize(
168
164
  'number, error',
169
165
  [
170
- ('123', 'Value should have at least 5 items after validation'),
171
- ('1234567890', 'Value should have at most 8 items after validation'),
172
- ('no_123', 'Value must contain only digits'),
166
+ (12345, 'Input should be a valid string'),
167
+ ('123', 'String should have at least 5 characters'),
168
+ ('1234567890', 'String should have at most 8 characters'),
169
+ ('no_123', "String should match pattern '^\\d+$'"),
173
170
  ],
174
171
  )
175
172
  def test_invalid_digits(number, error):
@@ -178,14 +175,6 @@ def test_invalid_digits(number, error):
178
175
  assert error in str(exception.value)
179
176
 
180
177
 
181
- def test_card_query_exp_cvv_if_number_set():
182
- values = dict(number='123456', exp_month=1, exp_year=2026)
183
- card_query = CardQuery(**values)
184
- assert all(
185
- getattr(card_query, key) == value for key, value in values.items()
186
- )
187
-
188
-
189
178
  @pytest.mark.parametrize(
190
179
  'input_value',
191
180
  [
@@ -210,7 +199,7 @@ def test_update_one_property_at_a_time_request():
210
199
  UserCredentialUpdateRequest(user_id='US123', password='123456')
211
200
 
212
201
  req = UserCredentialUpdateRequest(password='123456')
213
- assert not req.is_active and req.password == '123456'
202
+ assert not req.is_active and req.password.get_secret_value() == '123456'
214
203
 
215
204
  req = UserCredentialUpdateRequest(is_active=True)
216
205
  assert req.is_active and not req.password
@@ -219,7 +208,10 @@ def test_update_one_property_at_a_time_request():
219
208
  @pytest.mark.parametrize(
220
209
  'data,expected_dict',
221
210
  [
222
- (dict(password='123456'), dict(password='123456', is_active=None)),
211
+ (
212
+ dict(password='123456'),
213
+ dict(password=SecretStr('123456'), is_active=None),
214
+ ),
223
215
  (dict(is_active=True), dict(password=None, is_active=True)),
224
216
  (dict(), dict(password=None, is_active=None)),
225
217
  ],
@@ -430,7 +422,10 @@ def test_user_update_request():
430
422
  update_req = UserUpdateRequest(**request)
431
423
  beneficiaries = [b.model_dump() for b in update_req.beneficiaries]
432
424
  assert beneficiaries == request['beneficiaries']
433
- assert update_req.curp_document_uri == request['curp_document_uri']
425
+ assert (
426
+ update_req.curp_document_uri.unicode_string()
427
+ == request['curp_document_uri']
428
+ )
434
429
 
435
430
  request['beneficiaries'] = [
436
431
  dict(
@@ -564,31 +559,16 @@ def test_bank_account_validation_clabe_request():
564
559
  assert BankAccountValidationRequest(account_number='646180157098510917')
565
560
 
566
561
 
567
- def test_rfc_field():
568
- with pytest.raises(ValueError):
569
- Rfc.validate('')
570
- Rfc.validate('invalid')
571
- Rfc.validate('ThisValueIsTooLongForRFC')
572
-
573
- assert Rfc.validate('TAXM840916123')
574
-
575
-
576
562
  def test_user_lists_request():
577
563
  UserListsRequest(names='Pedro', first_surname='Paramo')
578
564
  with pytest.raises(ValueError):
579
565
  UserListsRequest()
580
566
 
581
567
 
582
- class TestIntModel(BaseModel):
568
+ class IntModel(BaseModel):
583
569
  value: StrictPositiveInt
584
570
 
585
571
 
586
- @pytest.mark.parametrize("value", [100, 1, 21_474_836_47])
587
- def test_strict_positive_int_valid(value):
588
- model = TestIntModel(value=value)
589
- assert model.value == value
590
-
591
-
592
572
  @pytest.mark.parametrize(
593
573
  "value, expected_error, expected_message",
594
574
  [
@@ -605,4 +585,4 @@ def test_strict_positive_int_valid(value):
605
585
  )
606
586
  def test_strict_positive_int_invalid(value, expected_error, expected_message):
607
587
  with pytest.raises(expected_error, match=expected_message):
608
- TestIntModel(value=value)
588
+ IntModel(value=value)
@@ -1,38 +0,0 @@
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 +0,0 @@
1
- __version__ = '2.0.0.dev13'