Skip to content
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
4 changes: 1 addition & 3 deletions src/together/lib/cli/api/beta/clusters/storage/retrieve.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
Expand Down
23 changes: 17 additions & 6 deletions src/together/lib/cli/api/beta/jig/jig.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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))


Expand Down Expand Up @@ -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]"
Expand Down Expand Up @@ -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(
Expand Down
5 changes: 2 additions & 3 deletions src/together/lib/cli/api/endpoints/retrieve.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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)
12 changes: 9 additions & 3 deletions src/together/lib/cli/api/evals/retrieve.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]"
)
23 changes: 20 additions & 3 deletions src/together/lib/cli/api/files/retrieve_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -35,11 +37,23 @@ async def retrieve_content(
console.print(f"[red]Invalid usage: Either --output <directory> 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)
Expand All @@ -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]")
3 changes: 1 addition & 2 deletions src/together/lib/cli/api/files/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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]")

Expand Down
2 changes: 1 addition & 1 deletion src/together/lib/cli/api/fine_tuning/cancel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 1 addition & 3 deletions src/together/lib/cli/api/fine_tuning/list.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]"
Expand Down
13 changes: 11 additions & 2 deletions src/together/lib/cli/api/telemetry/disable.py
Original file line number Diff line number Diff line change
@@ -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]")
13 changes: 11 additions & 2 deletions src/together/lib/cli/api/telemetry/enable.py
Original file line number Diff line number Diff line change
@@ -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]")
26 changes: 23 additions & 3 deletions src/together/lib/cli/api/telemetry/status.py
Original file line number Diff line number Diff line change
@@ -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]")
8 changes: 8 additions & 0 deletions tests/cli/test_analytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

import json
import subprocess
from pathlib import Path

import pytest
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 3 additions & 2 deletions tests/cli/test_evals.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
17 changes: 6 additions & 11 deletions tests/cli/test_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading