cuenca-validations 2.1.27__tar.gz → 2.1.28__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 (36) hide show
  1. {cuenca_validations-2.1.27 → cuenca_validations-2.1.28}/PKG-INFO +1 -1
  2. {cuenca_validations-2.1.27 → cuenca_validations-2.1.28}/cuenca_validations/types/requests.py +21 -7
  3. {cuenca_validations-2.1.27 → cuenca_validations-2.1.28}/cuenca_validations/validators.py +30 -0
  4. cuenca_validations-2.1.28/cuenca_validations/version.py +1 -0
  5. {cuenca_validations-2.1.27 → cuenca_validations-2.1.28}/cuenca_validations.egg-info/PKG-INFO +1 -1
  6. {cuenca_validations-2.1.27 → cuenca_validations-2.1.28}/cuenca_validations.egg-info/SOURCES.txt +2 -1
  7. {cuenca_validations-2.1.27 → cuenca_validations-2.1.28}/tests/test_requests.py +29 -0
  8. cuenca_validations-2.1.28/tests/test_validators.py +35 -0
  9. cuenca_validations-2.1.27/cuenca_validations/version.py +0 -1
  10. {cuenca_validations-2.1.27 → cuenca_validations-2.1.28}/LICENSE +0 -0
  11. {cuenca_validations-2.1.27 → cuenca_validations-2.1.28}/README.md +0 -0
  12. {cuenca_validations-2.1.27 → cuenca_validations-2.1.28}/cuenca_validations/__init__.py +0 -0
  13. {cuenca_validations-2.1.27 → cuenca_validations-2.1.28}/cuenca_validations/card_bins.py +0 -0
  14. {cuenca_validations-2.1.27 → cuenca_validations-2.1.28}/cuenca_validations/errors.py +0 -0
  15. {cuenca_validations-2.1.27 → cuenca_validations-2.1.28}/cuenca_validations/py.typed +0 -0
  16. {cuenca_validations-2.1.27 → cuenca_validations-2.1.28}/cuenca_validations/types/__init__.py +0 -0
  17. {cuenca_validations-2.1.27 → cuenca_validations-2.1.28}/cuenca_validations/types/card.py +0 -0
  18. {cuenca_validations-2.1.27 → cuenca_validations-2.1.28}/cuenca_validations/types/enums.py +0 -0
  19. {cuenca_validations-2.1.27 → cuenca_validations-2.1.28}/cuenca_validations/types/files.py +0 -0
  20. {cuenca_validations-2.1.27 → cuenca_validations-2.1.28}/cuenca_validations/types/general.py +0 -0
  21. {cuenca_validations-2.1.27 → cuenca_validations-2.1.28}/cuenca_validations/types/helpers.py +0 -0
  22. {cuenca_validations-2.1.27 → cuenca_validations-2.1.28}/cuenca_validations/types/identities.py +0 -0
  23. {cuenca_validations-2.1.27 → cuenca_validations-2.1.28}/cuenca_validations/types/morals.py +0 -0
  24. {cuenca_validations-2.1.27 → cuenca_validations-2.1.28}/cuenca_validations/types/queries.py +0 -0
  25. {cuenca_validations-2.1.27 → cuenca_validations-2.1.28}/cuenca_validations/typing.py +0 -0
  26. {cuenca_validations-2.1.27 → cuenca_validations-2.1.28}/cuenca_validations.egg-info/dependency_links.txt +0 -0
  27. {cuenca_validations-2.1.27 → cuenca_validations-2.1.28}/cuenca_validations.egg-info/requires.txt +0 -0
  28. {cuenca_validations-2.1.27 → cuenca_validations-2.1.28}/cuenca_validations.egg-info/top_level.txt +0 -0
  29. {cuenca_validations-2.1.27 → cuenca_validations-2.1.28}/setup.cfg +0 -0
  30. {cuenca_validations-2.1.27 → cuenca_validations-2.1.28}/setup.py +0 -0
  31. {cuenca_validations-2.1.27 → cuenca_validations-2.1.28}/tests/__init__.py +0 -0
  32. {cuenca_validations-2.1.27 → cuenca_validations-2.1.28}/tests/test_card.py +0 -0
  33. {cuenca_validations-2.1.27 → cuenca_validations-2.1.28}/tests/test_errors.py +0 -0
  34. {cuenca_validations-2.1.27 → cuenca_validations-2.1.28}/tests/test_helpers.py +0 -0
  35. {cuenca_validations-2.1.27 → cuenca_validations-2.1.28}/tests/test_statement.py +0 -0
  36. {cuenca_validations-2.1.27 → cuenca_validations-2.1.28}/tests/test_types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cuenca_validations
