cuenca-validations 0.11.32.dev5__py3-none-any.whl → 2.0.0.dev1__py3-none-any.whl

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.
@@ -1,3 +1,4 @@
1
+ '''
1
2
  __all__ = [
2
3
  'ApiError',
3
4
  'AuthMethodNotAllowedError',
@@ -7,20 +8,15 @@ __all__ = [
7
8
  'InvalidOTPCodeError',
8
9
  'MissingAuthorizationHeaderError',
9
10
  'NoPasswordFoundError',
10
- 'NotDigitError',
11
11
  'TooManyAttemptsError',
12
12
  'UserLocationError',
13
13
  'UserNotLoggedInError',
14
14
  'WrongCredsError',
15
15
  ]
16
16
 
17
- from pydantic.errors import (
18
- NotDigitError as PydanticNotDigitError,
19
- PydanticValueError,
20
- )
17
+ from pydantic_core import PydanticCustomError
21
18
 
22
-
23
- class CardBinValidationError(PydanticValueError):
19
+ class CardBinValidationError(PydanticCustomError):
24
20
  code = 'payment_card_number.bin'
25
21
  msg_template = (
26
22
  'The card number contains a BIN (first six digits) that does not have'
@@ -30,10 +26,6 @@ class CardBinValidationError(PydanticValueError):
30
26
  )
31
27
 
32
28
 
33
- class NotDigitError(PydanticNotDigitError):
34
- code = 'digits'
35
- msg_template = 'value is not all digits'
36
-
37
29
 
38
30
  class CuencaError(Exception):
39
31
  """Exceptions related to ApiKeys, Login, Password, etc"""
@@ -118,3 +110,5 @@ ERROR_CODES = {
118
110
  InvalidOTPCodeError,
119
111
  ]
120
112
  }
113
+
114
+ '''
@@ -52,7 +52,6 @@ __all__ = [
52
52
  'PageSize',
53
53
  'PartnerRequest',
54
54
  'PartnerUpdateRequest',
55
- 'PaymentCardNumber',
56
55
  'PhoneNumber',
57
56
  'PlatformRequest',
58
57
  'PlatformType',
@@ -108,7 +107,7 @@ __all__ = [
108
107
  'get_state_name',
109
108
  ]
110
109
 
111
- from .card import PaymentCardNumber, StrictPaymentCardNumber
110
+ from .card import StrictPaymentCardNumber
112
111
  from .enums import (
113
112
  AuthorizerTransaction,
114
113
  BankAccountStatus,
@@ -182,7 +181,6 @@ from .queries import (
182
181
  EventQuery,
183
182
  FileQuery,
184
183
  IdentityQuery,
185
- PageSize,
186
184
  QueryParams,
187
185
  SessionQuery,
188
186
  StatementQuery,
@@ -1,53 +1,26 @@
1
- from typing import TYPE_CHECKING, ClassVar
2
-
3
- from pydantic.types import PaymentCardNumber as PydanticPaymentCardNumber
4
- from pydantic.validators import (
5
- constr_length_validator,
6
- constr_strip_whitespace,
7
- str_validator,
8
- )
1
+ from pydantic import BaseModel, field_validator
2
+ from pydantic_core import PydanticCustomError
3
+ from pydantic_extra_types.payment import PaymentCardBrand, PaymentCardNumber
9
4
 
10
5
  from ..card_bins import CARD_BINS
11
- from ..errors import CardBinValidationError
12
-
13
- if TYPE_CHECKING:
14
- from pydantic.typing import CallableGenerator
15
-
16
-
17
- class PaymentCardNumber(PydanticPaymentCardNumber):
18
- min_length: ClassVar[int] = 16
19
- max_length: ClassVar[int] = 16
20
-
21
- def __init__(self, card_number: str):
22
- self.bin = card_number[:6]
23
- self.last4 = card_number[-4:]
24
- self.brand = self._get_brand(card_number)
25
- self.bank_code = CARD_BINS.get(self.bin)
26
6
 
27
- @classmethod
28
- def __get_validators__(cls) -> 'CallableGenerator':
29
- yield str_validator
30
- yield constr_strip_whitespace
31
- yield constr_length_validator
32
- yield cls.validate_digits
33
- yield cls.validate_luhn_check_digit
34
- yield cls
35
7
 
8
+ class StrictPaymentCardNumber(BaseModel):
36
9
 
37
- class StrictPaymentCardNumber(PaymentCardNumber):
38
- """
39
- requires that the BIN be associated to a known BIN for a Mexican bank
40
- """
10
+ card_number: PaymentCardNumber
41
11
 
42
- bank_code: str
43
-
44
- @classmethod
45
- def __get_validators__(cls) -> 'CallableGenerator':
46
- yield from PaymentCardNumber.__get_validators__()
47
- yield cls.validate_bin
48
-
49
- @classmethod
12
+ @field_validator('card_number')
50
13
  def validate_bin(cls, card_number: PaymentCardNumber) -> PaymentCardNumber:
51
- if card_number.bank_code is None:
52
- raise CardBinValidationError
14
+ if card_number.bin not in CARD_BINS:
15
+ raise PydanticCustomError(
16
+ 'payment_card_number.bin', 'Invalid BIN: Bank code not found.'
17
+ )
53
18
  return card_number
19
+
20
+ @property
21
+ def brand(self) -> PaymentCardBrand:
22
+ return self.card_number.brand
23
+
24
+ @property
25
+ def bank_code(self) -> str:
26
+ return CARD_BINS[self.card_number.bin]
@@ -570,6 +570,7 @@ class UserStatus(str, Enum):
570
570
  in_review = 'in_review'
571
571
  pld_blocked = 'pld_blocked'
572
572
  inactive = 'inactive'
573
+ user_blocked = 'user_blocked'
573
574
 
574
575
 
575
576
  class SessionType(str, Enum):
@@ -678,8 +679,3 @@ class SATRegimeCode(str, Enum):
678
679
  ING_PREM = "615" # Régimen de los ingresos por obtención de premios
679
680
  AE_PLAT_TEC = "625" # Régimen de las Actividades Empresariales con ingresos a través de Plataformas Tecnológicas # noqa: E501
680
681
  RS_CONF = "626" # Régimen Simplificado de Confianza
681
-
682
-
683
- class PasswordType(str, Enum):
684
- numeric = 'numeric'
685
- alphanumeric = 'alphanumeric'
@@ -1,12 +1,12 @@
1
1
  from typing import Optional
2
2
 
3
- from pydantic import BaseModel, HttpUrl
3
+ from pydantic import AnyHttpUrl, BaseModel
4
4
 
5
5
  from .enums import KYCFileType
6
6
 
7
7
 
8
8
  class BatchFileMetadata(BaseModel):
9
- id: Optional[str]
9
+ id: Optional[str] = None
10
10
  is_back: bool
11
11
  type: KYCFileType
12
- url: HttpUrl
12
+ url: AnyHttpUrl
@@ -1,14 +1,12 @@
1
1
  import json
2
- from typing import TYPE_CHECKING, Optional, Type
2
+ from typing import Generator, Optional
3
3
 
4
- from pydantic import ConstrainedFloat, ConstrainedInt, ConstrainedStr
4
+ from pydantic import BeforeValidator, Field
5
+ from typing_extensions import Annotated
5
6
 
6
- from ..validators import sanitize_dict, sanitize_item, validate_digits
7
+ from ..validators import sanitize_dict, sanitize_item
7
8
  from .enums import State
8
9
 
9
- if TYPE_CHECKING:
10
- from pydantic.typing import CallableGenerator
11
-
12
10
 
13
11
  class SantizedDict(dict):
14
12
  def __init__(self, *args, **kwargs):
@@ -21,43 +19,63 @@ class JSONEncoder(json.JSONEncoder):
21
19
  return sanitize_item(o, default=super().default)
22
20
 
23
21
 
24
- class StrictPositiveInt(ConstrainedInt):
25
- """
26
- - strict: ensures a float isn't passed in by accident
27
- - gt (greater than): ensures the value is strictly above 0
28
- """
22
+ def validate_strict_positive_int(value: int) -> int:
23
+ if not isinstance(value, int):
24
+ raise ValueError("Value must be an integer")
25
+ if value <= 0:
26
+ raise ValueError("Value must be greater than 0")
27
+ if value > 21_474_836_47:
28
+ raise ValueError("Value must be less than 21_474_836_47")
29
+ return value
29
30
 
30
- strict = True
31
- gt = 0
32
- le = 21_474_836_47 # Max value in DB
33
31
 
34
- ...
32
+ StrictPositiveInt = Annotated[
33
+ int, BeforeValidator(validate_strict_positive_int)
34
+ ]
35
35
 
36
36
 
37
- class StrictPositiveFloat(ConstrainedFloat):
38
- """
39
- - strict: ensures an integer isn't passed in by accident
40
- - ge (greater than or equal): ensures the value is above 0
41
- """
37
+ def validate_strict_positive_float(value: float) -> float:
38
+ if not isinstance(value, float):
39
+ raise ValueError("Value must be a float")
40
+ if value <= 0:
41
+ raise ValueError("Value must be greater than 0")
42
+ return value
42
43
 
43
- strict = True
44
- ge = 0
45
44
 
46
- ...
45
+ StrictPositiveFloat = Annotated[
46
+ float, BeforeValidator(validate_strict_positive_float)
47
+ ]
47
48
 
48
49
 
49
- def digits(
50
- min_length: Optional[int] = None, max_length: Optional[int] = None
51
- ) -> Type[str]:
52
- namespace = dict(min_length=min_length, max_length=max_length)
53
- return type('DigitsValue', (Digits,), namespace)
50
+ # Clase base para validación
51
+ class Digits:
52
+ min_length: Optional[int] = None
53
+ max_length: Optional[int] = None
54
54
 
55
+ @classmethod
56
+ def __get_validators__(cls) -> Generator:
57
+ yield cls.validate_digits
55
58
 
56
- class Digits(ConstrainedStr):
57
59
  @classmethod
58
- def __get_validators__(cls) -> 'CallableGenerator':
59
- yield from ConstrainedStr.__get_validators__()
60
- yield validate_digits
60
+ def validate_digits(cls, value: str) -> str:
61
+ if not value.isdigit():
62
+ raise ValueError("Value must contain only digits.")
63
+ if cls.min_length is not None and len(value) < cls.min_length:
64
+ raise ValueError(
65
+ f"Value must have at least {cls.min_length} characters."
66
+ )
67
+ if cls.max_length is not None and len(value) > cls.max_length:
68
+ raise ValueError(
69
+ f"Value must have at most {cls.max_length} characters."
70
+ )
71
+ return value
72
+
73
+
74
+ # Función para crear tipos personalizados
75
+ def digits(min_length: Optional[int] = None, max_length: Optional[int] = None):
76
+ return Annotated[
77
+ str, Field(min_length=min_length, max_length=max_length), Digits
78
+ ]
61
79
 
62
80
 
63
81
  names_state = {
@@ -2,8 +2,13 @@ import datetime as dt
2
2
  import re
3
3
  from typing import Any, Dict, List, Optional
4
4
 
5
- from pydantic import BaseModel, IPvAnyAddress
6
- from pydantic.class_validators import root_validator
5
+ from pydantic import (
6
+ BaseModel,
7
+ ConfigDict,
8
+ Field,
9
+ IPvAnyAddress,
10
+ model_validator,
11
+ )
7
12
  from pydantic.types import StrictStr
8
13
 
9
14
  from .enums import Country, KYCFileType, State, VerificationStatus
@@ -14,12 +19,34 @@ class PhoneNumber(StrictStr):
14
19
  max_length = 15
15
20
  regex = re.compile(r'^\+?[0-9]{10,14}$')
16
21
 
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
+
17
33
 
18
34
  class CurpField(StrictStr):
19
35
  min_length = 18
20
36
  max_length = 18
21
37
  regex = re.compile(r'^[A-Z]{4}[0-9]{6}[A-Z]{6}[A-Z|0-9][0-9]$')
22
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
+ }
49
+
23
50
 
24
51
  class Rfc(StrictStr):
25
52
  min_length = 12
@@ -42,9 +69,8 @@ class Address(BaseModel):
42
69
  country: Optional[Country] = None
43
70
  city: Optional[str] = None
44
71
  full_name: Optional[str] = None
45
-
46
- class Config:
47
- schema_extra = {
72
+ model_config = ConfigDict(
73
+ json_schema_extra={
48
74
  "example": {
49
75
  "street": "Reforma",
50
76
  "ext_number": "265",
@@ -56,8 +82,10 @@ class Address(BaseModel):
56
82
  "city": "Cuauhtémoc",
57
83
  }
58
84
  }
85
+ )
59
86
 
60
- @root_validator()
87
+ @model_validator(mode='before')
88
+ @classmethod
61
89
  def full_name_complete(cls, values: Dict[str, Any]) -> Dict[str, Any]:
62
90
  if values.get('full_name'):
63
91
  return values
@@ -74,9 +102,8 @@ class Beneficiary(BaseModel):
74
102
  phone_number: PhoneNumber
75
103
  user_relationship: str
76
104
  percentage: int
77
-
78
- class Config:
79
- schema_extra = {
105
+ model_config = ConfigDict(
106
+ json_schema_extra={
80
107
  "example": {
81
108
  "name": "Juan Perez",
82
109
  "birth_date": "1907-07-06",
@@ -85,74 +112,58 @@ class Beneficiary(BaseModel):
85
112
  "percentage": 100,
86
113
  }
87
114
  }
115
+ )
88
116
 
89
117
 
90
118
  class VerificationErrors(BaseModel):
91
- identifier: str
92
- error: str
93
- code: str
94
- message: Optional[str]
95
-
96
- class Config:
97
- fields = {
98
- 'identifier': {
99
- 'description': 'Unique identifier for the step validation'
100
- },
101
- 'error': {
102
- 'description': 'Error throwed on validation,'
103
- ' can be StepError or SystemError in case of '
104
- 'KYCProvider intermittence'
105
- },
106
- 'code': {
107
- 'description': 'Specific code of the failure in the step.'
108
- },
109
- 'message': {'description': 'Error description'},
110
- }
111
-
112
- schema_extra = {
119
+ identifier: str = Field(
120
+ ..., description='Unique identifier for the step validation'
121
+ )
122
+ error: str = Field(
123
+ ...,
124
+ description='Error throwed on validation,'
125
+ ' can be StepError or SystemError in case of '
126
+ 'KYCProvider intermittence',
127
+ )
128
+ code: str = Field(
129
+ ..., description='Specific code of the failure in the step.'
130
+ )
131
+ message: Optional[str] = Field(None, description='Error description')
132
+ model_config = ConfigDict(
133
+ json_schema_extra={
113
134
  "example": {
114
135
  "identifier": "age-check",
115
136
  "error": 'StepError',
116
137
  "code": "underage.noDOB",
117
138
  "message": "The date of birth could not be obtained",
118
139
  }
119
- }
140
+ },
141
+ )
120
142
 
121
143
 
122
144
  class KYCFile(BaseModel):
123
145
  type: KYCFileType
124
- uri_front: str
125
- uri_back: Optional[str] = None
146
+ uri_front: str = Field(..., description='API uri to fetch the file')
147
+ uri_back: Optional[str] = Field(
148
+ None, description='API uri to fetch the file'
149
+ )
126
150
  is_mx: bool = True
127
151
  data: Optional[dict] = None
128
- status: Optional[VerificationStatus] = None
129
- errors: Optional[List[VerificationErrors]]
130
- verification_id: Optional[str]
131
- attempt: Optional[int]
132
-
133
- class Config:
134
- fields = {
135
- 'uri_front': {'description': 'API uri to fetch the file'},
136
- 'uri_back': {'description': 'API uri to fetch the file'},
137
- 'status': {
138
- 'description': 'The status of the file depends '
139
- 'on KYCValidation'
140
- },
141
- 'errors': {
142
- 'description': 'List of document errors found '
143
- 'during kyc validation'
144
- },
145
- 'attempt': {
146
- 'description': 'The number of kyc_validation '
147
- 'intents for this docuemnt'
148
- },
149
- 'verification_id': {
150
- 'description': 'The provider identifier of the '
151
- 'validation result'
152
- },
153
- }
154
-
155
- schema_extra = {
152
+ status: Optional[VerificationStatus] = Field(
153
+ None, description='The status of the file depends on KYCValidation'
154
+ )
155
+ errors: Optional[List[VerificationErrors]] = Field(
156
+ None, description='List of document errors found during kyc validation'
157
+ )
158
+ verification_id: Optional[str] = Field(
159
+ None, description='The provider identifier of the validation result'
160
+ )
161
+ attempt: Optional[int] = Field(
162
+ None,
163
+ description='The number of kyc_validation intents for this document',
164
+ )
165
+ model_config = ConfigDict(
166
+ json_schema_extra={
156
167
  "example": {
157
168
  "type": "ine",
158
169
  "is_mx": True,
@@ -162,19 +173,20 @@ class KYCFile(BaseModel):
162
173
  "status": "created",
163
174
  "errors": [],
164
175
  }
165
- }
176
+ },
177
+ )
166
178
 
167
179
 
168
180
  class TOSAgreement(BaseModel):
169
181
  version: str
170
182
  ip: IPvAnyAddress
171
- location: Optional[str]
172
-
173
- class Config:
174
- schema_extra = {
183
+ location: Optional[str] = None
184
+ model_config = ConfigDict(
185
+ json_schema_extra={
175
186
  "example": {
176
187
  "version": "2022-01-01",
177
188
  "ip": "192.168.0.1",
178
189
  "location": "19.427224, -99.168082",
179
190
  }
180
191
  }
192
+ )
@@ -49,6 +49,8 @@ class VulnerableActivityDetails(BaseModel):
49
49
 
50
50
 
51
51
  class PhysicalPerson(BaseModel):
52
+ model_config = dict(arbitrary_types_allowed=True)
53
+
52
54
  names: str
53
55
  first_surname: str
54
56
  second_surname: Optional[str] = None
@@ -1,8 +1,14 @@
1
1
  import datetime as dt
2
2
  from typing import Optional
3
3
 
4
- from pydantic import BaseModel, EmailStr, Extra, validator
5
- from pydantic.types import ConstrainedInt, PositiveInt
4
+ from pydantic import (
5
+ BaseModel,
6
+ ConfigDict,
7
+ EmailStr,
8
+ PositiveInt,
9
+ conint,
10
+ field_validator,
11
+ )
6
12
 
7
13
  from ..typing import DictStrAny
8
14
  from ..validators import sanitize_dict
@@ -23,14 +29,11 @@ from .identities import CurpField
23
29
  MAX_PAGE_SIZE = 100
24
30
 
25
31
 
26
- class PageSize(ConstrainedInt):
27
- gt = 0
28
- le = MAX_PAGE_SIZE
29
-
30
-
31
32
  class QueryParams(BaseModel):
32
33
  count: bool = False
33
- page_size: PageSize = PageSize(MAX_PAGE_SIZE)
34
+ page_size: conint( # type: ignore[valid-type]
35
+ gt=0, le=MAX_PAGE_SIZE
36
+ ) = MAX_PAGE_SIZE # Add default value
34
37
  limit: Optional[PositiveInt] = None
35
38
  user_id: Optional[str] = None
36
39
  created_before: Optional[dt.datetime] = None
@@ -38,9 +41,9 @@ class QueryParams(BaseModel):
38
41
  related_transaction: Optional[str] = None
39
42
  platform_id: Optional[str] = None
40
43
 
41
- class Config:
42
- extra = Extra.forbid # raise ValidationError if there are extra fields
43
- fields = {
44
+ model_config = ConfigDict(
45
+ extra="forbid",
46
+ json_schema_extra={
44
47
  'count': {'description': 'Set `true` value to get only a counter'},
45
48
  'page_size': {'description': 'Number of items per page'},
46
49
  'limit': {'description': 'Limit of items to query'},
@@ -54,7 +57,8 @@ class QueryParams(BaseModel):
54
57
  'or greater than this value, this field represents the min '
55
58
  'creation date.'
56
59
  },
57
- }
60
+ },
61
+ )
58
62
 
59
63
  def dict(self, *args, **kwargs) -> DictStrAny:
60
64
  kwargs.setdefault('exclude_none', True)
@@ -93,14 +97,32 @@ class CardTransactionQuery(TransactionQuery):
93
97
  class ApiKeyQuery(QueryParams):
94
98
  active: Optional[bool] = None
95
99
 
96
- class Config(QueryParams.Config):
97
- fields = {
98
- **QueryParams.Config.fields,
99
- 'active': {
100
- 'description': 'Set `true` value to fetch active keys or '
101
- '`false` to fetch deactivated keys'
102
- },
103
- }
100
+ model_config = ConfigDict(
101
+ extra="forbid",
102
+ json_schema_extra={
103
+ 'properties': {
104
+ 'active': {
105
+ 'description': 'Set `true` value to fetch active keys or '
106
+ '`false` to fetch deactivated keys'
107
+ },
108
+ 'count': {
109
+ 'description': 'Set `true` value to get only a counter'
110
+ },
111
+ 'page_size': {'description': 'Number of items per page'},
112
+ 'limit': {'description': 'Limit of items to query'},
113
+ 'created_before': {
114
+ 'description': 'Filtered items have a `created_at` '
115
+ 'date equal or lower than this value, this field '
116
+ 'represents the max creation date.'
117
+ },
118
+ 'created_after': {
119
+ 'description': 'Filtered items have a `created_at` '
120
+ 'date equal or greater than this value, this field '
121
+ 'represents the min creation date.'
122
+ },
123
+ }
124
+ },
125
+ )
104
126
 
105
127
 
106
128
  class CardQuery(QueryParams):
@@ -116,10 +138,15 @@ class CardQuery(QueryParams):
116
138
  status: Optional[CardStatus] = None
117
139
  type: Optional[CardType] = None
118
140
 
119
- @validator('exp_month', 'exp_year', 'cvv2', 'cvv')
120
- def query_by_exp_cvv_if_number_set(cls, v, values):
121
- if not values['number']:
122
- raise ValueError('Number must be set to query by exp or cvv')
141
+ @field_validator('exp_month', 'exp_year', 'cvv2', 'cvv')
142
+ def query_by_exp_cvv_if_number_set(cls, v, info):
143
+ if not info.data.get('number'):
144
+ raise ValueError(
145
+ '''
146
+ exp_month, exp_year, cvv and cvv2 can
147
+ only be set when number is provided
148
+ '''
149
+ )
123
150
  return v
124
151
 
125
152
 
@@ -127,9 +154,9 @@ class StatementQuery(QueryParams):
127
154
  year: int
128
155
  month: int
129
156
 
130
- @validator('month')
131
- def validate_year_month(cls, month, values):
132
- year = values['year']
157
+ @field_validator('month')
158
+ def validate_year_month(cls, month, info):
159
+ year = info.data['year']
133
160
  month_now = dt.date.today().replace(day=1)
134
161
  month_set = dt.date(year, month, 1)
135
162
  if month_set >= month_now:
@@ -163,6 +190,8 @@ class UserQuery(QueryParams):
163
190
 
164
191
 
165
192
  class IdentityQuery(QueryParams):
193
+ model_config = dict(arbitrary_types_allowed=True)
194
+
166
195
  curp: Optional[CurpField] = None
167
196
  rfc: Optional[str] = None
168
197
  status: Optional[UserStatus] = None