Skip to content

Commit 5d3737a

Browse files
committed
🚸 Various changes to improve usability.
Fixed only partial information when using , added more sub-commands to .
1 parent 82f7759 commit 5d3737a

File tree

4 files changed

+146
-19
lines changed

4 files changed

+146
-19
lines changed

CHANGELOG.md

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
1+
# Unreleased
2+
3+
- Various fixes to columns in `list` sub-commands, this includes fixing `N/A` where data could not be found.
4+
- Added `pull` command to `run` for retrieving artifacts.
5+
- `--plain` mode now displays all data as expected, with formatting disabled.
6+
- Added additional sub-commands for `Tag` type.
7+
18
# [v1.0.1](https://github.com/simvue-io/simvue-cli/releases/tag/v1.0.1) - 2025-03-31
2-
* Fixed typing bug for Generator.
9+
10+
- Fixed typing bug for Generator.
311

412
# [v1.0.0](https://github.com/simvue-io/simvue-cli/releases/tag/v1.0.0) - 2025-03-31
5-
* Added list commands for viewing objects including runs, folders, tags etc.
6-
* Added user and tenant creation.
7-
* Added virtual environment initialisation from runs.
8-
* Added JSON dumping of objects.
9-
* Added command for checking server availability.
10-
* Added ability to attach metrics to an existing run.
11-
* Added creation of new runs from CLI.
13+
14+
- Added list commands for viewing objects including runs, folders, tags etc.
15+
- Added user and tenant creation.
16+
- Added virtual environment initialisation from runs.
17+
- Added JSON dumping of objects.
18+
- Added command for checking server availability.
19+
- Added ability to attach metrics to an existing run.
20+
- Added creation of new runs from CLI.

