Skip to content

Commit 7506e21

Browse files
[Feature]: Add JSON output option for CLI commands #3322 (WIP)
Review feedback
1 parent 5e1932d commit 7506e21

9 files changed

Lines changed: 124 additions & 99 deletions

File tree

src/dstack/_internal/cli/commands/offer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
from dstack._internal.cli.utils.run import print_offers_json, print_run_plan
1414
from dstack._internal.core.errors import CLIError
1515
from dstack._internal.core.models.configurations import ApplyConfigurationType, TaskConfiguration
16+
from dstack._internal.core.models.gpus import GpuGroup
1617
from dstack._internal.core.models.runs import RunSpec
17-
from dstack._internal.server.schemas.gpus import GpuGroup
1818
from dstack.api.utils import load_profile
1919

2020

src/dstack/_internal/cli/models/offer.py

Lines changed: 0 additions & 19 deletions
This file was deleted.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from typing import List, Literal, Optional
2+
3+
from dstack._internal.core.models.common import CoreConfig, generate_dual_core_model
4+
from dstack._internal.core.models.gpus import GpuGroup
5+
from dstack._internal.core.models.instances import InstanceOfferWithAvailability
6+
from dstack._internal.core.models.resources import ResourcesSpec
7+
from dstack._internal.utils.json_utils import pydantic_orjson_dumps_with_indent
8+
9+
10+
class OfferRequirementsConfig(CoreConfig):
11+
json_dumps = pydantic_orjson_dumps_with_indent
12+
13+
14+
class OfferRequirements(generate_dual_core_model(OfferRequirementsConfig)):
15+
"""Profile/requirements output model for CLI commands."""
16+
17+
resources: ResourcesSpec
18+
max_price: Optional[float] = None
19+
spot: Optional[bool] = None
20+
reservation: Optional[str] = None
21+
22+
23+
class OfferCommandOutputConfig(CoreConfig):
24+
json_dumps = pydantic_orjson_dumps_with_indent
25+
26+
27+
class OfferCommandOutput(generate_dual_core_model(OfferCommandOutputConfig)):
28+
"""JSON output model for `dstack offer` command."""
29+
30+
project: str
31+
user: str
32+
requirements: OfferRequirements
33+
offers: List[InstanceOfferWithAvailability]
34+
total_offers: int
35+
36+
37+
class OfferCommandGroupByGpuOutputConfig(CoreConfig):
38+
json_dumps = pydantic_orjson_dumps_with_indent
39+
40+
41+
class OfferCommandGroupByGpuOutput(generate_dual_core_model(OfferCommandGroupByGpuOutputConfig)):
42+
"""JSON output model for `dstack offer` command with GPU grouping."""
43+
44+
project: str
45+
requirements: OfferRequirements
46+
group_by: List[Literal["gpu", "backend", "region", "count"]]
47+
gpus: List[GpuGroup]

src/dstack/_internal/cli/utils/gpu.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33

44
from rich.table import Table
55

6-
from dstack._internal.cli.models.offer import OfferCommandOutput
6+
from dstack._internal.cli.models.offers import OfferCommandGroupByGpuOutput, OfferRequirements
77
from dstack._internal.cli.utils.common import console
8+
from dstack._internal.core.models.gpus import GpuGroup
89
from dstack._internal.core.models.profiles import SpotPolicy
910
from dstack._internal.core.models.runs import Requirements, RunSpec, get_policy_map
10-
from dstack._internal.server.schemas.gpus import GpuGroup
1111

1212

