Skip to content
Open
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
2 changes: 2 additions & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@
## Deprecations

## New additions
* Added new `TOON` output format (`--format toon`) — token-efficient structured output for LLM consumption.

## Fixes and improvements
* Commands that previously emitted no output under `--format csv` (e.g. `snow stage diff`, `snow app diff`) now return structured results for all structured output formats.


# v3.23.0
Expand Down
6 changes: 6 additions & 0 deletions pylock.toml
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,12 @@ version = "0.13.3"
sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", upload-time = 2025-06-05T07:13:44Z, size = 185207, hashes = { sha256 = "430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1" } }
wheels = [{ url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", upload-time = 2025-06-05T07:13:43Z, size = 38901, hashes = { sha256 = "c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0" } }]

[[packages]]
name = "toon-format"
version = "0.9.0b1"
sdist = { url = "https://files.pythonhosted.org/packages/fa/15/d23d6d3e36aa4ec96dd5692bc7715fe17015b669e8f0d1c5c7fa906a3ceb/toon_format-0.9.0b1.tar.gz", upload-time = 2025-11-08T19:22:53Z, size = 87398, hashes = { sha256 = "8f391dd6ad9677c78366bd8eb6762d064a2183f67b9b7da1f348fdb6ee8738e7" } }
wheels = [{ url = "https://files.pythonhosted.org/packages/63/f3/27ab1d982bb81bf9ac5be70b4c774996eb8562b93c77e93c253c22be951f/toon_format-0.9.0b1-py3-none-any.whl", upload-time = 2025-11-08T19:22:51Z, size = 36173, hashes = { sha256 = "efeee919501f91137f017f7bed34789c067f76178111aa872a49bc8653dce3be" } }]

[[packages]]
name = "typer"
version = "0.17.3"
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ dependencies = [
'snowflake-snowpark-python==1.53.0',
"snowflake.core==1.10.0",
"tomlkit==0.13.3",
"toon-format==0.9.0b1",
"typer==0.17.3",
"urllib3>=2.6.3,<3",
"websocket-client>=1.6.0,<2",
Expand Down
1 change: 1 addition & 0 deletions snyk/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ snowflake-core==1.10.0
snowflake-snowpark-python==1.53.0
sortedcontainers==2.4.0
tomlkit==0.13.3
toon-format==0.9.0b1
typer==0.17.3
typing-extensions==4.14.1
typing-inspection==0.4.2
Expand Down
71 changes: 62 additions & 9 deletions src/snowflake/cli/_app/printing.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import csv
import json
import math
import sys
from datetime import date, datetime, time
from decimal import Decimal
Expand All @@ -29,6 +30,7 @@
from rich.console import Console
from rich.live import Live
from rich.table import Table
from toon_format import encode as toon_encode
from snowflake.cli.api.cli_global_context import get_cli_context
from snowflake.cli.api.output.formats import OutputFormat
from snowflake.cli.api.output.types import (
Expand Down Expand Up @@ -224,20 +226,64 @@ def __to_str(val):
return str(val)


def is_structured_format(output_format):
return output_format.is_json or output_format == OutputFormat.CSV
def _to_encodable_value(value):
"""Convert a value to a primitive encodable type (same semantics as CustomJSONEncoder)."""
if isinstance(value, str):
return sanitize_for_terminal(value)
if isinstance(value, (date, datetime, time)):
return value.isoformat()
if isinstance(value, Path):
return value.as_posix()
if isinstance(value, Decimal):
return str(value)
if isinstance(value, (bytes, bytearray)):
return value.hex()
if isinstance(value, float) and not math.isfinite(value):
# toon_encode would collapse non-finite floats to null, making them
# indistinguishable from SQL NULL; use the same tokens JSON output emits
if math.isnan(value):
return "NaN"
return "Infinity" if value > 0 else "-Infinity"
if isinstance(value, dict):
# keys can be server-controlled column names — sanitize them too
return {
sanitize_for_terminal(str(k)): _to_encodable_value(v)
for k, v in value.items()
}
if isinstance(value, (list, tuple)):
return [_to_encodable_value(v) for v in value]
if value is None or isinstance(value, (int, float, bool)):
return value
return str(value)


def _print_toon_result(result: CommandResult):
"""Print a single CommandResult as a TOON document."""
if isinstance(result, CollectionResult):
# ponytail: toon-format has no streaming encoder; we materialize all rows
# in memory, same ceiling as pre-streaming --format json.
data: Any = [_to_encodable_value(row) for row in result.result]
elif isinstance(result, (ObjectResult, MessageResult)):
# ObjectResult(None) — e.g. SingleQueryResult of an empty cursor —
# encodes as `null`, matching the JSON output for the same result
data = _to_encodable_value(result.result)
else:
return
print(toon_encode(data), flush=True)


def print_structured(
result: CommandResult, output_format: OutputFormat = OutputFormat.JSON
):
"""Handles outputs like json, csv and other structured and parsable formats with streaming."""
printed_end_line = False
# Non-JSON structured formats share the same dispatch shape; JSON is the fallback.
printer = _RESULT_PRINTERS.get(output_format)

if isinstance(result, MultipleResults):
if output_format == OutputFormat.CSV:
if printer:
for command_result in result.result:
_print_csv_result_streaming(command_result)
printer(command_result)
print(flush=True)
printed_end_line = True
else:
Expand All @@ -246,15 +292,15 @@ def print_structured(
# A StreamResult prints each value onto its own line
# instead of joining all the values into a JSON array or CSV entry set
for r in result.result:
if output_format == OutputFormat.CSV:
_print_csv_result_streaming(r)
if printer:
printer(r)
else:
json.dump(r, sys.stdout, cls=StreamingJSONEncoder)
print(flush=True)
printed_end_line = True
else:
if output_format == OutputFormat.CSV:
_print_csv_result_streaming(result)
if printer:
printer(result)
printed_end_line = True
else:
_print_json_result_streaming(result)
Expand Down Expand Up @@ -310,6 +356,13 @@ def _print_csv_result_streaming(result: CommandResult):
_print_message_result_as_csv(result)


# Per-result printers for non-JSON structured formats (JSON is the dispatch fallback)
_RESULT_PRINTERS = {
OutputFormat.CSV: _print_csv_result_streaming,
OutputFormat.TOON: _print_toon_result,
}


def _stream_json(result):
"""Simple helper for streaming multiple results as a JSON."""
indent_size = 2
Expand Down Expand Up @@ -365,7 +418,7 @@ def print_result(cmd_result: CommandResult, output_format: OutputFormat | None =
match cmd_result:
case EmptyResult():
return
case _ if is_structured_format(output_format):
case _ if output_format.is_structured:
print_structured(cmd_result, output_format)
case MultipleResults() | StreamResult():
for res in cmd_result.result:
Expand Down
3 changes: 1 addition & 2 deletions src/snowflake/cli/_plugins/apps/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,8 +417,7 @@ def _resolve(
encoding=encoding,
)

is_json = get_cli_context().output_format.is_json
if is_json:
if get_cli_context().output_format.is_structured:
return ObjectResult({"success": not dry_run, **resolved_values})

if dry_run:
Expand Down
2 changes: 1 addition & 1 deletion src/snowflake/cli/_plugins/helpers/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,7 @@ def generate_project_schema(
# Under a structured output format, return the schema as an object so the
# command emits the JSON Schema document itself rather than a stringified
# blob nested under a "message" key.
if get_cli_context().output_format.is_json:
if get_cli_context().output_format.is_structured:
return ObjectResult(schema)

return MessageResult(json.dumps(schema, indent=2, sort_keys=True))
Expand Down
6 changes: 3 additions & 3 deletions src/snowflake/cli/_plugins/nativeapp/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,9 +345,9 @@ def app_diff(
diff = ws.perform_action(
package_id,
EntityActions.DIFF,
print_to_console=not cli_context.output_format.is_json,
print_to_console=not cli_context.output_format.is_structured,
)
if cli_context.output_format.is_json:
if cli_context.output_format.is_structured:
return ObjectResult(diff.to_dict())

return None
Expand Down Expand Up @@ -726,7 +726,7 @@ def app_validate(
)
package_id = options["package_entity_id"]
package = ws.get_entity(package_id)
if cli_context.output_format.is_json:
if cli_context.output_format.is_structured:
return ObjectResult(
package.get_validation_result(
action_ctx=ws.action_ctx,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def release_channel_list(
release_channel=channel,
)

if cli_context.output_format.is_json:
if cli_context.output_format.is_structured:
return CollectionResult(channels)


Expand Down
2 changes: 1 addition & 1 deletion src/snowflake/cli/_plugins/nativeapp/version/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ def create(
)

message = "Version create is now complete."
if cli_context.output_format.is_json:
if cli_context.output_format.is_structured:
return ObjectResult(
{
"message": message,
Expand Down
2 changes: 1 addition & 1 deletion src/snowflake/cli/_plugins/stage/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ def stage_diff(
local_root=Path(folder_name),
stage_path=StageManager.stage_path_parts_from_str(stage_name), # noqa: SLF001
)
if get_cli_context().output_format.is_json:
if get_cli_context().output_format.is_structured:
return ObjectResult(diff.to_dict())
else:
print_diff_to_console(diff)
Expand Down
4 changes: 2 additions & 2 deletions src/snowflake/cli/_plugins/streamlit/log_streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,13 +153,13 @@ def _result_for_entry(entry, output_format: OutputFormat) -> CommandResult:
"""
Pick the CommandResult subclass best matched to the current output format.

- JSON / CSV: yield a dict via ObjectResult so the framework produces
- JSON / CSV / TOON: yield a dict via ObjectResult so the framework produces
valid JSONL / CSV rows that downstream tools (e.g. ``jq``) can parse.
- TABLE / plain: yield a pre-formatted line via MessageResult so each
log entry renders as a single human-readable row instead of a
two-column key/value table.
"""
if output_format == OutputFormat.CSV or output_format.is_json:
if output_format.is_structured:
return ObjectResult(entry.to_dict())
return MessageResult(entry.format_line())

Expand Down
5 changes: 1 addition & 4 deletions src/snowflake/cli/api/cli_global_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,10 +293,7 @@ def silent(self) -> bool:
@property
def _should_force_mute_intermediate_output(self) -> bool:
"""Computes whether cli_console output should be muted."""
return (
self._manager.output_format.is_json
or self._manager.output_format == OutputFormat.CSV
)
return self._manager.output_format.is_structured

@property
def enhanced_exit_codes(self) -> bool:
Expand Down
5 changes: 5 additions & 0 deletions src/snowflake/cli/api/output/formats.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ class OutputFormat(Enum):
JSON = "JSON"
JSON_EXT = "JSON_EXT"
CSV = "CSV"
TOON = "TOON"

@property
def is_json(self) -> bool:
return self in (OutputFormat.JSON, OutputFormat.JSON_EXT)

@property
def is_structured(self) -> bool:
return self.is_json or self in (OutputFormat.CSV, OutputFormat.TOON)
2 changes: 1 addition & 1 deletion tests/__snapshots__/test_docs_generation_output.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@
Keep the session active indefinitely, even if there is no activity from the user.

</dd>
<dt>`--format [TABLE|JSON|JSON_EXT|CSV]`</dt>
<dt>`--format [TABLE|JSON|JSON_EXT|CSV|TOON]`</dt>
<dd>

Specifies the output format. Default: TABLE.
Expand Down
Loading