diff --git a/docs/output_format.md b/docs/output_format.md index 4f182dd6..9545a09b 100644 --- a/docs/output_format.md +++ b/docs/output_format.md @@ -21,12 +21,63 @@ We have introduced a new `--format` option to provide a unified way to specify t It accepts the following values: * `csv`: Display output as comma-separated values. * `table`: Display output as a formatted table (default). -* `json`: Display output as JSON (to be fully implemented in future updates). +* `json`: Display output as JSON. Example: ```sh kaggle competitions list --format csv kaggle competitions list --format table +kaggle competitions list --format json +``` + +For most commands, the JSON output is a list of objects representing the rows, with keys corresponding to the column headers. For detailed commands like `topics show`, it returns a structured object: +```json +{ + "topic": { ... }, + "comments": [ ... ] +} +``` + +### Projections (Field Selection) + +The `--format` option supports optional `gcloud`-style field selection (projections) by appending a comma-separated list of fields in parentheses to the format name. This allows you to limit the output to only the specified fields and control their order. + +Projections are supported for all formats (`csv`, `table`, `json`). + +Example: +```sh +# Only show 'ref' and 'reward' columns for competitions in a table +kaggle competitions list --format "table(ref,reward)" + +# Export only 'id' and 'publicScore' to JSON for team submissions +kaggle competitions team-submissions --format "json(id,publicScore)" + +# Export only 'name' and 'size' to CSV for dataset files +kaggle datasets files -d zillow/zecon --format "csv(name,size)" +``` + +You can specify fields using either their field names (e.g. `totalBytes`) or their display labels (e.g. `size`). If a field is not recognized, the CLI will display an error listing the allowed fields. + +#### Special Case: Topics Show + +For `topics show` commands, which output both a parent topic and a list of comments, the projection is applied to both types of objects. Fields matching the topic are applied to the topic output, and fields matching comments are applied to the comment output. + +Example: +```sh +kaggle forums topics show 123 --format "json(title,content)" +``` +In this case, `title` (which is a topic field) will be preserved in the topic output, and `content` (which is a comment field) will be preserved in the comments output. +```json +{ + "topic": { + "title": "Test Title" + }, + "comments": [ + { + "content": "Comment Content" + } + ] +} ``` ### Mutual Exclusion diff --git a/src/kaggle/api/kaggle_api_extended.py b/src/kaggle/api/kaggle_api_extended.py index 017ff807..7e00392b 100644 --- a/src/kaggle/api/kaggle_api_extended.py +++ b/src/kaggle/api/kaggle_api_extended.py @@ -1000,12 +1000,53 @@ def _get_output_format(self, csv_display: Optional[bool], output_format: Optiona if csv_display: return OutputFormat.CSV if output_format: + format_name, _ = _parse_format(output_format) try: - return OutputFormat(output_format) + return OutputFormat(format_name) except ValueError: return OutputFormat.TABLE return OutputFormat.TABLE + def _resolve_projection(self, output_format, fields, labels=None): + """Resolves projection fields from --format option. + + Args: + output_format (str): The format option value, e.g. "json(field1,field2)". + fields (list): The list of available field names. + labels (list): The list of labels corresponding to fields. + + Returns: + tuple: (resolved_fields, resolved_labels) + """ + if labels is None: + labels = fields + + if not output_format: + return fields, labels + + format_name, proj_labels = _parse_format(output_format) + if not proj_labels: + return fields, labels + + resolved_fields = [] + resolved_labels = [] + + label_to_field = dict(zip(labels, fields)) + field_to_label = dict(zip(fields, labels)) + + for proj in proj_labels: + if proj in label_to_field: + resolved_fields.append(label_to_field[proj]) + resolved_labels.append(proj) + elif proj in field_to_label: + resolved_fields.append(proj) + resolved_labels.append(field_to_label[proj]) + else: + allowed = sorted(list(set(fields + labels))) + raise ValueError(f"Unknown field in projection: {proj!r}. " f"Allowed fields: {', '.join(allowed)}") + + return resolved_fields, resolved_labels + def _load_config(self) -> None: """Load configuration from file and environment variables.""" config_values = self.read_config_file(quiet=True) @@ -1584,13 +1625,12 @@ def competitions_list_cli( if response and response.next_page_token: print("Next Page Token = {}".format(response.next_page_token)) if response and response.competitions: - output_fmt = self._get_output_format(csv_display, output_format) - if output_fmt == OutputFormat.CSV: - self.print_csv(response.competitions, self.competition_fields) - elif output_fmt == OutputFormat.JSON: - print("JSON format not yet implemented", file=sys.stderr) - else: - self.print_table(response.competitions, self.competition_fields) + self.print_results( + response.competitions, + self.competition_fields, + csv_display=csv_display, + output_format=output_format, + ) else: print("No competitions found") @@ -1825,13 +1865,12 @@ def competition_submissions_cli( competition, page_number=page, page_token=page_token, page_size=page_size ) if submissions: - output_fmt = self._get_output_format(csv_display, output_format) - if output_fmt == OutputFormat.CSV: - self.print_csv(submissions, self.submission_fields) - elif output_fmt == OutputFormat.JSON: - print("JSON format not yet implemented", file=sys.stderr) - else: - self.print_table(submissions, self.submission_fields) + self.print_results( + submissions, + self.submission_fields, + csv_display=csv_display, + output_format=output_format, + ) else: print("No submissions found") @@ -1893,13 +1932,13 @@ def competition_list_files_cli( if next_page_token: print("Next Page Token = {}".format(next_page_token)) if result: - output_fmt = self._get_output_format(csv_display, output_format) - if output_fmt == OutputFormat.CSV: - self.print_csv(result.files, self.competition_file_fields, self.competition_file_labels) - elif output_fmt == OutputFormat.JSON: - print("JSON format not yet implemented", file=sys.stderr) - else: - self.print_table(result.files, self.competition_file_fields, self.competition_file_labels) + self.print_results( + result.files, + self.competition_file_fields, + self.competition_file_labels, + csv_display=csv_display, + output_format=output_format, + ) else: print("No files found") @@ -2083,13 +2122,12 @@ def competition_leaderboard_cli( if view: results = self.competition_leaderboard_view(competition, page_size, page_token) if results: - output_fmt = self._get_output_format(csv_display, output_format) - if output_fmt == OutputFormat.CSV: - self.print_csv(results, self.competition_leaderboard_fields) - elif output_fmt == OutputFormat.JSON: - print("JSON format not yet implemented", file=sys.stderr) - else: - self.print_table(results, self.competition_leaderboard_fields) + self.print_results( + results, + self.competition_leaderboard_fields, + csv_display=csv_display, + output_format=output_format, + ) else: print("No results found") @@ -2129,13 +2167,12 @@ def competition_team_submissions_cli(self, team_id, csv_display=False, quiet=Fal if not submissions: print("No submissions found") return - output_fmt = self._get_output_format(csv_display, output_format) - if output_fmt == OutputFormat.CSV: - self.print_csv(submissions, self.team_public_submission_fields) - elif output_fmt == OutputFormat.JSON: - print("JSON format not yet implemented", file=sys.stderr) - else: - self.print_table(submissions, self.team_public_submission_fields) + self.print_results( + submissions, + self.team_public_submission_fields, + csv_display=csv_display, + output_format=output_format, + ) def competition_list_episodes(self, submission_id: int): """List episodes for a submission in a simulation competition. @@ -2163,13 +2200,12 @@ def competition_list_episodes_cli(self, submission_id, csv_display=False, quiet= """ episodes = self.competition_list_episodes(submission_id) if episodes: - output_fmt = self._get_output_format(csv_display, output_format) - if output_fmt == OutputFormat.CSV: - self.print_csv(episodes, self.episode_fields) - elif output_fmt == OutputFormat.JSON: - print("JSON format not yet implemented", file=sys.stderr) - else: - self.print_table(episodes, self.episode_fields) + self.print_results( + episodes, + self.episode_fields, + csv_display=csv_display, + output_format=output_format, + ) if not quiet: print( '\nUse "kaggle competitions replay " to download a replay, ' @@ -2296,13 +2332,12 @@ def competition_list_pages_cli( pages = self.competition_list_pages(competition, page_name=page_name) if pages: fields = ["name", "content"] if content else self.competition_page_fields - output_fmt = self._get_output_format(csv_display, output_format) - if output_fmt == OutputFormat.CSV: - self.print_csv(pages, fields) - elif output_fmt == OutputFormat.JSON: - print("JSON format not yet implemented", file=sys.stderr) - else: - self.print_table(pages, fields) + self.print_results( + pages, + fields, + csv_display=csv_display, + output_format=output_format, + ) else: print("No pages found") @@ -2376,14 +2411,12 @@ def competition_list_topics_cli( ) topics = response.topics if topics: - fields = self.competition_topic_fields - output_fmt = self._get_output_format(csv_display, output_format) - if output_fmt == OutputFormat.CSV: - self.print_csv(topics, fields) - elif output_fmt == OutputFormat.JSON: - print("JSON format not yet implemented", file=sys.stderr) - else: - self.print_table(topics, fields) + self.print_results( + topics, + self.competition_topic_fields, + csv_display=csv_display, + output_format=output_format, + ) else: print("No topics found") @@ -2456,14 +2489,12 @@ def competition_list_topic_messages_cli( ) messages = self._flatten_topic_messages(response.messages) if messages: - fields = self.competition_topic_message_fields - output_fmt = self._get_output_format(csv_display, output_format) - if output_fmt == OutputFormat.CSV: - self.print_csv(messages, fields) - elif output_fmt == OutputFormat.JSON: - print("JSON format not yet implemented", file=sys.stderr) - else: - self.print_table(messages, fields) + self.print_results( + messages, + self.competition_topic_message_fields, + csv_display=csv_display, + output_format=output_format, + ) else: print("No messages found") @@ -2499,14 +2530,12 @@ def forums_list_cli(self, csv_display=False, quiet=False, output_format=None): response = self.forums_list() forums = response.forums if forums: - fields = self.forum_fields - output_fmt = self._get_output_format(csv_display, output_format) - if output_fmt == OutputFormat.CSV: - self.print_csv(forums, fields) - elif output_fmt == OutputFormat.JSON: - print("JSON format not yet implemented", file=sys.stderr) - else: - self.print_table(forums, fields) + self.print_results( + forums, + self.forum_fields, + csv_display=csv_display, + output_format=output_format, + ) else: print("No forums found") @@ -2756,14 +2785,12 @@ def forums_list_topics_cli( ) topics = response.topics if topics: - fields = self.forum_topic_fields - output_fmt = self._get_output_format(csv_display, output_format) - if output_fmt == OutputFormat.CSV: - self.print_csv(topics, fields) - elif output_fmt == OutputFormat.JSON: - print("JSON format not yet implemented", file=sys.stderr) - else: - self.print_table(topics, fields) + self.print_results( + topics, + self.forum_topic_fields, + csv_display=csv_display, + output_format=output_format, + ) if not quiet and response.next_page_token: print(f"Next page token: {response.next_page_token}") else: @@ -2901,14 +2928,27 @@ def forums_topic_show_cli( return output_fmt = self._get_output_format(csv_display, output_format) + all_fields = self.forum_topic_fields + self.forum_comment_fields + resolved_all, _ = self._resolve_projection(output_format, all_fields) + resolved_topic_fields = [f for f in resolved_all if f in self.forum_topic_fields] + resolved_comment_fields = [f for f in resolved_all if f in self.forum_comment_fields] + if output_fmt == OutputFormat.CSV: - # In CSV mode, print the topic then flat comments - self.print_csv([topic], self.forum_topic_fields) + if resolved_topic_fields: + self.print_csv([topic], resolved_topic_fields) flat = self._flatten_discussion_comments(comments) - if flat: - self.print_csv(flat, self.forum_comment_fields) + if flat and resolved_comment_fields: + self.print_csv(flat, resolved_comment_fields) elif output_fmt == OutputFormat.JSON: - print("JSON format not yet implemented", file=sys.stderr) + output = {} + if resolved_topic_fields: + topic_json = self.get_json_serializable([topic], resolved_topic_fields)[0] + output["topic"] = topic_json + if resolved_comment_fields: + flat_comments = self._flatten_discussion_comments(comments) + comments_json = self.get_json_serializable(flat_comments, resolved_comment_fields) + output["comments"] = comments_json + print(json.dumps(output, indent=2)) else: # Pretty-print the topic header print(f"Topic #{topic.id}: {topic.title}") @@ -3007,14 +3047,12 @@ def dataset_list_topics_cli( ) topics = response.topics if topics: - fields = self.forum_topic_fields - output_fmt = self._get_output_format(csv_display, output_format) - if output_fmt == OutputFormat.CSV: - self.print_csv(topics, fields) - elif output_fmt == OutputFormat.JSON: - print("JSON format not yet implemented", file=sys.stderr) - else: - self.print_table(topics, fields) + self.print_results( + topics, + self.forum_topic_fields, + csv_display=csv_display, + output_format=output_format, + ) if not quiet and response.next_page_token: print(f"Next page token: {response.next_page_token}") else: @@ -3055,14 +3093,12 @@ def kernel_list_topics_cli( ) topics = response.topics if topics: - fields = self.forum_topic_fields - output_fmt = self._get_output_format(csv_display, output_format) - if output_fmt == OutputFormat.CSV: - self.print_csv(topics, fields) - elif output_fmt == OutputFormat.JSON: - print("JSON format not yet implemented", file=sys.stderr) - else: - self.print_table(topics, fields) + self.print_results( + topics, + self.forum_topic_fields, + csv_display=csv_display, + output_format=output_format, + ) if not quiet and response.next_page_token: print(f"Next page token: {response.next_page_token}") else: @@ -3103,14 +3139,12 @@ def model_list_topics_cli( ) topics = response.topics if topics: - fields = self.forum_topic_fields - output_fmt = self._get_output_format(csv_display, output_format) - if output_fmt == OutputFormat.CSV: - self.print_csv(topics, fields) - elif output_fmt == OutputFormat.JSON: - print("JSON format not yet implemented", file=sys.stderr) - else: - self.print_table(topics, fields) + self.print_results( + topics, + self.forum_topic_fields, + csv_display=csv_display, + output_format=output_format, + ) if not quiet and response.next_page_token: print(f"Next page token: {response.next_page_token}") else: @@ -3151,14 +3185,12 @@ def benchmark_list_topics_cli( ) topics = response.topics if topics: - fields = self.forum_topic_fields - output_fmt = self._get_output_format(csv_display, output_format) - if output_fmt == OutputFormat.CSV: - self.print_csv(topics, fields) - elif output_fmt == OutputFormat.JSON: - print("JSON format not yet implemented", file=sys.stderr) - else: - self.print_table(topics, fields) + self.print_results( + topics, + self.forum_topic_fields, + csv_display=csv_display, + output_format=output_format, + ) if not quiet and response.next_page_token: print(f"Next page token: {response.next_page_token}") else: @@ -3297,13 +3329,13 @@ def dataset_list_cli( sort_by, size, file_type, license_name, tag_ids, search, user, mine, page, max_size, min_size ) if datasets: - output_fmt = self._get_output_format(csv_display, output_format) - if output_fmt == OutputFormat.CSV: - self.print_csv(datasets, self.dataset_fields, self.dataset_labels) - elif output_fmt == OutputFormat.JSON: - print("JSON format not yet implemented", file=sys.stderr) - else: - self.print_table(datasets, self.dataset_fields, self.dataset_labels) + self.print_results( + datasets, + self.dataset_fields, + self.dataset_labels, + csv_display=csv_display, + output_format=output_format, + ) else: print("No datasets found") @@ -3568,13 +3600,12 @@ def dataset_list_files_cli( print("Next Page Token = {}".format(next_page_token)) fields = ["name", "size", "creationDate"] ApiDatasetFile.size = ApiDatasetFile.total_bytes # type: ignore[attr-defined] - output_fmt = self._get_output_format(csv_display, output_format) - if output_fmt == OutputFormat.CSV: - self.print_csv(result.files, fields) - elif output_fmt == OutputFormat.JSON: - print("JSON format not yet implemented", file=sys.stderr) - else: - self.print_table(result.files, fields) + self.print_results( + result.files, + fields, + csv_display=csv_display, + output_format=output_format, + ) else: print("No files found") @@ -4557,13 +4588,12 @@ def kernels_list_cli( ) fields = ["ref", "title", "author", "lastRunTime", "totalVotes"] if kernels: - output_fmt = self._get_output_format(csv_display, output_format) - if output_fmt == OutputFormat.CSV: - self.print_csv(kernels, fields) - elif output_fmt == OutputFormat.JSON: - print("JSON format not yet implemented", file=sys.stderr) - else: - self.print_table(kernels, fields) + self.print_results( + kernels, + fields, + csv_display=csv_display, + output_format=output_format, + ) else: print("Not found") @@ -4607,13 +4637,12 @@ def quota_view_cli(self, csv_display=False, output_format=None): print("No quota information available") return fields = ["resource", "used", "remaining", "total", "refreshAt"] - output_fmt = self._get_output_format(csv_display, output_format) - if output_fmt == OutputFormat.CSV: - self.print_csv(rows, fields) - elif output_fmt == OutputFormat.JSON: - print("JSON format not yet implemented", file=sys.stderr) - else: - self.print_table(rows, fields) + self.print_results( + rows, + fields, + csv_display=csv_display, + output_format=output_format, + ) def kernels_list_files(self, kernel, page_token=None, page_size=20): """Lists files for a kernel. @@ -4659,13 +4688,12 @@ def kernels_list_files_cli( if next_page_token: print("Next Page Token = {}".format(next_page_token)) fields = ["name", "size", "creationDate"] - output_fmt = self._get_output_format(csv_display, output_format) - if output_fmt == OutputFormat.CSV: - self.print_csv(result.files, fields) - elif output_fmt == OutputFormat.JSON: - print("JSON format not yet implemented", file=sys.stderr) - else: - self.print_table(result.files, fields) + self.print_results( + result.files, + fields, + csv_display=csv_display, + output_format=output_format, + ) def kernels_initialize(self, folder: str) -> str: """Initializes a new kernel in a specified folder from a template. @@ -5540,13 +5568,12 @@ def model_list_cli( models = self.model_list(sort_by, search, owner, page_size, page_token) fields = ["id", "ref", "title", "subtitle", "author"] if models: - output_fmt = self._get_output_format(csv_display, output_format) - if output_fmt == OutputFormat.CSV: - self.print_csv(models, fields) - elif output_fmt == OutputFormat.JSON: - print("JSON format not yet implemented", file=sys.stderr) - else: - self.print_table(models, fields) + self.print_results( + models, + fields, + csv_display=csv_display, + output_format=output_format, + ) else: print("No models found") @@ -6092,14 +6119,12 @@ def model_instance_files_cli( model_instance, page_token=page_token, page_size=page_size, csv_display=csv_display ) if result and result.files is not None: - fields = self.dataset_file_fields - output_fmt = self._get_output_format(csv_display, output_format) - if output_fmt == OutputFormat.CSV: - self.print_csv(result.files, fields) - elif output_fmt == OutputFormat.JSON: - print("JSON format not yet implemented", file=sys.stderr) - else: - self.print_table(result.files, fields) + self.print_results( + result.files, + self.dataset_file_fields, + csv_display=csv_display, + output_format=output_format, + ) def model_instances_list(self, model_instance, page_size=20, page_token=None) -> ApiListModelInstancesResponse: owner_slug, model_slug = self.split_model_string(model_instance) @@ -6128,13 +6153,13 @@ def model_instances_list_cli( print("Next Page Token = {}".format(response.next_page_token)) instances = response.instances if instances: - output_fmt = self._get_output_format(csv_display, output_format) - if output_fmt == OutputFormat.CSV: - self.print_csv(instances, self.model_instance_fields, self.model_instance_labels) - elif output_fmt == OutputFormat.JSON: - print("JSON format not yet implemented", file=sys.stderr) - else: - self.print_table(instances, self.model_instance_fields, self.model_instance_labels) + self.print_results( + instances, + self.model_instance_fields, + self.model_instance_labels, + csv_display=csv_display, + output_format=output_format, + ) else: print("No instances found") @@ -6441,13 +6466,13 @@ def model_instance_version_files_cli( if result and result.files is not None: fields = ["name", "size", "creation_date"] labels = ["name", "size", "creationDate"] - output_fmt = self._get_output_format(csv_display, output_format) - if output_fmt == OutputFormat.CSV: - self.print_csv(result.files, fields, labels) - elif output_fmt == OutputFormat.JSON: - print("JSON format not yet implemented", file=sys.stderr) - else: - self.print_table(result.files, fields, labels) + self.print_results( + result.files, + fields, + labels, + csv_display=csv_display, + output_format=output_format, + ) def model_instance_versions_list( self, model_instance, page_size=20, page_token=None @@ -6480,17 +6505,13 @@ def model_instance_versions_list_cli( print("Next Page Token = {}".format(response.next_page_token)) versions = response.version_list if versions: - output_fmt = self._get_output_format(csv_display, output_format) - if output_fmt == OutputFormat.CSV: - self.print_csv( - versions.versions, self.model_instance_version_fields, self.model_instance_version_labels - ) - elif output_fmt == OutputFormat.JSON: - print("JSON format not yet implemented", file=sys.stderr) - else: - self.print_table( - versions.versions, self.model_instance_version_fields, self.model_instance_version_labels - ) + self.print_results( + versions.versions, + self.model_instance_version_fields, + self.model_instance_version_labels, + csv_display=csv_display, + output_format=output_format, + ) else: print("No versions found") @@ -6671,6 +6692,62 @@ def print_csv(self, items, fields, labels=None): i_fields = [self.string(getattr(i, self.camel_to_snake(f))) for f in fields] writer.writerow(i_fields) + def get_json_serializable(self, items, fields, labels=None): + """Converts a set of fields from a set of items into a JSON-serializable list of dicts. + + Args: + items: A list of items to convert. + fields: A list of fields to select from the items. + labels: The labels for the fields (defaults to fields). + + Returns: + list: A list of dictionaries containing the selected fields. + """ + if labels is None: + labels = fields + json_items = [] + for i in items: + item_dict = {} + for f, label in zip(fields, labels): + val = getattr(i, self.camel_to_snake(f)) + if isinstance(val, datetime): + val = val.isoformat() + elif not isinstance(val, (int, float, bool, str)) and val is not None: + val = str(val) + item_dict[label] = val + json_items.append(item_dict) + return json_items + + def print_json(self, items, fields, labels=None): + """Prints a set of fields from a set of items in JSON format. + + Args: + items: A list of items to print. + fields: A list of fields to select from the items. + labels: The labels for the fields (defaults to fields). + """ + print(json.dumps(self.get_json_serializable(items, fields, labels), indent=2)) + + def print_results(self, items, fields, labels=None, csv_display=False, output_format=None): + """Prints results in the selected format, applying projection if present. + + Args: + items: A list of items to print. + fields: A list of fields to select from the items. + labels: The labels for the fields (defaults to fields). + csv_display: If True, print CSV (legacy option). + output_format: The format option value, e.g. "json(field1,field2)". + """ + output_fmt = self._get_output_format(csv_display, output_format) + resolved_fields, resolved_labels = self._resolve_projection(output_format, fields, labels) + + if output_fmt == OutputFormat.CSV: + self.print_csv(items, resolved_fields, resolved_labels) + elif output_fmt == OutputFormat.JSON: + self.print_json(items, resolved_fields, resolved_labels) + else: + self.print_table(items, resolved_fields, resolved_labels) + def string(self, item): return item if isinstance(item, str) else str(item) diff --git a/src/kaggle/cli.py b/src/kaggle/cli.py index 4d6e83ec..dccfd223 100644 --- a/src/kaggle/cli.py +++ b/src/kaggle/cli.py @@ -116,7 +116,6 @@ def _add_output_format_args(parser) -> None: group.add_argument( "--format", dest="output_format", - choices=[str(f) for f in OutputFormat], help=Help.param_format, ) diff --git a/src/kaggle/test/test_discussions_cli.py b/src/kaggle/test/test_discussions_cli.py index 04b900f0..e9aa5cc0 100644 --- a/src/kaggle/test/test_discussions_cli.py +++ b/src/kaggle/test/test_discussions_cli.py @@ -632,3 +632,77 @@ def test_benchmark_list_topics_correct_request(self, api): def test_benchmark_list_topics_invalid_slug(self, api): with pytest.raises(ValueError, match="Benchmark must be specified"): api.benchmark_list_topics(benchmark="too/many/slashes") + + +class TestForumsTopicShowCliOutput: + """Verify forums_topic_show_cli output formatting.""" + + def test_json_output(self, api, capsys): + mock_topic = MagicMock() + mock_topic.id = 123 + mock_topic.title = "Test Title" + mock_topic.author_name = "test-author" + mock_topic.post_date = "2026-06-01" + mock_topic.votes = 5 + mock_topic.comment_count = 1 + mock_topic.content = "Test Content" + + mock_comment = MagicMock() + mock_comment.id = 456 + mock_comment.author_name = "comment-author" + mock_comment.post_date = "2026-06-02" + mock_comment.votes = 2 + mock_comment.content = "Comment Content" + mock_comment.replies = [] + + api.forums_topic_show = MagicMock(return_value=(mock_topic, [mock_comment], "")) + + import json + + api.forums_topic_show_cli(topic_ref="123", output_format="json") + + captured = capsys.readouterr() + output = json.loads(captured.out) + + assert output["topic"]["id"] == 123 + assert output["topic"]["title"] == "Test Title" + assert output["topic"]["authorName"] == "test-author" + + assert len(output["comments"]) == 1 + assert output["comments"][0]["id"] == 456 + assert output["comments"][0]["authorName"] == "comment-author" + + def test_json_projection_output(self, api, capsys): + mock_topic = MagicMock() + mock_topic.id = 123 + mock_topic.title = "Test Title" + mock_topic.author_name = "test-author" + mock_topic.post_date = "2026-06-01" + mock_topic.votes = 5 + mock_topic.comment_count = 1 + + mock_comment = MagicMock() + mock_comment.id = 456 + mock_comment.author_name = "comment-author" + mock_comment.post_date = "2026-06-02" + mock_comment.votes = 2 + mock_comment.content = "Comment Content" + mock_comment.replies = [] + + api.forums_topic_show = MagicMock(return_value=(mock_topic, [mock_comment], "")) + + import json + + api.forums_topic_show_cli(topic_ref="123", output_format="json(title,content)") + + captured = capsys.readouterr() + output = json.loads(captured.out) + + assert list(output["topic"].keys()) == ["title"] + assert output["topic"]["title"] == "Test Title" + assert "id" not in output["topic"] + + assert len(output["comments"]) == 1 + assert list(output["comments"][0].keys()) == ["content"] + assert output["comments"][0]["content"] == "Comment Content" + assert "id" not in output["comments"][0] diff --git a/tests/test_quota.py b/tests/test_quota.py index 8a58a581..f75be1ef 100644 --- a/tests/test_quota.py +++ b/tests/test_quota.py @@ -125,6 +125,67 @@ def test_quota_view_cli_format_table(self, mock_view): self.assertIn("TPU", output) self.assertIn("5.00h", output) + @patch.object(KaggleApi, "quota_view") + def test_quota_view_cli_format_json(self, mock_view): + mock_view.return_value = _build_response( + gpu=_mock_quota(5, 30), + tpu=_mock_quota(2, 20), + refresh_time=datetime(2026, 6, 1, tzinfo=timezone.utc), + ) + + captured = io.StringIO() + sys.stdout = captured + try: + self.api.quota_view_cli(output_format="json") + finally: + sys.stdout = sys.__stdout__ + + import json + + output = json.loads(captured.getvalue()) + self.assertEqual(len(output), 2) + self.assertEqual(output[0]["resource"], "GPU") + self.assertEqual(output[0]["used"], "5.00h") + self.assertEqual(output[0]["remaining"], "25.00h") + self.assertEqual(output[0]["total"], "30.00h") + self.assertEqual(output[0]["refreshAt"], "2026-06-01T00:00:00+00:00") + + self.assertEqual(output[1]["resource"], "TPU") + self.assertEqual(output[1]["used"], "2.00h") + self.assertEqual(output[1]["remaining"], "18.00h") + self.assertEqual(output[1]["total"], "20.00h") + self.assertEqual(output[1]["refreshAt"], "2026-06-01T00:00:00+00:00") + + @patch.object(KaggleApi, "quota_view") + def test_quota_view_cli_format_json_projection(self, mock_view): + mock_view.return_value = _build_response( + gpu=_mock_quota(5, 30), + tpu=_mock_quota(2, 20), + refresh_time=datetime(2026, 6, 1, tzinfo=timezone.utc), + ) + + captured = io.StringIO() + sys.stdout = captured + try: + self.api.quota_view_cli(output_format="json(resource,remaining)") + finally: + sys.stdout = sys.__stdout__ + + import json + + output = json.loads(captured.getvalue()) + self.assertEqual(len(output), 2) + self.assertEqual(list(output[0].keys()), ["resource", "remaining"]) + self.assertEqual(output[0]["resource"], "GPU") + self.assertEqual(output[0]["remaining"], "25.00h") + self.assertNotIn("used", output[0]) + self.assertNotIn("total", output[0]) + self.assertNotIn("refreshAt", output[0]) + + self.assertEqual(list(output[1].keys()), ["resource", "remaining"]) + self.assertEqual(output[1]["resource"], "TPU") + self.assertEqual(output[1]["remaining"], "18.00h") + @patch.object(KaggleApi, "quota_view") def test_quota_view_cli_skips_missing_accelerator(self, mock_view): mock_view.return_value = _build_response(gpu=_mock_quota(1, 30), tpu=None) diff --git a/tests/test_team_submissions.py b/tests/test_team_submissions.py index 90a32e0b..ba1245da 100644 --- a/tests/test_team_submissions.py +++ b/tests/test_team_submissions.py @@ -112,6 +112,58 @@ def test_cli_format_table_output(self, mock_view): self.assertIn("22", output) self.assertIn("100.0", output) + @patch.object(KaggleApi, "competition_team_submissions") + def test_cli_format_json_output(self, mock_view): + mock_view.return_value = [ + _mock_submission(11, "2026-01-02T00:00:00Z", "100.0"), + _mock_submission(22, "2026-01-01T00:00:00Z", "50.0"), + ] + + captured = io.StringIO() + sys.stdout = captured + try: + self.api.competition_team_submissions_cli(team_id=42, output_format="json") + finally: + sys.stdout = sys.__stdout__ + + import json + + output = json.loads(captured.getvalue()) + self.assertEqual(len(output), 2) + self.assertEqual(output[0]["id"], 11) + self.assertEqual(output[0]["dateSubmitted"], "2026-01-02T00:00:00Z") + self.assertEqual(output[0]["publicScore"], "100.0") + self.assertEqual(output[1]["id"], 22) + self.assertEqual(output[1]["dateSubmitted"], "2026-01-01T00:00:00Z") + self.assertEqual(output[1]["publicScore"], "50.0") + + @patch.object(KaggleApi, "competition_team_submissions") + def test_cli_format_json_projection_output(self, mock_view): + mock_view.return_value = [ + _mock_submission(11, "2026-01-02T00:00:00Z", "100.0"), + _mock_submission(22, "2026-01-01T00:00:00Z", "50.0"), + ] + + captured = io.StringIO() + sys.stdout = captured + try: + self.api.competition_team_submissions_cli(team_id=42, output_format="json(id,publicScore)") + finally: + sys.stdout = sys.__stdout__ + + import json + + output = json.loads(captured.getvalue()) + self.assertEqual(len(output), 2) + self.assertEqual(list(output[0].keys()), ["id", "publicScore"]) + self.assertEqual(output[0]["id"], 11) + self.assertEqual(output[0]["publicScore"], "100.0") + self.assertNotIn("dateSubmitted", output[0]) + + self.assertEqual(list(output[1].keys()), ["id", "publicScore"]) + self.assertEqual(output[1]["id"], 22) + self.assertEqual(output[1]["publicScore"], "50.0") + @patch.object(KaggleApi, "competition_team_submissions") def test_cli_empty(self, mock_view): mock_view.return_value = []