def getErrorMessage(clientResponse, exception=None): completeErrorMessage = f'{HttpClientConstant.ERROR_AT_CLIENT_CALL_MESSAGE}{c.DOT_SPACE}{HttpClientConstant.CLIENT_DID_NOT_SENT_ANY_MESSAGE}' errorMessage = HttpClientConstant.CLIENT_DID_NOT_SENT_ANY_MESSAGE possibleErrorMessage = None bodyAsJson = {} try : bodyAsJson = clientResponse.json() except Exception as innerException : bodyAsJsonException = FlaskUtil.safellyGetResponseJson(clientResponse) log.log(getErrorMessage, f'Invalid client response: {bodyAsJsonException}', exception=innerException) log.debug(getErrorMessage, f'Not possible to get error message from client response: {bodyAsJsonException}. Proceeding with value {bodyAsJson} by default', exception=innerException, muteStackTrace=True) try: if ObjectHelper.isNotNone(clientResponse): if ObjectHelper.isDictionary(bodyAsJson): possibleErrorMessage = bodyAsJson.get('message', bodyAsJson.get('error')).strip() if ObjectHelper.isList(bodyAsJson) and 0 < len(bodyAsJson): possibleErrorMessage = bodyAsJson[0].get('message', bodyAsJson[0].get('error')).strip() if ObjectHelper.isNotNone(possibleErrorMessage) and StringHelper.isNotBlank(possibleErrorMessage): errorMessage = f'{c.LOG_CAUSE}{possibleErrorMessage}' else: log.debug(getErrorMessage, f'Client response {FlaskUtil.safellyGetResponseJson(clientResponse)}') exceptionPortion = HttpClientConstant.ERROR_AT_CLIENT_CALL_MESSAGE if ObjectHelper.isNone(exception) or StringHelper.isBlank(exception) else str(exception) completeErrorMessage = f'{exceptionPortion}{c.DOT_SPACE}{errorMessage}' except Exception as exception: log.warning(getErrorMessage, f'Not possible to get error message. Returning {completeErrorMessage} by default', exception=exception) return completeErrorMessage
def importResource(resourceName, resourceModuleName=None): if not resourceName in IGNORE_REOURCE_LIST: resource = None if not resourceModuleName: resourceModuleName = resourceName try: module = __import__(resourceModuleName) except: log.warning( importResource, f'Not possible to import "{resourceName}" resource from "{resourceModuleName}" module. Going for a second attempt' ) try: module = importlib.import_module(resourceModuleName) except Exception as exception: module = None log.error( importResource, f'Not possible to import "{resourceName}" resource from "{resourceModuleName}" module in the second attempt either', exception) if module: try: resource = getattr(module, resourceName) except Exception as exception: log.warning( importResource, f'Not possible to import "{resourceName}" resource from "{resourceModuleName}" module. cause: {str(exception)}' ) return resource
def innerResourceInstanceMethod(*args,**kwargs) : resourceInstance = args[0] completeResponse = None if logRequest : log.prettyJson( resourceInstanceMethod, 'bodyRequest', json.loads(Serializer.jsonifyIt(args[1:])), condition = logRequest, logLevel = log.DEBUG ) try : FlaskManager.validateKwargs( kwargs, resourceInstance, innerResourceInstanceMethod, requestHeaderClass = requestHeaderClass, requestParamClass = requestParamClass ) FlaskManager.validateArgs(args, requestClass, innerResourceInstanceMethod) completeResponse = resourceInstanceMethod(*args,**kwargs) FlaskManager.validateResponseClass(responseClass, completeResponse) except Exception as exception : log.warning(innerResourceInstanceMethod, 'Not posssible to complete request', exception=exception) raise exception controllerResponse = completeResponse[0] if ObjectHelper.isNotNone(completeResponse[0]) else {'message' : completeResponse[1].enumName} if logResponse : log.prettyJson( resourceInstanceMethod, 'bodyResponse', json.loads(Serializer.jsonifyIt(controllerResponse)), condition = logResponse, logLevel = log.DEBUG ) return completeResponse[0]
def getErrorMessage(self, response) : errorMessage = 'WitAi did not sent any message' try : errorMessage = response.json()['error'] except Exception as exception : log.warning(self.getErrorMessage, 'Not possible to get error message from response', exception=exception) return errorMessage
def mustLogWithoutColors(): # Arrange noExceptionThrown = 'exception not thrown' someLogMessage = 'some log message' someExceptionMessage = 'some exception message' someInnerExceptionMessage = 'some inner exception message' exception = None someExceptionMessageWithStackTrace = f'{someExceptionMessage} with stacktrace' someExceptionMessageWithoutStackTrace = f'{someExceptionMessage} without stacktrace' def controlableException(logType, muteStackTrace=False): try: raise Exception( someExceptionMessageWithoutStackTrace if muteStackTrace else someExceptionMessageWithStackTrace) except Exception as exception: if logType in OPTIONAL_EXCEPTION_LOG_TYPES: logType(logType, someLogMessage, exception=exception, muteStackTrace=muteStackTrace) else: logType(logType, someLogMessage, exception, muteStackTrace=muteStackTrace) # Act log.success(log.success, someLogMessage) log.setting(log.setting, someLogMessage) log.debug(log.debug, someLogMessage) log.warning(log.warning, someLogMessage) controlableException(log.log) controlableException(log.debug) controlableException(log.warning) controlableException(log.wraper) controlableException(log.failure) controlableException(log.error) controlableException(log.test) controlableException(log.log, muteStackTrace=True) controlableException(log.debug, muteStackTrace=True) controlableException(log.warning, muteStackTrace=True) controlableException(log.wraper, muteStackTrace=True) controlableException(log.failure, muteStackTrace=True) controlableException(log.error, muteStackTrace=True) controlableException(log.test, muteStackTrace=True) log.log(log.log, someLogMessage, None) log.debug(log.debug, someLogMessage, None) log.warning(log.warning, someLogMessage, None) log.wraper(log.wraper, noExceptionThrown, None) log.failure(log.failure, noExceptionThrown, None) log.error(log.error, noExceptionThrown, None) log.test(log.test, someLogMessage, None) # Assert assert 'my environment' == EnvironmentHelper.get( SettingHelper.ACTIVE_ENVIRONMENT)
def getJwtMannager(appInstance, jwtSecret, algorithm=None, headerName=None, headerType=None): if not jwtSecret: log.warning( getJwtMannager, f'Not possible to instanciate sessionManager{c.DOT_SPACE_CAUSE}Missing jwt secret at {ConfigurationKeyConstant.API_SESSION_SECRET}' ) else: jwtManager = JwtManager( jwtSecret, ConverterStatic.getValueOrDefault( algorithm, JwtConstant.DEFAULT_JWT_SESSION_ALGORITHM), ConverterStatic.getValueOrDefault( headerName, JwtConstant.DEFAULT_JWT_SESSION_HEADER_NAME), ConverterStatic.getValueOrDefault( headerType, JwtConstant.DEFAULT_JWT_SESSION_HEADER_TYPE)) if SettingHelper.activeEnvironmentIsLocal(): info = { 'secret': jwtManager.secret, 'algorithm': jwtManager.algorithm, 'headerName': jwtManager.headerName, 'headerType': jwtManager.headerType } log.prettyJson(getJwtMannager, f'JWT session', info, logLevel=log.SETTING) return jwtManager
def validateResponseClass(responseClass, controllerResponse): if isNotPythonFrameworkHttpsResponse(controllerResponse): raiseBadResponseImplementation( f'Python Framework response cannot be null. It should be a list like this: [{"RESPONSE_CLASS" if ObjectHelper.isNone(responseClass) else responseClass if ObjectHelper.isNotList(responseClass) else responseClass[0]}, HTTPS_CODE]' ) if ObjectHelper.isNotNone(responseClass): if Serializer.isSerializerList(responseClass): if 0 == len(responseClass): log.warning(validateResponseClass, f'"responseClass" was not defined') elif 1 == len(responseClass): if ObjectHelper.isNotList(responseClass[0]): if not isinstance(controllerResponse[0], responseClass[0]): raiseBadResponseImplementation( f'Response class does not match expected class. Expected "{responseClass[0].__name__}", response "{controllerResponse[0].__class__.__name__}"' ) elif ObjectHelper.isNotList(responseClass[0][0]): if ObjectHelper.isNotList(controllerResponse[0]): raiseBadResponseImplementation( f'Response is not a list. Expected "{responseClass[0].__class__.__name__}", but found "{controllerResponse[0].__class__.__name__}"' ) elif Serializer.isSerializerList( controllerResponse[0] ) and 0 < len(controllerResponse[0]) and not isinstance( controllerResponse[0][0], responseClass[0][0]): raiseBadResponseImplementation( f'Response element class does not match expected element class. Expected "{responseClass[0][0].__name__}", response "{controllerResponse[0][0].__class__.__name__}"' ) else: if not isinstance(controllerResponse[0], responseClass): raiseBadResponseImplementation( f'Response class does not match expected class. Expected "{responseClass.__name__}", response "{controllerResponse[0].__class__.__name__}"' ) else: log.warning(validateResponseClass, f'"responseClass" was not defined')
def run(self): try: self.model.metadata.create_all(self.engine) except Exception as firstException: waittingTime = 30 log.warning( self.run, f'Not possible to run. Going for a second attemp in {waittingTime} seconds', exception=firstException) time.sleep(waittingTime) try: self.model.metadata.create_all(self.engine) except Exception as secondException: waittingTime = 30 log.warning( self.run, f'Not possible to run either. Going for a third and last attemp in {waittingTime} seconds', exception=secondException) time.sleep(waittingTime) try: self.model.metadata.create_all(self.engine) except Exception as secondException: log.error(self.run, 'Not possible to run', exception) raise exception log.debug(self.run, 'Database tables created')
def getNullableApi(): api = None try: api = getApi() except Exception as exception: log.warning(getNullableApi, 'Not possible to get api', exception=exception) return api
def getJwtMannager(appInstance, jwtSecret): if not jwtSecret: log.warning( JWTManager, f'Not possible to instanciate jwtManager{DOT_SPACE_CAUSE}Missing jwt secret' ) else: jwtMannager = JWTManager(appInstance) appInstance.config[KW_JWT_SECRET_KEY] = jwtSecret appInstance.config[KW_JWT_BLACKLIST_ENABLED] = True return jwtMannager
def getCompleteResponse(clientResponse, responseClass, produces, fallbackStatus=HttpStatus.INTERNAL_SERVER_ERROR): responseBody, responseHeaders, responseStatus = dict(), dict(), fallbackStatus responseHeaders = FlaskUtil.safellyGetResponseHeaders(clientResponse) responseBody = FlaskUtil.safellyGetResponseJson(clientResponse) try : responseStatus = HttpStatus.map(HttpStatus.NOT_FOUND if ObjectHelper.isNone(clientResponse.status_code) else clientResponse.status_code) except Exception as exception : responseStatus = HttpStatus.map(fallbackStatus) log.warning(getCompleteResponse, f'Not possible to get client response status. Returning {responseStatus} by default', exception=exception) responseHeaders = { **{HttpDomain.HeaderKey.CONTENT_TYPE: produces}, **responseHeaders } responseStatus = ConverterStatic.getValueOrDefault(responseStatus, HttpStatus.map(fallbackStatus)) if ObjectHelper.isNone(responseClass): return responseBody, responseHeaders, responseStatus return Serializer.convertFromJsonToObject(responseBody, responseClass), responseHeaders, responseStatus
def close(self): try: close_all_sessions() self.engine.dispose() # NOTE: close required before dispose! except Exception as firstException: log.warning( self.close, 'not possible to close connections. Going for a second attempt', exception=firstException) try: close_all_sessions() self.engine.dispose() # NOTE: close required before dispose! except Exception as secondException: log.error( self.close, 'not possible to close connections at the second attempt either', secondException) raise secondException log.debug(self.close, 'Connections closed')
def handleLogErrorException(exception, resourceInstance, resourceInstanceMethod, apiInstance): if not (isinstance(exception.__class__, GlobalException) or GlobalException.__name__ == exception.__class__.__name__): log.warning( handleLogErrorException, f'Failed to excecute {resourceInstanceMethod.__name__} method due to {exception.__class__.__name__} exception', exception=exception) message = None status = None logMessage = None if (isinstance(exception.__class__, NoAuthorizationError) or NoAuthorizationError.__name__ == exception.__class__.__name__ or isinstance(exception.__class__, RevokedTokenError) or RevokedTokenError.__name__ == exception.__class__.__name__ or isinstance(exception.__class__, ExpiredSignatureError) or ExpiredSignatureError.__name__ == exception.__class__.__name__): if not message: message = c.NOTHING message += str(exception) status = HttpStatus.UNAUTHORIZED if ObjectHelper.isNotEmpty(str(exception)): logMessage = str(exception) exception = GlobalException(message=message, logMessage=logMessage, logResource=resourceInstance, logResourceMethod=resourceInstanceMethod, status=status) try: if not exception.logResource or c.NOTHING == exception.logResource or not resourceInstance == exception.logResource: exception.logResource = resourceInstance if not exception.logResourceMethod or c.NOTHING == exception.logResourceMethod or not resourceInstanceMethod == exception.logResourceMethod: exception.logResourceMethod = resourceInstanceMethod httpErrorLog = ErrorLog.ErrorLog() httpErrorLog.override(exception) apiInstance.repository.saveAndCommit(httpErrorLog) except Exception as errorLogException: log.warning(resourceInstance.__class__, f'Failed to persist {ErrorLog.ErrorLog.__name__}', exception=errorLogException) return exception
def getObjectAsDictionary(instance, fieldsToExpand=[EXPAND_ALL_FIELDS], visitedIdInstances=None) : # print(instance) if ObjectHelper.isNone(visitedIdInstances) : visitedIdInstances = [] if ObjectHelper.isNativeClassInstance(instance) or ObjectHelper.isNone(instance) : return instance if EnumAnnotation.isEnumItem(instance) : return instance.enumValue if isDatetimeRelated(instance) : return str(instance) # print(f'{instance} not in {visitedIdInstances}: {instance not in visitedIdInstances}') isVisitedInstance = id(instance) in visitedIdInstances innerVisitedIdInstances = [*visitedIdInstances.copy()] if ObjectHelper.isDictionary(instance) and not isVisitedInstance : for key,value in instance.items() : instance[key] = getObjectAsDictionary(value, visitedIdInstances=innerVisitedIdInstances) return instance elif isSerializerCollection(instance) : objectValueList = [] for innerObject in instance : innerAttributeValue = getObjectAsDictionary(innerObject, visitedIdInstances=innerVisitedIdInstances) if ObjectHelper.isNotNone(innerAttributeValue) : objectValueList.append(innerAttributeValue) return objectValueList elif not isVisitedInstance : jsonInstance = {} try : # print(id(instance)) innerVisitedIdInstances.append(id(instance)) atributeNameList = ReflectionHelper.getAttributeNameList(instance.__class__) for attributeName in atributeNameList : attributeValue = getattr(instance, attributeName) if ReflectionHelper.isNotMethodInstance(attributeValue): jsonInstance[attributeName] = getObjectAsDictionary(attributeValue, visitedIdInstances=innerVisitedIdInstances) else : jsonInstance[attributeName] = None except Exception as exception : log.warning(getObjectAsDictionary, f'Not possible to get attribute name list from {ReflectionHelper.getName(ReflectionHelper.getClass(instance, muteLogs=True), muteLogs=True)}', exception=exception) if ObjectHelper.isNotEmpty(jsonInstance) : return jsonInstance return str(instance)
def addResource(apiInstance, appInstance): apiInstance.sessionManager = None try: apiInstance.sessionManager = getJwtMannager( appInstance, apiInstance.globals.getApiSetting( ConfigurationKeyConstant.API_SESSION_SECRET), algorithm=apiInstance.globals.getApiSetting( ConfigurationKeyConstant.API_SESSION_ALGORITHM), headerName=apiInstance.globals.getApiSetting( ConfigurationKeyConstant.API_SESSION_HEADER), headerType=apiInstance.globals.getApiSetting( ConfigurationKeyConstant.API_SESSION_TYPE)) apiInstance.sessionManager.api = apiInstance except Exception as exception: log.warning(addResource, 'Not possible to add SessionManager', exception=exception) if ObjectHelper.isNotNone(apiInstance.sessionManager): log.success(initialize, 'SessionManager created') return apiInstance.sessionManager
def innerResourceInstanceMethod(*args, **kwargs): resourceInstanceName = methodClassName[:-len( FlaskManager.KW_SCHEDULER_RESOURCE)] resourceInstanceName = f'{resourceInstanceName[0].lower()}{resourceInstanceName[1:]}' args = FlaskManager.getArgumentInFrontOfArgs( args, ReflectionHelper.getAttributeOrMethod( apiInstance.resource.scheduler, resourceInstanceName)) resourceInstance = args[0] muteLogs = resourceInstance.muteLogs or resourceMethod.muteLogs if resourceInstance.enabled and not resourceInstance.disabled and not resourceMethod.disabled: if not muteLogs: log.debug( resourceMethod, f'{resourceMethod.shedulerId} scheduler started with args={methodArgs} and kwargs={methodKwargs}' ) methodReturn = None try: FlaskManager.validateArgs(args, requestClass, innerResourceInstanceMethod) methodReturn = resourceMethod(*args, **kwargs) except Exception as exception: if not muteLogs: log.warning( resourceMethod, f'Not possible to run {resourceMethod.shedulerId} properly', exception=exception, muteStackTrace=True) FlaskManager.raiseAndPersistGlobalException( exception, resourceInstance, resourceMethod) if not muteLogs: log.debug( resourceMethod, f'{resourceMethod.shedulerId} scheduler finished') return methodReturn if not muteLogs: log.warning( resourceMethod, f'{resourceMethod.shedulerId} scheduler didn{c.SINGLE_QUOTE}t started. {"Schedulers are disabled" if not resourceInstance.enabled else "This scheduler is disabled" if resourceInstance.disabled else "This scheduler method is disabled"}' )
def getJwtMannager(appInstance, jwtSecret, algorithm=None, headerName=None, headerType=None): if ObjectHelper.isNone(jwtSecret): log.warning( getJwtMannager, f'Not possible to instanciate securityManager{c.DOT_SPACE_CAUSE}Missing jwt secret at {ConfigurationKeyConstant.API_SECURITY_SECRET}' ) else: jwtMannager = JWTManager(appInstance) appInstance.config[JwtConstant.KW_JWT_SECRET_KEY] = jwtSecret appInstance.config[JwtConstant.KW_JWT_BLACKLIST_ENABLED] = True appInstance.config[ JwtConstant.KW_JWT_ALGORITHM] = ConverterStatic.getValueOrDefault( algorithm, JwtConstant.DEFAULT_JWT_SECURITY_ALGORITHM) appInstance.config[ JwtConstant. KW_JWT_HEADER_NAME] = ConverterStatic.getValueOrDefault( headerName, JwtConstant.DEFAULT_JWT_SECURITY_HEADER_NAME) appInstance.config[ JwtConstant. KW_JWT_HEADER_TYPE] = ConverterStatic.getValueOrDefault( headerType, JwtConstant.DEFAULT_JWT_SECURITY_HEADER_TYPE) if SettingHelper.activeEnvironmentIsLocal(): info = { 'secret': jwtSecret, 'algorithm': appInstance.config[JwtConstant.KW_JWT_ALGORITHM], 'headerName': appInstance.config[JwtConstant.KW_JWT_HEADER_NAME], 'headerType': appInstance.config[JwtConstant.KW_JWT_HEADER_TYPE] } log.prettyJson(getJwtMannager, f'JWT security', info, logLevel=log.SETTING) return jwtMannager
def retrieveApiInstance(apiInstance=None, arguments=None): if isApiInstance(apiInstance): return apiInstance if isApiInstance(API_INSTANCE_HOLDER.get(KEY_API_INSTANCE)): return API_INSTANCE_HOLDER.get(KEY_API_INSTANCE) if ObjectHelper.isNone(apiInstance) and ObjectHelper.isNotNone(arguments): try: apiInstance = arguments[0].globals.api except Exception as exception: log.warning( retrieveApiInstance, f'''Not possible to retrieve api instance by {arguments}. Going for another approach''', exception=exception) if not isApiInstance(apiInstance): log.warning( retrieveApiInstance, f'''Not possible to retrieve api instance. Going for a slower approach''' ) apiInstance = getNullableApi() if ObjectHelper.isNone(apiInstance): raise Exception('Not possible to retrieve api instance') API_INSTANCE_HOLDER[KEY_API_INSTANCE] = apiInstance return apiInstance
def addHeadersListToUrlVerb(verb, url, endPointUrl, requestHeaderClass, documentation): if ObjectHelper.isList(requestHeaderClass) and 0 == len( requestHeaderClass): log.warning( addHeadersListToUrlVerb, f'Invalid request header class. requestHeaderClass: {requestHeaderClass}' ) if ObjectHelper.isNotNone(requestHeaderClass): log.log( addHeadersListToUrlVerb, f'verb: {verb}, url: {url}, requestHeaderClass: {requestHeaderClass}' ) if ObjectHelper.isNotList(requestHeaderClass): for attributeName in ReflectionHelper.getAttributeOrMethodNameList( requestHeaderClass): documentation[k.PATHS][url][verb][k.PARAMETERS].append({ k.NAME: attributeName, k.IN: v.HEADER, k.TYPE: v.STRING, k.REQUIRED: True, k.DESCRIPTION: None }) elif 1 == len(requestHeaderClass): if ObjectHelper.isNotNone(requestHeaderClass[0]): if ObjectHelper.isNotList(requestHeaderClass[0]): addHeadersListToUrlVerb(verb, url, endPointUrl, requestHeaderClass[0], documentation) ###-, where=where elif ObjectHelper.isList(requestHeaderClass[0]) and 1 == len( requestHeaderClass[0]): if ObjectHelper.isNotNone(requestHeaderClass[0] [0]) and ObjectHelper.isNotList( requestHeaderClass[0][0]): # addHeadersListToUrlVerb(verb, url, endPointUrl, requestHeaderClass[0][0], documentation) ###-, where=where log.warning( addHeadersListToUrlVerb, f'Request header class as list not implemented yet. requestHeaderClass: {requestHeaderClass}' ) else: log.warning( addHeadersListToUrlVerb, f'Unexpected request header class. requestHeaderClass: {requestHeaderClass}' )
def warning(self, message, exception=None): if c.TRUE == self.warningStatus: log.warning(self.__class__, message, exception=exception)
def handleLogErrorException(exception, resourceInstance, resourceInstanceMethod, context, apiInstance=None): if not (isinstance(exception, GlobalException) or GlobalException.__name__ == exception.__class__.__name__): log.debug( handleLogErrorException, f'Failed to excecute {resourceInstanceMethod.__name__} method due to {exception.__class__.__name__} exception', exception=exception) message = None status = None logMessage = None if (isinstance(exception, NoAuthorizationError) or NoAuthorizationError.__name__ == exception.__class__.__name__ or isinstance(exception, RevokedTokenError) or RevokedTokenError.__name__ == exception.__class__.__name__ or isinstance(exception, InvalidSignatureError) or InvalidSignatureError.__name__ == exception.__class__.__name__ or isinstance(exception, ExpiredSignatureError) or ExpiredSignatureError.__name__ == exception.__class__.__name__): message = 'Unauthorized' if ObjectHelper.isNone( exception) or StringHelper.isBlank( str(exception)) else str(exception) status = HttpStatus.UNAUTHORIZED if ObjectHelper.isNotNone(exception) and StringHelper.isNotBlank( str(exception)): logMessage = str(exception) else: logMessage = DEFAULT_LOG_MESSAGE exception = GlobalException(message=message, logMessage=logMessage, logResource=resourceInstance, logResourceMethod=resourceInstanceMethod, status=status) try: if not context == exception.context: exception = GlobalException( message=exception.message, logMessage=exception.logMessage, logResource=resourceInstance, logResourceMethod=resourceInstanceMethod, status=exception.status, context=context) else: if not exception.logResource or c.NOTHING == exception.logResource or not resourceInstance == exception.logResource: exception.logResource = resourceInstance if not exception.logResourceMethod or c.NOTHING == exception.logResourceMethod or not resourceInstanceMethod == exception.logResourceMethod: exception.logResourceMethod = resourceInstanceMethod httpErrorLog = ErrorLog.ErrorLog() httpErrorLog.override(exception) if ObjectHelper.isNone(apiInstance): from python_framework import FlaskManager apiInstance = FlaskManager.getApi() else: apiInstance = apiInstance try: apiInstance.repository.commit() except Exception as preCommitException: log.warning( handleLogErrorException, f'Failed to pre commit before persist {ErrorLog.ErrorLog.__name__}', exception=preCommitException) apiInstance.repository.saveAndCommit(httpErrorLog) except Exception as errorLogException: log.warning(handleLogErrorException, f'Failed to persist {ErrorLog.ErrorLog.__name__}', exception=errorLogException) return exception