From 70e866f83a6b1bc7727544b8b114606c81ef9b6c Mon Sep 17 00:00:00 2001 From: cordeirops Date: Wed, 19 Nov 2025 19:37:57 -0300 Subject: [PATCH 01/12] Add TaskMissingError exception and handle missing pipeline steps - Introduced TaskMissingError to signal when a pipeline lacks steps. - Updated the compiler to raise TaskMissingError if no steps are present. - Mapped TaskMissingError to RPCTaskIsMissing for RPC error handling. - Enhanced error handling in the notebook compilation process to provide user guidance for missing steps. - Added utility functions in the UI to display informative dialogs for missing pipeline steps. Signed-off-by: cordeirops --- backend/kale/compiler.py | 7 +++ backend/kale/errors.py | 12 +++++ backend/kale/rpc/errors.py | 9 ++++ backend/kale/rpc/nb.py | 64 +++++++++++++++++------ labextension/src/lib/RPCUtils.tsx | 70 ++++++++++++++++++++++++-- labextension/src/widgets/LeftPanel.tsx | 53 ++++++++++++++++++- 6 files changed, 195 insertions(+), 20 deletions(-) create mode 100644 backend/kale/errors.py diff --git a/backend/kale/compiler.py b/backend/kale/compiler.py index 663f68bd2..792e0dfb2 100644 --- a/backend/kale/compiler.py +++ b/backend/kale/compiler.py @@ -12,6 +12,7 @@ from kale import __version__ as KALE_VERSION from kale.pipeline import Pipeline, Step, PipelineParam from kale.common import kfputils, utils, graphutils +from kale.errors import TaskMissingError log = logging.getLogger(__name__) @@ -89,6 +90,12 @@ def generate_dsl(self): Returns (str): A Python executable script """ + # Fail early if there are no steps in the pipeline. Raise a domain- + # specific exception so higher layers (RPC) can map it to an RPC + # error without coupling core to the RPC layer. + if not hasattr(self.pipeline, 'steps') or not self.pipeline.steps: + raise TaskMissingError('Task is missing from pipeline.') + # List of lightweight components generated code lightweight_components = [ self.generate_lightweight_component(step) diff --git a/backend/kale/errors.py b/backend/kale/errors.py new file mode 100644 index 000000000..9d689b250 --- /dev/null +++ b/backend/kale/errors.py @@ -0,0 +1,12 @@ +"""Core domain exceptions for Kale (non-RPC). +Keep domain-specific exceptions here so core modules (compiler, processor) +can raise them without depending on RPC layer. +""" + + +class TaskMissingError(Exception): + """Raised when a Pipeline has no steps/tasks. + This is a domain-level exception and will be mapped to an RPC error + (RPCTaskIsMissing) by the RPC handler. + """ + pass diff --git a/backend/kale/rpc/errors.py b/backend/kale/rpc/errors.py index b56e32471..204c203c5 100644 --- a/backend/kale/rpc/errors.py +++ b/backend/kale/rpc/errors.py @@ -16,6 +16,7 @@ class Code(enum.Enum): INTERNAL_ERROR = 4 SERVICE_UNAVAILABLE = 5 UNHANDLED_ERROR = 6 + TASK_IS_MISSING = 7 class _RPCError(Exception): @@ -76,6 +77,14 @@ class RPCServiceUnavailableError(_RPCError): message = "Service is Unavailable" +class RPCTaskIsMissing(_RPCError): + """Task is missing from pipeline, you must add a pipeline step.""" + + name = "taskIsMissing" + code = Code.TASK_IS_MISSING + message = "Task is missing from pipeline, you must add a pipeline step." + + class RPCUnhandledError(_RPCError): """Unhandled RPC Error.""" diff --git a/backend/kale/rpc/nb.py b/backend/kale/rpc/nb.py index cfc2bbf8f..2e201a6be 100644 --- a/backend/kale/rpc/nb.py +++ b/backend/kale/rpc/nb.py @@ -10,7 +10,8 @@ from kale import marshal from kale.rpc.log import create_adapter from kale import Compiler, NotebookProcessor -from kale.rpc.errors import RPCInternalError +from kale.rpc.errors import RPCInternalError, RPCTaskIsMissing +from kale.errors import TaskMissingError from kale.common import podutils, kfputils, kfutils, astutils KALE_MARSHAL_DIR_POSTFIX = ".kale.marshal.dir" @@ -80,20 +81,53 @@ def get_base_image(request): def compile_notebook(request, source_notebook_path, notebook_metadata_overrides=None, debug=False): """Compile the notebook to KFP DSL.""" - processor = NotebookProcessor(source_notebook_path, - notebook_metadata_overrides) - pipeline = processor.run() - imports_and_functions = processor.get_imports_and_functions() - script_path = Compiler(pipeline, imports_and_functions).compile() - # FIXME: Why were we tapping into the Kale logger? - # instance = Kale(source_notebook_path, notebook_metadata_overrides, debug) - # instance.logger = request.log if hasattr(request, "log") else logger - - package_path = kfputils.compile_pipeline(script_path, - pipeline.config.pipeline_name) - - return {"pipeline_package_path": os.path.relpath(package_path), - "pipeline_metadata": pipeline.config.to_dict()} + try: + processor = NotebookProcessor(source_notebook_path, + notebook_metadata_overrides) + pipeline = processor.run() + imports_and_functions = processor.get_imports_and_functions() + script_path = Compiler(pipeline, imports_and_functions).compile() + + """FIXME: Why were we tapping into the Kale logger? + instance = Kale(source_notebook_path, + notebook_metadata_overrides, debug) + instance.logger = request.log if + hasattr(request, "log") else logger""" + + package_path = kfputils.compile_pipeline(script_path, + pipeline.config.pipeline_name) + + return {"pipeline_package_path": os.path.relpath(package_path), + "pipeline_metadata": pipeline.config.to_dict()} + except TaskMissingError as e: + # Domain-specific exception raised by core components when no + # pipeline steps are present. Map it to a specific RPC error so the + # frontend receives a concrete error code and message. Provide a + # slightly more actionable `details` field so the UI can display + # guidance to the user about how to fix the issue (e.g., tag a + # cell as a step or set `steps_defaults` in metadata). + msg = str(e) + request.log.exception("TaskMissingError during notebook " + "compilation: %s", msg) + raise RPCTaskIsMissing(details=msg, trans_id=request.trans_id) + except ValueError as e: + # kfp.compiler or graph_component may + # raise ValueError for other + # reasons; map them to an internal + # RPC error (unless they match the + # specific message — kept for backward compatibility). + msg = str(e) + request.log.exception("ValueError during notebook" + " compilation: %s", msg) + if 'Task is missing from pipeline' in msg: + raise RPCTaskIsMissing(details=msg, trans_id=request.trans_id) + raise RPCInternalError(details=msg, trans_id=request.trans_id) + except Exception as e: + # Let the run dispatcher handle generic exceptions as unhandled, + # but log for debug purposes. + request.log.exception("Unexpected error during " + "notebook compilation: %s", e) + raise def validate_notebook(request, source_notebook_path, diff --git a/labextension/src/lib/RPCUtils.tsx b/labextension/src/lib/RPCUtils.tsx index 4a180304a..2cd5eaf61 100644 --- a/labextension/src/lib/RPCUtils.tsx +++ b/labextension/src/lib/RPCUtils.tsx @@ -5,8 +5,10 @@ import * as React from 'react'; import { NotebookPanel } from '@jupyterlab/notebook'; import { Kernel } from '@jupyterlab/services'; import NotebookUtils from './NotebookUtils'; +// @ts-expect-error This module is not typed +import SanitizedHTML from 'react-sanitized-html'; import { isError, IError, IOutput } from '@jupyterlab/nbformat'; -import { Notification } from '@jupyterlab/apputils'; +import { Notification, Dialog, showDialog } from '@jupyterlab/apputils'; export const globalUnhandledRejection = async (event: any) => { console.error(event.reason); @@ -26,7 +28,7 @@ export const globalUnhandledRejection = async (event: any) => { const extensionName = getExtensionName(stackLines) Notification.error(`An unhandled error has been thrown.`, { actions: [ - { label: 'Details', callback: () => NotebookUtils.showMessage(alert_string, + { label: 'Details', callback: () => NotebookUtils.showMessage(alert_string, ["An unhandled error was thrown from:", extensionName, "Please see console for more details." @@ -72,6 +74,7 @@ export enum RPC_CALL_STATUS { InternalError = 4, ServiceUnavailable = 5, UnhandledError = 6, + TaskIsMissing = 7, } const getRpcCodeName = (code: number) => { @@ -88,6 +91,8 @@ const getRpcCodeName = (code: number) => { return 'InternalError'; case RPC_CALL_STATUS.ServiceUnavailable: return 'ServiceUnavailable'; + case RPC_CALL_STATUS.TaskIsMissing: + return 'TaskIsMissing'; default: return 'UnhandledError'; } @@ -238,6 +243,10 @@ export const showRpcError = async ( error: IRPCError, refresh: boolean = false, ): Promise => { + if (error && error.code === RPC_CALL_STATUS.TaskIsMissing) { + return await handleMissingPipelineStep(error); + } + await showError( 'An RPC Error has occurred', 'RPC', @@ -314,7 +323,7 @@ export abstract class BaseError extends Error { super(message); this.name = this.constructor.name; this.stack = new Error(message).stack; - + Object.setPrototypeOf(this, BaseError.prototype); } @@ -367,3 +376,58 @@ export class RPCError extends BaseError { await showRpcError(this.error, refresh); } } + + +/** + * handleMissingPipelineStep - Async handler for missing pipeline step RPC errors. + * + * Displays a modal dialog that informs the user that a pipeline step is missing, + * shows the RPC-provided message and details, and suggests remediation steps: + * tagging a code cell as a pipeline step or setting the notebook metadata + * `steps_defaults` to include the step name(s). The dialog body is rendered as + * sanitized HTML with a restricted set of allowed tags and attributes to avoid + * injection issues. The dialog offers two buttons: "Open docs" and "Close". + * Selecting "Open docs" opens the Kale README/docs page in a new browser tab. + * + * @param error - The IRPCError instance containing `err_message` and `err_details` + * to present in the dialog body. + * @returns A Promise that resolves once the user dismisses the dialog. If the + * user clicked "Open docs", the documentation page will be opened + * in a new tab as a fallback/help resource. + */ +async function handleMissingPipelineStep(error: IRPCError) { + const title = 'Pipeline step missing'; + const bodyLines = [ + `Message: ${error.err_message}`, + '', + `Details: ${error.err_details}`, + '', + 'You can fix this by tagging a code cell as a pipeline step (e.g. using the Left Panel), or by setting the notebook metadata `steps_defaults` to a list with the step name(s).', + ]; + const body = ( +
+ {bodyLines.map((s: string, i: number) => ( + + +
+
+ ))} +
+ ); + const buttons: ReadonlyArray = [ + Dialog.okButton({ label: 'Open docs' }), + Dialog.cancelButton({ label: 'Close' }), + ]; + const result = await showDialog({ title, body, buttons }); + const clicked = result.button ? (result.button.label as string) : ''; + if (clicked === 'Open docs') { + // Open the documentation in a new tab (repo README as fallback) + const docsUrl = 'https://github.com/kubeflow-kale/kale#readme'; + window.open(docsUrl, '_blank'); + } + return; +} + diff --git a/labextension/src/widgets/LeftPanel.tsx b/labextension/src/widgets/LeftPanel.tsx index e90295b9a..bc5f7ac66 100644 --- a/labextension/src/widgets/LeftPanel.tsx +++ b/labextension/src/widgets/LeftPanel.tsx @@ -93,6 +93,41 @@ export class KubeflowKaleLeftPanel extends React.Component { // init state default values state = DefaultState; + // Return the notebook file name without extension (e.g. 'MyNotebook' from 'path/to/MyNotebook.ipynb') + getNotebookFileName = (notebook: NotebookPanel | null): string => { + if (!notebook || !notebook.context || !notebook.context.path) { + return ''; + } + const path = notebook.context.path as string; + const base = path.split('/').pop() || ''; + return base.replace(/\.ipynb$/i, ''); + }; + + // Sanitize a name to match the pipeline name regex: + // '^[a-z0-9]([-a-z0-9]*[a-z0-9])?$' + // Steps: + // - lowercase + // - replace invalid chars with '-' + // - collapse multiple '-' into one + // - trim leading/trailing '-' + // - if result is empty, return a fallback unique name + sanitizePipelineName = (name: string): string => { + if (!name) return ''; + let s = name.toLowerCase(); + // replace any char that is not [a-z0-9-] with '-' + s = s.replace(/[^a-z0-9-]+/g, '-'); + // collapse multiple hyphens + s = s.replace(/-+/g, '-'); + // trim leading/trailing hyphens + s = s.replace(/^-+|-+$/g, ''); + // ensure it starts and ends with alphanumeric; if not, fallback + if (!/^[a-z0-9].*[a-z0-9]$/.test(s)) { + // fallback: use a predictable but unique name + return 'pipeline-' + Date.now().toString(36); + } + return s; + }; + getActiveNotebook = (): NotebookPanel | null => { return this.props.tracker.currentWidget; }; @@ -318,11 +353,19 @@ export class KubeflowKaleLeftPanel extends React.Component { } } + // Use notebook filename as default pipeline name when not provided in metadata + const defaultPipelineName = this.getNotebookFileName(notebook); + const sanitizedDefaultPipelineName = this.sanitizePipelineName( + defaultPipelineName + ); const metadata: IKaleNotebookMetadata = { ...notebookMetadata, experiment: experiment, experiment_name: experiment_name, - pipeline_name: notebookMetadata['pipeline_name'] || '', + pipeline_name: + notebookMetadata['pipeline_name'] && notebookMetadata['pipeline_name'] !== '' + ? notebookMetadata['pipeline_name'] + : sanitizedDefaultPipelineName, pipeline_description: notebookMetadata['pipeline_description'] || '', docker_image: notebookMetadata['docker_image'] || @@ -333,11 +376,17 @@ export class KubeflowKaleLeftPanel extends React.Component { metadata: metadata }); } else { + // If no notebook metadata exists, set pipeline_name to the sanitized notebook filename + const defaultPipelineName = this.getNotebookFileName(notebook); + const sanitizedDefaultPipelineName = this.sanitizePipelineName( + defaultPipelineName + ); this.setState(prevState => ({ metadata: { ...DefaultState.metadata, experiment: prevState.metadata.experiment, - experiment_name: prevState.metadata.experiment_name + experiment_name: prevState.metadata.experiment_name, + pipeline_name: sanitizedDefaultPipelineName } })); } From 20bdb69ea59c2aad06326ddbf3ba405cdffe5f5b Mon Sep 17 00:00:00 2001 From: cordeirops Date: Wed, 19 Nov 2025 22:20:43 -0300 Subject: [PATCH 02/12] Fix code format Signed-off-by: cordeirops --- backend/kale/errors.py | 1 + labextension/src/widgets/LeftPanel.tsx | 13 ++++++------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/kale/errors.py b/backend/kale/errors.py index 9d689b250..3afdda157 100644 --- a/backend/kale/errors.py +++ b/backend/kale/errors.py @@ -6,6 +6,7 @@ class TaskMissingError(Exception): """Raised when a Pipeline has no steps/tasks. + This is a domain-level exception and will be mapped to an RPC error (RPCTaskIsMissing) by the RPC handler. """ diff --git a/labextension/src/widgets/LeftPanel.tsx b/labextension/src/widgets/LeftPanel.tsx index bc5f7ac66..8751aec73 100644 --- a/labextension/src/widgets/LeftPanel.tsx +++ b/labextension/src/widgets/LeftPanel.tsx @@ -355,15 +355,15 @@ export class KubeflowKaleLeftPanel extends React.Component { // Use notebook filename as default pipeline name when not provided in metadata const defaultPipelineName = this.getNotebookFileName(notebook); - const sanitizedDefaultPipelineName = this.sanitizePipelineName( - defaultPipelineName - ); + const sanitizedDefaultPipelineName = + this.sanitizePipelineName(defaultPipelineName); const metadata: IKaleNotebookMetadata = { ...notebookMetadata, experiment: experiment, experiment_name: experiment_name, pipeline_name: - notebookMetadata['pipeline_name'] && notebookMetadata['pipeline_name'] !== '' + notebookMetadata['pipeline_name'] && + notebookMetadata['pipeline_name'] !== '' ? notebookMetadata['pipeline_name'] : sanitizedDefaultPipelineName, pipeline_description: notebookMetadata['pipeline_description'] || '', @@ -378,9 +378,8 @@ export class KubeflowKaleLeftPanel extends React.Component { } else { // If no notebook metadata exists, set pipeline_name to the sanitized notebook filename const defaultPipelineName = this.getNotebookFileName(notebook); - const sanitizedDefaultPipelineName = this.sanitizePipelineName( - defaultPipelineName - ); + const sanitizedDefaultPipelineName = + this.sanitizePipelineName(defaultPipelineName); this.setState(prevState => ({ metadata: { ...DefaultState.metadata, From 5bffb89506c153be5b789610210c748342ef3888 Mon Sep 17 00:00:00 2001 From: cordeirops Date: Wed, 19 Nov 2025 22:22:38 -0300 Subject: [PATCH 03/12] Fix code format Signed-off-by: cordeirops --- backend/kale/errors.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/kale/errors.py b/backend/kale/errors.py index 3afdda157..c7ab98c64 100644 --- a/backend/kale/errors.py +++ b/backend/kale/errors.py @@ -1,4 +1,5 @@ """Core domain exceptions for Kale (non-RPC). + Keep domain-specific exceptions here so core modules (compiler, processor) can raise them without depending on RPC layer. """ From 89e56fd1b535499f556f429d297e64b3e572e055 Mon Sep 17 00:00:00 2001 From: cordeirops Date: Wed, 19 Nov 2025 22:25:58 -0300 Subject: [PATCH 04/12] Fix lint check Signed-off-by: cordeirops --- labextension/src/widgets/LeftPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/labextension/src/widgets/LeftPanel.tsx b/labextension/src/widgets/LeftPanel.tsx index 8751aec73..506f51ee2 100644 --- a/labextension/src/widgets/LeftPanel.tsx +++ b/labextension/src/widgets/LeftPanel.tsx @@ -112,7 +112,7 @@ export class KubeflowKaleLeftPanel extends React.Component { // - trim leading/trailing '-' // - if result is empty, return a fallback unique name sanitizePipelineName = (name: string): string => { - if (!name) return ''; + if (!name) {return '';} let s = name.toLowerCase(); // replace any char that is not [a-z0-9-] with '-' s = s.replace(/[^a-z0-9-]+/g, '-'); From 4e0d52384745d98aa1dc5999d14f9fc47ba9af2d Mon Sep 17 00:00:00 2001 From: cordeirops Date: Wed, 19 Nov 2025 22:29:32 -0300 Subject: [PATCH 05/12] Fix lint check Signed-off-by: cordeirops --- labextension/src/widgets/LeftPanel.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/labextension/src/widgets/LeftPanel.tsx b/labextension/src/widgets/LeftPanel.tsx index 506f51ee2..5df376920 100644 --- a/labextension/src/widgets/LeftPanel.tsx +++ b/labextension/src/widgets/LeftPanel.tsx @@ -112,7 +112,9 @@ export class KubeflowKaleLeftPanel extends React.Component { // - trim leading/trailing '-' // - if result is empty, return a fallback unique name sanitizePipelineName = (name: string): string => { - if (!name) {return '';} + if (!name) { + return ''; + } let s = name.toLowerCase(); // replace any char that is not [a-z0-9-] with '-' s = s.replace(/[^a-z0-9-]+/g, '-'); From bf5e734f0f6ac36f77837417c3807cb78b9b4073 Mon Sep 17 00:00:00 2001 From: cordeirops Date: Tue, 13 Jan 2026 17:20:40 -0300 Subject: [PATCH 06/12] Fix issues pointed out by Jesuino Signed-off-by: cordeirops --- labextension/src/lib/RPCUtils.tsx | 98 +++++++++++++++++-------------- 1 file changed, 55 insertions(+), 43 deletions(-) diff --git a/labextension/src/lib/RPCUtils.tsx b/labextension/src/lib/RPCUtils.tsx index 2cd5eaf61..4ad45a825 100644 --- a/labextension/src/lib/RPCUtils.tsx +++ b/labextension/src/lib/RPCUtils.tsx @@ -21,26 +21,35 @@ export const globalUnhandledRejection = async (event: any) => { // isolate the segments const stackLines = errorStack.split('\n'); // create alert string - const alert_string = 'Unhandled Error' + const alert_string = 'Unhandled Error'; // call the toast pop up - if (errorStack.includes('lab/extensions/')){ + if (errorStack.includes('lab/extensions/')) { // if the error is caused by a jupyterlab extension, try to isolate the extension name - const extensionName = getExtensionName(stackLines) + const extensionName = getExtensionName(stackLines); Notification.error(`An unhandled error has been thrown.`, { actions: [ - { label: 'Details', callback: () => NotebookUtils.showMessage(alert_string, - ["An unhandled error was thrown from:", - extensionName, - "Please see console for more details." - ]) } + { + label: 'Details', + callback: () => + NotebookUtils.showMessage(alert_string, [ + 'An unhandled error was thrown from:', + extensionName, + 'Please see console for more details.' + ]) + } ], autoClose: 3000 }); } else { Notification.error(`An unhandled error has been thrown.`, { actions: [ - { label: 'Details', callback: () => NotebookUtils.showMessage(alert_string, - ["Please see console for more details."]) } + { + label: 'Details', + callback: () => + NotebookUtils.showMessage(alert_string, [ + 'Please see console for more details.' + ]) + } ], autoClose: 3000 }); @@ -48,9 +57,9 @@ export const globalUnhandledRejection = async (event: any) => { } }; -function getExtensionName(stackLines: Array){ - const urlSplit = stackLines.slice(1,2).toString(); - const extensionSplit = urlSplit.split('@').slice(1,2).toString(); +function getExtensionName(stackLines: Array) { + const urlSplit = stackLines.slice(1, 2).toString(); + const extensionSplit = urlSplit.split('@').slice(1, 2).toString(); const extensionParts = extensionSplit.split('/'); extensionParts.pop(); const extensionName = extensionParts.join('/'); @@ -74,7 +83,7 @@ export enum RPC_CALL_STATUS { InternalError = 4, ServiceUnavailable = 5, UnhandledError = 6, - TaskIsMissing = 7, + TaskIsMissing = 7 } const getRpcCodeName = (code: number) => { @@ -98,6 +107,8 @@ const getRpcCodeName = (code: number) => { } }; +const OPEN_DOCS_LABEL = 'Open docs'; + export const rokErrorTooltip = (rokError: IRPCError) => { return ( @@ -126,12 +137,12 @@ export const executeRpc = async ( env: Kernel.IKernelConnection | NotebookPanel, func: string, kwargs: any = {}, - ctx: { nb_path: string | null } = { nb_path: null }, + ctx: { nb_path: string | null } = { nb_path: null } ) => { const cmd: string = 'from kale.rpc.run import run as __kale_rpc_run\n' + `__kale_rpc_result = __kale_rpc_run("${func}", '${serialize( - kwargs, + kwargs )}', '${serialize(ctx)}')`; console.log('Executing command: ' + cmd); const expressions = { result: '__kale_rpc_result' }; @@ -140,10 +151,10 @@ export const executeRpc = async ( output = env instanceof NotebookPanel ? await NotebookUtils.sendKernelRequestFromNotebook( - env, - cmd, - expressions, - ) + env, + cmd, + expressions + ) : await NotebookUtils.sendKernelRequest(env, cmd, expressions); } catch (e) { if (typeof e === 'object' && e !== null) { @@ -152,7 +163,7 @@ export const executeRpc = async ( const error = { rpc: `${func}`, status: `${(e as IError).ename}: ${(e as IError).evalue}`, - output: (e as IError).traceback, + output: (e as IError).traceback }; throw new KernelError(error); } @@ -168,7 +179,7 @@ export const executeRpc = async ( const error = { rpc: `${func}`, status: output.result.status, - output: output, + output: output }; throw new KernelError(error); } @@ -187,7 +198,7 @@ export const executeRpc = async ( rpc: `${func}`, err_message: 'Failed to parse response as JSON', error: error, - jsonData: json_data, + jsonData: json_data }; throw new JSONParseError(jsonError); } @@ -199,12 +210,11 @@ export const executeRpc = async ( err_message: parsedResult.err_message, err_details: parsedResult.err_details, err_cls: parsedResult.err_cls, - trans_id: parsedResult.trans_id, + trans_id: parsedResult.trans_id }; throw new RPCError(error); } return parsedResult.result; - }; export const showError = async ( @@ -215,11 +225,11 @@ export const showError = async ( refresh: boolean = true, method: string | null = null, code: number | null = null, - trans_id: number | null = null, + trans_id: number | null = null ): Promise => { const msg: string[] = [ `Browser: ${navigator ? navigator.userAgent : 'other'}`, - `Type: ${type}`, + `Type: ${type}` ]; if (method) { msg.push(`Method: ${method}()`); @@ -241,7 +251,7 @@ export const showError = async ( export const showRpcError = async ( error: IRPCError, - refresh: boolean = false, + refresh: boolean = false ): Promise => { if (error && error.code === RPC_CALL_STATUS.TaskIsMissing) { return await handleMissingPipelineStep(error); @@ -255,7 +265,7 @@ export const showRpcError = async ( refresh, error.rpc, error.code, - error.trans_id, + error.trans_id ); }; @@ -265,7 +275,7 @@ export const _legacy_executeRpc = async ( kernel: Kernel.IKernelConnection, func: string, args: any = {}, - nb_path: string | null = null, + nb_path: string | null = null ) => { if (!nb_path && notebook) { nb_path = notebook.context.path; @@ -298,7 +308,7 @@ export const _legacy_executeRpcAndShowRPCError = async ( kernel: Kernel.IKernelConnection, func: string, args: any = {}, - nb_path: string | null = null, + nb_path: string | null = null ) => { try { const result = await _legacy_executeRpc( @@ -306,7 +316,7 @@ export const _legacy_executeRpcAndShowRPCError = async ( kernel, func, args, - nb_path, + nb_path ); return result; } catch (error) { @@ -319,7 +329,10 @@ export const _legacy_executeRpcAndShowRPCError = async ( }; export abstract class BaseError extends Error { - constructor(message: string, public error: any) { + constructor( + message: string, + public error: any + ) { super(message); this.name = this.constructor.name; this.stack = new Error(message).stack; @@ -343,7 +356,7 @@ export class KernelError extends BaseError { this.error.status, JSON.stringify(this.error.output, null, 3), refresh, - this.error.rpc, + this.error.rpc ); } } @@ -361,7 +374,7 @@ export class JSONParseError extends BaseError { this.error.error.message, this.error.json_data, refresh, - this.error.rpc, + this.error.rpc ); } } @@ -377,7 +390,6 @@ export class RPCError extends BaseError { } } - /** * handleMissingPipelineStep - Async handler for missing pipeline step RPC errors. * @@ -402,7 +414,7 @@ async function handleMissingPipelineStep(error: IRPCError) { '', `Details: ${error.err_details}`, '', - 'You can fix this by tagging a code cell as a pipeline step (e.g. using the Left Panel), or by setting the notebook metadata `steps_defaults` to a list with the step name(s).', + 'You can fix this by tagging a code cell as a pipeline step (via the cell edit/pencil icon), or by setting the notebook metadata steps_defaults to a list with the step name(s).' ]; const body = (
@@ -411,23 +423,23 @@ async function handleMissingPipelineStep(error: IRPCError) { + html={s} + />
))}
); const buttons: ReadonlyArray = [ - Dialog.okButton({ label: 'Open docs' }), - Dialog.cancelButton({ label: 'Close' }), + Dialog.okButton({ label: OPEN_DOCS_LABEL }), + Dialog.cancelButton({ label: 'Close' }) ]; const result = await showDialog({ title, body, buttons }); const clicked = result.button ? (result.button.label as string) : ''; - if (clicked === 'Open docs') { + if (clicked === OPEN_DOCS_LABEL) { // Open the documentation in a new tab (repo README as fallback) - const docsUrl = 'https://github.com/kubeflow-kale/kale#readme'; + const docsUrl = 'https://github.com/kubeflow/kale#readme'; window.open(docsUrl, '_blank'); } return; } - From d95a5f806ba112f0687e7dfacb669378009cd48d Mon Sep 17 00:00:00 2001 From: cordeirops Date: Wed, 4 Feb 2026 14:28:54 -0300 Subject: [PATCH 07/12] Fix issues pointed out by Hannah Signed-off-by: cordeirops --- backend/kale/compiler.py | 8 ++-- backend/kale/errors.py | 14 ------- backend/kale/rpc/errors.py | 7 ---- backend/kale/rpc/nb.py | 32 ++++++---------- examples/base/Untitled.ipynb | 33 +++++++++++++++++ examples/base/candies_sharing.ipynb | 2 +- labextension/src/lib/RPCUtils.tsx | 57 ++++------------------------- 7 files changed, 56 insertions(+), 97 deletions(-) delete mode 100644 backend/kale/errors.py create mode 100644 examples/base/Untitled.ipynb diff --git a/backend/kale/compiler.py b/backend/kale/compiler.py index 792e0dfb2..2378382d1 100644 --- a/backend/kale/compiler.py +++ b/backend/kale/compiler.py @@ -12,7 +12,7 @@ from kale import __version__ as KALE_VERSION from kale.pipeline import Pipeline, Step, PipelineParam from kale.common import kfputils, utils, graphutils -from kale.errors import TaskMissingError + log = logging.getLogger(__name__) @@ -90,11 +90,9 @@ def generate_dsl(self): Returns (str): A Python executable script """ - # Fail early if there are no steps in the pipeline. Raise a domain- - # specific exception so higher layers (RPC) can map it to an RPC - # error without coupling core to the RPC layer. + # Fail early if there are no steps in the pipeline. if not hasattr(self.pipeline, 'steps') or not self.pipeline.steps: - raise TaskMissingError('Task is missing from pipeline.') + raise ValueError('Task is missing from pipeline.') # List of lightweight components generated code lightweight_components = [ diff --git a/backend/kale/errors.py b/backend/kale/errors.py deleted file mode 100644 index c7ab98c64..000000000 --- a/backend/kale/errors.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Core domain exceptions for Kale (non-RPC). - -Keep domain-specific exceptions here so core modules (compiler, processor) -can raise them without depending on RPC layer. -""" - - -class TaskMissingError(Exception): - """Raised when a Pipeline has no steps/tasks. - - This is a domain-level exception and will be mapped to an RPC error - (RPCTaskIsMissing) by the RPC handler. - """ - pass diff --git a/backend/kale/rpc/errors.py b/backend/kale/rpc/errors.py index 204c203c5..3d1e81fe0 100644 --- a/backend/kale/rpc/errors.py +++ b/backend/kale/rpc/errors.py @@ -16,7 +16,6 @@ class Code(enum.Enum): INTERNAL_ERROR = 4 SERVICE_UNAVAILABLE = 5 UNHANDLED_ERROR = 6 - TASK_IS_MISSING = 7 class _RPCError(Exception): @@ -77,12 +76,6 @@ class RPCServiceUnavailableError(_RPCError): message = "Service is Unavailable" -class RPCTaskIsMissing(_RPCError): - """Task is missing from pipeline, you must add a pipeline step.""" - - name = "taskIsMissing" - code = Code.TASK_IS_MISSING - message = "Task is missing from pipeline, you must add a pipeline step." class RPCUnhandledError(_RPCError): diff --git a/backend/kale/rpc/nb.py b/backend/kale/rpc/nb.py index 2e201a6be..10bd9fc66 100644 --- a/backend/kale/rpc/nb.py +++ b/backend/kale/rpc/nb.py @@ -10,8 +10,7 @@ from kale import marshal from kale.rpc.log import create_adapter from kale import Compiler, NotebookProcessor -from kale.rpc.errors import RPCInternalError, RPCTaskIsMissing -from kale.errors import TaskMissingError +from kale.rpc.errors import RPCInternalError, RPCUnhandledError from kale.common import podutils, kfputils, kfutils, astutils KALE_MARSHAL_DIR_POSTFIX = ".kale.marshal.dir" @@ -99,28 +98,19 @@ def compile_notebook(request, source_notebook_path, return {"pipeline_package_path": os.path.relpath(package_path), "pipeline_metadata": pipeline.config.to_dict()} - except TaskMissingError as e: - # Domain-specific exception raised by core components when no - # pipeline steps are present. Map it to a specific RPC error so the - # frontend receives a concrete error code and message. Provide a - # slightly more actionable `details` field so the UI can display - # guidance to the user about how to fix the issue (e.g., tag a - # cell as a step or set `steps_defaults` in metadata). - msg = str(e) - request.log.exception("TaskMissingError during notebook " - "compilation: %s", msg) - raise RPCTaskIsMissing(details=msg, trans_id=request.trans_id) except ValueError as e: - # kfp.compiler or graph_component may - # raise ValueError for other - # reasons; map them to an internal - # RPC error (unless they match the - # specific message — kept for backward compatibility). msg = str(e) - request.log.exception("ValueError during notebook" - " compilation: %s", msg) + request.log.exception("ValueError during notebook compilation: %s", msg) if 'Task is missing from pipeline' in msg: - raise RPCTaskIsMissing(details=msg, trans_id=request.trans_id) + # Provide guidance to the user about how to fix the issue. + raise RPCUnhandledError( + details=( + "The pipeline does not have any steps. " + "Please tag a cell as a pipeline step or set " + "`steps_defaults` in the notebook metadata." + ), + trans_id=request.trans_id + ) raise RPCInternalError(details=msg, trans_id=request.trans_id) except Exception as e: # Let the run dispatcher handle generic exceptions as unhandled, diff --git a/examples/base/Untitled.ipynb b/examples/base/Untitled.ipynb new file mode 100644 index 000000000..daf492bd3 --- /dev/null +++ b/examples/base/Untitled.ipynb @@ -0,0 +1,33 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "d84421b3-217c-4d37-8b38-2ca7f7967f45", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.19" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/base/candies_sharing.ipynb b/examples/base/candies_sharing.ipynb index 87f718358..1cfad349d 100644 --- a/examples/base/candies_sharing.ipynb +++ b/examples/base/candies_sharing.ipynb @@ -207,7 +207,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.18" + "version": "3.10.19" } }, "nbformat": 4, diff --git a/labextension/src/lib/RPCUtils.tsx b/labextension/src/lib/RPCUtils.tsx index 4ad45a825..0fb4b37a2 100644 --- a/labextension/src/lib/RPCUtils.tsx +++ b/labextension/src/lib/RPCUtils.tsx @@ -8,7 +8,7 @@ import NotebookUtils from './NotebookUtils'; // @ts-expect-error This module is not typed import SanitizedHTML from 'react-sanitized-html'; import { isError, IError, IOutput } from '@jupyterlab/nbformat'; -import { Notification, Dialog, showDialog } from '@jupyterlab/apputils'; +import { Notification } from '@jupyterlab/apputils'; export const globalUnhandledRejection = async (event: any) => { console.error(event.reason); @@ -82,8 +82,7 @@ export enum RPC_CALL_STATUS { NotFound = 3, InternalError = 4, ServiceUnavailable = 5, - UnhandledError = 6, - TaskIsMissing = 7 + UnhandledError = 6 } const getRpcCodeName = (code: number) => { @@ -100,14 +99,12 @@ const getRpcCodeName = (code: number) => { return 'InternalError'; case RPC_CALL_STATUS.ServiceUnavailable: return 'ServiceUnavailable'; - case RPC_CALL_STATUS.TaskIsMissing: - return 'TaskIsMissing'; default: return 'UnhandledError'; } }; -const OPEN_DOCS_LABEL = 'Open docs'; + export const rokErrorTooltip = (rokError: IRPCError) => { return ( @@ -151,10 +148,10 @@ export const executeRpc = async ( output = env instanceof NotebookPanel ? await NotebookUtils.sendKernelRequestFromNotebook( - env, - cmd, - expressions - ) + env, + cmd, + expressions + ) : await NotebookUtils.sendKernelRequest(env, cmd, expressions); } catch (e) { if (typeof e === 'object' && e !== null) { @@ -253,9 +250,6 @@ export const showRpcError = async ( error: IRPCError, refresh: boolean = false ): Promise => { - if (error && error.code === RPC_CALL_STATUS.TaskIsMissing) { - return await handleMissingPipelineStep(error); - } await showError( 'An RPC Error has occurred', @@ -407,39 +401,4 @@ export class RPCError extends BaseError { * user clicked "Open docs", the documentation page will be opened * in a new tab as a fallback/help resource. */ -async function handleMissingPipelineStep(error: IRPCError) { - const title = 'Pipeline step missing'; - const bodyLines = [ - `Message: ${error.err_message}`, - '', - `Details: ${error.err_details}`, - '', - 'You can fix this by tagging a code cell as a pipeline step (via the cell edit/pencil icon), or by setting the notebook metadata steps_defaults to a list with the step name(s).' - ]; - const body = ( -
- {bodyLines.map((s: string, i: number) => ( - - -
-
- ))} -
- ); - const buttons: ReadonlyArray = [ - Dialog.okButton({ label: OPEN_DOCS_LABEL }), - Dialog.cancelButton({ label: 'Close' }) - ]; - const result = await showDialog({ title, body, buttons }); - const clicked = result.button ? (result.button.label as string) : ''; - if (clicked === OPEN_DOCS_LABEL) { - // Open the documentation in a new tab (repo README as fallback) - const docsUrl = 'https://github.com/kubeflow/kale#readme'; - window.open(docsUrl, '_blank'); - } - return; -} + From 49f230acd983ddd7ae13fc8f944542884a160398 Mon Sep 17 00:00:00 2001 From: cordeirops Date: Wed, 4 Feb 2026 15:33:38 -0300 Subject: [PATCH 08/12] Fix backend lint and frontend syntax errors Signed-off-by: cordeirops --- backend/kale/compiler.py | 7 ++++--- labextension/src/lib/RPCUtils.tsx | 20 ++------------------ 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/backend/kale/compiler.py b/backend/kale/compiler.py index 2378382d1..540e9acec 100644 --- a/backend/kale/compiler.py +++ b/backend/kale/compiler.py @@ -1,12 +1,13 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright (c) 2019–2025 The Kale Contributors. +import argparse +import logging import os import re -import logging -import argparse -import autopep8 from typing import NamedTuple + +import autopep8 from jinja2 import Environment, PackageLoader, FileSystemLoader from kale import __version__ as KALE_VERSION diff --git a/labextension/src/lib/RPCUtils.tsx b/labextension/src/lib/RPCUtils.tsx index 0fb4b37a2..aa9ed0895 100644 --- a/labextension/src/lib/RPCUtils.tsx +++ b/labextension/src/lib/RPCUtils.tsx @@ -47,6 +47,8 @@ export const globalUnhandledRejection = async (event: any) => { label: 'Details', callback: () => NotebookUtils.showMessage(alert_string, [ + 'An unhandled error was thrown from:', + extensionName, 'Please see console for more details.' ]) } @@ -384,21 +386,3 @@ export class RPCError extends BaseError { } } -/** - * handleMissingPipelineStep - Async handler for missing pipeline step RPC errors. - * - * Displays a modal dialog that informs the user that a pipeline step is missing, - * shows the RPC-provided message and details, and suggests remediation steps: - * tagging a code cell as a pipeline step or setting the notebook metadata - * `steps_defaults` to include the step name(s). The dialog body is rendered as - * sanitized HTML with a restricted set of allowed tags and attributes to avoid - * injection issues. The dialog offers two buttons: "Open docs" and "Close". - * Selecting "Open docs" opens the Kale README/docs page in a new browser tab. - * - * @param error - The IRPCError instance containing `err_message` and `err_details` - * to present in the dialog body. - * @returns A Promise that resolves once the user dismisses the dialog. If the - * user clicked "Open docs", the documentation page will be opened - * in a new tab as a fallback/help resource. - */ - From 8cbdb0f6cd7251b1994723eb7d34c5ad5f9dce9f Mon Sep 17 00:00:00 2001 From: cordeirops Date: Wed, 4 Feb 2026 15:34:43 -0300 Subject: [PATCH 09/12] FIx problems Signed-off-by: cordeirops --- backend/kale/rpc/nb.py | 9 ++++----- examples/base/Untitled.ipynb | 33 --------------------------------- 2 files changed, 4 insertions(+), 38 deletions(-) delete mode 100644 examples/base/Untitled.ipynb diff --git a/backend/kale/rpc/nb.py b/backend/kale/rpc/nb.py index 10bd9fc66..775203de5 100644 --- a/backend/kale/rpc/nb.py +++ b/backend/kale/rpc/nb.py @@ -1,17 +1,16 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright (c) 2019–2025 The Kale Contributors. +import logging import os import shutil -import logging from tabulate import tabulate -from kale import marshal -from kale.rpc.log import create_adapter -from kale import Compiler, NotebookProcessor +from kale import Compiler, NotebookProcessor, marshal +from kale.common import astutils, kfputils, podutils from kale.rpc.errors import RPCInternalError, RPCUnhandledError -from kale.common import podutils, kfputils, kfutils, astutils +from kale.rpc.log import create_adapter KALE_MARSHAL_DIR_POSTFIX = ".kale.marshal.dir" KALE_PIPELINE_STEP_ENV = "KALE_PIPELINE_STEP" diff --git a/examples/base/Untitled.ipynb b/examples/base/Untitled.ipynb deleted file mode 100644 index daf492bd3..000000000 --- a/examples/base/Untitled.ipynb +++ /dev/null @@ -1,33 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "id": "d84421b3-217c-4d37-8b38-2ca7f7967f45", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.19" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} From feb29d565f10d67ad0521ed4c003c1fb48a30b77 Mon Sep 17 00:00:00 2001 From: Pedro Sbaraini Cordeiro Date: Wed, 4 Feb 2026 15:44:14 -0300 Subject: [PATCH 10/12] Add extension name retrieval for error notifications Signed-off-by: cordeirops --- labextension/src/lib/RPCUtils.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/labextension/src/lib/RPCUtils.tsx b/labextension/src/lib/RPCUtils.tsx index 73ad0d430..b0c7dca27 100644 --- a/labextension/src/lib/RPCUtils.tsx +++ b/labextension/src/lib/RPCUtils.tsx @@ -48,6 +48,7 @@ export const globalUnhandledRejection = async (event: any) => { autoClose: 3000 }); } else { + const extensionName = getExtensionName(stackLines); Notification.error(`An unhandled error has been thrown.`, { actions: [ { From 4e4d4663d9dff6e35aa73126ecda90459ee5a5fc Mon Sep 17 00:00:00 2001 From: cordeirops Date: Wed, 4 Feb 2026 15:59:56 -0300 Subject: [PATCH 11/12] FIx lint errors Signed-off-by: cordeirops --- backend/kale/compiler.py | 1 - labextension/src/widgets/LeftPanel.tsx | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/kale/compiler.py b/backend/kale/compiler.py index fb5e995fa..34ef06138 100644 --- a/backend/kale/compiler.py +++ b/backend/kale/compiler.py @@ -25,7 +25,6 @@ from kale.common import graphutils, kfputils, utils from kale.pipeline import Pipeline, PipelineParam, Step - log = logging.getLogger(__name__) PY_FN_TEMPLATE = "py_function_template.jinja2" diff --git a/labextension/src/widgets/LeftPanel.tsx b/labextension/src/widgets/LeftPanel.tsx index 968b5883c..c56a785e6 100644 --- a/labextension/src/widgets/LeftPanel.tsx +++ b/labextension/src/widgets/LeftPanel.tsx @@ -397,8 +397,8 @@ export class KubeflowKaleLeftPanel extends React.Component { ...DefaultState.metadata, experiment: prevState.metadata.experiment, experiment_name: prevState.metadata.experiment_name, - pipeline_name: sanitizedDefaultPipelineName - } + pipeline_name: sanitizedDefaultPipelineName, + }, })); } } else { From 7dd496e6856b8f8dc5144c81b6439eac5f584f9e Mon Sep 17 00:00:00 2001 From: cordeirops Date: Wed, 4 Feb 2026 16:10:38 -0300 Subject: [PATCH 12/12] format backend with ruff Signed-off-by: cordeirops --- backend/kale/compiler.py | 4 ++-- backend/kale/rpc/errors.py | 2 -- backend/kale/rpc/nb.py | 19 +++++++++---------- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/backend/kale/compiler.py b/backend/kale/compiler.py index 34ef06138..e20592c60 100644 --- a/backend/kale/compiler.py +++ b/backend/kale/compiler.py @@ -104,8 +104,8 @@ def generate_dsl(self): Returns (str): A Python executable script """ # Fail early if there are no steps in the pipeline. - if not hasattr(self.pipeline, 'steps') or not self.pipeline.steps: - raise ValueError('Task is missing from pipeline.') + if not hasattr(self.pipeline, "steps") or not self.pipeline.steps: + raise ValueError("Task is missing from pipeline.") # List of lightweight components generated code lightweight_components = [ diff --git a/backend/kale/rpc/errors.py b/backend/kale/rpc/errors.py index 1a19d4c87..7fc595e41 100644 --- a/backend/kale/rpc/errors.py +++ b/backend/kale/rpc/errors.py @@ -97,8 +97,6 @@ class RPCServiceUnavailableError(_RPCError): message = "Service is Unavailable" - - class RPCUnhandledError(_RPCError): """Unhandled RPC Error.""" diff --git a/backend/kale/rpc/nb.py b/backend/kale/rpc/nb.py index 41faa387c..c0089991e 100644 --- a/backend/kale/rpc/nb.py +++ b/backend/kale/rpc/nb.py @@ -96,8 +96,7 @@ def get_base_image(request): def compile_notebook(request, source_notebook_path, notebook_metadata_overrides=None, debug=False): """Compile the notebook to KFP DSL.""" try: - processor = NotebookProcessor(source_notebook_path, - notebook_metadata_overrides) + processor = NotebookProcessor(source_notebook_path, notebook_metadata_overrides) pipeline = processor.run() imports_and_functions = processor.get_imports_and_functions() script_path = Compiler(pipeline, imports_and_functions).compile() @@ -108,15 +107,16 @@ def compile_notebook(request, source_notebook_path, notebook_metadata_overrides= instance.logger = request.log if hasattr(request, "log") else logger""" - package_path = kfputils.compile_pipeline(script_path, - pipeline.config.pipeline_name) + package_path = kfputils.compile_pipeline(script_path, pipeline.config.pipeline_name) - return {"pipeline_package_path": os.path.relpath(package_path), - "pipeline_metadata": pipeline.config.to_dict()} + return { + "pipeline_package_path": os.path.relpath(package_path), + "pipeline_metadata": pipeline.config.to_dict(), + } except ValueError as e: msg = str(e) request.log.exception("ValueError during notebook compilation: %s", msg) - if 'Task is missing from pipeline' in msg: + if "Task is missing from pipeline" in msg: # Provide guidance to the user about how to fix the issue. raise RPCUnhandledError( details=( @@ -124,14 +124,13 @@ def compile_notebook(request, source_notebook_path, notebook_metadata_overrides= "Please tag a cell as a pipeline step or set " "`steps_defaults` in the notebook metadata." ), - trans_id=request.trans_id + trans_id=request.trans_id, ) raise RPCInternalError(details=msg, trans_id=request.trans_id) except Exception as e: # Let the run dispatcher handle generic exceptions as unhandled, # but log for debug purposes. - request.log.exception("Unexpected error during " - "notebook compilation: %s", e) + request.log.exception("Unexpected error during notebook compilation: %s", e) raise