Skip to content

Commit 073b2be

Browse files
authored
feat(cli): add competitions init and create commands (#1080)
Wrap kagglesdk 0.1.31's new create_competition RPC behind a metadata-file flow that mirrors `kaggle datasets {init,create}` and `kaggle models {init,create}`: - `kaggle competitions init [folder]` writes a competition-metadata.json template with INSERT_*_HERE placeholders for the required fields (title, slug, briefDescription) plus all optional fields. - `kaggle competitions create -p folder` reads the metadata file, validates the required fields are filled in, parses the privacy string into the CompetitionPrivacy enum (and reward.id into RewardTypeId when supplied), and calls create_competition. Smoke entry added for `competitions init` (idempotent template write).
1 parent 1be23a2 commit 073b2be

4 files changed

Lines changed: 365 additions & 1 deletion

File tree

src/kaggle/api/kaggle_api_extended.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@
100100
ApiListCompetitionPagesResponse,
101101
ApiCreateCompetitionPageRequest,
102102
ApiCompetitionPage,
103+
ApiCreateCompetitionRequest,
104+
ApiCreateCompetitionResponse,
103105
ApiLaunchCompetitionRequest,
104106
ApiListCompetitionTopicsRequest,
105107
ApiListCompetitionTopicsResponse,
@@ -131,12 +133,15 @@
131133
)
132134
from kagglesdk.competitions.types.competition_enums import (
133135
CompetitionListTab,
136+
CompetitionPrivacy,
134137
HostSegment,
135138
CompetitionSortBy,
136139
SubmissionGroup,
137140
SubmissionSortBy,
138141
)
139142

143+
from kagglesdk.competitions.types.competition import Reward, RewardTypeId
144+
140145
from kagglesdk.common.types.cropped_image_upload import CroppedImageUpload, CroppedImageRectangle
141146

