diff --git a/src/together/lib/cli/api/beta/clusters/storage/retrieve.py b/src/together/lib/cli/api/beta/clusters/storage/retrieve.py index 51379ea5..5349636c 100644 --- a/src/together/lib/cli/api/beta/clusters/storage/retrieve.py +++ b/src/together/lib/cli/api/beta/clusters/storage/retrieve.py @@ -1,7 +1,5 @@ from __future__ import annotations -from rich import print_json - from together._utils._json import openapi_dumps from together.lib.cli.utils.config import CLIConfigParameter from together.lib.cli.utils._console import console @@ -18,7 +16,7 @@ async def retrieve( request = config.client.beta.clusters.storage.retrieve(volume_id) if config.json: - print_json(openapi_dumps(await request).decode("utf-8")) + console.print_json(openapi_dumps(await request).decode("utf-8")) return storage = await show_loading_status("Retrieving storage volume...", request) diff --git a/src/together/lib/cli/api/beta/jig/jig.py b/src/together/lib/cli/api/beta/jig/jig.py index 26a91d13..a07a90ac 100644 --- a/src/together/lib/cli/api/beta/jig/jig.py +++ b/src/together/lib/cli/api/beta/jig/jig.py @@ -713,7 +713,7 @@ def deploy( raise if detach or no_track: - console.print_json(json.dumps(response.model_dump())) + console.print_json(openapi_dumps(response).decode("utf-8")) return self.track(response) @@ -931,9 +931,13 @@ def _print_cli_result(result: Any) -> None: return if isinstance(result, str): console.print(result) - elif hasattr(result, "json") and callable(result.json): - console.print_json(json.dumps(result.json())) - else: + return + if hasattr(result, "json") and callable(result.json): + console.print_json(openapi_dumps(result.json()).decode("utf-8")) + return + try: + console.print_json(openapi_dumps(result).decode("utf-8")) + except TypeError: console.print(str(result)) @@ -1222,7 +1226,7 @@ async def jig_volumes_list( data, next_cursor = mock_pagination(list_resp.data or [], cursor_field="id", cursor=after) if config.json: - console.print_json(openapi_dumps(list_resp).decode()) + console.print_json(openapi_dumps(list_resp).decode("utf-8")) return EMPTY_MESSAGE = "You don't have any volumes yet. To create your first volume run:\n [dim]-[/dim] [primary]tg beta jig volumes create[/primary]" @@ -1253,7 +1257,14 @@ async def jig_volumes_describe( except NotFoundError: _jig_fail(f"Volume {name} not found") else: - console.print_json(openapi_dumps(vol).decode()) + if config.json: + console.print_json(openapi_dumps(vol).decode("utf-8")) + else: + console.print(f"[bold dim]ID[/bold dim] [blue]{vol.id or '—'}[/blue]") + console.print(f"[bold dim]Name[/bold dim] [blue]{vol.name or '—'}[/blue]") + console.print(f"[bold dim]Created[/bold dim] [blue]{vol.created_at or '—'}[/blue]") + console.print(f"[bold dim]Updated[/bold dim] [blue]{vol.updated_at or '—'}[/blue]") + console.print(f"[bold dim]Version[/bold dim] [blue]{vol.current_version!s}[/blue]") def dockerfile_cli( diff --git a/src/together/lib/cli/api/endpoints/retrieve.py b/src/together/lib/cli/api/endpoints/retrieve.py index 5228107f..35891984 100644 --- a/src/together/lib/cli/api/endpoints/retrieve.py +++ b/src/together/lib/cli/api/endpoints/retrieve.py @@ -1,9 +1,8 @@ from __future__ import annotations -from rich import print_json - from together._utils._json import openapi_dumps from together.lib.cli.utils.config import CLIConfigParameter +from together.lib.cli.utils._console import console from together.lib.cli.components.loader import show_loading_status from together.lib.cli.api.endpoints._utils import print_endpoint, handle_endpoint_api_errors @@ -18,7 +17,7 @@ async def retrieve( endpoint = await show_loading_status("Loading endpoint...", config.client.endpoints.retrieve(endpoint_id)) if config.json: - print_json(openapi_dumps(endpoint).decode("utf-8")) + console.print_json(openapi_dumps(endpoint).decode("utf-8")) return print_endpoint(endpoint) diff --git a/src/together/lib/cli/api/evals/retrieve.py b/src/together/lib/cli/api/evals/retrieve.py index 9b035268..34d947a3 100644 --- a/src/together/lib/cli/api/evals/retrieve.py +++ b/src/together/lib/cli/api/evals/retrieve.py @@ -3,6 +3,7 @@ from typing import Annotated from cyclopts import Parameter +from rich.markup import escape as escape_rich_markup from together._utils._json import openapi_dumps from together.lib.cli.utils.config import CLIConfigParameter @@ -19,6 +20,11 @@ async def retrieve( response = await show_loading_status("Retrieving eval...", config.client.evals.retrieve(evaluation_id)) if config.json: console.print_json(openapi_dumps(response).decode("utf-8")) - else: - # TODO: Add a pretty print for this - console.print_json(openapi_dumps(response).decode("utf-8")) + return + + wid = response.workflow_id or evaluation_id + console.print( + f"[dim]Eval[/dim] [bold]{escape_rich_markup(str(wid))}[/bold] — " + f"[dim]status[/dim] [bold]{escape_rich_markup(str(response.status))}[/bold] — " + f"[dim]type[/dim] [bold]{escape_rich_markup(str(response.type))}[/bold]" + ) diff --git a/src/together/lib/cli/api/files/retrieve_content.py b/src/together/lib/cli/api/files/retrieve_content.py index eb5b6ea1..ff52d538 100644 --- a/src/together/lib/cli/api/files/retrieve_content.py +++ b/src/together/lib/cli/api/files/retrieve_content.py @@ -2,12 +2,14 @@ import os import sys +import base64 from typing import Optional, Annotated from pathlib import Path from cyclopts import Parameter, validators from together import AsyncTogether +from together._utils._json import openapi_dumps from together.lib.cli.utils.config import CLIConfigParameter from together.lib.cli.utils._console import console from together.lib.cli.components.loader import show_loading_status @@ -35,11 +37,23 @@ async def retrieve_content( console.print(f"[red]Invalid usage: Either --output or --stdout must be specified[/red]") sys.exit(1) + if stdout is True and output is not None: + console.print(f"[red]Invalid usage: --stdout and --output cannot be used together[/red]") + sys.exit(1) + response = await show_loading_status("Retrieving file contents...", config.client.files.content(id=id)) if stdout: - bytes = await response.read() - console.print(bytes.decode("utf-8")) + raw = await response.read() + if config.json: + try: + payload = {"id": id, "content": raw.decode("utf-8")} + except UnicodeDecodeError: + payload = {"id": id, "content_base64": base64.b64encode(raw).decode("ascii")} + console.print_json(openapi_dumps(payload).decode("utf-8")) + else: + console.print(raw.decode("utf-8")) + return if output is not None: os.makedirs(os.path.dirname(output) or ".", exist_ok=True) @@ -49,4 +63,7 @@ async def retrieve_content( response = await config.client.files.content(id=id) await response.write_to_file(out_path) - console.print(f"File saved to [blue]{out_path}[/blue]") + if config.json: + console.print_json(openapi_dumps({"id": id, "path": str(out_path)}).decode("utf-8")) + else: + console.print(f"File saved to [blue]{out_path}[/blue]") diff --git a/src/together/lib/cli/api/files/upload.py b/src/together/lib/cli/api/files/upload.py index 13665053..85615054 100644 --- a/src/together/lib/cli/api/files/upload.py +++ b/src/together/lib/cli/api/files/upload.py @@ -2,7 +2,6 @@ import os import sys -import json as json_lib from typing import Optional, Annotated, cast, get_args from pathlib import Path @@ -33,7 +32,7 @@ async def upload( report = check_file(file) if report["is_check_passed"] is False: if config.json: - console.print_json(json_lib.dumps(report)) + console.print_json(openapi_dumps(report).decode("utf-8")) else: console.print(f"[red]❌ {escape_rich_markup(str(report['message']))}[/red]") diff --git a/src/together/lib/cli/api/fine_tuning/cancel.py b/src/together/lib/cli/api/fine_tuning/cancel.py index 67a72d1a..9f299edf 100644 --- a/src/together/lib/cli/api/fine_tuning/cancel.py +++ b/src/together/lib/cli/api/fine_tuning/cancel.py @@ -37,7 +37,7 @@ async def cancel( confirm_response = input(f"Do you want to cancel job {fine_tune_id}? [y/N]") if "y" not in confirm_response.lower(): if config.json: - console.print_json('{"status": "Cancel not submitted"}') + console.print_json(openapi_dumps({"status": "Cancel not submitted"}).decode("utf-8")) else: console.print("Cancel not submitted") return diff --git a/src/together/lib/cli/api/fine_tuning/list.py b/src/together/lib/cli/api/fine_tuning/list.py index 107422a4..ed747f64 100644 --- a/src/together/lib/cli/api/fine_tuning/list.py +++ b/src/together/lib/cli/api/fine_tuning/list.py @@ -2,8 +2,6 @@ from datetime import datetime, timezone -from rich import print_json - from together.lib.utils import finetune_price_to_dollars from together._utils._json import openapi_dumps from together.lib.utils.tools import format_datetime @@ -46,7 +44,7 @@ async def list( fine_tunings_to_display, next_cursor = mock_pagination(response.data, cursor_field="id", cursor=after) if config.json: - print_json(openapi_dumps(fine_tunings_to_display).decode("utf-8")) + console.print_json(openapi_dumps(fine_tunings_to_display).decode("utf-8")) return EMPTY_MESSAGE = "You don't have any finetuned models yet. To fine tune your first model run:\n [dim]-[/dim] [primary]tg ft create[/primary]" diff --git a/src/together/lib/cli/api/telemetry/disable.py b/src/together/lib/cli/api/telemetry/disable.py index 837905d5..c094e040 100644 --- a/src/together/lib/cli/api/telemetry/disable.py +++ b/src/together/lib/cli/api/telemetry/disable.py @@ -1,16 +1,25 @@ from __future__ import annotations +from together._utils._json import openapi_dumps from together.lib.cli._track_cli import ( load_telemetry_config, save_telemetry_config, telemetry_config_path, ) +from together.lib.cli.utils.config import CLIConfigParameter from together.lib.cli.utils._console import console -def disable() -> None: +def disable( + *, + config: CLIConfigParameter, +) -> None: """Explicitly Disable telemetry""" cfg = load_telemetry_config() cfg["telemetry_enabled"] = False save_telemetry_config(cfg) - console.print(f"Telemetry: [blue]Disabled[/blue]\n[dim](saved to {telemetry_config_path()})[/dim]") + path = telemetry_config_path() + if config.json: + console.print_json(openapi_dumps({"telemetry_enabled": False, "saved_to": str(path)}).decode("utf-8")) + return + console.print(f"Telemetry: [blue]Disabled[/blue]\n[dim](saved to {path})[/dim]") diff --git a/src/together/lib/cli/api/telemetry/enable.py b/src/together/lib/cli/api/telemetry/enable.py index c1e4fc1c..ef1e38d3 100644 --- a/src/together/lib/cli/api/telemetry/enable.py +++ b/src/together/lib/cli/api/telemetry/enable.py @@ -1,16 +1,25 @@ from __future__ import annotations +from together._utils._json import openapi_dumps from together.lib.cli._track_cli import ( load_telemetry_config, save_telemetry_config, telemetry_config_path, ) +from together.lib.cli.utils.config import CLIConfigParameter from together.lib.cli.utils._console import console -def enable() -> None: +def enable( + *, + config: CLIConfigParameter, +) -> None: """Enable telemetry""" cfg = load_telemetry_config() cfg["telemetry_enabled"] = True save_telemetry_config(cfg) - console.print(f"Telemetry: [blue]Enabled[/blue]\n[dim](saved to {telemetry_config_path()})[/dim]") + path = telemetry_config_path() + if config.json: + console.print_json(openapi_dumps({"telemetry_enabled": True, "saved_to": str(path)}).decode("utf-8")) + return + console.print(f"Telemetry: [blue]Enabled[/blue]\n[dim](saved to {path})[/dim]") diff --git a/src/together/lib/cli/api/telemetry/status.py b/src/together/lib/cli/api/telemetry/status.py index 4cc1b3bd..8fbfd743 100644 --- a/src/together/lib/cli/api/telemetry/status.py +++ b/src/together/lib/cli/api/telemetry/status.py @@ -1,16 +1,36 @@ +from __future__ import annotations + +from together._utils._json import openapi_dumps from together.lib.cli._track_cli import ( _env_telemetry_disabled, _config_telemetry_disabled, ) +from together.lib.cli.utils.config import CLIConfigParameter from together.lib.cli.utils._console import console -def status() -> None: +def status( + *, + config: CLIConfigParameter, +) -> None: """Check to see if telemetry is enabled or disabled.""" - if _config_telemetry_disabled(): + by_config = _config_telemetry_disabled() + by_env = _env_telemetry_disabled() + + if config.json: + if by_config: + payload = {"telemetry": "disabled", "reason": "config_file"} + elif by_env: + payload = {"telemetry": "disabled", "reason": "environment"} + else: + payload = {"telemetry": "enabled"} + console.print_json(openapi_dumps(payload).decode("utf-8")) + return + + if by_config: console.print("Telemetry: [blue]Disabled[/blue]") return - if _env_telemetry_disabled(): + if by_env: console.print("Telemetry: [blue]Disabled[/blue] [dim](via environment variable)[/dim]") return console.print("Telemetry: [blue]Enabled[/blue]") diff --git a/tests/cli/test_analytics.py b/tests/cli/test_analytics.py index 32c194a9..2c85672d 100644 --- a/tests/cli/test_analytics.py +++ b/tests/cli/test_analytics.py @@ -3,6 +3,7 @@ from __future__ import annotations import json +import subprocess from pathlib import Path import pytest @@ -46,6 +47,13 @@ def test_telemetry_enable_then_status(isolated_cli_config: Path, cli_runner: Cli assert "Enabled" in r2.output +def test_telemetry_json_mode_pipes_to_jq(cli_runner: CliRunner) -> None: + r = cli_runner.invoke(["telemetry", "status", "--json"]) + assert r.exit_code == 0 + jq = subprocess.run(["jq"], input=r.out_out, capture_output=True, text=True) + assert jq.returncode == 0, jq.stderr + + def test_telemetry_status_shows_env_opt_out( isolated_cli_config: Path, monkeypatch: pytest.MonkeyPatch, diff --git a/tests/cli/test_evals.py b/tests/cli/test_evals.py index af7d8c10..319a95a6 100644 --- a/tests/cli/test_evals.py +++ b/tests/cli/test_evals.py @@ -48,9 +48,10 @@ class TestEvalsRetrieveAndStatus: @pytest.mark.respx(base_url=base_url) def test_retrieve(self, respx_mock: MockRouter, cli_runner: CliRunner) -> None: respx_mock.get("/evaluation/eval-wf-1").mock(return_value=httpx.Response(200, json=_EVAL_JOB)) - result = cli_runner.invoke(["evals", "retrieve", "eval-wf-1"]) + result = cli_runner.invoke(["evals", "retrieve", "eval-wf-1", "--json"]) assert result.exit_code == 0 - assert json.loads(result.output)["workflow_id"] == "eval-wf-1" + payload = json.loads(result.out_out.lstrip("\n")) + assert payload["workflow_id"] == "eval-wf-1" @pytest.mark.respx(base_url=base_url) def test_status(self, respx_mock: MockRouter, cli_runner: CliRunner) -> None: diff --git a/tests/cli/test_files.py b/tests/cli/test_files.py index bfa7356a..dd823d2b 100644 --- a/tests/cli/test_files.py +++ b/tests/cli/test_files.py @@ -147,20 +147,15 @@ def test_specifying_stdout(self, respx_mock: MockRouter, cli_runner: CliRunner) respx_mock.get("/files/file-1/content").mock(return_value=httpx.Response(200, content=b"stdout-bytes")) result = cli_runner.invoke(["files", "retrieve-content", "file-1", "--stdout"]) assert result.exit_code == 0 - assert result.output == "stdout-bytes\n" + # Rich loading status may leave a leading newline before body output + assert result.output.lstrip("\n") == "stdout-bytes\n" - @pytest.mark.respx(base_url=base_url) - def test_specifying_both_output_and_stdout( - self, respx_mock: MockRouter, tmp_path: Path, cli_runner: CliRunner - ) -> None: - respx_mock.get("/files/file-1/content").mock(return_value=httpx.Response(200, content=b"to-stdout")) + def test_specifying_both_output_and_stdout(self, tmp_path: Path, cli_runner: CliRunner) -> None: out = tmp_path / "should-not-exist.bin" result = cli_runner.invoke(["files", "retrieve-content", "file-1", "--stdout", "--output", str(out)]) - assert result.exit_code == 0 - assert result.output.startswith("to-stdout\n") - assert "File saved to" in result.output - assert str(out) in result.output.replace("\n", "") - assert out.exists() + assert result.exit_code == 1 + assert "cannot be used together" in result.output + assert not out.exists() class TestFilesUpload: