cuenca-validations 2.0.0.dev12__tar.gz → 2.0.0.dev14__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.dev12 → cuenca_validations-2.0.0.dev14}/PKG-INFO +2 -1
  2. {cuenca_validations-2.0.0.dev12 → cuenca_validations-2.0.0.dev14}/cuenca_validations/__init__.py +4 -0
  3. {cuenca_validations-2.0.0.dev12 → cuenca_validations-2.0.0.dev14}/cuenca_validations/errors.py +20 -0
  4. {cuenca_validations-2.0.0.dev12 → cuenca_validations-2.0.0.dev14}/cuenca_validations/types/__init__.py +2 -6
  5. cuenca_validations-2.0.0.dev14/cuenca_validations/types/card.py +36 -0
  6. {cuenca_validations-2.0.0.dev12 → cuenca_validations-2.0.0.dev14}/cuenca_validations/types/files.py +2 -3
  7. {cuenca_validations-2.0.0.dev12 → cuenca_validations-2.0.0.dev14}/cuenca_validations/types/general.py +9 -21
  8. {cuenca_validations-2.0.0.dev12 → cuenca_validations-2.0.0.dev14}/cuenca_validations/types/identities.py +18 -30
  9. {cuenca_validations-2.0.0.dev12 → cuenca_validations-2.0.0.dev14}/cuenca_validations/types/queries.py +0 -12
  10. {cuenca_validations-2.0.0.dev12 → cuenca_validations-2.0.0.dev14}/cuenca_validations/types/requests.py +36 -93
  11. cuenca_validations-2.0.0.dev14/cuenca_validations/version.py +1 -0
  12. {cuenca_validations-2.0.0.dev12 → cuenca_validations-2.0.0.dev14}/cuenca_validations.egg-info/PKG-INFO +2 -1
  13. {cuenca_validations-2.0.0.dev12 → cuenca_validations-2.0.0.dev14}/cuenca_validations.egg-info/requires.txt +1 -0
  14. {cuenca_validations-2.0.0.dev12 → cuenca_validations-2.0.0.dev14}/setup.py +1 -0
  15. {cuenca_validations-2.0.0.dev12 → cuenca_validations-2.0.0.dev14}/tests/test_card.py +6 -4
  16. {cuenca_validations-2.0.0.dev12 → cuenca_validations-2.0.0.dev14}/tests/test_types.py +19 -19
  17. cuenca_validations-2.0.0.dev12/cuenca_validations/types/card.py +0 -38
  18. cuenca_validations-2.0.0.dev12/cuenca_validations/version.py +0 -1
  19. {cuenca_validations-2.0.0.dev12 → cuenca_validations-2.0.0.dev14}/LICENSE +0 -0
  20. {cuenca_validations-2.0.0.dev12 → cuenca_validations-2.0.0.dev14}/README.md +0 -0
  21. {cuenca_validations-2.0.0.dev12 → cuenca_validations-2.0.0.dev14}/cuenca_validations/card_bins.py +0 -0
  22. {cuenca_validations-2.0.0.dev12 → cuenca_validations-2.0.0.dev14}/cuenca_validations/py.typed +0 -0
  23. {cuenca_validations-2.0.0.dev12 → cuenca_validations-2.0.0.dev14}/cuenca_validations/types/enums.py +0 -0
  24. {cuenca_validations-2.0.0.dev12 → cuenca_validations-2.0.0.dev14}/cuenca_validations/types/morals.py +0 -0
  25. {cuenca_validations-2.0.0.dev12 → cuenca_validations-2.0.0.dev14}/cuenca_validations/typing.py +0 -0
  26. {cuenca_validations-2.0.0.dev12 → cuenca_validations-2.0.0.dev14}/cuenca_validations/validators.py +0 -0
  27. {cuenca_validations-2.0.0.dev12 → cuenca_validations-2.0.0.dev14}/cuenca_validations.egg-info/SOURCES.txt +0 -0
  28. {cuenca_validations-2.0.0.dev12 → cuenca_validations-2.0.0.dev14}/cuenca_validations.egg-info/dependency_links.txt +0 -0
  29. {cuenca_validations-2.0.0.dev12 → cuenca_validations-2.0.0.dev14}/cuenca_validations.egg-info/top_level.txt +0 -0
  30. {cuenca_validations-2.0.0.dev12 → cuenca_validations-2.0.0.dev14}/setup.cfg +0 -0
  31. {cuenca_validations-2.0.0.dev12 → cuenca_validations-2.0.0.dev14}/tests/__init__.py +0 -0
  32. {cuenca_validations-2.0.0.dev12 → cuenca_validations-2.0.0.dev14}/tests/test_errors.py +0 -0
  33. {cuenca_validations-2.0.0.dev12 → cuenca_validations-2.0.0.dev14}/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.dev12