3
- Version: 2.1.27
3
+ Version: 2.1.28
4
4
  Summary: Cuenca common validations
5
5
  Home-page: https://github.com/cuenca-mx/cuenca-validations
6
6
  Author: Cuenca
@@ -56,6 +56,7 @@ from ..types.enums import (
56
56
  WebhookObject,
57
57
  )
58
58
  from ..typing import DictStrAny
59
+ from ..validators import normalize_email, normalize_phone_number
59
60
  from .card import (
60
61
  Cvv,
61
62
  ExpMonth,
@@ -543,6 +544,16 @@ class UserUpdateRequest(BaseRequest):
543
544
  raise ValueError('At least one parameter must be provided')
544
545
  return values
545
546
 
547
+ @field_validator('email_address', mode='before')
548
+ @classmethod
549
+ def validate_email_address(cls, v: Optional[str]) -> Optional[str]:
550
+ return normalize_email(v) if v else v
551
+
552
+ @field_validator('phone_number', mode='before')
553
+ @classmethod
554
+ def validate_phone_number(cls, v: Optional[str]) -> Optional[str]:
555
+ return normalize_phone_number(v) if v else v
556
+
546
557
  @field_validator('beneficiaries')
547
558
  @classmethod
548
559
  def beneficiary_percentage(
@@ -641,13 +652,16 @@ class VerificationRequest(BaseRequest):
641
652
  },
642
653
  )
643
654
 
644
- @field_validator('recipient')
645
- def validate_sender(cls, recipient: str, values):
646
- return (
647
- EmailStr(recipient)
648
- if type == VerificationType.email
649
- else PhoneNumber(recipient)
650
- )
655
+ @model_validator(mode='before')
656
+ @classmethod
657
+ def normalize_recipient(cls, data):
658
+ _type = data.get('type')
659
+ recipient = data.get('recipient')
660
+ if _type == VerificationType.email:
661
+ data['recipient'] = normalize_email(recipient)
662
+ else:
663
+ data['recipient'] = normalize_phone_number(recipient)
664
+ return data
651
665
 
652
666
 
653
667
  class VerificationAttemptRequest(BaseRequest):
@@ -1,8 +1,38 @@
1
1
  import base64
2
2
  import datetime as dt
3
+ import re
3
4
  from enum import Enum
4
5
  from typing import Any, Callable, Optional, Union
5
6
 
7
+ SANITIZE_PHONE_NUMBER = re.compile(r'[+.()\-\s]')
8
+ STRIP_MX_MOBILE_PREFIX = re.compile(r'(52)(?:(?:044)|1)?(\d{10})$')
9
+ STRIP_US_DUPLICATE_PREFIX = re.compile(r'^11(\d{10})$')
10
+
11
+
12
+ def normalize_email(email: str) -> str:
13
+ """Lowercase email and strip plus labels from the local part.
14
+
15
+ mateohhr@Yahoo.com -> mateohhr@yahoo.com
16
+ guerradzul+cuenca@gmail.com -> guerradzul@gmail.com
17
+ """
18
+ local, _, domain = email.partition('@')
19
+ return f'{local.split("+")[0]}@{domain}'.lower()
20
+
21
+
22
+ def normalize_phone_number(phone_number: str) -> str:
23
+ """Sanitize and normalize phone numbers to E.164 format.
24
+
25
+ Handles:
26
+ - Special characters: +52 (55) 1234-5678 -> +525512345678
27
+ - MX mobile prefix: +5215512345678 -> +525512345678
28
+ - MX 044 prefix: +520445512345678 -> +525512345678
29
+ - US duplicate prefix: +116504401222 -> +16504401222
30
+ """
31
+ pn = SANITIZE_PHONE_NUMBER.sub('', phone_number)
32
+ pn = STRIP_MX_MOBILE_PREFIX.sub(r'\1\2', pn)
33
+ pn = STRIP_US_DUPLICATE_PREFIX.sub(r'1\1', pn)
34
+ return f'+{pn}'
35
+
6
36
 
