Skip to content

Add request and response for code submissions #704

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Mar 3, 2025
Merged
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
55 changes: 50 additions & 5 deletions kaggle/api/kaggle_api_extended.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
import urllib3.exceptions as urllib3_exceptions
from requests import RequestException

from kaggle.models.kaggle_models_extended import ResumableUploadResult, File
from kaggle.models.kaggle_models_extended import ResumableUploadResult, File, \
Kernel

from requests.adapters import HTTPAdapter
from slugify import slugify
Expand All @@ -61,7 +62,8 @@
SettingsLicense, DatasetCollaborator
from kagglesdk.kernels.types.kernels_api_service import ApiListKernelsRequest, \
ApiListKernelFilesRequest, ApiSaveKernelRequest, ApiGetKernelRequest, \
ApiListKernelSessionOutputRequest, ApiGetKernelSessionStatusRequest
ApiListKernelSessionOutputRequest, ApiGetKernelSessionStatusRequest, \
ApiSaveKernelResponse
from kagglesdk.kernels.types.kernels_enums import KernelsListSortType, \
KernelsListViewType
from kagglesdk.models.types.model_api_service import ApiListModelsRequest, \
Expand Down Expand Up @@ -290,7 +292,7 @@ class KaggleApi:

args = {} # DEBUG Add --local to use localhost
if os.environ.get('KAGGLE_API_ENVIRONMENT') == 'LOCALHOST':
args = {'--local'}
args = {'--verbose','--local'}

