Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions docs/kernels.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,30 @@ kaggle kernels status kerneler/sqlite-global-default

This command tells you whether the latest run of your kernel is still running, completed successfully, or failed.

## `kaggle kernels cancel`

Requests cancellation of one active kernel session.

**Usage:**

```bash
kaggle kernels cancel <SESSION_ID>
```

**Arguments:**

* `<SESSION_ID>`: The numeric version number printed after `kaggle kernels push` succeeds.

**Example:**

```bash
kaggle kernels cancel 341475263
```

**Purpose:**

Use this command to stop a queued or running session without deleting the kernel or its version history.

## `kaggle kernels delete`

Deletes a kernel from Kaggle.
Expand Down
34 changes: 34 additions & 0 deletions src/kaggle/api/kaggle_api_extended.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,8 @@
ApiGetKernelRequest,
ApiListKernelSessionOutputRequest,
ApiGetKernelSessionStatusRequest,
ApiCancelKernelSessionRequest,
ApiCancelKernelSessionResponse,
ApiSaveKernelResponse,
ApiKernelMetadata,
ApiDeleteKernelRequest,
Expand Down Expand Up @@ -7332,6 +7334,38 @@ def kernels_status_cli(self, kernel, kernel_opt=None):
else:
print('%s has status "%s"' % (kernel, status))

def kernels_cancel(self, kernel_session_id: int) -> ApiCancelKernelSessionResponse:
"""Cancels one active kernel session.

Args:
kernel_session_id: The numeric session id emitted as ``versionNumber`` by ``kernels_push``.

Returns:
The cancellation response from Kaggle.

Raises:
ValueError: If the session id is not positive or Kaggle rejects the cancellation.
"""
if kernel_session_id < 1:
raise ValueError("Kernel session id must be positive")

request = ApiCancelKernelSessionRequest()
request.kernel_session_id = kernel_session_id
with self.build_kaggle_client() as kaggle:
response: ApiCancelKernelSessionResponse = kaggle.kernels.kernels_api_client.cancel_kernel_session(request)
if response.error_message:
raise ValueError(response.error_message)
return response

def kernels_cancel_cli(self, kernel_session_id: int) -> None:
"""Cancels one active kernel session from the command line.

Args:
kernel_session_id: The numeric session id emitted as ``versionNumber`` by ``kernels_push``.
"""
self.kernels_cancel(kernel_session_id)
print(f"Kernel session {kernel_session_id} cancellation requested")

def kernels_logs(self, kernel: str | None) -> str:
"""Retrieves the execution log for a specified kernel.

Expand Down
12 changes: 12 additions & 0 deletions src/kaggle/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1437,6 +1437,15 @@ def parse_kernels(subparsers) -> None:
parser_kernels_status._action_groups.append(parser_kernels_status_optional)
parser_kernels_status.set_defaults(func=api.kernels_status_cli)

# Kernels cancel
parser_kernels_cancel = subparsers_kernels.add_parser(
"cancel", formatter_class=argparse.RawTextHelpFormatter, help=Help.command_kernels_cancel
)
parser_kernels_cancel_optional = parser_kernels_cancel._action_groups.pop()
parser_kernels_cancel_optional.add_argument("kernel_session_id", type=int, help=Help.param_kernel_session_id)
parser_kernels_cancel._action_groups.append(parser_kernels_cancel_optional)
parser_kernels_cancel.set_defaults(func=api.kernels_cancel_cli)

# Kernels logs
parser_kernels_logs = subparsers_kernels.add_parser(
"logs", formatter_class=argparse.RawTextHelpFormatter, help=Help.command_kernels_logs
Expand Down Expand Up @@ -2548,6 +2557,7 @@ class Help(object):
"pull",
"output",
"status",
"cancel",
"logs",
"update",
"delete",
Expand Down Expand Up @@ -2710,6 +2720,7 @@ class Help(object):
command_kernels_pull = "Pull down code from a kernel"
command_kernels_output = "Get data output from the latest kernel run"
command_kernels_status = "Display the status of the latest kernel run"
command_kernels_cancel = "Cancel an active kernel session"
command_kernels_logs = "Print the execution logs from the latest kernel run"
command_kernels_delete = "Delete a kernel"
command_kernels_topics = "List discussion topics for a kernel"
Expand Down Expand Up @@ -2958,6 +2969,7 @@ class Help(object):
)
param_kernel_logs_follow = "Stream live execution logs from the running session (like tail -f)"
param_kernel_logs_interval = argparse.SUPPRESS # Deprecated; live streaming is push-based.
param_kernel_session_id = "Numeric session id emitted as the kernel version number by 'kaggle kernels push'"

# Models params
param_model = "Model URL suffix in format <owner>/<model-name>"
Expand Down
16 changes: 16 additions & 0 deletions tests/unit/test_cli_kernels.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# coding=utf-8
from unittest.mock import MagicMock

import pytest


Expand Down Expand Up @@ -247,6 +249,20 @@ def test_kernels_status_parser_with_option_kernel_succeeds(parser):
assert kwargs["kernel_opt"] == "owner/kernel-name"


def test_kernels_cancel_parser_succeeds(parser):
func, kwargs = parser.dispatch(["kernels", "cancel", "341475263"])
assert func.__name__ == "kernels_cancel_cli"
assert kwargs["kernel_session_id"] == 341475263


def test_kernels_cancel_cli_delegates_to_api(api, capsys):
api.kernels_cancel = MagicMock()
api.kernels_cancel_cli(341475263)

api.kernels_cancel.assert_called_once_with(341475263)
assert capsys.readouterr().out == "Kernel session 341475263 cancellation requested\n"


def test_kernels_logs_parser_default_succeeds(parser):
func, kwargs = parser.dispatch(["kernels", "logs"])
assert func.__name__ == "kernels_logs_cli"
Expand Down
42 changes: 42 additions & 0 deletions tests/unit/test_kernels_cancel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from unittest.mock import MagicMock, patch

import pytest

from kaggle.api.kaggle_api_extended import KaggleApi
from kagglesdk.kernels.types.kernels_api_service import ApiCancelKernelSessionResponse


class TestKernelsCancel:
def setup_method(self):
self.api = KaggleApi.__new__(KaggleApi)

@patch.object(KaggleApi, "build_kaggle_client")
def test_kernels_cancel_sends_the_session_id(self, mock_client):
response = ApiCancelKernelSessionResponse()
client = MagicMock()
client.kernels.kernels_api_client.cancel_kernel_session.return_value = response
mock_client.return_value.__enter__ = MagicMock(return_value=client)
mock_client.return_value.__exit__ = MagicMock(return_value=False)

result = self.api.kernels_cancel(341475263)

assert result is response
request = client.kernels.kernels_api_client.cancel_kernel_session.call_args.args[0]
assert request.kernel_session_id == 341475263

@pytest.mark.parametrize("kernel_session_id", [0, -1])
def test_kernels_cancel_rejects_non_positive_session_ids(self, kernel_session_id):
with pytest.raises(ValueError, match="must be positive"):
self.api.kernels_cancel(kernel_session_id)

@patch.object(KaggleApi, "build_kaggle_client")
def test_kernels_cancel_surfaces_api_rejection(self, mock_client):
response = ApiCancelKernelSessionResponse()
response.error_message = "Session is not active"
client = MagicMock()
client.kernels.kernels_api_client.cancel_kernel_session.return_value = response
mock_client.return_value.__enter__ = MagicMock(return_value=client)
mock_client.return_value.__exit__ = MagicMock(return_value=False)

with pytest.raises(ValueError, match="Session is not active"):
self.api.kernels_cancel(341475263)
Loading