src/simvue_cli/actions.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,20 @@ def create_simvue_s3_storage(
405405
return _storage
406406

407407

408+
def create_simvue_tag(
409+
name: str,
410+
color: str | None,
411+
description: str | None,
412+
) -> Tag:
413+
_tag = Tag.new(name=name)
414+
if color:
415+
_tag.colour = color
416+
if description:
417+
_tag.description = description
418+
_tag.commit()
419+
return _tag
420+
421+
408422
def create_user_alert(
409423
name: str, trigger_abort: bool, email_notify: bool, description: str | None
410424
) -> Alert:
@@ -585,6 +599,11 @@ def get_user(user_id: str) -> User:
585599
return User(identifier=user_id)
586600

587601

602+
def get_artifact(artifact_id: str) -> Tag:
603+
"""Retrieve an artifact from the server"""
604+
return Artifact(identifier=artifact_id)
605+
606+
588607
def delete_tenant(tenant_id: str) -> None:
589608
"""Delete a given tenant from the Simvue server"""
590609
_tenant = get_tenant(tenant_id)

src/simvue_cli/cli/__init__.py

Lines changed: 104 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
import tabulate
2626
import requests
2727
import simvue as simvue_client
28-
from simvue.api.objects import Alert, Run, Folder, S3Storage, Tag, Storage
28+
from simvue.api.objects import Alert, Run, Folder, S3Storage, Tag, Storage, Artifact
2929
from simvue.api.objects.administrator import User, Tenant
3030
from simvue.exception import ObjectNotFoundError
3131

@@ -910,10 +910,27 @@ def get_folder_json(folder_id: str) -> None:
910910
@simvue.group("tag")
911911
@click.pass_context
912912
def simvue_tag(ctx) -> None:
913-
"""Create or retrieve Simvue runs"""
913+
"""Create or retrieve Simvue tags"""
914914
pass
915915

916916

917+
@simvue_tag.command("create")
918+
@click.pass_context
919+
@click.argument("name", type=SimvueName)
920+
@click.option(
921+
"--color",
922+
type=str,
923+
default=None,
924+
help="Color for this tag, e.g. '#fffff', 'blue', 'rgb(23, 54, 34)'",
925+
)
926+
@click.option("--description", type=str, default=None, help="Description for this tag.")
927+
def create_tag(ctx, **kwargs) -> None:
928+
"""Create a tag"""
929+
result = simvue_cli.actions.create_simvue_tag(**kwargs)
930+
alert_id = result.id
931+
click.echo(alert_id if ctx.obj["plain"] else click.style(alert_id))
932+
933+
917934
@simvue_tag.command("list")
918935
@click.pass_context
919936
@click.option(
@@ -961,6 +978,7 @@ def tag_list(
961978
color: bool,
962979
**kwargs,
963980
) -> None:
981+
"""Retrieve tags list from Simvue server."""
964982
tags = simvue_cli.actions.get_tag_list(**kwargs)
965983
if not tags:
966984
return
@@ -1007,6 +1025,60 @@ def get_tag_json(tag_id: str) -> None:
10071025
click.echo(error_msg, fg="red", bold=True)
10081026

10091027

1028+
@simvue_tag.command("remove")
1029+
@click.pass_context
1030+
@click.argument("tag_ids", type=str, nargs=-1, required=False)
1031+
@click.option(
1032+
"-i",
1033+
"--interactive",
1034+
help="Prompt for confirmation on removal",
1035+
type=bool,
1036+
default=False,
1037+
is_flag=True,
1038+
)
1039+
def delete_tag(ctx, tag_ids: list[str] | None, interactive: bool) -> None:
1040+
"""Remove a tag from the Simvue server"""
1041+
if not tag_ids:
1042+
tag_ids = []
1043+
for line in sys.stdin:
1044+
if not line.strip():
1045+
continue
1046+
tag_ids += [k.strip() for k in line.split(" ")]
1047+
1048+
for tag_id in tag_ids:
1049+
try:
1050+
simvue_cli.actions.get_tag(tag_id)
1051+
except (ObjectNotFoundError, RuntimeError):
1052+
error_msg = f"Tag '{tag_id}' not found"
1053+
if ctx.obj["plain"]:
1054+
print(error_msg)
1055+
else:
1056+
click.secho(error_msg, fg="red", bold=True)
1057+
sys.exit(1)
1058+
1059+
if interactive:
1060+
remove = click.confirm(f"Remove tag '{tag_id}'?")
1061+
if not remove:
1062+
continue
1063+
1064+
try:
1065+
simvue_cli.actions.delete_tag(tag_id)
1066+
except ValueError as e:
1067+
click.echo(
1068+
e.args[0]
1069+
if ctx.obj["plain"]
1070+
else click.style(e.args[0], fg="red", bold=True)
1071+
)
1072+
sys.exit(1)
1073+
1074+
response_message = f"Tag '{tag_id}' removed successfully."
1075+
1076+
if ctx.obj["plain"]:
1077+
print(response_message)
1078+
else:
1079+
click.secho(response_message, bold=True, fg="green")
1080+
1081+
10101082
@simvue.group("admin")
10111083
@click.pass_context
10121084
def admin(ctx) -> None:
@@ -1619,15 +1691,23 @@ def list_storages(
16191691
help="Specify target language",
16201692
type=click.Choice(["python", "rust", "julia", "nodejs"]),
16211693
)
1622-
@click.option("--run", required=True, help="ID of run to clone environment from")
1694+
@click.option(
1695+
"--run", required=False, help="ID of run to clone environment from", default=""
1696+
)
16231697
@click.option(
16241698
"--allow-existing",
16251699
is_flag=True,
16261700
help="Install dependencies in an existing environment",
16271701
)
16281702
@click.argument("venv_directory", type=click.Path(exists=False))
16291703
def venv_setup(ctx, **kwargs) -> None:
1630-
"""Initialise virtual environments from run metadata."""
1704+
"""Initialise virtual environments from run metadata.
1705+
1706+
If a run ID is not provided via --run it is read from stdin.
1707+
"""
1708+
if not kwargs.get("run"):
1709+
kwargs["run"] = input()
1710+
16311711
try:
16321712
simvue_cli.actions.create_environment(**kwargs)
16331713
except (FileExistsError, RuntimeError) as e:
@@ -1746,5 +1826,25 @@ def artifact_list(
17461826
click.echo(table)
17471827

17481828

1829+
@simvue_artifact.command("json")
1830+
@click.pass_context
1831+
@click.argument("artifact_id", required=False)
1832+
def get_artifact_json(ctx, artifact_id: str) -> None:
1833+
"""Retrieve artifact information from Simvue server
1834+
1835+
If no ARTIFACT_ID is provided the input is read from stdin
1836+
"""
1837+
if not artifact_id:
1838+
artifact_id = input()
1839+
1840+
try:
1841+
artifact: Artifact = simvue_cli.actions.get_artifact(artifact_id)
1842+
artifact_info = artifact.to_dict()
1843+
click.echo(json.dumps(dict(artifact_info.items()), indent=2))
1844+
except ObjectNotFoundError as e:
1845+
error_msg = f"Failed to retrieve artifact '{artifact_id}': {e.args[0]}"
1846+
click.echo(error_msg, fg="red", bold=True)
1847+
1848+
17491849
if __name__ in "__main__":
17501850
simvue()

src/simvue_cli/cli/display.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ def format_tags(
107107
out_config["tags"] = out_config.get("tags") or {}
108108

109109
if plain_text:
110-
return ", ".join(tags)
110+
return f"[{', '.join(tags)}]" if tags else ""
111111

112112
tag_out: list[str] = []
113113

@@ -129,7 +129,7 @@ def format_tags(
129129
out_config["tags"][tag] = color
130130
tag_out.append(click.style(tag, fg=CLICK_COLORS[color], bg=color, bold=True))
131131

132-
return ", ".join(tag_out)
132+
return " ".join(tag_out)
133133

134134

135135
# Allocate functions to format each column type
@@ -174,12 +174,11 @@ def create_objects_display(
174174
either a click formatted string or original text
175175
"""
176176

177-
if plain_text:
178-
return " ".join(obj.id for _, obj in objects)
179-
180177
# Remove 'is_' prefix from relevant columns and format
181178
table_headers = [
182-
click.style(c.replace("is_", ""), bold=True)
179+
c.replace("is_", "")
180+
if plain_text
181+
else click.style(c.replace("is_", ""), bold=True)
183182
for c in (("#", *columns) if enumerate_ else columns)
184183
]
185184

@@ -206,7 +205,7 @@ def create_objects_display(
206205
row.append(str(value))
207206
contents.append(row)
208207

209-
if not format:
208+
if not format or plain_text:
210209
objs_list: list[str] = ["\t".join(c) for c in contents]
211210
return "\n".join(objs_list)
212211

0 commit comments

Comments
 (0)