7
37
  def sanitize_dict(d: dict) -> dict:
8
38
  for k, v in d.items():
@@ -0,0 +1 @@
1
+ __version__ = '2.1.28'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cuenca_validations
3
- Version: 2.1.27
3
+ Version: 2.1.28
4
4
  Summary: Cuenca common validations
5
5
  Home-page: https://github.com/cuenca-mx/cuenca-validations
6
6
  Author: Cuenca
@@ -30,4 +30,5 @@ tests/test_errors.py
30
30
  tests/test_helpers.py
31
31
  tests/test_requests.py
32
32
  tests/test_statement.py
33
- tests/test_types.py
33
+ tests/test_types.py
34
+ tests/test_validators.py
@@ -1,9 +1,12 @@
1
1
  import pytest
2
2
  from pydantic import ValidationError
3
+ from pydantic_extra_types.phone_numbers import PhoneNumber
3
4
 
5
+ from cuenca_validations.types.enums import VerificationType
4
6
  from cuenca_validations.types.requests import (
5
7
  UserTOSAgreementRequest,
6
8
  UserUpdateRequest,
9
+ VerificationRequest,
7
10
  )
8
11
  from cuenca_validations.typing import DictStrAny
9
12
 
@@ -53,3 +56,29 @@ def test_update_user_update_govt() -> None:
53
56
  with pytest.raises(ValueError) as ex:
54
57
  UserUpdateRequest(**govt_id)
55
58
  assert 'uri_back must be provided for type ine' in str(ex.value)
59
+
60
+
61
+ def test_verification_request_normalizes_email() -> None:
62
+ req = VerificationRequest(
63
+ recipient='user+cuenca@Gmail.com',
64
+ type=VerificationType.email,
65
+ )
66
+ assert req.recipient == 'user@gmail.com'
67
+
68
+
69
+ def test_verification_request_normalizes_phone() -> None:
70
+ req = VerificationRequest(
71
+ recipient='+116504401222',
72
+ type=VerificationType.phone,
73
+ )
74
+ assert req.recipient == '+16504401222'
75
+
76
+
77
+ def test_user_update_request_normalizes_email() -> None:
78
+ req = UserUpdateRequest(email_address='user+tag@Gmail.com')
79
+ assert req.email_address == 'user@gmail.com'
80
+
81
+
82
+ def test_user_update_request_normalizes_phone() -> None:
83
+ req = UserUpdateRequest(phone_number=PhoneNumber('+116504401222'))
84
+ assert req.phone_number == '+16504401222'
@@ -0,0 +1,35 @@
1
+ import pytest
2
+
3
+ from cuenca_validations.validators import (
4
+ normalize_email,
5
+ normalize_phone_number,
6
+ )
7
+
8
+
9
+ @pytest.mark.parametrize(
10
+ 'raw, normalized',
11
+ [
12
+ ('user@Yahoo.com', 'user@yahoo.com'), # uppercase domain
13
+ ('user@iCloud.com', 'user@icloud.com'), # uppercase domain
14
+ ('user@Gmail.com', 'user@gmail.com'), # uppercase domain
15
+ ('user+cuenca@gmail.com', 'user@gmail.com'), # plus label
16
+ ('user@gmail.com', 'user@gmail.com'), # already normalized
17
+ ],
18
+ )
19
+ def test_normalize_email(raw: str, normalized: str) -> None:
20
+ assert normalize_email(raw) == normalized
21
+
22
+
23
+ @pytest.mark.parametrize(
24
+ 'raw, normalized',
25
+ [
26
+ ('+116503456789', '+16503456789'), # US duplicate country code
27
+ ('+5215512345678', '+525512345678'), # MX mobile prefix
28
+ ('+520445512345678', '+525512345678'), # MX 044 prefix
29
+ ('+52 (55) 1234-5678', '+525512345678'), # special characters
30
+ ('+525512345678', '+525512345678'), # already correct MX
31
+ ('+16503456789', '+16503456789'), # already correct US
32
+ ],
33
+ )
34
+ def test_normalize_phone_number(raw: str, normalized: str) -> None:
35
+ assert normalize_phone_number(raw) == normalized
@@ -1 +0,0 @@
1
- __version__ = '2.1.27'