3737])
3838
3939
40- class _Validator (object ):
41- """A collection of data validation utilities.
40+ class _Unspecified (object ):
41+ pass
4242
43- Methods provided in this class raise ValueErrors if any validations fail. Normal returns
44- signal success.
45- """
43+ # Use this internally, until sentinels are available in the public API.
44+ _UNSPECIFIED = _Unspecified ()
45+
46+
47+ class _Validator (object ):
48+ """A collection of data validation utilities."""
4649
4750 @classmethod
48- def validate_uid (cls , uid ):
51+ def validate_uid (cls , uid , required = False ):
52+ if uid is None and not required :
53+ return None
4954 if not isinstance (uid , six .string_types ) or not uid or len (uid ) > 128 :
5055 raise ValueError (
5156 'Invalid uid: "{0}". The uid must be a non-empty string with no more than 128 '
5257 'characters.' .format (uid ))
58+ return uid
5359
5460 @classmethod
55- def validate_email (cls , email ):
56- if not isinstance (email , six .string_types ) or not email :
61+ def validate_email (cls , email , required = False ):
62+ if email is None and not required :
63+ return None
64+ if not isinstance (email , six .string_types ):
5765 raise ValueError (
5866 'Invalid email: "{0}". Email must be a non-empty string.' .format (email ))
5967 parts = email .split ('@' )
6068 if len (parts ) != 2 or not parts [0 ] or not parts [1 ]:
6169 raise ValueError ('Malformed email address string: "{0}".' .format (email ))
70+ return email
6271
6372 @classmethod
64- def validate_phone (cls , phone ):
73+ def validate_phone (cls , phone , required = False ):
6574 """Validates the specified phone number.
6675
6776 Phone number vlidation is very lax here. Backend will enforce E.164 spec compliance, and
6877 normalize accordingly. Here we check if the number starts with + sign, and contains at
6978 least one alphanumeric character.
7079 """
71- if not isinstance (phone , six .string_types ) or not phone :
80+ if phone is None and not required :
81+ return None
82+ if not isinstance (phone , six .string_types ):
7283 raise ValueError ('Invalid phone number: "{0}". Phone number must be a non-empty '
7384 'string.' .format (phone ))
7485 if not phone .startswith ('+' ) or not re .search ('[a-zA-Z0-9]' , phone ):
7586 raise ValueError ('Invalid phone number: "{0}". Phone number must be a valid, E.164 '
7687 'compliant identifier.' .format (phone ))
88+ return phone
7789
7890 @classmethod
79- def validate_password (cls , password ):
91+ def validate_password (cls , password , required = False ):
92+ if password is None and not required :
93+ return None
8094 if not isinstance (password , six .string_types ) or len (password ) < 6 :
8195 raise ValueError (
8296 'Invalid password string. Password must be a string at least 6 characters long.' )
97+ return password
8398
8499 @classmethod
85- def validate_email_verified (cls , email_verified ):
86- if not isinstance (email_verified , bool ):
87- raise ValueError (
88- 'Invalid email verified status: "{0}". Email verified status must be '
89- 'boolean.' .format (email_verified ))
90-
91- @classmethod
92- def validate_display_name (cls , display_name ):
100+ def validate_display_name (cls , display_name , required = False ):
101+ if display_name is None and not required :
102+ return None
93103 if not isinstance (display_name , six .string_types ) or not display_name :
94104 raise ValueError (
95105 'Invalid display name: "{0}". Display name must be a non-empty '
96106 'string.' .format (display_name ))
107+ return display_name
97108
98109 @classmethod
99- def validate_photo_url (cls , photo_url ):
110+ def validate_photo_url (cls , photo_url , required = False ):
111+ if photo_url is None and not required :
112+ return None
100113 if not isinstance (photo_url , six .string_types ) or not photo_url :
101114 raise ValueError (
102115 'Invalid photo URL: "{0}". Photo URL must be a non-empty '
@@ -105,63 +118,53 @@ def validate_photo_url(cls, photo_url):
105118 parsed = urllib .parse .urlparse (photo_url )
106119 if not parsed .netloc :
107120 raise ValueError ('Malformed photo URL: "{0}".' .format (photo_url ))
121+ return photo_url
108122 except Exception :
109123 raise ValueError ('Malformed photo URL: "{0}".' .format (photo_url ))
110124
111125 @classmethod
112- def validate_valid_since (cls , valid_since ):
113- # isinstance(True, int) is True hence the extra check
114- if valid_since is None or isinstance (valid_since , bool ) or not isinstance (valid_since , int ):
115- raise ValueError (
116- 'Invalid time string for: "{0}". Valid Since must be an int' .format (valid_since ))
117- if int (valid_since ) <= 0 :
118- raise ValueError (
119- 'Invalid valid_since: must be a positive interger. {0}' .format (valid_since ))
120-
121- @classmethod
122- def validate_disabled (cls , disabled ):
123- if not isinstance (disabled , bool ):
124- raise ValueError (
125- 'Invalid disabled status: "{0}". Disabled status must be '
126- 'boolean.' .format (disabled ))
127-
128- @classmethod
129- def validate_delete_list (cls , delete_attr ):
130- if not isinstance (delete_attr , list ) or not delete_attr :
131- raise ValueError (
132- 'Invalid delete list: "{0}". Delete list must be a '
133- 'non-empty list.' .format (delete_attr ))
126+ def validate_timestamp (cls , timestamp , label , required = False ):
127+ if timestamp is None and not required :
128+ return None
129+ if isinstance (timestamp , bool ):
130+ raise ValueError ('Boolean value specified as timestamp.' )
131+ try :
132+ timestamp_int = int (timestamp )
133+ if timestamp_int <= 0 :
134+ raise ValueError ('{0} timestamp must be a positive interger.' .format (label ))
135+ return timestamp_int
136+ except TypeError :
137+ raise ValueError ('Invalid type for timestamp value: {0}.' .format (timestamp ))
134138
135139 @classmethod
136- def validate_custom_claims (cls , custom_claims ):
140+ def validate_custom_claims (cls , custom_claims , required = False ):
137141 """Validates the specified custom claims.
138142
139- Custom claims must be specified as a JSON string.The string must not exceed 1000
143+ Custom claims must be specified as a JSON string. The string must not exceed 1000
140144 characters, and the parsed JSON payload must not contain reserved JWT claims.
141145 """
142- if not isinstance (custom_claims , six .string_types ) or not custom_claims :
143- raise ValueError (
144- 'Invalid custom claims: "{0}". Custom claims must be a non-empty JSON '
145- 'string.' .format (custom_claims ))
146-
147- if len (custom_claims ) > MAX_CLAIMS_PAYLOAD_SIZE :
146+ if custom_claims is None and not required :
147+ return None
148+ claims_str = str (custom_claims )
149+ if len (claims_str ) > MAX_CLAIMS_PAYLOAD_SIZE :
148150 raise ValueError (
149151 'Custom claims payload must not exceed {0} '
150152 'characters.' .format (MAX_CLAIMS_PAYLOAD_SIZE ))
151153 try :
152- parsed = json .loads (custom_claims )
154+ parsed = json .loads (claims_str )
153155 except Exception :
154156 raise ValueError ('Failed to parse custom claims string as JSON.' )
155- else :
156- if not isinstance (parsed , dict ):
157- raise ValueError ('Custom claims must be parseable as a JSON object.' )
158- invalid_claims = RESERVED_CLAIMS .intersection (set (parsed .keys ()))
159- if len (invalid_claims ) > 1 :
160- joined = ', ' .join (sorted (invalid_claims ))
161- raise ValueError ('Claims "{0}" are reserved, and must not be set.' .format (joined ))
162- elif len (invalid_claims ) == 1 :
163- raise ValueError (
164- 'Claim "{0}" is reserved, and must not be set.' .format (invalid_claims .pop ()))
157+
158+ if not isinstance (parsed , dict ):
159+ raise ValueError ('Custom claims must be parseable as a JSON object.' )
160+ invalid_claims = RESERVED_CLAIMS .intersection (set (parsed .keys ()))
161+ if len (invalid_claims ) > 1 :
162+ joined = ', ' .join (sorted (invalid_claims ))
163+ raise ValueError ('Claims "{0}" are reserved, and must not be set.' .format (joined ))
164+ elif len (invalid_claims ) == 1 :
165+ raise ValueError (
166+ 'Claim "{0}" is reserved, and must not be set.' .format (invalid_claims .pop ()))
167+ return claims_str
165168
166169
167170class ApiCallError (Exception ):
@@ -176,67 +179,20 @@ def __init__(self, code, message, error=None):
176179class UserManager (object ):
177180 """Provides methods for interacting with the Google Identity Toolkit."""
178181
179- _VALIDATORS = {
180- 'customAttributes' : _Validator .validate_custom_claims ,
181- 'deleteAttribute' : _Validator .validate_delete_list ,
182- 'deleteProvider' : _Validator .validate_delete_list ,
183- 'disabled' : _Validator .validate_disabled ,
184- 'disableUser' : _Validator .validate_disabled ,
185- 'displayName' : _Validator .validate_display_name ,
186- 'email' : _Validator .validate_email ,
187- 'emailVerified' : _Validator .validate_email_verified ,
188- 'localId' : _Validator .validate_uid ,
189- 'password' : _Validator .validate_password ,
190- 'phoneNumber' : _Validator .validate_phone ,
191- 'photoUrl' : _Validator .validate_photo_url ,
192- 'validSince' : _Validator .validate_valid_since ,
193- }
194-
195- _CREATE_USER_FIELDS = {
196- 'uid' : 'localId' ,
197- 'display_name' : 'displayName' ,
198- 'email' : 'email' ,
199- 'email_verified' : 'emailVerified' ,
200- 'phone_number' : 'phoneNumber' ,
201- 'photo_url' : 'photoUrl' ,
202- 'password' : 'password' ,
203- 'disabled' : 'disabled' ,
204- }
205-
206- _UPDATE_USER_FIELDS = {
207- 'display_name' : 'displayName' ,
208- 'email' : 'email' ,
209- 'email_verified' : 'emailVerified' ,
210- 'phone_number' : 'phoneNumber' ,
211- 'photo_url' : 'photoUrl' ,
212- 'password' : 'password' ,
213- 'disabled' : 'disableUser' ,
214- 'custom_claims' : 'customAttributes' ,
215- 'valid_since' : 'validSince' ,
216- }
217-
218- _REMOVABLE_FIELDS = {
219- 'displayName' : 'DISPLAY_NAME' ,
220- 'photoUrl' : 'PHOTO_URL'
221- }
222-
223182 def __init__ (self , client ):
224183 self ._client = client
225184
226185 def get_user (self , ** kwargs ):
227186 """Gets the user data corresponding to the provided key."""
228187 if 'uid' in kwargs :
229188 key , key_type = kwargs .pop ('uid' ), 'user ID'
230- _Validator .validate_uid (key )
231- payload = {'localId' : [key ]}
189+ payload = {'localId' : [_Validator .validate_uid (key , required = True )]}
232190 elif 'email' in kwargs :
233191 key , key_type = kwargs .pop ('email' ), 'email'
234- _Validator .validate_email (key )
235- payload = {'email' : [key ]}
192+ payload = {'email' : [_Validator .validate_email (key , required = True )]}
236193 elif 'phone_number' in kwargs :
237194 key , key_type = kwargs .pop ('phone_number' ), 'phone number'
238- _Validator .validate_phone (key )
239- payload = {'phoneNumber' : [key ]}
195+ payload = {'phoneNumber' : [_Validator .validate_phone (key , required = True )]}
240196 else :
241197 raise ValueError ('Unsupported keyword arguments: {0}.' .format (kwargs ))
242198
@@ -272,10 +228,20 @@ def list_users(self, page_token=None, max_results=MAX_LIST_USERS_RESULTS):
272228 except requests .exceptions .RequestException as error :
273229 self ._handle_http_error (USER_DOWNLOAD_ERROR , 'Failed to download user accounts.' , error )
274230
275- def create_user (self , ** kwargs ):
231+ def create_user (self , uid = None , display_name = None , email = None , phone_number = None ,
232+ photo_url = None , password = None , disabled = None , email_verified = None ):
276233 """Creates a new user account with the specified properties."""
277- payload = self ._init_payload ('create_user' , UserManager ._CREATE_USER_FIELDS , ** kwargs )
278- self ._validate (payload , self ._VALIDATORS , 'create user' )
234+ payload = {
235+ 'localId' : _Validator .validate_uid (uid ),
236+ 'displayName' : _Validator .validate_display_name (display_name ),
237+ 'email' : _Validator .validate_email (email ),
238+ 'phoneNumber' : _Validator .validate_phone (phone_number ),
239+ 'photoUrl' : _Validator .validate_photo_url (photo_url ),
240+ 'password' : _Validator .validate_password (password ),
241+ 'emailVerified' : bool (email_verified ) if email_verified is not None else None ,
242+ 'disabled' : bool (disabled ) if disabled is not None else None ,
243+ }
244+ payload = {k : v for k , v in payload .items () if v is not None }
279245 try :
280246 response = self ._client .request ('post' , 'signupNewUser' , json = payload )
281247 except requests .exceptions .RequestException as error :
@@ -285,30 +251,47 @@ def create_user(self, **kwargs):
285251 raise ApiCallError (USER_CREATE_ERROR , 'Failed to create new user.' )
286252 return response .get ('localId' )
287253
288- def update_user (self , uid , ** kwargs ):
254+ def update_user (self , uid , display_name = _UNSPECIFIED , email = None , phone_number = _UNSPECIFIED ,
255+ photo_url = _UNSPECIFIED , password = None , disabled = None , email_verified = None ,
256+ valid_since = None , custom_claims = _UNSPECIFIED ):
289257 """Updates an existing user account with the specified properties"""
290- _Validator .validate_uid (uid )
291- payload = self ._init_payload ('update_user' , UserManager ._UPDATE_USER_FIELDS , ** kwargs )
292- payload ['localId' ] = uid
258+ payload = {
259+ 'localId' : _Validator .validate_uid (uid , required = True ),
260+ 'email' : _Validator .validate_email (email ),
261+ 'password' : _Validator .validate_password (password ),
262+ 'validSince' : _Validator .validate_timestamp (valid_since , 'valid_since' ),
263+ 'emailVerified' : bool (email_verified ) if email_verified is not None else None ,
264+ 'disableUser' : bool (disabled ) if disabled is not None else None ,
265+ }
293266
294267 remove = []
295- for key , value in UserManager ._REMOVABLE_FIELDS .items ():
296- if key in payload and payload [key ] is None :
297- remove .append (value )
298- del payload [key ]
268+ if display_name is not _UNSPECIFIED :
269+ if display_name is None :
270+ remove .append ('DISPLAY_NAME' )
271+ else :
272+ payload ['displayName' ] = _Validator .validate_display_name (display_name )
273+ if photo_url is not _UNSPECIFIED :
274+ if photo_url is None :
275+ remove .append ('PHOTO_URL' )
276+ else :
277+ payload ['photoUrl' ] = _Validator .validate_photo_url (photo_url )
299278 if remove :
300- payload ['deleteAttribute' ] = sorted (remove )
301- if 'phoneNumber' in payload and payload ['phoneNumber' ] is None :
302- payload ['deleteProvider' ] = ['phone' ]
303- del payload ['phoneNumber' ]
304- if 'customAttributes' in payload :
305- custom_claims = payload ['customAttributes' ]
279+ payload ['deleteAttribute' ] = remove
280+
281+ if phone_number is not _UNSPECIFIED :
282+ if phone_number is None :
283+ payload ['deleteProvider' ] = ['phone' ]
284+ else :
285+ payload ['phoneNumber' ] = _Validator .validate_phone (phone_number )
286+
287+ if custom_claims is not _UNSPECIFIED :
306288 if custom_claims is None :
307289 custom_claims = {}
308- if isinstance (custom_claims , dict ):
309- payload ['customAttributes' ] = json .dumps (custom_claims )
290+ json_claims = json .dumps (custom_claims ) if isinstance (
291+ custom_claims , dict ) else custom_claims
292+ payload ['customAttributes' ] = _Validator .validate_custom_claims (json_claims )
310293
311- self . _validate ( payload , self . _VALIDATORS , 'update user' )
294+ payload = { k : v for k , v in payload . items () if v is not None }
312295 try :
313296 response = self ._client .request ('post' , 'setAccountInfo' , json = payload )
314297 except requests .exceptions .RequestException as error :
@@ -321,7 +304,7 @@ def update_user(self, uid, **kwargs):
321304
322305 def delete_user (self , uid ):
323306 """Deletes the user identified by the specified user ID."""
324- _Validator .validate_uid (uid )
307+ _Validator .validate_uid (uid , required = True )
325308 try :
326309 response = self ._client .request ('post' , 'deleteAccount' , json = {'localId' : uid })
327310 except requests .exceptions .RequestException as error :
@@ -338,24 +321,6 @@ def _handle_http_error(self, code, msg, error):
338321 msg += '\n Reason: {0}' .format (error )
339322 raise ApiCallError (code , msg , error )
340323
341- def _init_payload (self , operation , fields , ** kwargs ):
342- payload = {}
343- for key , value in fields .items ():
344- if key in kwargs :
345- payload [value ] = kwargs .pop (key )
346- if kwargs :
347- unexpected_keys = ', ' .join (kwargs .keys ())
348- raise ValueError (
349- 'Unsupported arguments: "{0}" in call to {1}()' .format (unexpected_keys , operation ))
350- return payload
351-
352- def _validate (self , properties , validators , operation ):
353- for key , value in properties .items ():
354- validator = validators .get (key )
355- if not validator :
356- raise ValueError ('Unsupported property: "{0}" in {1} call.' .format (key , operation ))
357- validator (value )
358-
359324
360325class UserIterator (object ):
361326 """An iterator that allows iterating over user accounts, one at a time.
0 commit comments