-
Notifications
You must be signed in to change notification settings - Fork 316
/
phonenumber.py
171 lines (144 loc) · 5.78 KB
/
phonenumber.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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
from __future__ import annotations
from functools import total_ordering
from typing import TYPE_CHECKING
import phonenumbers
from django.conf import settings
from django.core import validators
if TYPE_CHECKING:
# Use ‘from typing import Self’ from Python 3.11 onwards.
from typing_extensions import Self
@total_ordering
class PhoneNumber(phonenumbers.PhoneNumber):
"""
A extended version of phonenumbers.PhoneNumber that provides
some neat and more pythonic, easy to access methods. This makes using a
PhoneNumber instance much easier, especially in templates and such.
"""
format_map = {
"E164": phonenumbers.PhoneNumberFormat.E164,
"INTERNATIONAL": phonenumbers.PhoneNumberFormat.INTERNATIONAL,
"NATIONAL": phonenumbers.PhoneNumberFormat.NATIONAL,
"RFC3966": phonenumbers.PhoneNumberFormat.RFC3966,
}
@classmethod
def from_string(cls, phone_number, region=None) -> Self:
"""
:arg str phone_number: parse this :class:`str` as a phone number.
:keyword str region: 2-letter country code as defined in ISO 3166-1.
When not supplied, defaults to :setting:`PHONENUMBER_DEFAULT_REGION`
"""
phone_number_obj = cls()
if region is None:
region = getattr(settings, "PHONENUMBER_DEFAULT_REGION", None)
phonenumbers.parse(
number=phone_number,
region=region,
keep_raw_input=True,
numobj=phone_number_obj,
)
return phone_number_obj
def __str__(self):
if self.is_valid():
format_string = getattr(settings, "PHONENUMBER_DEFAULT_FORMAT", "E164")
fmt = self.format_map[format_string]
return self.format_as(fmt)
else:
return self.raw_input
def __repr__(self):
if not self.is_valid():
return f"Invalid{type(self).__name__}(raw_input={self.raw_input})"
return super().__repr__()
def is_valid(self):
"""
Whether the number supplied is actually valid.
:return: ``True`` when the phone number is valid.
:rtype: bool
"""
return phonenumbers.is_valid_number(self)
def format_as(self, format):
return phonenumbers.format_number(self, format)
@property
def as_international(self):
return self.format_as(phonenumbers.PhoneNumberFormat.INTERNATIONAL)
@property
def as_e164(self):
return self.format_as(phonenumbers.PhoneNumberFormat.E164)
@property
def as_national(self):
return self.format_as(phonenumbers.PhoneNumberFormat.NATIONAL)
@property
def as_rfc3966(self):
return self.format_as(phonenumbers.PhoneNumberFormat.RFC3966)
def __len__(self):
return len(str(self))
def __eq__(self, other):
"""
Override parent equality because we store only string representation
of phone number, so we must compare only this string representation
"""
if other in validators.EMPTY_VALUES:
return False
elif isinstance(other, str):
default_region = getattr(settings, "PHONENUMBER_DEFAULT_REGION", None)
other = to_python(other, region=default_region)
elif isinstance(other, type(self)):
# Nothing to do. Good to compare.
pass
elif isinstance(other, phonenumbers.PhoneNumber):
# The parent class of PhoneNumber does not have .is_valid().
# We need to make it match ours.
old_other = other
other = type(self)()
other.merge_from(old_other)
else:
return False
format_string = getattr(settings, "PHONENUMBER_DB_FORMAT", "E164")
fmt = self.format_map[format_string]
self_str = self.format_as(fmt) if self.is_valid() else self.raw_input
other_str = other.format_as(fmt) if other.is_valid() else other.raw_input
return self_str == other_str
def __lt__(self, other):
if isinstance(other, phonenumbers.PhoneNumber):
old_other = other
other = type(self)()
other.merge_from(old_other)
elif not isinstance(other, type(self)):
raise TypeError(
"'<' not supported between instances of "
"'%s' and '%s'" % (type(self).__name__, type(other).__name__)
)
invalid = None
if not self.is_valid():
invalid = self
elif not other.is_valid():
invalid = other
if invalid is not None:
raise ValueError("Invalid phone number: %r" % invalid)
format_string = getattr(settings, "PHONENUMBER_DB_FORMAT", "E164")
fmt = self.format_map[format_string]
return self.format_as(fmt) < other.format_as(fmt)
def __hash__(self):
return hash(str(self))
def to_python(value, region=None):
if value in validators.EMPTY_VALUES: # None or ''
phone_number = value
elif isinstance(value, str):
try:
phone_number = PhoneNumber.from_string(phone_number=value, region=region)
except phonenumbers.NumberParseException:
# the string provided is not a valid PhoneNumber.
phone_number = PhoneNumber(raw_input=value)
elif isinstance(value, PhoneNumber):
phone_number = value
elif isinstance(value, phonenumbers.PhoneNumber):
phone_number = PhoneNumber()
phone_number.merge_from(value)
else:
raise TypeError("Can't convert %s to PhoneNumber." % type(value).__name__)
return phone_number
def validate_region(region):
if region is not None and region not in phonenumbers.SUPPORTED_REGIONS:
raise ValueError(
"“%s” is not a valid region code. Choices are %r"
% (region, phonenumbers.SUPPORTED_REGIONS)
)