3
+ Version: 2.0.0.dev14
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'
@@ -1,6 +1,9 @@
1
+ from pydantic_core import PydanticCustomError
2
+
1
3
  __all__ = [
2
4
  'ApiError',
3
5
  'AuthMethodNotAllowedError',
6
+ 'CardBinValidationError',
4
7
  'CuencaError',
5
8
  'ERROR_CODES',
6
9
  'InvalidOTPCodeError',
@@ -13,6 +16,23 @@ __all__ = [
13
16
  ]
14
17
 
15
18
 
19
+ class CardBinValidationError(PydanticCustomError):
20
+ code = 'payment_card_number.bin'
21
+ msg_template = (
22
+ 'The card number contains a BIN (first six digits) that does not have'
23
+ 'a known association with a Mexican bank. To add the association,'
24
+ 'please file an issue:'
25
+ 'https://github.com/cuenca-mx/cuenca-validations/issues'
26
+ )
27
+
28
+ def __new__(cls):
29
+ return super().__new__(
30
+ cls,
31
+ error_type=cls.code,
32
+ message_template=cls.msg_template,
33
+ )
34
+
35
+
16
36
  class CuencaError(Exception):
17
37
  """Exceptions related to ApiKeys, Login, Password, etc"""
18
38
 
@@ -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,12 +150,10 @@ 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 (
@@ -0,0 +1,36 @@
1
+ from typing import Annotated
2
+
3
+ from pydantic import Field, StringConstraints
4
+ from pydantic_core import core_schema
5
+ from pydantic_extra_types.payment import PaymentCardNumber
6
+
7
+ from ..card_bins import CARD_BINS
8
+ from ..errors import CardBinValidationError
9
+
10
+ ExpMonth = Annotated[int, Field(strict=True, ge=1, le=12)]
11
+ ExpYear = Annotated[int, Field(strict=True, ge=18, le=99)]
12
+ Cvv2 = Annotated[
13
+ str,
14
+ StringConstraints(
15
+ strip_whitespace=True,
16
+ min_length=3,
17
+ max_length=3,
18
+ pattern=r'\d{3}',
19
+ ),
20
+ ]
21
+
22
+
23
+ class StrictPaymentCardNumber(PaymentCardNumber):
24
+
25
+ @classmethod
26
+ def validate(
27
+ cls, card_number: str, validation_info: core_schema.ValidationInfo
28
+ ) -> 'StrictPaymentCardNumber':
29
+ card = super().validate(card_number, validation_info)
30
+ if card.bin not in CARD_BINS:
31
+ raise CardBinValidationError
32
+ return cls(card)
33
+
34
+ @property
35
+ def bank_code(self) -> str:
36
+ 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
 
@@ -9,18 +9,21 @@ from pydantic import (
9
9
  StringConstraints,
10
10
  model_validator,
11
11
  )
12
- from pydantic.types import StrictStr
12
+ from pydantic_extra_types.phone_numbers import PhoneNumber
13
13
 
14
14
  from .enums import Country, KYCFileType, State, VerificationStatus
15
15
 
16
- PhoneNumber = Annotated[
16
+ Password = Annotated[
17
17
  str,
18
- StringConstraints(
19
- min_length=10, max_length=15, pattern=r'^\+?[0-9]{10,14}$'
18
+ Field(
19
+ min_length=6,
20
+ max_length=128,
21
+ description=(
22
+ 'Any str with at least 6 characters, maximum 128 characters'
23
+ ),
20
24
  ),
21
25
  ]
22
26
 
23
-
24
27
  CurpField = Annotated[
25
28
  str,
26
29
  StringConstraints(
@@ -31,25 +34,13 @@ CurpField = Annotated[
31
34
  ]
32
35
 
33
36
 
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
- }
37
+ Rfc = Annotated[
38
+ str,
39
+ StringConstraints(
40
+ min_length=12,
41
+ max_length=13,
42
+ ),
43
+ ]
53
44
 
54
45
 
55
46
  class Address(BaseModel):
@@ -110,17 +101,14 @@ class Beneficiary(BaseModel):
110
101
 
111
102
  class VerificationErrors(BaseModel):
112
103
  identifier: str = Field(
113
- ..., description='Unique identifier for the step validation'
104
+ description='Unique identifier for the step validation'
114
105
  )
115
106
  error: str = Field(
116
- ...,
117
107
  description='Error throwed on validation,'
118
108
  ' can be StepError or SystemError in case of '
119
109
  'KYCProvider intermittence',
120
110
  )
121
- code: str = Field(
122
- ..., description='Specific code of the failure in the step.'
123
- )
111
+ code: str = Field(description='Specific code of the failure in the step.')
124
112
  message: Optional[str] = Field(None, description='Error description')
125
113
  model_config = ConfigDict(
126
114
  json_schema_extra={
@@ -136,7 +124,7 @@ class VerificationErrors(BaseModel):
136
124
 
137
125
  class KYCFile(BaseModel):
138
126
  type: KYCFileType
139
- uri_front: str = Field(..., description='API uri to fetch the file')
127
+ uri_front: str = Field(description='API uri to fetch the file')
140
128
  uri_back: Optional[str] = Field(
141
129
  None, description='API uri to fetch the file'
142
130
  )
@@ -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
@@ -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
+ Cvv2,
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
63
  CurpField,
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: Cvv2
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[Cvv2] = None
184
+ cvv2: Optional[Cvv2] = None
185
+ icvv: Optional[Cvv2] = 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,
@@ -453,7 +403,7 @@ class UserRequest(BaseModel):
453
403
  None, description='if you want to create with specific `id`'
454
404
  )
455
405
  curp: CurpField = Field(
456
- ..., description='Previously validated in `curp_validations`'
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'
@@ -531,7 +481,7 @@ class UserUpdateRequest(BaseModel):
531
481
  status: Optional[UserStatus] = None
532
482
  terms_of_service: Optional[TOSRequest] = None
533
483
  platform_terms_of_service: Optional[TOSAgreement] = None
534
- curp_document_uri: Optional[HttpUrlString] = None
484
+ curp_document_uri: Optional[HttpUrl] = None
535
485
 
536
486
  @field_validator('beneficiaries')
537
487
  @classmethod
@@ -544,14 +494,7 @@ class UserUpdateRequest(BaseModel):
544
494
 
545
495
 
546
496
  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
- )
497
+ password: Password
555
498
  user_id: Optional[str] = Field(None, description='Deprecated field')
556
499
  model_config = ConfigDict(
557
500
  json_schema_extra={'example': {'password': 'supersecret'}},
@@ -561,8 +504,8 @@ class UserLoginRequest(BaseRequest):
561
504
  class SessionRequest(BaseRequest):
562
505
  user_id: str
563
506
  type: SessionType
564
- success_url: Optional[AnyUrlString] = None
565
- failure_url: Optional[AnyUrlString] = None
507
+ success_url: Optional[AnyUrl] = None
508
+ failure_url: Optional[AnyUrl] = None
566
509
  model_config = ConfigDict(
567
510
  json_schema_extra={
568
511
  'example': {
@@ -576,13 +519,13 @@ class SessionRequest(BaseRequest):
576
519
 
577
520
 
578
521
  class EndpointRequest(BaseRequest):
579
- url: HttpUrlString
522
+ url: HttpUrl
580
523
  events: Optional[list[WebhookEvent]] = None
581
524
  user_id: Optional[str] = None
582
525
 
583
526
 
584
527
  class EndpointUpdateRequest(BaseRequest):
585
- url: Optional[HttpUrlString] = None
528
+ url: Optional[HttpUrl] = None
586
529
  is_enable: Optional[bool] = None
587
530
  events: Optional[list[WebhookEvent]] = None
588
531
 
@@ -597,7 +540,7 @@ class FileUploadRequest(BaseRequest):
597
540
 
598
541
  class FileRequest(BaseModel):
599
542
  is_back: Optional[bool] = False
600
- url: HttpUrlString
543
+ url: HttpUrl
601
544
  type: KYCFileType
602
545
 
603
546
 
@@ -0,0 +1 @@
1
+ __version__ = '2.0.0.dev14'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: cuenca_validations
3
- Version: 2.0.0.dev12
3
+ Version: 2.0.0.dev14
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',
@@ -2,6 +2,7 @@ import pytest
2
2
  from pydantic import BaseModel, ValidationError
3
3
  from pydantic_extra_types.payment import PaymentCardBrand
4
4
 
5
+ from cuenca_validations.errors import CardBinValidationError
5
6
  from cuenca_validations.types import StrictPaymentCardNumber
6
7
 
7
8
  VALID_BBVA = '4772130000000003'
@@ -15,10 +16,11 @@ class CardModel(BaseModel):
15
16
  def test_invalid_bin_strict_payment():
16
17
  with pytest.raises(ValidationError) as exc_info:
17
18
  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
19
+ assert exc_info.value.errors()[0] == dict(
20
+ loc=('card_number',),
21
+ type=CardBinValidationError.code,
22
+ msg=CardBinValidationError.msg_template,
23
+ input=INVALID_BIN,
22
24
  )
23
25
 
24
26
 
@@ -10,13 +10,13 @@ from pydantic import BaseModel, ValidationError
10
10
  from cuenca_validations.types import (
11
11
  Address,
12
12
  CardQuery,
13
- Digits,
14
13
  JSONEncoder,
15
14
  QueryParams,
16
15
  Rfc,
17
16
  SantizedDict,
18
17
  SessionRequest,
19
18
  TransactionStatus,
19
+ digits,
20
20
  get_state_name,
21
21
  )
22
22
  from cuenca_validations.types.enums import (
@@ -146,28 +146,29 @@ def test_invalid_class():
146
146
 
147
147
 
148
148
  class Accounts(BaseModel):
149
- number: Digits(5, 8) # type: ignore
149
+ number: digits(5, 8) # type: ignore
150
150
 
151
151
 
152
152
  @pytest.mark.parametrize(
153
153
  "input_number, expected",
154
154
  [
155
155
  ('123456', '123456'),
156
- (123456, '123456'),
157
156
  ('0012312', '0012312'),
158
157
  ],
159
158
  )
160
159
  def test_only_digits(input_number, expected):
161
160
  acc = Accounts(number=input_number)
161
+ print(acc.model_dump())
162
162
  assert acc.number == expected
163
163
 
164
164
 
165
165
  @pytest.mark.parametrize(
166
166
  'number, error',
167
167
  [
168
- ('123', 'Value should have at least 5 items after validation'),
169
- ('1234567890', 'Value should have at most 8 items after validation'),
170
- ('no_123', 'Value must contain only digits'),
168
+ (12345, 'Input should be a valid string'),
169
+ ('123', 'String should have at least 5 characters'),
170
+ ('1234567890', 'String should have at most 8 characters'),
171
+ ('no_123', "String should match pattern '^\\d+$'"),
171
172
  ],
172
173
  )
173
174
  def test_invalid_digits(number, error):
@@ -176,14 +177,6 @@ def test_invalid_digits(number, error):
176
177
  assert error in str(exception.value)
177
178
 
178
179
 
179
- def test_card_query_exp_cvv_if_number_set():
180
- values = dict(number='123456', exp_month=1, exp_year=2026)
181
- card_query = CardQuery(**values)
182
- assert all(
183
- getattr(card_query, key) == value for key, value in values.items()
184
- )
185
-
186
-
187
180
  @pytest.mark.parametrize(
188
181
  'input_value',
189
182
  [
@@ -428,7 +421,10 @@ def test_user_update_request():
428
421
  update_req = UserUpdateRequest(**request)
429
422
  beneficiaries = [b.model_dump() for b in update_req.beneficiaries]
430
423
  assert beneficiaries == request['beneficiaries']
431
- assert update_req.curp_document_uri == request['curp_document_uri']
424
+ assert (
425
+ update_req.curp_document_uri.unicode_string()
426
+ == request['curp_document_uri']
427
+ )
432
428
 
433
429
  request['beneficiaries'] = [
434
430
  dict(
@@ -562,13 +558,17 @@ def test_bank_account_validation_clabe_request():
562
558
  assert BankAccountValidationRequest(account_number='646180157098510917')
563
559
 
564
560
 
561
+ class TestRfc(BaseModel):
562
+ rfc: Rfc
563
+
564
+
565
565
  def test_rfc_field():
566
566
  with pytest.raises(ValueError):
567
- Rfc.validate('')
568
- Rfc.validate('invalid')
569
- Rfc.validate('ThisValueIsTooLongForRFC')
567
+ TestRfc(rfc='')
568
+ TestRfc(rfc='invalid')
569
+ TestRfc(rfc='ThisValueIsTooLongForRFC')
570
570
 
571
- assert Rfc.validate('TAXM840916123')
571
+ assert TestRfc(rfc='TAXM840916123')
572
572
 
573
573
 
574
574
  def test_user_lists_request():
@@ -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.dev12'