cuenca-validations 2.0.0.dev4__tar.gz → 2.0.0.dev5__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 (32) hide show
  1. {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/PKG-INFO +1 -1
  2. {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/errors.py +0 -13
  3. {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/types/card.py +12 -0
  4. {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/types/general.py +13 -28
  5. {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/types/identities.py +10 -0
  6. {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/validators.py +5 -0
  7. cuenca_validations-2.0.0.dev5/cuenca_validations/version.py +1 -0
  8. {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations.egg-info/PKG-INFO +1 -1
  9. {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations.egg-info/SOURCES.txt +1 -0
  10. {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/tests/test_card.py +5 -6
  11. cuenca_validations-2.0.0.dev5/tests/test_errors.py +59 -0
  12. {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/tests/test_types.py +79 -9
  13. cuenca_validations-2.0.0.dev4/cuenca_validations/version.py +0 -1
  14. {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/LICENSE +0 -0
  15. {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/README.md +0 -0
  16. {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/__init__.py +0 -0
  17. {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/card_bins.py +0 -0
  18. {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/py.typed +0 -0
  19. {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/types/__init__.py +0 -0
  20. {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/types/enums.py +0 -0
  21. {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/types/files.py +0 -0
  22. {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/types/morals.py +0 -0
  23. {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/types/queries.py +0 -0
  24. {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/types/requests.py +0 -0
  25. {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/typing.py +0 -0
  26. {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations.egg-info/dependency_links.txt +0 -0
  27. {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations.egg-info/requires.txt +0 -0
  28. {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations.egg-info/top_level.txt +0 -0
  29. {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/setup.cfg +0 -0
  30. {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/setup.py +0 -0
  31. {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/tests/__init__.py +0 -0
  32. {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/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.dev4
3
+ Version: 2.0.0.dev5
4
4
  Summary: Cuenca common validations
5
5
  Home-page: https://github.com/cuenca-mx/cuenca-validations
6
6
  Author: Cuenca
@@ -1,7 +1,6 @@
1
1
  __all__ = [
2
2
  'ApiError',
3
3
  'AuthMethodNotAllowedError',
4
- 'CardBinValidationError',
5
4
  'CuencaError',
6
5
  'ERROR_CODES',
7
6
  'InvalidOTPCodeError',
@@ -13,18 +12,6 @@ __all__ = [
13
12
  'WrongCredsError',
14
13
  ]
15
14
 
16
- from pydantic_core import PydanticCustomError
17
-
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
15
 
29
16
  class CuencaError(Exception):
30
17
  """Exceptions related to ApiKeys, Login, Password, etc"""
@@ -21,6 +21,18 @@ class StrictPaymentCardNumber(BaseModel):
21
21
  def brand(self) -> PaymentCardBrand:
22
22
  return self.card_number.brand
23
23
 
24
+ @property
25
+ def last4(self) -> str:
26
+ return self.card_number.last4
27
+
28
+ @property
29
+ def masked(self) -> str:
30
+ return self.card_number.masked
31
+
32
+ @property
33
+ def bin(self) -> str:
34
+ return self.card_number.bin
35
+
24
36
  @property
25
37
  def bank_code(self) -> str:
26
38
  return CARD_BINS[self.card_number.bin]
@@ -1,5 +1,5 @@
1
1
  import json
2
- from typing import Generator, Optional
2
+ from typing import Any, Optional
3
3
 
4
4
  from pydantic import BeforeValidator, Field
5
5
  from typing_extensions import Annotated
@@ -47,34 +47,19 @@ StrictPositiveFloat = Annotated[
47
47
  ]
48
48
 
49
49
 
50
- # Clase base para validación
51
- class Digits:
52
- min_length: Optional[int] = None
53
- max_length: Optional[int] = None
54
-
55
- @classmethod
56
- def __get_validators__(cls) -> Generator:
57
- yield cls.validate_digits
58
-
59
- @classmethod
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):
50
+ def validate_only_digits(value: str) -> str:
51
+ if not value.isdigit():
52
+ raise ValueError("Value must contain only digits")
53
+ return value
54
+
55
+
56
+ def digits(
57
+ min_length: Optional[int] = None, max_length: Optional[int] = None
58
+ ) -> Any:
76
59
  return Annotated[
77
- str, Field(min_length=min_length, max_length=max_length), Digits
60
+ str,
61
+ BeforeValidator(validate_only_digits),
62
+ Field(min_length=min_length, max_length=max_length),
78
63
  ]
79
64
 
80
65
 
@@ -58,6 +58,16 @@ class Rfc(StrictStr):
58
58
  raise ValueError('Not a valid RFC.')
59
59
  return cls(rfc)
60
60
 
61
+ @classmethod
62
+ def __get_pydantic_core_schema__(
63
+ cls, source_type: Any, handler: Any
64
+ ) -> Dict[str, Any]:
65
+ return {
66
+ 'type': 'str',
67
+ 'min_length': cls.min_length,
68
+ 'max_length': cls.max_length,
69
+ }
70
+
61
71
 
62
72
  class Address(BaseModel):
63
73
  street: Optional[str] = None
@@ -4,6 +4,7 @@ from enum import Enum
4
4
  from typing import Any, Callable, List, Optional, Union
5
5
 
6
6
  from dateutil.relativedelta import relativedelta
7
+ from pydantic import AnyUrl, HttpUrl
7
8
 
8
9
 
9
10
  def sanitize_dict(d: dict) -> dict:
@@ -26,6 +27,10 @@ def sanitize_item(
26
27
  rv = item.astimezone(dt.timezone.utc).isoformat()
27
28
  else:
28
29
  rv = item.isoformat()
30
+ elif isinstance(item, HttpUrl):
31
+ rv = str(item)
32
+ elif isinstance(item, AnyUrl):
33
+ rv = str(item)
29
34
  elif isinstance(item, list):
30
35
  rv = [
31
36
  sanitize_dict(e) if isinstance(e, dict) else sanitize_item(e)
@@ -0,0 +1 @@
1
+ __version__ = '2.0.0.dev5'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cuenca_validations
3
- Version: 2.0.0.dev4
3
+ Version: 2.0.0.dev5
4
4
  Summary: Cuenca common validations
5
5
  Home-page: https://github.com/cuenca-mx/cuenca-validations
6
6
  Author: Cuenca
@@ -25,5 +25,6 @@ cuenca_validations/types/queries.py
25
25
  cuenca_validations/types/requests.py
26
26
  tests/__init__.py
27
27
  tests/test_card.py
28
+ tests/test_errors.py
28
29
  tests/test_statement.py
29
30
  tests/test_types.py
@@ -10,15 +10,14 @@ INVALID_BIN = '4050000000000001'
10
10
 
11
11
  def test_invalid_bin_strict_payment():
12
12
  with pytest.raises(ValidationError) as exc_info:
13
- card = StrictPaymentCardNumber(card_number=INVALID_BIN)
14
- print(card.card_number.bin)
13
+ StrictPaymentCardNumber(card_number=INVALID_BIN)
15
14
  assert 'Invalid BIN: Bank code not found.' in str(exc_info.value)
16
15
 
17
16
 
18
17
  def test_valid_bin_strict_payment():
19
18
  card = StrictPaymentCardNumber(card_number=VALID_BBVA)
20
- assert card.card_number.brand == PaymentCardBrand.visa
21
- assert card.card_number.bin == '477213'
22
- assert card.card_number.last4 == '0003'
23
- assert card.card_number.masked == '477213******0003'
19
+ assert card.brand == PaymentCardBrand.visa
20
+ assert card.bin == '477213'
21
+ assert card.last4 == '0003'
22
+ assert card.masked == '477213******0003'
24
23
  assert card.bank_code == '40012'
@@ -0,0 +1,59 @@
1
+ import pytest
2
+
3
+ from cuenca_validations.errors import (
4
+ ERROR_CODES,
5
+ ApiError,
6
+ AuthMethodNotAllowedError,
7
+ CuencaError,
8
+ InvalidOTPCodeError,
9
+ MissingAuthorizationHeaderError,
10
+ NoPasswordFoundError,
11
+ TooManyAttemptsError,
12
+ UserLocationError,
13
+ UserNotLoggedInError,
14
+ WrongCredsError,
15
+ )
16
+
17
+
18
+ def test_cuenca_error_base():
19
+ assert issubclass(CuencaError, Exception)
20
+
21
+
22
+ def test_error_codes_and_status():
23
+ test_cases = [
24
+ (WrongCredsError, 101, 401),
25
+ (MissingAuthorizationHeaderError, 102, 401),
26
+ (UserNotLoggedInError, 103, 401),
27
+ (NoPasswordFoundError, 104, 401),
28
+ (AuthMethodNotAllowedError, 106, 401),
29
+ (TooManyAttemptsError, 107, 403),
30
+ (UserLocationError, 108, 401),
31
+ (InvalidOTPCodeError, 109, 401),
32
+ (ApiError, 500, 500),
33
+ ]
34
+
35
+ for error_class, expected_code, expected_status in test_cases:
36
+ assert error_class.code == expected_code
37
+ assert error_class.status_code == expected_status
38
+
39
+
40
+ def test_error_messages():
41
+ test_message = "Mensaje de prueba"
42
+
43
+ for error_class in ERROR_CODES.values():
44
+ with pytest.raises(error_class) as exc_info:
45
+ raise error_class(test_message)
46
+ assert str(exc_info.value) == test_message
47
+
48
+
49
+ def test_error_codes_mapping():
50
+ for error_class in ERROR_CODES.values():
51
+ assert ERROR_CODES[error_class.code] == error_class
52
+
53
+
54
+ def test_api_error():
55
+ with pytest.raises(ApiError) as exc_info:
56
+ raise ApiError("Error interno")
57
+ assert str(exc_info.value) == "Error interno"
58
+ assert exc_info.value.code == 500
59
+ assert exc_info.value.status_code == 500
@@ -5,7 +5,7 @@ from enum import Enum
5
5
 
6
6
  import pytest
7
7
  from freezegun import freeze_time
8
- from pydantic import BaseModel, Field, ValidationError
8
+ from pydantic import AnyUrl, BaseModel, HttpUrl, ValidationError
9
9
 
10
10
  from cuenca_validations.types import (
11
11
  Address,
@@ -16,6 +16,7 @@ from cuenca_validations.types import (
16
16
  SantizedDict,
17
17
  SessionRequest,
18
18
  TransactionStatus,
19
+ digits,
19
20
  get_state_name,
20
21
  )
21
22
  from cuenca_validations.types.enums import (
@@ -24,6 +25,10 @@ from cuenca_validations.types.enums import (
24
25
  SessionType,
25
26
  State,
26
27
  )
28
+ from cuenca_validations.types.general import (
29
+ StrictPositiveFloat,
30
+ StrictPositiveInt,
31
+ )
27
32
  from cuenca_validations.types.requests import (
28
33
  ApiKeyUpdateRequest,
29
34
  BankAccountValidationRequest,
@@ -127,6 +132,17 @@ def test_json_encoder(value, result):
127
132
  assert decoded['value'] == result
128
133
 
129
134
 
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
+
130
146
  def test_invalid_class():
131
147
  """
132
148
  For a class that doesn't have a `to_dict` method and it is not a type of
@@ -143,9 +159,7 @@ def test_invalid_class():
143
159
 
144
160
 
145
161
  class Accounts(BaseModel):
146
- number: str = Field(
147
- min_length=5, max_length=8, pattern=r'^\d+$' # Only allows digits
148
- )
162
+ number: digits(5, 8) # type: ignore
149
163
 
150
164
 
151
165
  def test_only_digits():
@@ -156,15 +170,15 @@ def test_only_digits():
156
170
  @pytest.mark.parametrize(
157
171
  'number, error',
158
172
  [
159
- ('123', 'String should have at least 5 characters'),
160
- ('1234567890', 'String should have at most 8 characters'),
161
- ('no_123', 'String should match pattern'),
173
+ ('123', 'Value should have at least 5 items after validation'),
174
+ ('1234567890', 'Value should have at most 8 items after validation'),
175
+ ('no_123', 'Value must contain only digits'),
162
176
  ],
163
177
  )
164
178
  def test_invalid_digits(number, error):
165
- with pytest.raises(ValueError) as exception:
179
+ with pytest.raises(ValidationError) as exception:
166
180
  Accounts(number=number)
167
- assert error in str(exception)
181
+ assert error in str(exception.value)
168
182
 
169
183
 
170
184
  def test_card_query_exp_cvv_if_number_set():
@@ -566,3 +580,59 @@ def test_user_lists_request():
566
580
  UserListsRequest(names='Pedro', first_surname='Paramo')
567
581
  with pytest.raises(ValueError):
568
582
  UserListsRequest()
583
+
584
+
585
+ class TestFloatModel(BaseModel):
586
+ value: StrictPositiveFloat
587
+
588
+
589
+ def test_strict_positive_float_valid():
590
+ model = TestFloatModel(value=10.5)
591
+ assert model.value == 10.5
592
+ model = TestFloatModel(value=0.000001)
593
+ assert model.value == 0.000001
594
+
595
+
596
+ def test_strict_positive_float_invalid():
597
+ with pytest.raises(ValueError, match="Value must be greater than 0"):
598
+ TestFloatModel(value=0.0)
599
+ with pytest.raises(ValueError, match="Value must be greater than 0"):
600
+ TestFloatModel(value=-1.5)
601
+ with pytest.raises(ValueError, match="Value must be a float"):
602
+ TestFloatModel(value=5)
603
+ with pytest.raises(ValueError, match="Value must be a float"):
604
+ TestFloatModel(value="10.5")
605
+
606
+
607
+ class TestIntModel(BaseModel):
608
+ value: StrictPositiveInt
609
+
610
+
611
+ def test_strict_positive_int_valid():
612
+ model = TestIntModel(value=100)
613
+ assert model.value == 100
614
+
615
+ model = TestIntModel(value=1)
616
+ assert model.value == 1
617
+
618
+ model = TestIntModel(value=21_474_836_47)
619
+ assert model.value == 21_474_836_47
620
+
621
+
622
+ def test_strict_positive_int_invalid():
623
+ with pytest.raises(ValueError, match="Value must be greater than 0"):
624
+ TestIntModel(value=0)
625
+
626
+ with pytest.raises(ValueError, match="Value must be greater than 0"):
627
+ TestIntModel(value=-5)
628
+
629
+ with pytest.raises(
630
+ ValueError, match="Value must be less than 21_474_836_47"
631
+ ):
632
+ TestIntModel(value=21_474_836_48)
633
+
634
+ with pytest.raises(ValueError, match="Value must be an integer"):
635
+ TestIntModel(value=5.5)
636
+
637
+ with pytest.raises(ValueError, match="Value must be an integer"):
638
+ TestIntModel(value="10")
@@ -1 +0,0 @@
1
- __version__ = '2.0.0.dev4'