#!/usr/bin/env python """Utility functions and classes for BQ CLI errors.""" import textwrap from typing import Dict, List, Optional import bq_flags from utils import bq_logging P12_DEPRECATION_MESSAGE = ( 'BQ CLI no longer supports the deprecated P12 format by default. To migrate' ' to the new JSON service account key format, follow the steps in' ' https://cloud.google.com/iam/docs/keys-create-delete#creating. To force' ' BQ CLI to recognize P12 keys, re-run the command with' ' --nouse_google_auth.' ) class BigqueryError(Exception): """Class to represent a BigQuery error.""" class BigqueryTypeError(BigqueryError): """A BQ CLI type error that should not show a stack trace.""" class BigqueryCommunicationError(BigqueryError): """Error communicating with the server.""" class BigqueryInterfaceError(BigqueryError): """Response from server missing required fields.""" class BigqueryServiceError(BigqueryError): """Base class of Bigquery-specific error responses. The BigQuery server received request and returned an error. """ def __init__( self, message: str, error: Dict[str, str], error_list: List[Dict[str, str]], job_ref: Optional[str] = None, *args, **kwds, ): # pylint: disable=g-doc-args # pylint: disable=keyword-arg-before-vararg """Initializes a BigqueryServiceError. Args: message: A user-facing error message. error: The error dictionary, code may inspect the 'reason' key. error_list: A list of additional entries, for example a load job may contain multiple errors here for each error encountered during processing. job_ref: Optional JobReference string, if this error was encountered while processing a job. """ super().__init__(message, *args, **kwds) self.error = error self.error_list = error_list self.job_ref = job_ref def __repr__(self): return '%s: error=%s, error_list=%s, job_ref=%s' % ( self.__class__.__name__, self.error, self.error_list, self.job_ref, ) class BigqueryNotFoundError(BigqueryServiceError): """The requested resource or identifier was not found.""" class BigqueryDuplicateError(BigqueryServiceError): """The requested resource or identifier already exists.""" class BigqueryAccessDeniedError(BigqueryServiceError): """The user does not have access to the requested resource.""" class BigqueryInvalidQueryError(BigqueryServiceError): """The SQL statement is invalid.""" class BigqueryTermsOfServiceError(BigqueryAccessDeniedError): """User has not ACK'd ToS.""" class BigqueryBackendError(BigqueryServiceError): """A backend error typically corresponding to retriable HTTP 5xx failures.""" class BigqueryClientError(BigqueryError): """Invalid use of BigqueryClient.""" class BigqueryClientConfigurationError(BigqueryClientError): """Invalid configuration of BigqueryClient.""" class BigquerySchemaError(BigqueryClientError): """Error in locating or parsing the schema.""" class BigqueryTableConstraintsError(BigqueryClientError): """Error in locating or parsing the table constraints.""" def CreateBigqueryError( error: Dict[str, str], server_error: Dict[str, str], error_ls: List[Dict[str, str]], job_ref: Optional[str] = None, session_id: Optional[str] = None, ) -> BigqueryError: """Returns a BigqueryError for json error embedded in server_error. If error_ls contains any errors other than the given one, those are also included in the returned message. Args: error: The primary error to convert. server_error: The error returned by the server. (This is only used in the case that error is malformed.) error_ls: Additional errors to include in the error message. job_ref: String representation a JobReference, if this is an error associated with a job. session_id: Id of the session if the job is part of one. Returns: BigqueryError representing error. """ reason = error.get('reason') if job_ref: message = f"Error processing job '{job_ref}': {error.get('message')}" else: message = error.get('message', '') # We don't want to repeat the "main" error message. new_errors = [err for err in error_ls if err != error] if new_errors: message += '\nFailure details:\n' wrap_error_message = True new_error_messages = [ ': '.join(filter(None, [err.get('location'), err.get('message')])) for err in new_errors ] if wrap_error_message: message += '\n'.join( textwrap.fill(msg, initial_indent=' - ', subsequent_indent=' ') for msg in new_error_messages ) else: error_message = '\n'.join(new_error_messages) if error_message: message += '- ' + error_message if session_id: message += '\nIn session: %s' % session_id # Sometimes we will have type(message) being , for example # from an invalid query containing a non-English string. Reduce this # to now -- otherwise it's a trap for any code that # tries to %s-format the exception later: str() uses 'ascii' codec. # And the message is for display only, so this shouldn't confuse other code. message = bq_logging.EncodeForPrinting(message) if not reason or not message: return BigqueryInterfaceError( 'Error reported by server with missing error fields. ' 'Server returned: %s' % (str(server_error),) ) if reason == 'notFound': return BigqueryNotFoundError(message, error, error_ls, job_ref=job_ref) if reason == 'duplicate': return BigqueryDuplicateError(message, error, error_ls, job_ref=job_ref) if reason == 'accessDenied': return BigqueryAccessDeniedError(message, error, error_ls, job_ref=job_ref) if reason == 'invalidQuery': return BigqueryInvalidQueryError(message, error, error_ls, job_ref=job_ref) if reason == 'termsOfServiceNotAccepted': return BigqueryTermsOfServiceError( message, error, error_ls, job_ref=job_ref ) if reason == 'backendError': return BigqueryBackendError(message, error, error_ls, job_ref=job_ref) # We map the remaining errors to BigqueryServiceError. return BigqueryServiceError(message, error, error_ls, job_ref=job_ref)