1313
def print_gpu_json(
@@ -17,14 +17,14 @@ def print_gpu_json(
1717
project: str,
1818
):
1919
"""Print GPU information in JSON format."""
20-
req = Requirements(
20+
req = OfferRequirements(
2121
resources=run_spec.configuration.resources,
2222
max_price=run_spec.merged_profile.max_price,
2323
spot=get_policy_map(run_spec.merged_profile.spot_policy, default=SpotPolicy.AUTO),
2424
reservation=run_spec.configuration.reservation,
2525
)
2626

27-
output = OfferCommandOutput(
27+
output = OfferCommandGroupByGpuOutput(
2828
project=project,
2929
requirements=req,
3030
group_by=group_by,

src/dstack/_internal/cli/utils/run.py

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from rich.markup import escape
55
from rich.table import Table
66

7+
from dstack._internal.cli.models.offers import OfferCommandOutput, OfferRequirements
78
from dstack._internal.cli.utils.common import NO_OFFERS_WARNING, add_row_from_dict, console
89
from dstack._internal.core.models.backends.base import BackendType
910
from dstack._internal.core.models.configurations import DevEnvironmentConfiguration
@@ -14,6 +15,7 @@
1415
)
1516
from dstack._internal.core.models.profiles import (
1617
DEFAULT_RUN_TERMINATION_IDLE_TIME,
18+
SpotPolicy,
1719
TerminationPolicy,
1820
)
1921
from dstack._internal.core.models.runs import (
@@ -24,6 +26,7 @@
2426
ProbeSpec,
2527
RunPlan,
2628
RunStatus,
29+
get_policy_map,
2730
)
2831
from dstack._internal.core.models.runs import (
2932
Run as CoreRun,
@@ -43,33 +46,22 @@ def print_offers_json(run_plan: RunPlan, run_spec):
4346
"""Print offers information in JSON format."""
4447
job_plan = run_plan.job_plans[0]
4548

46-
output = {
47-
"project": run_plan.project_name,
48-
"user": run_plan.user,
49-
"resources": job_plan.job_spec.requirements.resources.dict(),
50-
"max_price": (job_plan.job_spec.requirements.max_price),
51-
"spot": run_spec.configuration.spot_policy,
52-
"reservation": run_plan.run_spec.configuration.reservation,
53-
"offers": [],
54-
"total_offers": job_plan.total_offers,
55-
}
56-
57-
for offer in job_plan.offers:
58-
output["offers"].append(
59-
{
60-
"backend": ("ssh" if offer.backend.value == "remote" else offer.backend.value),
61-
"region": offer.region,
62-
"instance_type": offer.instance.name,
63-
"resources": offer.instance.resources.dict(),
64-
"spot": offer.instance.resources.spot,
65-
"price": float(offer.price),
66-
"availability": offer.availability.value,
67-
}
68-
)
49+
requirements = OfferRequirements(
50+
resources=job_plan.job_spec.requirements.resources,
51+
max_price=job_plan.job_spec.requirements.max_price,
52+
spot=get_policy_map(run_spec.configuration.spot_policy, default=SpotPolicy.AUTO),
53+
reservation=run_plan.run_spec.configuration.reservation,
54+
)
6955

70-
import json
56+
output = OfferCommandOutput(
57+
project=run_plan.project_name,
58+
user=run_plan.user,
59+
requirements=requirements,
60+
offers=job_plan.offers,
61+
total_offers=job_plan.total_offers,
62+
)
7163

72-
print(json.dumps(output, indent=2))
64+
print(output.json())
7365

7466

7567
def print_run_plan(
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from typing import List, Literal, Optional
2+
3+
import gpuhunt
4+
5+
from dstack._internal.core.models.backends.base import BackendType
6+
from dstack._internal.core.models.common import CoreModel
7+
from dstack._internal.core.models.instances import InstanceAvailability
8+
from dstack._internal.core.models.resources import Range
9+
10+
11+
class BackendGpu(CoreModel):
12+
"""GPU specification from a backend offer."""
13+
14+
name: str
15+
memory_mib: int
16+
vendor: gpuhunt.AcceleratorVendor
17+
availability: InstanceAvailability
18+
spot: bool
19+
count: int
20+
price: float
21+
region: str
22+
23+
24+
class BackendGpus(CoreModel):
25+
"""Backend GPU specifications."""
26+
27+
backend_type: BackendType
28+
gpus: List[BackendGpu]
29+
regions: List[str]
30+
31+
32+
class GpuGroup(CoreModel):
33+
"""GPU group that can handle all grouping scenarios."""
34+
35+
name: str
36+
memory_mib: int
37+
vendor: gpuhunt.AcceleratorVendor
38+
availability: List[InstanceAvailability]
39+
spot: List[Literal["spot", "on-demand"]]
40+
count: Range[int]
41+
price: Range[float]
42+
backends: Optional[List[BackendType]] = None
43+
backend: Optional[BackendType] = None
44+
regions: Optional[List[str]] = None
45+
region: Optional[str] = None

src/dstack/_internal/server/schemas/gpus.py

Lines changed: 1 addition & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,12 @@
11
from typing import List, Literal, Optional
22

3-
import gpuhunt
43
from pydantic import Field
54

6-
from dstack._internal.core.models.backends.base import BackendType
75
from dstack._internal.core.models.common import CoreModel
8-
from dstack._internal.core.models.instances import InstanceAvailability
9-
from dstack._internal.core.models.resources import Range
6+
from dstack._internal.core.models.gpus import GpuGroup
107
from dstack._internal.core.models.runs import RunSpec
118

129

13-
class BackendGpu(CoreModel):
14-
"""GPU specification from a backend offer."""
15-
16-
name: str
17-
memory_mib: int
18-
vendor: gpuhunt.AcceleratorVendor
19-
availability: InstanceAvailability
20-
spot: bool
21-
count: int
22-
price: float
23-
region: str
24-
25-
26-
class BackendGpus(CoreModel):
27-
"""Backend GPU specifications."""
28-
29-
backend_type: BackendType
30-
gpus: List[BackendGpu]
31-
regions: List[str]
32-
33-
3410
class ListGpusRequest(CoreModel):
3511
"""Request for listing GPUs with optional grouping."""
3612

@@ -42,22 +18,6 @@ class ListGpusRequest(CoreModel):
4218
)
4319

4420

45-
class GpuGroup(CoreModel):
46-
"""GPU group that can handle all grouping scenarios."""
47-
48-
name: str
49-
memory_mib: int
50-
vendor: gpuhunt.AcceleratorVendor
51-
availability: List[InstanceAvailability]
52-
spot: List[Literal["spot", "on-demand"]]
53-
count: Range[int]
54-
price: Range[float]
55-
backends: Optional[List[BackendType]] = None
56-
backend: Optional[BackendType] = None
57-
regions: Optional[List[str]] = None
58-
region: Optional[str] = None
59-
60-
6121
class ListGpusResponse(CoreModel):
6222
"""Response containing GPU specifications."""
6323

src/dstack/_internal/server/services/gpus.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,13 @@
33
from dstack._internal.core.backends.base.backend import Backend
44
from dstack._internal.core.errors import ServerClientError
55
from dstack._internal.core.models.backends.base import BackendType
6+
from dstack._internal.core.models.gpus import BackendGpu, BackendGpus, GpuGroup
67
from dstack._internal.core.models.instances import InstanceOfferWithAvailability
78
from dstack._internal.core.models.profiles import SpotPolicy
89
from dstack._internal.core.models.resources import Range
910
from dstack._internal.core.models.runs import Requirements, RunSpec, get_policy_map
1011
from dstack._internal.server.models import ProjectModel
11-
from dstack._internal.server.schemas.gpus import (
12-
BackendGpu,
13-
BackendGpus,
14-
GpuGroup,
15-
ListGpusResponse,
16-
)
12+
from dstack._internal.server.schemas.gpus import ListGpusResponse
1713
from dstack._internal.server.services.offers import get_offers_by_requirements
1814
from dstack._internal.utils.common import get_or_error
1915

src/dstack/api/server/_gpus.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
from typing import List, Optional
1+
from typing import List, Literal, Optional, cast
22

33
from pydantic import parse_obj_as
44

55
from dstack._internal.core.compatibility.gpus import get_list_gpus_excludes
6+
from dstack._internal.core.models.gpus import GpuGroup
67
from dstack._internal.core.models.runs import RunSpec
7-
from dstack._internal.server.schemas.gpus import GpuGroup, ListGpusRequest, ListGpusResponse
8+
from dstack._internal.server.schemas.gpus import ListGpusRequest, ListGpusResponse
89
from dstack.api.server._group import APIClientGroup
910

1011

@@ -15,7 +16,10 @@ def list_gpus(
1516
run_spec: RunSpec,
1617
group_by: Optional[List[str]] = None,
1718
) -> List[GpuGroup]:
18-
body = ListGpusRequest(run_spec=run_spec, group_by=group_by)
19+
body = ListGpusRequest(
20+
run_spec=run_spec,
21+
group_by=cast(Optional[List[Literal["backend", "region", "count"]]], group_by),
22+
)
1923
resp = self._request(
2024
f"/api/project/{project_name}/gpus/list",
2125
body=body.json(exclude=get_list_gpus_excludes(body)),

0 commit comments

Comments
 (0)