def test_print_warning_ends_message_with_newline(self): message = 'test warning' with patch.object(display.sys.stderr, 'write') as mock_write: display.print_error(message) printed_message = mock_write.call_args[0][0] self.assertRegex(printed_message, '\n$', 'The warning message does not end in a newline.')
def write_file(filename, data, mode='w', continue_on_error=False, display_errors=True): """Writes data to a file. Args: filename: String, the path of the file to write to disk. data: Serializable data to write to the file. mode: String, the mode in which to open the file and write to it. continue_on_error: Boolean, If True, suppresses any IO errors and returns to the caller without any externalities. display_errors: Boolean, If True, prints error messages when errors are encountered and continue_on_error is True. Returns: Boolean, True if the write operation succeeded, or False if not. """ try: with _open_file(filename, mode) as f: f.write(data) return True except IOError as e: if continue_on_error: if display_errors: display.print_error(e) return False else: controlflow.system_error_exit(6, e)
def handle_oauth_token_error(e, soft_errors): """On a token error, exits the application and writes a message to stderr. Args: e: google.auth.exceptions.RefreshError, The error to handle. soft_errors: Boolean, if True, suppresses any applicable errors and instead returns to the caller. """ token_error = str(e).replace('.', '') if token_error in errors.OAUTH2_TOKEN_ERRORS or e.startswith( 'Invalid response'): if soft_errors: return if not GM_Globals[GM_CURRENT_API_USER]: display.print_error( MESSAGE_API_ACCESS_DENIED.format( GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID], ','.join(GM_Globals[GM_CURRENT_API_SCOPES]))) controlflow.system_error_exit(12, MESSAGE_API_ACCESS_CONFIG) else: controlflow.system_error_exit( 19, MESSAGE_SERVICE_NOT_APPLICABLE.format( GM_Globals[GM_CURRENT_API_USER])) controlflow.system_error_exit(18, f'Authentication Token Error - {str(e)}')
def test_print_warning_prints_error_prefix(self): message = 'test warning' with patch.object(display.sys.stderr, 'write') as mock_write: display.print_error(message) printed_message = mock_write.call_args[0][0] self.assertLess( printed_message.find(WARNING_PREFIX), printed_message.find(message), 'The warning prefix does not appear before the error message')
def system_error_exit(return_code, message): """Raises a system exit with the given return code and message. Args: return_code: Int, the return code to yield when the system exits. message: An error message to print before the system exits. """ if message: display.print_error(message) sys.exit(return_code)
def delete(): cd = gapi_directory.build() user_email = gam.normalizeEmailAddressOrUID(sys.argv[3]) print(f'Deleting account for {user_email}') try: gapi.call(cd.users(), 'delete', userKey=user_email, throw_reasons=[gapi_errors.ErrorReason.CONDITION_NOT_MET]) except gam.gapi.errors.GapiConditionNotMetError as err: display.print_error( f'{err} The user {user_email} may be (or have recently been) on Google Vault Hold and thus not eligible for deletion. You can check holds with "gam user <email> show vaultholds".' )
def run_dual(self, use_console_flow, authorization_prompt_message='', console_prompt_message='', web_success_message='', open_browser=True, redirect_uri_trailing_slash=True, **kwargs): mgr = multiprocessing.Manager() d = mgr.dict() d['trailing_slash'] = redirect_uri_trailing_slash d['open_browser'] = use_console_flow http_client = multiprocessing.Process(target=_wait_for_http_client, args=(d, )) user_input = multiprocessing.Process(target=_wait_for_user_input, args=(d, )) http_client.start() # we need to wait until web server starts on avail port # so we know redirect_uri to use while 'redirect_uri' not in d: sleep(0.1) self.redirect_uri = d['redirect_uri'] d['auth_url'], _ = self.authorization_url(**kwargs) print(MESSAGE_CONSOLE_AUTHORIZATION_PROMPT.format(url=d['auth_url'])) user_input.start() userInput = False while True: sleep(0.1) if not http_client.is_alive(): user_input.terminate() break elif not user_input.is_alive(): userInput = True http_client.terminate() break while True: code = d['code'] if code.startswith('http'): parsed_url = urlparse(code) parsed_params = parse_qs(parsed_url.query) code = parsed_params.get('code', [None])[0] try: self.fetch_token(code=code) break except Exception as e: if not userInput: controlflow.system_error_exit(8, str(e)) display.print_error(str(e)) _wait_for_user_input(d) sys.stdout.write(MESSAGE_AUTHENTICATION_COMPLETE) return self.credentials
def close_file(f, force_flush=False): """Closes a file. Args: f: The file to close force_flush: Flush file to disk emptying Python and OS caches. See: https://stackoverflow.com/a/13762137/1503886 Returns: Boolean, True if the file was successfully closed. False if an error was encountered while closing. """ if force_flush: f.flush() os.fsync(f.fileno()) try: f.close() return True except IOError as e: display.print_error(e) return False
def get_gapi_error_detail(e, soft_errors=False, silent_errors=False, retry_on_http_error=False): """Extracts error detail from a non-200 GAPI Response. Args: e: googleapiclient.HttpError, The HTTP Error received. soft_errors: Boolean, If true, causes error messages to be surpressed, rather than sending them to stderr. silent_errors: Boolean, If true, suppresses and ignores any errors from being displayed retry_on_http_error: Boolean, If true, will return -1 as the HTTP Response code, indicating that the request can be retried. TODO: Remove this param, as it seems to be outside the scope of this method. Returns: A tuple containing the HTTP Response code, GAPI error reason, and error message. """ try: error = json.loads(e.content.decode(UTF8)) except ValueError: error_content = e.content.decode(UTF8) if isinstance( e.content, bytes) else e.content if (e.resp['status'] == '503') and ( error_content == 'Quota exceeded for the current request'): return (e.resp['status'], ErrorReason.QUOTA_EXCEEDED.value, error_content) if (e.resp['status'] == '403') and (error_content.startswith( 'Request rate higher than configured')): return (e.resp['status'], ErrorReason.QUOTA_EXCEEDED.value, error_content) if (e.resp['status'] == '502') and ('Bad Gateway' in error_content): return (e.resp['status'], ErrorReason.BAD_GATEWAY.value, error_content) if (e.resp['status'] == '504') and ('Gateway Timeout' in error_content): return (e.resp['status'], ErrorReason.GATEWAY_TIMEOUT.value, error_content) if (e.resp['status'] == '403') and ('Invalid domain.' in error_content): error = _create_http_error_dict(403, ErrorReason.NOT_FOUND.value, 'Domain not found') elif (e.resp['status'] == '400') and ( 'InvalidSsoSigningKey' in error_content): error = _create_http_error_dict(400, ErrorReason.INVALID.value, 'InvalidSsoSigningKey') elif (e.resp['status'] == '400') and ('UnknownError' in error_content): error = _create_http_error_dict(400, ErrorReason.INVALID.value, 'UnknownError') elif retry_on_http_error: return (-1, None, None) elif soft_errors: if not silent_errors: display.print_error(error_content) return (0, None, None) else: controlflow.system_error_exit(5, error_content) # END: ValueError catch if 'error' in error: http_status = error['error']['code'] try: message = error['error']['errors'][0]['message'] except KeyError: message = error['error']['message'] if http_status == 404: if 'Requested entity was not found' in message or 'does not exist' in message: error = _create_http_error_dict(404, ErrorReason.NOT_FOUND.value, message) elif http_status == 500: if 'Failed to convert server response to JSON' in message: error = _create_http_error_dict(500, ErrorReason.INTERNAL_SERVER_ERROR.value, message) else: if 'error_description' in error: if error['error_description'] == 'Invalid Value': message = error['error_description'] http_status = 400 error = _create_http_error_dict(400, ErrorReason.INVALID.value, message) else: controlflow.system_error_exit(4, str(error)) else: controlflow.system_error_exit(4, str(error)) # Extract the error reason try: reason = error['error']['errors'][0]['reason'] if reason == 'notFound': if 'userKey' in message: reason = ErrorReason.USER_NOT_FOUND.value elif 'groupKey' in message: reason = ErrorReason.GROUP_NOT_FOUND.value elif 'memberKey' in message: reason = ErrorReason.MEMBER_NOT_FOUND.value elif 'Domain not found' in message: reason = ErrorReason.DOMAIN_NOT_FOUND.value elif 'Resource Not Found' in message: reason = ErrorReason.RESOURCE_NOT_FOUND.value elif reason == 'invalid': if 'userId' in message: reason = ErrorReason.USER_NOT_FOUND.value elif 'memberKey' in message: reason = ErrorReason.INVALID_MEMBER.value elif reason == 'failedPrecondition': if 'Bad Request' in message: reason = ErrorReason.BAD_REQUEST.value elif 'Mail service not enabled' in message: reason = ErrorReason.SERVICE_NOT_AVAILABLE.value elif reason == 'required': if 'memberKey' in message: reason = ErrorReason.MEMBER_NOT_FOUND.value elif reason == 'conditionNotMet': if 'Cyclic memberships not allowed' in message: reason = ErrorReason.CYCLIC_MEMBERSHIPS_NOT_ALLOWED.value except KeyError: reason = f'{http_status}' return (http_status, reason, message)
def call(service, function, silent_errors=False, soft_errors=False, throw_reasons=None, retry_reasons=None, **kwargs): """Executes a single request on a Google service function. Args: service: A Google service object for the desired API. function: String, The name of a service request method to execute. silent_errors: Bool, If True, error messages are suppressed when encountered. soft_errors: Bool, If True, writes non-fatal errors to stderr. throw_reasons: A list of Google HTTP error reason strings indicating the errors generated by this request should be re-thrown. All other HTTP errors are consumed. retry_reasons: A list of Google HTTP error reason strings indicating which error should be retried, using exponential backoff techniques, when the error reason is encountered. **kwargs: Additional params to pass to the request method. Returns: A response object for the corresponding Google API call. """ if throw_reasons is None: throw_reasons = [] if retry_reasons is None: retry_reasons = [] method = getattr(service, function) retries = 10 parameters = dict( list(kwargs.items()) + list(GM_Globals[GM_EXTRA_ARGS_DICT].items())) for n in range(1, retries + 1): try: return method(**parameters).execute() except googleapiclient.errors.HttpError as e: http_status, reason, message = errors.get_gapi_error_detail( e, soft_errors=soft_errors, silent_errors=silent_errors, retry_on_http_error=n < 3) if http_status == -1: # The error detail indicated that we should retry this request # We'll refresh credentials and make another pass service._http.credentials.refresh(transport.create_http()) continue if http_status == 0: return None is_known_error_reason = reason in [ r.value for r in errors.ErrorReason ] if is_known_error_reason and errors.ErrorReason( reason) in throw_reasons: if errors.ErrorReason( reason) in errors.ERROR_REASON_TO_EXCEPTION: raise errors.ERROR_REASON_TO_EXCEPTION[errors.ErrorReason( reason)](message) raise e if (n != retries) and (is_known_error_reason and errors.ErrorReason( reason) in errors.DEFAULT_RETRY_REASONS + retry_reasons): controlflow.wait_on_failure(n, retries, reason) continue if soft_errors: display.print_error( f'{http_status}: {message} - {reason}{["", ": Giving up."][n > 1]}' ) return None controlflow.system_error_exit( int(http_status), f'{http_status}: {message} - {reason}') except google.auth.exceptions.RefreshError as e: handle_oauth_token_error( e, soft_errors or errors.ErrorReason.SERVICE_NOT_AVAILABLE in throw_reasons) if errors.ErrorReason.SERVICE_NOT_AVAILABLE in throw_reasons: raise errors.GapiServiceNotAvailableError(str(e)) display.print_error( f'User {GM_Globals[GM_CURRENT_API_USER]}: {str(e)}') return None except ValueError as e: if hasattr(service._http, 'cache') and service._http.cache is not None: service._http.cache = None continue controlflow.system_error_exit(4, str(e)) except (httplib2.ServerNotFoundError, RuntimeError) as e: if n != retries: service._http.connections = {} controlflow.wait_on_failure(n, retries, str(e)) continue controlflow.system_error_exit(4, str(e)) except TypeError as e: controlflow.system_error_exit(4, str(e))
def test_print_warning_prints_to_stderr(self): message = 'test warning' with patch.object(display.sys.stderr, 'write') as mock_write: display.print_error(message) printed_message = mock_write.call_args[0][0] self.assertIn(message, printed_message)