/
phonenumbers.py
102 lines (77 loc) · 2.77 KB
/
phonenumbers.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
"""
Requires the phonenumbers_ package which can be installed with:
.. _phonenumbers: https://pypi.org/project/phonenumbers/
.. code-block:: bash
$ python3 -m pip install phantom-types[phonenumbers]
"""
from __future__ import annotations
from typing import cast
import phonenumbers
from typing_extensions import Final
from phantom import Phantom
from phantom.bounds import parse_str
from phantom.fn import excepts
from phantom.schema import Schema
__all__ = (
"InvalidPhoneNumber",
"normalize_phone_number",
"is_phone_number",
"is_formatted_phone_number",
"PhoneNumber",
"FormattedPhoneNumber",
)
class InvalidPhoneNumber(phonenumbers.NumberParseException, TypeError):
INVALID: Final = 99
def __init__(self, error_type: int = INVALID, msg: str = "Invalid number") -> None:
super().__init__(error_type, msg)
def _deconstruct_phone_number(
phone_number: str, country_code: str | None = None
) -> phonenumbers.PhoneNumber:
try:
parsed_number = phonenumbers.parse(phone_number, region=country_code)
except phonenumbers.NumberParseException as e:
raise InvalidPhoneNumber(e.error_type, e._msg)
if not phonenumbers.is_valid_number(parsed_number):
raise InvalidPhoneNumber
return parsed_number
def normalize_phone_number(
phone_number: str, country_code: str | None = None
) -> FormattedPhoneNumber:
"""
Normalize ``phone_number`` using :py:const:`phonenumbers.PhoneNumberFormat.E164`.
:raises InvalidPhoneNumber:
"""
normalized = phonenumbers.format_number(
_deconstruct_phone_number(phone_number, country_code),
phonenumbers.PhoneNumberFormat.E164,
)
return cast(FormattedPhoneNumber, normalized)
is_phone_number = excepts(InvalidPhoneNumber)(_deconstruct_phone_number)
def is_formatted_phone_number(number: str) -> bool:
try:
return number == normalize_phone_number(number)
except InvalidPhoneNumber:
return False
class PhoneNumber(str, Phantom, predicate=is_phone_number):
@classmethod
def __schema__(cls) -> Schema:
return {
**super().__schema__(), # type: ignore[misc]
"description": "A valid E.164 phone number.",
"type": "string",
"format": "E.164",
}
class FormattedPhoneNumber(PhoneNumber, predicate=is_formatted_phone_number):
@classmethod
def parse(cls, instance: object) -> FormattedPhoneNumber:
"""
Normalize number using :py:const:`phonenumbers.PhoneNumberFormat.E164`.
:raises InvalidPhoneNumber:
"""
return normalize_phone_number(parse_str(instance))
@classmethod
def __schema__(cls) -> Schema:
return {
**super().__schema__(), # type: ignore[misc]
"title": "PhoneNumber",
}