def return_handler(ret: Any, path_enum: EnumMeta, paths: dict, logger, event, context, debug: bool) -> Any: if not ret: return ret _payloads = [] try: if isinstance(ret, Payload): _payloads.append(ret.validate(path_enum)) elif isinstance(ret, list): for r in ret: if isinstance(r, Payload): _payloads.append(r.validate(path_enum)) except Exception as e: logger.debug(utils.exception_to_str(e)) raise lpipe.exceptions.FailButContinue( f"Something went wrong while extracting Payloads from a function return value: {ret}" ) from e if _payloads: logger.debug(f"{len(_payloads)} dynamic payloads received") for p in _payloads: logger.debug(f"Executing dynamic payload: {p}") try: ret = execute_payload(p, path_enum, paths, logger, event, context, debug) except Exception as e: logger.debug(utils.exception_to_str(e)) raise lpipe.exceptions.FailButContinue( f"Failed to execute returned Payload: {p}") from e return ret
def return_handler(ret: Any, state: State) -> Any: if not ret: return ret _payloads = [] try: if isinstance(ret, Payload): _payloads.append(ret.validate(state.path_enum)) elif isinstance(ret, list): for r in ret: if isinstance(r, Payload): _payloads.append(r.validate(state.path_enum)) except Exception as e: state.logger.debug(utils.exception_to_str(e)) raise lpipe.exceptions.FailButContinue( f"Something went wrong while extracting Payloads from a function return value: {ret}" ) from e if _payloads: state.logger.debug(f"{len(_payloads)} dynamic payloads received") for p in _payloads: state.logger.debug(f"executing dynamic payload: {p}") try: ret = execute_payload(payload=p, state=state) except Exception: state.logger.error(f"Failed to execute returned {p}") raise return ret
def execute_action( payload: Payload, path_enum: EnumMeta, paths: dict, action: Action, logger, event, context, debug: bool = False, exception_handler: FunctionType = None, ): """Execute functions, paths, and queues (shortcuts) in an Action. Args: payload (Payload): path_enum (EnumMeta): An Enum class which define the possible paths available in this lambda. paths (dict): Keys are path names / enums and values are a list of Action objects action: (Action): logger: event: https://docs.aws.amazon.com/lambda/latest/dg/python-handler.html context: https://docs.aws.amazon.com/lambda/latest/dg/python-handler.html debug (bool): exception_handler (FunctionType): A function which will be used to capture exceptions (e.g. contrib.sentry.capture) """ assert isinstance(action, Action) ret = None # Build action kwargs and validate type hints try: dummy = ["logger", "event"] action_kwargs = build_action_kwargs(action, { **{k: None for k in dummy}, **payload.kwargs }) for k in dummy: action_kwargs.pop(k, None) except (TypeError, AssertionError) as e: raise lpipe.exceptions.InvalidPayloadError( f"Failed to run {payload.path.name} {action} due to {utils.exception_to_str(e)}" ) from e default_kwargs = { "logger": logger, "event": PayloadEvent(event=event, context=context, payload=payload), } # Run action functions for f in action.functions: assert isinstance(f, FunctionType) try: # TODO: if ret, set _last_output _log_context = {"path": payload.path.name, "function": f.__name__} with logger.context(bind={ **_log_context, "kwargs": action_kwargs }): logger.log("Executing function.") with logger.context(bind=_log_context): ret = f(**{**action_kwargs, **default_kwargs}) ret = return_handler( ret=ret, path_enum=path_enum, paths=paths, logger=logger, event=event, context=context, debug=debug, ) except lpipe.exceptions.LPBaseException: # CAPTURES: # lpipe.exceptions.FailButContinue # lpipe.exceptions.FailCatastrophically raise except Exception as e: logger.error( f"Skipped {payload.path.name} {f.__name__} due to unhandled Exception. This is very serious; please update your function to handle this. Reason: {utils.exception_to_str(e)}" ) if exception_handler: exception_handler(e) if debug: raise lpipe.exceptions.FailCatastrophically( utils.exception_to_str(e)) from e payloads = [] for _path in action.paths: payloads.append( Payload( path=normalize.normalize_path(path_enum, _path), kwargs=action_kwargs, event_source=payload.event_source, ).validate(path_enum)) for _queue in action.queues: payloads.append( Payload(queue=_queue, kwargs=action_kwargs, event_source=payload.event_source).validate()) for p in payloads: ret = execute_payload(p, path_enum, paths, logger, event, context, debug) return ret
def execute_payload( payload: Payload, path_enum: EnumMeta, paths: dict, logger, event, context, debug=False, ): """Execute functions, paths, and shortcuts in a Path. Args: payload (Payload): path_enum (EnumMeta): An Enum class which define the possible paths available in this lambda. paths (dict): Keys are path names / enums and values are a list of Action objects logger: event: https://docs.aws.amazon.com/lambda/latest/dg/python-handler.html context: https://docs.aws.amazon.com/lambda/latest/dg/python-handler.html """ if not logger: logger = ServerlessLogger() ret = None if not isinstance(payload.path, path_enum): payload.path = clean_path(path_enum, payload.path) if isinstance(payload.path, Enum): # PATH # Allow someone to simplify their definition of a Path to a list of functions. if all([isinstance(f, FunctionType) for f in paths[payload.path]]): paths[payload.path] = [Action(functions=action)] for action in paths[payload.path]: assert isinstance(action, Action) # Build action kwargs and validate type hints try: dummy = ["logger", "event"] action_kwargs = build_action_kwargs(action, { **{k: None for k in dummy}, **payload.kwargs }) for k in dummy: action_kwargs.pop(k, None) except (TypeError, AssertionError) as e: raise InvalidPayloadError( f"Failed to run {payload.path.name} {action} due to {exception_to_str(e)}" ) from e # Run action functions for f in action.functions: assert isinstance(f, FunctionType) try: # TODO: if ret, set _last_output _log_context = { "path": payload.path.name, "function": f.__name__ } with logger.context(bind={ **_log_context, "kwargs": action_kwargs }): logger.log("Executing function.") with logger.context(bind=_log_context): ret = f( **{ **action_kwargs, "logger": logger, "event": { "event": event, "context": context, "payload": payload, }, }) if ret: _payloads = [] try: if isinstance(ret, Payload): _payloads.append(ret.validate(path_enum)) elif isinstance(ret, list): for r in ret: if isinstance(r, Payload): _payloads.append(r.validate(path_enum)) except Exception as e: logger.debug(exception_to_str(e)) raise FailButContinue( f"Something went wrong while extracting Payloads from a function return value. {ret}" ) from e for p in _payloads: logger.debug( f"Function returned a Payload. Executing. {p}") try: ret = execute_payload(p, path_enum, paths, logger, event, context, debug) except Exception as e: logger.debug(exception_to_str(e)) raise FailButContinue( f"Failed to execute returned Payload. {p}" ) from e except LpipeBaseException: # CAPTURES: # FailButContinue # FailCatastrophically raise except Exception as e: logger.error( f"Skipped {payload.path.name} {f.__name__} due to unhandled Exception. This is very serious; please update your function to handle this. Reason: {exception_to_str(e)}" ) sentry.capture(e) if debug: raise FailCatastrophically() from e # Run action paths / shortcuts for path_descriptor in action.paths: _payload = Payload( clean_path(path_enum, path_descriptor), action_kwargs, payload.event_source, ).validate(path_enum) ret = execute_payload(_payload, path_enum, paths, logger, event, context, debug) elif isinstance(payload.path, Queue): # SHORTCUT queue = payload.path assert isinstance(queue.type, QueueType) with logger.context( bind={ "path": queue.path, "queue_type": queue.type, "queue_name": queue.name, "kwargs": payload.kwargs, }): logger.log("Pushing record.") put_record(queue=queue, record={ "path": queue.path, "kwargs": payload.kwargs }) else: logger.info( f"Path should be a string (path name), Path (path Enum), or Queue: {payload.path})" ) return ret
def log_exception(state: State, e: BaseException): state.logger.error(utils.exception_to_str(e)) if state.exception_handler: state.exception_handler(e)