hypercli-cli 2026.3.22__tar.gz → 2026.3.25__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hypercli_cli-2026.3.22 → hypercli_cli-2026.3.25}/PKG-INFO +4 -4
- {hypercli_cli-2026.3.22 → hypercli_cli-2026.3.25}/hypercli_cli/agents.py +57 -1
- {hypercli_cli-2026.3.22 → hypercli_cli-2026.3.25}/hypercli_cli/cli.py +2 -1
- hypercli_cli-2026.3.25/hypercli_cli/files.py +111 -0
- {hypercli_cli-2026.3.22 → hypercli_cli-2026.3.25}/hypercli_cli/jobs.py +18 -2
- {hypercli_cli-2026.3.22 → hypercli_cli-2026.3.25}/pyproject.toml +4 -4
- {hypercli_cli-2026.3.22 → hypercli_cli-2026.3.25}/tests/test_exec_shell_dryrun.py +17 -0
- hypercli_cli-2026.3.25/tests/test_jobs_list_tags.py +70 -0
- hypercli_cli-2026.3.22/tests/test_jobs_list_tags.py +0 -39
- {hypercli_cli-2026.3.22 → hypercli_cli-2026.3.25}/.gitignore +0 -0
- {hypercli_cli-2026.3.22 → hypercli_cli-2026.3.25}/README.md +0 -0
- {hypercli_cli-2026.3.22 → hypercli_cli-2026.3.25}/hypercli_cli/__init__.py +0 -0
- {hypercli_cli-2026.3.22 → hypercli_cli-2026.3.25}/hypercli_cli/agent.py +0 -0
- {hypercli_cli-2026.3.22 → hypercli_cli-2026.3.25}/hypercli_cli/billing.py +0 -0
- {hypercli_cli-2026.3.22 → hypercli_cli-2026.3.25}/hypercli_cli/comfyui.py +0 -0
- {hypercli_cli-2026.3.22 → hypercli_cli-2026.3.25}/hypercli_cli/embed.py +0 -0
- {hypercli_cli-2026.3.22 → hypercli_cli-2026.3.25}/hypercli_cli/flow.py +0 -0
- {hypercli_cli-2026.3.22 → hypercli_cli-2026.3.25}/hypercli_cli/instances.py +0 -0
- {hypercli_cli-2026.3.22 → hypercli_cli-2026.3.25}/hypercli_cli/keys.py +0 -0
- {hypercli_cli-2026.3.22 → hypercli_cli-2026.3.25}/hypercli_cli/onboard.py +0 -0
- {hypercli_cli-2026.3.22 → hypercli_cli-2026.3.25}/hypercli_cli/output.py +0 -0
- {hypercli_cli-2026.3.22 → hypercli_cli-2026.3.25}/hypercli_cli/renders.py +0 -0
- {hypercli_cli-2026.3.22 → hypercli_cli-2026.3.25}/hypercli_cli/stt.py +0 -0
- {hypercli_cli-2026.3.22 → hypercli_cli-2026.3.25}/hypercli_cli/tui/__init__.py +0 -0
- {hypercli_cli-2026.3.22 → hypercli_cli-2026.3.25}/hypercli_cli/tui/job_monitor.py +0 -0
- {hypercli_cli-2026.3.22 → hypercli_cli-2026.3.25}/hypercli_cli/user.py +0 -0
- {hypercli_cli-2026.3.22 → hypercli_cli-2026.3.25}/hypercli_cli/voice.py +0 -0
- {hypercli_cli-2026.3.22 → hypercli_cli-2026.3.25}/hypercli_cli/wallet.py +0 -0
- {hypercli_cli-2026.3.22 → hypercli_cli-2026.3.25}/tests/test_agent_env_resolution.py +0 -0
- {hypercli_cli-2026.3.22 → hypercli_cli-2026.3.25}/tests/test_openclaw_config.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hypercli-cli
|
|
3
|
-
Version: 2026.3.
|
|
3
|
+
Version: 2026.3.25
|
|
4
4
|
Summary: CLI for HyperCLI - GPU orchestration and LLM API
|
|
5
5
|
Project-URL: Homepage, https://hypercli.com
|
|
6
6
|
Project-URL: Documentation, https://docs.hypercli.com
|
|
@@ -9,7 +9,7 @@ Author-email: HyperCLI <support@hypercli.com>
|
|
|
9
9
|
License: MIT
|
|
10
10
|
Requires-Python: >=3.10
|
|
11
11
|
Requires-Dist: httpx>=0.27.0
|
|
12
|
-
Requires-Dist: hypercli-sdk>=2026.3.
|
|
12
|
+
Requires-Dist: hypercli-sdk>=2026.3.25
|
|
13
13
|
Requires-Dist: mutagen>=1.47.0
|
|
14
14
|
Requires-Dist: pyyaml>=6.0
|
|
15
15
|
Requires-Dist: rich>=14.2.0
|
|
@@ -19,11 +19,11 @@ Provides-Extra: all
|
|
|
19
19
|
Requires-Dist: argon2-cffi>=25.0.0; extra == 'all'
|
|
20
20
|
Requires-Dist: eth-account>=0.13.0; extra == 'all'
|
|
21
21
|
Requires-Dist: faster-whisper>=1.1.0; extra == 'all'
|
|
22
|
-
Requires-Dist: hypercli-sdk[comfyui]>=2026.3.
|
|
22
|
+
Requires-Dist: hypercli-sdk[comfyui]>=2026.3.25; extra == 'all'
|
|
23
23
|
Requires-Dist: web3>=7.0.0; extra == 'all'
|
|
24
24
|
Requires-Dist: x402[evm,httpx]>=2.0.0; extra == 'all'
|
|
25
25
|
Provides-Extra: comfyui
|
|
26
|
-
Requires-Dist: hypercli-sdk[comfyui]>=2026.3.
|
|
26
|
+
Requires-Dist: hypercli-sdk[comfyui]>=2026.3.25; extra == 'comfyui'
|
|
27
27
|
Provides-Extra: dev
|
|
28
28
|
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
29
29
|
Requires-Dist: ruff>=0.3.0; extra == 'dev'
|
|
@@ -653,7 +653,10 @@ def cp(
|
|
|
653
653
|
local_path = agents.cp_from(pod, src_path, dst_path)
|
|
654
654
|
console.print(f"[green]✓[/green] Copied [bold]{src_agent_id[:12]}:{src_path}[/bold] to [bold]{local_path}[/bold]")
|
|
655
655
|
except Exception as e:
|
|
656
|
-
|
|
656
|
+
message = str(e)
|
|
657
|
+
if message.startswith("Path is a directory:"):
|
|
658
|
+
message = f"{message} Copy expects a file path, not a directory."
|
|
659
|
+
console.print(f"[red]❌ Copy failed: {message}[/red]")
|
|
657
660
|
raise typer.Exit(1)
|
|
658
661
|
|
|
659
662
|
|
|
@@ -1000,6 +1003,59 @@ def gateway_cron(
|
|
|
1000
1003
|
_run_async(_run())
|
|
1001
1004
|
|
|
1002
1005
|
|
|
1006
|
+
@app.command("cron-add")
|
|
1007
|
+
def gateway_cron_add(
|
|
1008
|
+
agent_id: str = typer.Argument(None, help="Agent ID or name"),
|
|
1009
|
+
job_json: str = typer.Argument(..., help='Cron job JSON, e.g. \'{"name":"backup","schedule":"0 * * * *","command":"echo hi"}\''),
|
|
1010
|
+
):
|
|
1011
|
+
"""Add a cron job to an agent's gateway."""
|
|
1012
|
+
pod = _require_openclaw_agent(_get_pod_with_token(agent_id))
|
|
1013
|
+
try:
|
|
1014
|
+
job_data = json.loads(job_json)
|
|
1015
|
+
except json.JSONDecodeError as e:
|
|
1016
|
+
console.print(f"[red]Invalid JSON: {e}[/red]")
|
|
1017
|
+
raise typer.Exit(1)
|
|
1018
|
+
|
|
1019
|
+
async def _run():
|
|
1020
|
+
result = await pod.cron_add(job_data)
|
|
1021
|
+
console.print(f"[green]Cron job added[/green]")
|
|
1022
|
+
console.print_json(json.dumps(result, default=str))
|
|
1023
|
+
|
|
1024
|
+
_run_async(_run())
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
@app.command("cron-remove")
|
|
1028
|
+
def gateway_cron_remove(
|
|
1029
|
+
agent_id: str = typer.Argument(None, help="Agent ID or name"),
|
|
1030
|
+
job_id: str = typer.Argument(..., help="Cron job ID to remove"),
|
|
1031
|
+
):
|
|
1032
|
+
"""Remove a cron job from an agent's gateway."""
|
|
1033
|
+
pod = _require_openclaw_agent(_get_pod_with_token(agent_id))
|
|
1034
|
+
|
|
1035
|
+
async def _run():
|
|
1036
|
+
await pod.cron_remove(job_id)
|
|
1037
|
+
console.print(f"[green]Cron job {job_id} removed[/green]")
|
|
1038
|
+
|
|
1039
|
+
_run_async(_run())
|
|
1040
|
+
|
|
1041
|
+
|
|
1042
|
+
@app.command("cron-run")
|
|
1043
|
+
def gateway_cron_run(
|
|
1044
|
+
agent_id: str = typer.Argument(None, help="Agent ID or name"),
|
|
1045
|
+
job_id: str = typer.Argument(..., help="Cron job ID to trigger"),
|
|
1046
|
+
):
|
|
1047
|
+
"""Manually trigger a cron job on an agent's gateway."""
|
|
1048
|
+
pod = _require_openclaw_agent(_get_pod_with_token(agent_id))
|
|
1049
|
+
|
|
1050
|
+
async def _run():
|
|
1051
|
+
result = await pod.cron_run(job_id)
|
|
1052
|
+
console.print(f"[green]Cron job {job_id} triggered[/green]")
|
|
1053
|
+
if result:
|
|
1054
|
+
console.print_json(json.dumps(result, default=str))
|
|
1055
|
+
|
|
1056
|
+
_run_async(_run())
|
|
1057
|
+
|
|
1058
|
+
|
|
1003
1059
|
@app.command("gateway-chat")
|
|
1004
1060
|
def gateway_chat(
|
|
1005
1061
|
agent_id: str = typer.Argument(None, help="Agent ID or name"),
|
|
@@ -8,7 +8,7 @@ from rich.prompt import Prompt
|
|
|
8
8
|
from hypercli import HyperCLI, APIError, configure
|
|
9
9
|
from hypercli.config import CONFIG_FILE
|
|
10
10
|
|
|
11
|
-
from . import agent, agents, billing, comfyui, flow, instances, jobs, keys, user, wallet
|
|
11
|
+
from . import agent, agents, billing, comfyui, files, flow, instances, jobs, keys, user, wallet
|
|
12
12
|
|
|
13
13
|
console = Console()
|
|
14
14
|
|
|
@@ -61,6 +61,7 @@ app.add_typer(agents.app, name="agents")
|
|
|
61
61
|
app.add_typer(agent.app, name="agent")
|
|
62
62
|
app.add_typer(billing.app, name="billing")
|
|
63
63
|
app.add_typer(comfyui.app, name="comfyui")
|
|
64
|
+
app.add_typer(files.app, name="files")
|
|
64
65
|
app.add_typer(flow.app, name="flow")
|
|
65
66
|
app.add_typer(instances.app, name="instances")
|
|
66
67
|
app.add_typer(keys.app, name="keys")
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""File management CLI commands"""
|
|
2
|
+
import os
|
|
3
|
+
import typer
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
|
|
7
|
+
app = typer.Typer(help="Upload, inspect, and delete files on the platform")
|
|
8
|
+
console = Console()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _get_client():
|
|
12
|
+
from hypercli import HyperCLI
|
|
13
|
+
return HyperCLI()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _fmt_size(size: int) -> str:
|
|
17
|
+
"""Format file size for display."""
|
|
18
|
+
if size < 1024:
|
|
19
|
+
return f"{size} B"
|
|
20
|
+
if size < 1024 * 1024:
|
|
21
|
+
return f"{size / 1024:.1f} KB"
|
|
22
|
+
return f"{size / (1024 * 1024):.1f} MB"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@app.command("upload")
|
|
26
|
+
def upload_file(
|
|
27
|
+
file_path: str = typer.Argument(..., help="Path to local file to upload"),
|
|
28
|
+
wait: bool = typer.Option(True, "--wait/--no-wait", help="Wait for upload to complete"),
|
|
29
|
+
):
|
|
30
|
+
"""Upload a file for use in flows and renders."""
|
|
31
|
+
if not os.path.isfile(file_path):
|
|
32
|
+
console.print(f"[red]File not found: {file_path}[/red]")
|
|
33
|
+
raise typer.Exit(1)
|
|
34
|
+
|
|
35
|
+
client = _get_client()
|
|
36
|
+
console.print(f"Uploading [cyan]{os.path.basename(file_path)}[/cyan] ({_fmt_size(os.path.getsize(file_path))})...")
|
|
37
|
+
f = client.files.upload(file_path)
|
|
38
|
+
console.print(f"[green]Uploaded[/green] ID: [bold]{f.id}[/bold]")
|
|
39
|
+
|
|
40
|
+
if wait and f.state == "processing":
|
|
41
|
+
console.print("Waiting for processing...", end="")
|
|
42
|
+
f = client.files.wait_ready(f.id)
|
|
43
|
+
console.print(f" [green]done[/green]")
|
|
44
|
+
|
|
45
|
+
console.print(f" Filename: {f.filename}")
|
|
46
|
+
console.print(f" Type: {f.content_type}")
|
|
47
|
+
console.print(f" Size: {_fmt_size(f.file_size)}")
|
|
48
|
+
console.print(f" State: {f.state or 'ready'}")
|
|
49
|
+
console.print(f" URL: [dim]{f.url}[/dim]")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@app.command("upload-url")
|
|
53
|
+
def upload_url(
|
|
54
|
+
url: str = typer.Argument(..., help="URL to download and upload"),
|
|
55
|
+
wait: bool = typer.Option(True, "--wait/--no-wait", help="Wait for upload to complete"),
|
|
56
|
+
):
|
|
57
|
+
"""Upload a file from a URL."""
|
|
58
|
+
client = _get_client()
|
|
59
|
+
console.print(f"Uploading from URL...")
|
|
60
|
+
f = client.files.upload_url(url)
|
|
61
|
+
console.print(f"[green]Queued[/green] ID: [bold]{f.id}[/bold]")
|
|
62
|
+
|
|
63
|
+
if wait and f.state == "processing":
|
|
64
|
+
console.print("Waiting for processing...", end="")
|
|
65
|
+
f = client.files.wait_ready(f.id)
|
|
66
|
+
console.print(f" [green]done[/green]")
|
|
67
|
+
|
|
68
|
+
console.print(f" Filename: {f.filename}")
|
|
69
|
+
console.print(f" State: {f.state or 'ready'}")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@app.command("get")
|
|
73
|
+
def get_file(
|
|
74
|
+
file_id: str = typer.Argument(..., help="File ID"),
|
|
75
|
+
):
|
|
76
|
+
"""Get file info and status."""
|
|
77
|
+
client = _get_client()
|
|
78
|
+
f = client.files.get(file_id)
|
|
79
|
+
|
|
80
|
+
table = Table(title=f"File {f.id[:12]}...")
|
|
81
|
+
table.add_column("Field", style="dim")
|
|
82
|
+
table.add_column("Value")
|
|
83
|
+
|
|
84
|
+
table.add_row("ID", f.id)
|
|
85
|
+
table.add_row("Filename", f.filename)
|
|
86
|
+
table.add_row("Type", f.content_type)
|
|
87
|
+
table.add_row("Size", _fmt_size(f.file_size))
|
|
88
|
+
table.add_row("State", f.state or "ready")
|
|
89
|
+
table.add_row("URL", f.url)
|
|
90
|
+
if f.error:
|
|
91
|
+
table.add_row("Error", f"[red]{f.error}[/red]")
|
|
92
|
+
if f.created_at:
|
|
93
|
+
table.add_row("Created", str(f.created_at))
|
|
94
|
+
|
|
95
|
+
console.print(table)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@app.command("delete")
|
|
99
|
+
def delete_file(
|
|
100
|
+
file_id: str = typer.Argument(..., help="File ID to delete"),
|
|
101
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
|
|
102
|
+
):
|
|
103
|
+
"""Delete an uploaded file."""
|
|
104
|
+
if not yes:
|
|
105
|
+
confirm = typer.confirm(f"Delete file {file_id[:12]}...?")
|
|
106
|
+
if not confirm:
|
|
107
|
+
raise typer.Abort()
|
|
108
|
+
|
|
109
|
+
client = _get_client()
|
|
110
|
+
client.files.delete(file_id)
|
|
111
|
+
console.print(f"[green]File {file_id[:12]}... deleted[/green]")
|
|
@@ -44,17 +44,28 @@ def _resolve_job_id(client: HyperCLI, job_id: str) -> str:
|
|
|
44
44
|
def list_jobs(
|
|
45
45
|
state: Optional[str] = typer.Option(None, "--state", "-s", help="Filter by state"),
|
|
46
46
|
tag: list[str] = typer.Option([], "--tag", help="Filter by tag as KEY=VALUE", metavar="KEY=VALUE"),
|
|
47
|
+
page: int = typer.Option(1, "--page", min=1, help="Page number (1-indexed)"),
|
|
48
|
+
page_size: int = typer.Option(50, "--page-size", min=1, max=100, help="Items per page (max 100)"),
|
|
47
49
|
fmt: str = typer.Option("table", "--output", "-o", help="Output format: table|json"),
|
|
48
50
|
):
|
|
49
51
|
"""List all jobs"""
|
|
50
52
|
client = get_client()
|
|
51
53
|
tags = _parse_tags(tag) if tag else None
|
|
52
54
|
with spinner("Fetching jobs..."):
|
|
53
|
-
|
|
55
|
+
result = client.jobs.list_page(state=state, tags=tags, page=page, page_size=page_size)
|
|
54
56
|
|
|
55
57
|
if fmt == "json":
|
|
56
|
-
output(
|
|
58
|
+
output(
|
|
59
|
+
{
|
|
60
|
+
"jobs": [job.__dict__ for job in result.jobs],
|
|
61
|
+
"total_count": result.total_count,
|
|
62
|
+
"page": result.page,
|
|
63
|
+
"page_size": result.page_size,
|
|
64
|
+
},
|
|
65
|
+
"json",
|
|
66
|
+
)
|
|
57
67
|
else:
|
|
68
|
+
jobs = result.jobs
|
|
58
69
|
if not jobs:
|
|
59
70
|
console.print("[dim]No jobs found[/dim]")
|
|
60
71
|
return
|
|
@@ -63,6 +74,11 @@ def list_jobs(
|
|
|
63
74
|
"table",
|
|
64
75
|
["job_id", "state", "gpu_type", "gpu_count", "region", "time_left", "runtime", "hostname"],
|
|
65
76
|
)
|
|
77
|
+
total_pages = max((result.total_count + result.page_size - 1) // result.page_size, 1)
|
|
78
|
+
console.print(
|
|
79
|
+
f"[dim]Page {result.page}/{total_pages} | "
|
|
80
|
+
f"{len(result.jobs)} shown | {result.total_count} total[/dim]"
|
|
81
|
+
)
|
|
66
82
|
|
|
67
83
|
|
|
68
84
|
@app.command("get")
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "hypercli-cli"
|
|
7
|
-
version = "2026.3.
|
|
7
|
+
version = "2026.3.25"
|
|
8
8
|
description = "CLI for HyperCLI - GPU orchestration and LLM API"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -13,7 +13,7 @@ authors = [
|
|
|
13
13
|
{ name = "HyperCLI", email = "support@hypercli.com" }
|
|
14
14
|
]
|
|
15
15
|
dependencies = [
|
|
16
|
-
"hypercli-sdk>=2026.3.
|
|
16
|
+
"hypercli-sdk>=2026.3.25",
|
|
17
17
|
"typer>=0.20.0",
|
|
18
18
|
"rich>=14.2.0",
|
|
19
19
|
"websocket-client>=1.6.0",
|
|
@@ -24,7 +24,7 @@ dependencies = [
|
|
|
24
24
|
|
|
25
25
|
[project.optional-dependencies]
|
|
26
26
|
comfyui = [
|
|
27
|
-
"hypercli-sdk[comfyui]>=2026.3.
|
|
27
|
+
"hypercli-sdk[comfyui]>=2026.3.25",
|
|
28
28
|
]
|
|
29
29
|
wallet = [
|
|
30
30
|
"x402[httpx,evm]>=2.0.0",
|
|
@@ -37,7 +37,7 @@ stt = [
|
|
|
37
37
|
"faster-whisper>=1.1.0",
|
|
38
38
|
]
|
|
39
39
|
all = [
|
|
40
|
-
"hypercli-sdk[comfyui]>=2026.3.
|
|
40
|
+
"hypercli-sdk[comfyui]>=2026.3.25",
|
|
41
41
|
"x402[httpx,evm]>=2.0.0",
|
|
42
42
|
"eth-account>=0.13.0",
|
|
43
43
|
"web3>=7.0.0",
|
|
@@ -172,3 +172,20 @@ def test_agent_shell_command(monkeypatch):
|
|
|
172
172
|
|
|
173
173
|
assert result.exit_code == 0
|
|
174
174
|
assert called["agent_id"] == "agent-xyz"
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_agents_cp_reports_directory_path_error(monkeypatch, tmp_path):
|
|
178
|
+
class FakeDeployments:
|
|
179
|
+
def cp_from(self, _pod, src_path, dst_path):
|
|
180
|
+
assert src_path == ".openclaw"
|
|
181
|
+
assert str(dst_path).endswith("download")
|
|
182
|
+
raise ValueError("Path is a directory: .openclaw. Use files_list(path) instead.")
|
|
183
|
+
|
|
184
|
+
monkeypatch.setattr("hypercli_cli.agents._get_deployments_client", lambda: FakeDeployments())
|
|
185
|
+
monkeypatch.setattr("hypercli_cli.agents._get_pod_with_token", lambda agent_id: SimpleNamespace(id=agent_id))
|
|
186
|
+
|
|
187
|
+
result = runner.invoke(app, ["agents", "cp", "agent-xyz:.openclaw", str(tmp_path / "download")])
|
|
188
|
+
|
|
189
|
+
assert result.exit_code == 1
|
|
190
|
+
assert "Path is a directory: .openclaw." in result.stdout
|
|
191
|
+
assert "Copy expects a file path, not a directory." in result.stdout
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from types import SimpleNamespace
|
|
2
|
+
|
|
3
|
+
from typer.testing import CliRunner
|
|
4
|
+
|
|
5
|
+
from hypercli_cli.cli import app
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
runner = CliRunner()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_jobs_list_passes_tags(monkeypatch):
|
|
12
|
+
captured = {}
|
|
13
|
+
|
|
14
|
+
class FakeJobs:
|
|
15
|
+
def list_page(self, state=None, tags=None, page=None, page_size=None):
|
|
16
|
+
captured["state"] = state
|
|
17
|
+
captured["tags"] = tags
|
|
18
|
+
captured["page"] = page
|
|
19
|
+
captured["page_size"] = page_size
|
|
20
|
+
return SimpleNamespace(jobs=[], total_count=0, page=page, page_size=page_size)
|
|
21
|
+
|
|
22
|
+
fake_client = SimpleNamespace(jobs=FakeJobs())
|
|
23
|
+
monkeypatch.setattr("hypercli_cli.jobs.get_client", lambda: fake_client)
|
|
24
|
+
|
|
25
|
+
result = runner.invoke(
|
|
26
|
+
app,
|
|
27
|
+
["jobs", "list", "--state", "running", "--tag", "team=ml", "--tag", "env=prod"],
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
assert result.exit_code == 0
|
|
31
|
+
assert captured == {
|
|
32
|
+
"state": "running",
|
|
33
|
+
"tags": {"team": "ml", "env": "prod"},
|
|
34
|
+
"page": 1,
|
|
35
|
+
"page_size": 50,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_jobs_list_passes_backend_pagination(monkeypatch):
|
|
40
|
+
captured = {}
|
|
41
|
+
|
|
42
|
+
class FakeJobs:
|
|
43
|
+
def list_page(self, state=None, tags=None, page=None, page_size=None):
|
|
44
|
+
captured["page"] = page
|
|
45
|
+
captured["page_size"] = page_size
|
|
46
|
+
return SimpleNamespace(jobs=[], total_count=0, page=page, page_size=page_size)
|
|
47
|
+
|
|
48
|
+
fake_client = SimpleNamespace(jobs=FakeJobs())
|
|
49
|
+
monkeypatch.setattr("hypercli_cli.jobs.get_client", lambda: fake_client)
|
|
50
|
+
|
|
51
|
+
result = runner.invoke(app, ["jobs", "list", "--page", "3", "--page-size", "25"])
|
|
52
|
+
|
|
53
|
+
assert result.exit_code == 0
|
|
54
|
+
assert captured == {"page": 3, "page_size": 25}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_jobs_list_rejects_invalid_tag(monkeypatch):
|
|
58
|
+
fake_client = SimpleNamespace(
|
|
59
|
+
jobs=SimpleNamespace(
|
|
60
|
+
list_page=lambda state=None, tags=None, page=None, page_size=None: SimpleNamespace(
|
|
61
|
+
jobs=[], total_count=0, page=page, page_size=page_size
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
monkeypatch.setattr("hypercli_cli.jobs.get_client", lambda: fake_client)
|
|
66
|
+
|
|
67
|
+
result = runner.invoke(app, ["jobs", "list", "--tag", "broken"])
|
|
68
|
+
|
|
69
|
+
assert result.exit_code == 1
|
|
70
|
+
assert "Expected KEY=VALUE" in result.stdout
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
from types import SimpleNamespace
|
|
2
|
-
|
|
3
|
-
from typer.testing import CliRunner
|
|
4
|
-
|
|
5
|
-
from hypercli_cli.cli import app
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
runner = CliRunner()
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def test_jobs_list_passes_tags(monkeypatch):
|
|
12
|
-
captured = {}
|
|
13
|
-
|
|
14
|
-
class FakeJobs:
|
|
15
|
-
def list(self, state=None, tags=None):
|
|
16
|
-
captured["state"] = state
|
|
17
|
-
captured["tags"] = tags
|
|
18
|
-
return []
|
|
19
|
-
|
|
20
|
-
fake_client = SimpleNamespace(jobs=FakeJobs())
|
|
21
|
-
monkeypatch.setattr("hypercli_cli.jobs.get_client", lambda: fake_client)
|
|
22
|
-
|
|
23
|
-
result = runner.invoke(
|
|
24
|
-
app,
|
|
25
|
-
["jobs", "list", "--state", "running", "--tag", "team=ml", "--tag", "env=prod"],
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
assert result.exit_code == 0
|
|
29
|
-
assert captured == {"state": "running", "tags": {"team": "ml", "env": "prod"}}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def test_jobs_list_rejects_invalid_tag(monkeypatch):
|
|
33
|
-
fake_client = SimpleNamespace(jobs=SimpleNamespace(list=lambda state=None, tags=None: []))
|
|
34
|
-
monkeypatch.setattr("hypercli_cli.jobs.get_client", lambda: fake_client)
|
|
35
|
-
|
|
36
|
-
result = runner.invoke(app, ["jobs", "list", "--tag", "broken"])
|
|
37
|
-
|
|
38
|
-
assert result.exit_code == 1
|
|
39
|
-
assert "Expected KEY=VALUE" in result.stdout
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|