# Kernels valid types
valid_push_kernel_types = ['script', 'notebook']
Expand Down Expand Up @@ -792,6 +794,39 @@ def competitions_list_cli(self,
else:
print('No competitions found')

def competition_submit_code(self, file_name, message, competition, kernel_slug=None, kernel_version=None, quiet=False):
""" Submit a competition.

Parameters
==========
file_name: the name of the output file created by the kernel
message: the submission description
competition: the competition name; if not given use the 'competition' config value
kernel_slug: the <owner>/<notebook> of the notebook to use for a code competition
kernel_version: the version number, returned by 'kaggle kernels push ...'
quiet: suppress verbose output (default is False)
"""
if competition is None:
competition = self.get_config_value(self.CONFIG_NAME_COMPETITION)
if competition is not None and not quiet:
print('Using competition: ' + competition)

if competition is None:
raise ValueError('No competition specified')
else:
if kernel_version is None:
raise ValueError('Kernel version must be specified')
with self.build_kaggle_client() as kaggle:
submit_request = ApiCreateCodeSubmissionRequest()
submit_request.file_name = file_name
submit_request.competition_name = competition
submit_request.kernel_slug = kernel_slug
submit_request.kernel_version = kernel_version
submit_request.submission_description = message
submit_response = kaggle.competitions.competition_api_client.create_code_submission(
submit_request)
return submit_response

def competition_submit(self, file_name, message, competition, quiet=False):
""" Submit a competition.

Expand Down Expand Up @@ -837,6 +872,8 @@ def competition_submit_cli(self,
file_name,
message,
competition,
kernel=None,
version=None,
competition_opt=None,
quiet=False):
""" Submit a competition using the client. Arguments are same as for
Expand All @@ -847,12 +884,20 @@ def competition_submit_cli(self,
file_name: the competition metadata file
message: the submission description
competition: the competition name; if not given use the 'competition' config value
kernel: the name of the kernel to submit to a code competition
version: the version of the kernel to submit to a code competition, e.g. '1'
quiet: suppress verbose output (default is False)
competition_opt: an alternative competition option provided by cli
"""
if kernel and not version or version and not kernel:
raise ValueError('Code competition submissions require both the output file name and the version label')
competition = competition or competition_opt
try:
submit_result = self.competition_submit(file_name, message, competition,
if kernel:
submit_result = self.competition_submit_code(file_name, message, competition,
kernel, version, quiet)
else:
submit_result = self.competition_submit(file_name, message, competition,
quiet)
except RequestException as e:
if e.response and e.response.status_code == 404:
Expand Down Expand Up @@ -2369,7 +2414,7 @@ def kernels_initialize_cli(self, folder=None):
meta_file = self.kernels_initialize(folder)
print('Kernel metadata template written to: ' + meta_file)

def kernels_push(self, folder, timeout=None):
def kernels_push(self, folder, timeout=None) -> ApiSaveKernelResponse:
""" Read the metadata file and kernel files from a notebook, validate
both, and use the Kernel API to push to Kaggle if all is valid.
Parameters
Expand Down
10 changes: 8 additions & 2 deletions kaggle/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,13 +245,17 @@ def parse_competitions(subparsers):
required=False,
help=argparse.SUPPRESS)
parser_competitions_submit_required.add_argument(
'-f', '--file', dest='file_name', required=True, help=Help.param_upfile)
'-f', '--file', dest='file_name', help=Help.param_upfile)
parser_competitions_submit_optional.add_argument(
'-k', '--kernel', dest='kernel', help=Help.param_code_kernel)
parser_competitions_submit_required.add_argument(
'-m',
'--message',
dest='message',
required=True,
help=Help.param_competition_message)
parser_competitions_submit_optional.add_argument(
'-v', '--version', dest='version', help=Help.param_code_version)
parser_competitions_submit_optional.add_argument(
'-q', '--quiet', dest='quiet', action='store_true', help=Help.param_quiet)
parser_competitions_submit._action_groups.append(
Expand Down Expand Up @@ -1420,7 +1424,9 @@ class Help(object):
param_delete_old_version = 'Delete old versions of this dataset'
param_force = ('Skip check whether local version of file is up to date, force'
' file download')
param_upfile = 'File for upload (full path)'
param_upfile = 'File for upload (full path), or the name of the output file produced by a kernel (for code competitions)'
param_code_kernel = 'Name of kernel (notebook) to submit to a code competition'
param_code_version = 'Version of kernel to submit to a code competition, e.g. "Version 1"'
param_csv = 'Print results in CSV format (if not set print in table format)'
param_page = 'Page number for results paging. Page size is 20 by default'
# NOTE: Default and max page size are set by the mid-tier code.
Expand Down
14 changes: 13 additions & 1 deletion kagglesdk/competitions/services/competition_api_service.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from kagglesdk.common.types.file_download import FileDownload
from kagglesdk.common.types.http_redirect import HttpRedirect
from kagglesdk.competitions.types.competition_api_service import ApiCreateSubmissionRequest, ApiCreateSubmissionResponse, ApiDownloadDataFileRequest, ApiDownloadDataFilesRequest, ApiDownloadLeaderboardRequest, ApiGetLeaderboardRequest, ApiGetLeaderboardResponse, ApiGetSubmissionRequest, ApiListCompetitionsRequest, ApiListCompetitionsResponse, ApiListDataFilesRequest, ApiListDataFilesResponse, ApiListSubmissionsRequest, ApiListSubmissionsResponse, ApiStartSubmissionUploadRequest, ApiStartSubmissionUploadResponse, ApiSubmission
from kagglesdk.competitions.types.competition_api_service import ApiCreateCodeSubmissionRequest, ApiCreateSubmissionRequest, ApiCreateSubmissionResponse, ApiDownloadDataFileRequest, ApiDownloadDataFilesRequest, ApiDownloadLeaderboardRequest, ApiGetLeaderboardRequest, ApiGetLeaderboardResponse, ApiGetSubmissionRequest, ApiListCompetitionsRequest, ApiListCompetitionsResponse, ApiListDataFilesRequest, ApiListDataFilesResponse, ApiListSubmissionsRequest, ApiListSubmissionsResponse, ApiStartSubmissionUploadRequest, ApiStartSubmissionUploadResponse, ApiSubmission
from kagglesdk.kaggle_http_client import KaggleHttpClient

class CompetitionApiClient(object):
Expand Down Expand Up @@ -80,6 +80,18 @@ def create_submission(self, request: ApiCreateSubmissionRequest = None) -> ApiCr

return self._client.call("competitions.CompetitionApiService", "ApiCreateSubmission", request, ApiCreateSubmissionResponse)

def create_code_submission(self, request: ApiCreateCodeSubmissionRequest = None) -> ApiCreateSubmissionResponse:
r"""
Args:
request (ApiCreateCodeSubmissionRequest):
The request object; initialized to empty instance if not specified.
"""

if request is None:
request = ApiCreateCodeSubmissionRequest()

return self._client.call("competitions.CompetitionApiService", "ApiCreateCodeSubmission", request, ApiCreateSubmissionResponse)

def get_submission(self, request: ApiGetSubmissionRequest = None) -> ApiSubmission:
r"""
Args:
Expand Down
101 changes: 101 additions & 0 deletions kagglesdk/competitions/types/competition_api_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,99 @@
from kagglesdk.kaggle_object import *
from typing import Optional, List

class ApiCreateCodeSubmissionRequest(KaggleObject):
r"""
Attributes:
competition_name (str)
kernel_slug (str)
kernel_version (str)
file_name (str)
submission_description (str)
"""

def __init__(self):
self._competition_name = ""
self._kernel_slug = ""
self._kernel_version = ""
self._file_name = ""
self._submission_description = None
self._freeze()

@property
def competition_name(self) -> str:
return self._competition_name

@competition_name.setter
def competition_name(self, competition_name: str):
if competition_name is None:
del self.competition_name
return
if not isinstance(competition_name, str):
raise TypeError('competition_name must be of type str')
self._competition_name = competition_name

@property
def kernel_slug(self) -> str:
return self._kernel_slug

@kernel_slug.setter
def kernel_slug(self, kernel_slug: str):
if kernel_slug is None:
del self.kernel_slug
return
if not isinstance(kernel_slug, str):
raise TypeError('kernel_slug must be of type str')
self._kernel_slug = kernel_slug

@property
def kernel_version(self) -> str:
return self._kernel_version

@kernel_version.setter
def kernel_version(self, kernel_version: str):
if kernel_version is None:
del self.kernel_version
return
if not isinstance(kernel_version, str):
raise TypeError('kernel_version must be of type str')
self._kernel_version = kernel_version

@property
def file_name(self) -> str:
return self._file_name

@file_name.setter
def file_name(self, file_name: str):
if file_name is None:
del self.file_name
return
if not isinstance(file_name, str):
raise TypeError('file_name must be of type str')
self._file_name = file_name

@property
def submission_description(self) -> str:
return self._submission_description or ""

@submission_description.setter
def submission_description(self, submission_description: str):
if submission_description is None:
del self.submission_description
return
if not isinstance(submission_description, str):
raise TypeError('submission_description must be of type str')
self._submission_description = submission_description

def endpoint(self):
path = '/api/v1/competitions/submissions/submit-notebook/{competition_name}'
return path.format_map(self.to_field_map(self))


@staticmethod
def method():
return 'POST'


class ApiCreateSubmissionRequest(KaggleObject):
r"""
Attributes:
Expand Down Expand Up @@ -1719,6 +1812,14 @@ def total_count(self, total_count: int):
self._total_count = total_count


ApiCreateCodeSubmissionRequest._fields = [
FieldMetadata("competitionName", "competition_name", "_competition_name", str, "", PredefinedSerializer()),
FieldMetadata("kernelSlug", "kernel_slug", "_kernel_slug", str, "", PredefinedSerializer()),
FieldMetadata("kernelVersion", "kernel_version", "_kernel_version", str, "", PredefinedSerializer()),
FieldMetadata("fileName", "file_name", "_file_name", str, "", PredefinedSerializer()),
FieldMetadata("submissionDescription", "submission_description", "_submission_description", str, None, PredefinedSerializer(), optional=True),
]

ApiCreateSubmissionRequest._fields = [
FieldMetadata("competitionName", "competition_name", "_competition_name", str, "", PredefinedSerializer()),
FieldMetadata("blobFileTokens", "blob_file_tokens", "_blob_file_tokens", str, "", PredefinedSerializer()),
Expand Down
2 changes: 1 addition & 1 deletion kagglesdk/kaggle_http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
# currently usable by the CLI.

# TODO: Extend kapigen to add a boolean to these requests indicating that they use forms.
REQUESTS_REQUIRING_FORMS = ['ApiUploadDatasetFileRequest', 'ApiCreateSubmissionRequest', 'ApiStartSubmissionUploadRequest', 'ApiUploadModelFileRequest']
REQUESTS_REQUIRING_FORMS = ['ApiUploadDatasetFileRequest', 'ApiCreateSubmissionRequest', 'ApiCreateCodeSubmissionRequest', 'ApiStartSubmissionUploadRequest', 'ApiUploadModelFileRequest']

def _headers_to_str(headers):
return '\n'.join(f'{k}: {v}' for k, v in headers.items())
Expand Down
16 changes: 0 additions & 16 deletions kagglesdk/models/types/model_api_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -1169,7 +1169,6 @@ class ApiListModelGatingUserConsentsRequest(KaggleObject):
model_slug (str)
review_status (GatingAgreementRequestsReviewStatus)
filters: a null value means the filter is off.
expiry_status (GatingAgreementRequestsExpiryStatus)
is_user_request_data_expired (bool)
page_size (int)
paging
Expand All @@ -1180,7 +1179,6 @@ def __init__(self):
self._owner_slug = ""
self._model_slug = ""
self._review_status = None
self._expiry_status = None
self._is_user_request_data_expired = None
self._page_size = None
self._page_token = None
Expand Down Expand Up @@ -1226,19 +1224,6 @@ def review_status(self, review_status: 'GatingAgreementRequestsReviewStatus'):
raise TypeError('review_status must be of type GatingAgreementRequestsReviewStatus')
self._review_status = review_status

@property
def expiry_status(self) -> 'GatingAgreementRequestsExpiryStatus':
return self._expiry_status or GatingAgreementRequestsExpiryStatus.GATING_AGREEMENT_REQUESTS_EXPIRY_STATUS_UNSPECIFIED

@expiry_status.setter
def expiry_status(self, expiry_status: 'GatingAgreementRequestsExpiryStatus'):
if expiry_status is None:
del self.expiry_status
return
if not isinstance(expiry_status, GatingAgreementRequestsExpiryStatus):
raise TypeError('expiry_status must be of type GatingAgreementRequestsExpiryStatus')
self._expiry_status = expiry_status

@property
def is_user_request_data_expired(self) -> bool:
return self._is_user_request_data_expired or False
Expand Down Expand Up @@ -3531,7 +3516,6 @@ def e(self, e: str):
FieldMetadata("ownerSlug", "owner_slug", "_owner_slug", str, "", PredefinedSerializer()),
FieldMetadata("modelSlug", "model_slug", "_model_slug", str, "", PredefinedSerializer()),
FieldMetadata("reviewStatus", "review_status", "_review_status", GatingAgreementRequestsReviewStatus, None, EnumSerializer(), optional=True),
FieldMetadata("expiryStatus", "expiry_status", "_expiry_status", GatingAgreementRequestsExpiryStatus, None, EnumSerializer(), optional=True),
FieldMetadata("isUserRequestDataExpired", "is_user_request_data_expired", "_is_user_request_data_expired", bool, None, PredefinedSerializer(), optional=True),
FieldMetadata("pageSize", "page_size", "_page_size", int, None, PredefinedSerializer(), optional=True),
FieldMetadata("pageToken", "page_token", "_page_token", str, None, PredefinedSerializer(), optional=True),
Expand Down
10 changes: 5 additions & 5 deletions kagglesdk/models/types/model_enums.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import enum

class GatingAgreementRequestsExpiryStatus(enum.Enum):
GATING_AGREEMENT_REQUESTS_EXPIRY_STATUS_UNSPECIFIED = 0
GATING_AGREEMENT_REQUESTS_EXPIRY_STATUS_NOT_EXPIRED = 1
GATING_AGREEMENT_REQUESTS_EXPIRY_STATUS_IS_EXPIRED = 2

class GatingAgreementRequestsReviewStatus(enum.Enum):
GATING_AGREEMENT_REQUESTS_REVIEW_STATUS_UNSPECIFIED = 0
GATING_AGREEMENT_REQUESTS_REVIEW_STATUS_PENDING = 1
Expand Down Expand Up @@ -58,3 +53,8 @@ class ModelVersionLinkType(enum.Enum):
MODEL_VERSION_LINK_TYPE_VERTEX_OPEN = 1
MODEL_VERSION_LINK_TYPE_VERTEX_DEPLOY = 2

class GatingAgreementRequestsExpiryStatus(enum.Enum):
GATING_AGREEMENT_REQUESTS_EXPIRY_STATUS_UNSPECIFIED = 0
GATING_AGREEMENT_REQUESTS_EXPIRY_STATUS_NOT_EXPIRED = 1
GATING_AGREEMENT_REQUESTS_EXPIRY_STATUS_IS_EXPIRED = 2

Loading