Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/sqlalchemy_utils/types/phone_number.py: 45%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

64 statements  

1""" 

2.. note:: 

3 

4 The `phonenumbers`_ package must be installed to use PhoneNumber types. 

5 

6.. _phonenumbers: https://github.com/daviddrysdale/python-phonenumbers 

7""" 

8 

9from sqlalchemy import exc, types 

10 

11from ..exceptions import ImproperlyConfigured 

12from ..utils import str_coercible 

13from .scalar_coercible import ScalarCoercible 

14 

15try: 

16 import phonenumbers 

17 from phonenumbers.phonenumber import PhoneNumber as BasePhoneNumber 

18 from phonenumbers.phonenumberutil import NumberParseException 

19except ImportError: 

20 phonenumbers = None 

21 BasePhoneNumber = object 

22 NumberParseException = Exception 

23 

24 

25class PhoneNumberParseException(NumberParseException, exc.DontWrapMixin): 

26 """ 

27 Wraps exceptions from phonenumbers with SQLAlchemy's DontWrapMixin 

28 so we get more meaningful exceptions on validation failure instead of the 

29 StatementException 

30 

31 Clients can catch this as either a PhoneNumberParseException or 

32 NumberParseException from the phonenumbers library. 

33 """ 

34 

35 pass 

36 

37 

38@str_coercible 

39class PhoneNumber(BasePhoneNumber): 

40 """ 

41 Extends a PhoneNumber class from `Python phonenumbers library`_. Adds 

42 different phone number formats to attributes, so they can be easily used 

43 in templates. Phone number validation method is also implemented. 

44 

45 Takes the raw phone number and country code as params and parses them 

46 into a PhoneNumber object. 

47 

48 .. _Python phonenumbers library: 

49 https://github.com/daviddrysdale/python-phonenumbers 

50 

51 

52 :: 

53 

54 from sqlalchemy_utils import PhoneNumber 

55 

56 

57 class User(self.Base): 

58 __tablename__ = 'user' 

59 id = sa.Column(sa.Integer, autoincrement=True, primary_key=True) 

60 name = sa.Column(sa.Unicode(255)) 

61 _phone_number = sa.Column(sa.Unicode(20)) 

62 country_code = sa.Column(sa.Unicode(8)) 

63 

64 phone_number = sa.orm.composite( 

65 PhoneNumber, 

66 _phone_number, 

67 country_code 

68 ) 

69 

70 

71 user = User(phone_number=PhoneNumber('0401234567', 'FI')) 

72 

73 user.phone_number.e164 # '+358401234567' 

74 user.phone_number.international # '+358 40 1234567' 

75 user.phone_number.national # '040 1234567' 

76 user.country_code # 'FI' 

77 

78 

79 :param raw_number: 

80 String representation of the phone number. 

81 :param region: 

82 Region of the phone number. 

83 :param check_region: 

84 Whether to check the supplied region parameter; 

85 should always be True for external callers. 

86 Can be useful for short codes or toll free 

87 """ 

88 

89 def __init__(self, raw_number, region=None, check_region=True): 

90 # Bail if phonenumbers is not found. 

91 if phonenumbers is None: 

92 raise ImproperlyConfigured( 

93 "The 'phonenumbers' package is required to use 'PhoneNumber'" 

94 ) 

95 

96 try: 

97 self._phone_number = phonenumbers.parse( 

98 raw_number, region, _check_region=check_region 

99 ) 

100 except NumberParseException as e: 

101 # Wrap exception so SQLAlchemy doesn't swallow it as a 

102 # StatementError 

103 # 

104 # Worth noting that if -1 shows up as the error_type 

105 # it's likely because the API has changed upstream and these 

106 # bindings need to be updated. 

107 raise PhoneNumberParseException(getattr(e, 'error_type', -1), str(e)) 

108 

109 super().__init__( 

110 country_code=self._phone_number.country_code, 

111 national_number=self._phone_number.national_number, 

112 extension=self._phone_number.extension, 

113 italian_leading_zero=self._phone_number.italian_leading_zero, 

114 raw_input=self._phone_number.raw_input, 

115 country_code_source=self._phone_number.country_code_source, 

116 preferred_domestic_carrier_code=( 

117 self._phone_number.preferred_domestic_carrier_code 

118 ), 

119 ) 

120 self.region = region 

121 self.national = phonenumbers.format_number( 

122 self._phone_number, phonenumbers.PhoneNumberFormat.NATIONAL 

123 ) 

124 self.international = phonenumbers.format_number( 

125 self._phone_number, phonenumbers.PhoneNumberFormat.INTERNATIONAL 

126 ) 

127 self.e164 = phonenumbers.format_number( 

128 self._phone_number, phonenumbers.PhoneNumberFormat.E164 

129 ) 

130 

131 def __composite_values__(self): 

132 return self.national, self.region 

133 

134 def is_valid_number(self): 

135 return phonenumbers.is_valid_number(self._phone_number) 

136 

137 def __unicode__(self): 

138 return self.national 

139 

140 def __hash__(self): 

141 return hash(self.e164) 

142 

143 

144class PhoneNumberType(ScalarCoercible, types.TypeDecorator): 

145 """ 

146 Changes PhoneNumber objects to a string representation on the way in and 

147 changes them back to PhoneNumber objects on the way out. If E164 is used 

148 as storing format, no country code is needed for parsing the database 

149 value to PhoneNumber object. 

150 

151 :: 

152 

153 class User(self.Base): 

154 __tablename__ = 'user' 

155 id = sa.Column(sa.Integer, autoincrement=True, primary_key=True) 

156 name = sa.Column(sa.Unicode(255)) 

157 phone_number = sa.Column(PhoneNumberType()) 

158 

159 

160 user = User(phone_number='+358401234567') 

161 

162 user.phone_number.e164 # '+358401234567' 

163 user.phone_number.international # '+358 40 1234567' 

164 user.phone_number.national # '040 1234567' 

165 """ 

166 

167 STORE_FORMAT = 'e164' 

168 impl = types.Unicode(20) 

169 python_type = PhoneNumber 

170 cache_ok = True 

171 

172 def __init__(self, region='US', max_length=20, *args, **kwargs): 

173 # Bail if phonenumbers is not found. 

174 if phonenumbers is None: 

175 raise ImproperlyConfigured( 

176 "The 'phonenumbers' package is required to use 'PhoneNumberType'" 

177 ) 

178 

179 super().__init__(*args, **kwargs) 

180 self.region = region 

181 self.impl = types.Unicode(max_length) 

182 

183 def process_bind_param(self, value, dialect): 

184 if value: 

185 if not isinstance(value, PhoneNumber): 

186 value = PhoneNumber(value, region=self.region) 

187 

188 if self.STORE_FORMAT == 'e164' and value.extension: 

189 return f'{value.e164};ext={value.extension}' 

190 

191 return getattr(value, self.STORE_FORMAT) 

192 

193 return value 

194 

195 def process_result_value(self, value, dialect): 

196 if value: 

197 return PhoneNumber(value, self.region) 

198 return value 

199 

200 def _coerce(self, value): 

201 if value and not isinstance(value, PhoneNumber): 

202 value = PhoneNumber(value, region=self.region) 

203 

204 return value or None