142147
from kagglesdk.datasets.types.dataset_api_service import (
@@ -764,6 +769,7 @@ class KaggleApi:
764769
KERNEL_METADATA_FILE = "kernel-metadata.json"
765770
MODEL_METADATA_FILE = "model-metadata.json"
766771
MODEL_INSTANCE_METADATA_FILE = "model-instance-metadata.json"
772+
COMPETITION_METADATA_FILE = "competition-metadata.json"
767773
MAX_NUM_INBOX_FILES_TO_UPLOAD = 1000
768774
MAX_UPLOAD_RESUME_ATTEMPTS = 10
769775

@@ -2471,6 +2477,130 @@ def competition_launch_cli(
24712477
else:
24722478
print(f'Competition "{competition_name}" launched.')
24732479

2480+
def competition_initialize(self, folder: str) -> str:
2481+
"""Initialize a folder with a competition-metadata.json template.
2482+
2483+
Args:
2484+
folder (str): Folder in which to write the template.
2485+
2486+
Returns:
2487+
str: Path to the written metadata file.
2488+
"""
2489+
if not os.path.isdir(folder):
2490+
raise ValueError("Invalid folder: " + folder)
2491+
2492+
meta_data = {
2493+
"title": "INSERT_TITLE_HERE",
2494+
"slug": "INSERT_SLUG_HERE",
2495+
"briefDescription": "INSERT_BRIEF_DESCRIPTION_HERE",
2496+
"privacy": "PUBLIC",
2497+
"disableKernels": False,
2498+
"hackathon": False,
2499+
"cloneCompetitionId": None,
2500+
"cloneExcludeCompetitionData": None,
2501+
"clonePageNames": None,
2502+
"licenseId": None,
2503+
"organizationId": None,
2504+
"numPrizes": None,
2505+
"restrictLinkToEmailList": None,
2506+
"reward": None,
2507+
}
2508+
meta_file = os.path.join(folder, self.COMPETITION_METADATA_FILE)
2509+
with open(meta_file, "w") as f:
2510+
json.dump(meta_data, f, indent=2)
2511+
2512+
print("Competition metadata template written to: " + meta_file)
2513+
return meta_file
2514+
2515+
def competition_initialize_cli(self, folder=None):
2516+
folder = folder or os.getcwd()
2517+
self.competition_initialize(folder)
2518+
2519+
def competition_create_new(self, folder: str) -> ApiCreateCompetitionResponse:
2520+
"""Create a new competition from ``competition-metadata.json`` in ``folder``.
2521+
2522+
Args:
2523+
folder (str): Folder containing competition-metadata.json.
2524+
2525+
Returns:
2526+
ApiCreateCompetitionResponse: with id, ref, url of the new competition.
2527+
"""
2528+
if not os.path.isdir(folder):
2529+
raise ValueError("Invalid folder: " + folder)
2530+
2531+
meta_file = os.path.join(folder, self.COMPETITION_METADATA_FILE)
2532+
if not os.path.isfile(meta_file):
2533+
raise ValueError("Metadata file not found: " + self.COMPETITION_METADATA_FILE)
2534+
2535+
with open(meta_file) as f:
2536+
meta = json.load(f)
2537+
2538+
title = cast(str, self.get_or_fail(meta, "title"))
2539+
slug = cast(str, self.get_or_fail(meta, "slug"))
2540+
brief_description = cast(str, self.get_or_fail(meta, "briefDescription"))
2541+
privacy_str = cast(str, self.get_or_fail(meta, "privacy"))
2542+
2543+
if title == "INSERT_TITLE_HERE":
2544+
raise ValueError("Default title detected, please update competition-metadata.json before creating")
2545+
if slug == "INSERT_SLUG_HERE":
2546+
raise ValueError("Default slug detected, please update competition-metadata.json before creating")
2547+
if brief_description == "INSERT_BRIEF_DESCRIPTION_HERE":
2548+
raise ValueError(
2549+
"Default briefDescription detected, please update competition-metadata.json before creating"
2550+
)
2551+
2552+
try:
2553+
privacy = CompetitionPrivacy[privacy_str.upper()]
2554+
except KeyError as exc:
2555+
valid = [n for n in CompetitionPrivacy.__members__ if n != "COMPETITION_PRIVACY_UNSPECIFIED"]
2556+
raise ValueError(f"Invalid privacy '{privacy_str}'. Valid: {', '.join(valid)}") from exc
2557+
2558+
request = ApiCreateCompetitionRequest()
2559+
request.title = title
2560+
request.slug = slug
2561+
request.brief_description = brief_description
2562+
request.privacy = privacy
2563+
2564+
if meta.get("disableKernels") is not None:
2565+
request.disable_kernels = bool(meta["disableKernels"])
2566+
if meta.get("hackathon") is not None:
2567+
request.hackathon = bool(meta["hackathon"])
2568+
if meta.get("restrictLinkToEmailList") is not None:
2569+
request.restrict_link_to_email_list = bool(meta["restrictLinkToEmailList"])
2570+
if meta.get("cloneCompetitionId") is not None:
2571+
request.clone_competition_id = int(meta["cloneCompetitionId"])
2572+
if meta.get("cloneExcludeCompetitionData") is not None:
2573+
request.clone_exclude_competition_data = bool(meta["cloneExcludeCompetitionData"])
2574+
if meta.get("clonePageNames"):
2575+
request.clone_page_names = list(meta["clonePageNames"])
2576+
if meta.get("licenseId") is not None:
2577+
request.license_id = int(meta["licenseId"])
2578+
if meta.get("organizationId") is not None:
2579+
request.organization_id = int(meta["organizationId"])
2580+
if meta.get("numPrizes") is not None:
2581+
request.num_prizes = int(meta["numPrizes"])
2582+
if meta.get("reward") is not None:
2583+
reward_meta = meta["reward"]
2584+
reward = Reward()
2585+
reward_id_str = cast(str, self.get_or_fail(reward_meta, "id"))
2586+
try:
2587+
reward.id = RewardTypeId[reward_id_str.upper()]
2588+
except KeyError as exc:
2589+
valid = [n for n in RewardTypeId.__members__ if n != "REWARD_TYPE_ID_UNSPECIFIED"]
2590+
raise ValueError(f"Invalid reward.id '{reward_id_str}'. Valid: {', '.join(valid)}") from exc
2591+
reward.quantity = int(self.get_or_fail(reward_meta, "quantity"))
2592+
if reward_meta.get("clarification") is not None:
2593+
reward.clarification = reward_meta["clarification"]
2594+
request.reward = reward
2595+
2596+
with self.build_kaggle_client() as kaggle:
2597+
return kaggle.competitions.competition_api_client.create_competition(request)
2598+
2599+
def competition_create_new_cli(self, folder=None):
2600+
folder = folder or os.getcwd()
2601+
response = self.competition_create_new(folder)
2602+
print(f"Competition created: {response.url}")
2603+
24742604
def competition_list_topics(self, competition: str, sort_by: Optional[str] = None, page: Optional[int] = None):
24752605
"""List discussion topics for a competition.
24762606

src/kaggle/cli.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,35 @@ def parse_competitions(subparsers) -> None:
490490
parser_competitions_launch._action_groups.append(parser_competitions_launch_optional)
491491
parser_competitions_launch.set_defaults(func=api.competition_launch_cli)
492492

493+
# Competitions init (write competition-metadata.json template)
494+
parser_competitions_init = subparsers_competitions.add_parser(
495+
"init", formatter_class=argparse.RawTextHelpFormatter, help=Help.command_competitions_init
496+
)
497+
parser_competitions_init_optional = parser_competitions_init._action_groups.pop()
498+
parser_competitions_init_optional.add_argument(
499+
"folder",
500+
nargs="?",
501+
default=None,
502+
help="Folder to write competition-metadata.json into (default: current directory).",
503+
)
504+
parser_competitions_init._action_groups.append(parser_competitions_init_optional)
505+
parser_competitions_init.set_defaults(func=api.competition_initialize_cli)
506+
507+
# Competitions create (read competition-metadata.json and create competition)
508+
parser_competitions_create = subparsers_competitions.add_parser(
509+
"create", formatter_class=argparse.RawTextHelpFormatter, help=Help.command_competitions_create
510+
)
511+
parser_competitions_create_optional = parser_competitions_create._action_groups.pop()
512+
parser_competitions_create_optional.add_argument(
513+
"-p",
514+
"--path",
515+
dest="folder",
516+
required=False,
517+
help="Folder containing competition-metadata.json (default: current directory).",
518+
)
519+
parser_competitions_create._action_groups.append(parser_competitions_create_optional)
520+
parser_competitions_create.set_defaults(func=api.competition_create_new_cli)
521+
493522
shared_topics = _get_shared_topics_parser()
494523
shared_competition_topics = _get_shared_competition_topics_parser()
495524

@@ -1998,6 +2027,8 @@ class Help(object):
19982027
"logs",
19992028
"pages",
20002029
"launch",
2030+
"init",
2031+
"create",
20012032
"topics",
20022033
"topic-messages",
20032034
]
@@ -2134,6 +2165,8 @@ class Help(object):
21342165
command_competitions_pages = "List pages for a competition"
21352166
command_competitions_pages_create = "Create a new page on a competition you host"
21362167
command_competitions_launch = "Launch a competition you host, optionally at a future UTC time"
2168+
command_competitions_init = "Initialize folder with a competition-metadata.json template"
2169+
command_competitions_create = "Create a new competition from competition-metadata.json"
21372170
command_competitions_topics = "List discussion topics for a competition"
21382171
command_competitions_topic_messages = "List messages within a competition discussion topic"
21392172

tests/test_commands.sh

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,11 @@ kaggle c pages titanic
3939
kaggle c pages titanic -v -q
4040
kaggle c pages titanic --page-name rules --content
4141
kaggle c pages list titanic --page-name rules
42-
rm -r titanic.zip tost sample_submission.csv leaders leaderboard.txt
42+
echo "kaggle competitions init"
43+
mkdir -p tests/comp-init
44+
kaggle c init tests/comp-init
45+
cat tests/comp-init/competition-metadata.json
46+
rm -r titanic.zip tost sample_submission.csv leaders leaderboard.txt tests/comp-init
4347

4448
echo "kaggle kernels list"
4549
kaggle k list -m -s Exercise --page-size 5 -p 2 -v --sort-by dateRun

0 commit comments

Comments
 (0)