dstack 0.19.18__py3-none-any.whl → 0.19.19__py3-none-any.whl
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.
Potentially problematic release.
This version of dstack might be problematic. Click here for more details.
- dstack/_internal/cli/services/configurators/fleet.py +99 -1
- dstack/_internal/cli/services/profile.py +1 -1
- dstack/_internal/core/compatibility/runs.py +12 -1
- dstack/_internal/core/compatibility/volumes.py +2 -0
- dstack/_internal/core/models/common.py +38 -2
- dstack/_internal/core/models/configurations.py +9 -1
- dstack/_internal/core/models/fleets.py +2 -1
- dstack/_internal/core/models/profiles.py +8 -5
- dstack/_internal/core/models/resources.py +15 -8
- dstack/_internal/core/models/runs.py +41 -138
- dstack/_internal/core/models/volumes.py +14 -0
- dstack/_internal/core/services/diff.py +30 -10
- dstack/_internal/core/services/ssh/attach.py +2 -0
- dstack/_internal/server/app.py +17 -9
- dstack/_internal/server/background/__init__.py +5 -3
- dstack/_internal/server/background/tasks/process_gateways.py +46 -28
- dstack/_internal/server/background/tasks/process_idle_volumes.py +139 -0
- dstack/_internal/server/background/tasks/process_submitted_jobs.py +2 -0
- dstack/_internal/server/migrations/versions/35e90e1b0d3e_add_rolling_deployment_fields.py +6 -6
- dstack/_internal/server/migrations/versions/d5863798bf41_add_volumemodel_last_job_processed_at.py +40 -0
- dstack/_internal/server/models.py +1 -0
- dstack/_internal/server/routers/backends.py +23 -16
- dstack/_internal/server/routers/files.py +7 -6
- dstack/_internal/server/routers/fleets.py +47 -36
- dstack/_internal/server/routers/gateways.py +27 -18
- dstack/_internal/server/routers/instances.py +18 -13
- dstack/_internal/server/routers/logs.py +7 -3
- dstack/_internal/server/routers/metrics.py +14 -8
- dstack/_internal/server/routers/projects.py +33 -22
- dstack/_internal/server/routers/repos.py +7 -6
- dstack/_internal/server/routers/runs.py +49 -28
- dstack/_internal/server/routers/secrets.py +20 -15
- dstack/_internal/server/routers/server.py +7 -4
- dstack/_internal/server/routers/users.py +22 -19
- dstack/_internal/server/routers/volumes.py +34 -25
- dstack/_internal/server/schemas/logs.py +2 -2
- dstack/_internal/server/schemas/runs.py +17 -5
- dstack/_internal/server/services/fleets.py +354 -72
- dstack/_internal/server/services/gateways/__init__.py +13 -4
- dstack/_internal/server/services/gateways/client.py +5 -3
- dstack/_internal/server/services/instances.py +8 -0
- dstack/_internal/server/services/jobs/__init__.py +45 -0
- dstack/_internal/server/services/jobs/configurators/base.py +7 -0
- dstack/_internal/server/services/locking.py +3 -1
- dstack/_internal/server/services/logging.py +4 -2
- dstack/_internal/server/services/logs/__init__.py +15 -2
- dstack/_internal/server/services/logs/aws.py +2 -4
- dstack/_internal/server/services/logs/filelog.py +33 -27
- dstack/_internal/server/services/logs/gcp.py +3 -5
- dstack/_internal/server/services/proxy/repo.py +4 -1
- dstack/_internal/server/services/runs.py +115 -32
- dstack/_internal/server/services/services/__init__.py +2 -1
- dstack/_internal/server/services/users.py +3 -1
- dstack/_internal/server/services/volumes.py +13 -0
- dstack/_internal/server/settings.py +7 -2
- dstack/_internal/server/statics/index.html +1 -1
- dstack/_internal/server/statics/{main-d1ac2e8c38ed5f08a114.js → main-64f8273740c4b52c18f5.js} +6 -6
- dstack/_internal/server/statics/{main-d1ac2e8c38ed5f08a114.js.map → main-64f8273740c4b52c18f5.js.map} +1 -1
- dstack/_internal/server/testing/common.py +41 -5
- dstack/_internal/server/utils/routers.py +31 -8
- dstack/_internal/utils/json_utils.py +54 -0
- dstack/api/_public/runs.py +13 -2
- dstack/api/server/_runs.py +12 -2
- dstack/version.py +1 -1
- {dstack-0.19.18.dist-info → dstack-0.19.19.dist-info}/METADATA +7 -5
- {dstack-0.19.18.dist-info → dstack-0.19.19.dist-info}/RECORD +69 -66
- {dstack-0.19.18.dist-info → dstack-0.19.19.dist-info}/WHEEL +0 -0
- {dstack-0.19.18.dist-info → dstack-0.19.19.dist-info}/entry_points.txt +0 -0
- {dstack-0.19.18.dist-info → dstack-0.19.19.dist-info}/licenses/LICENSE.md +0 -0
|
@@ -12,6 +12,7 @@ from dstack._internal.server.security.permissions import Authenticated
|
|
|
12
12
|
from dstack._internal.server.services import files
|
|
13
13
|
from dstack._internal.server.settings import SERVER_CODE_UPLOAD_LIMIT
|
|
14
14
|
from dstack._internal.server.utils.routers import (
|
|
15
|
+
CustomORJSONResponse,
|
|
15
16
|
get_base_api_additional_responses,
|
|
16
17
|
get_request_size,
|
|
17
18
|
)
|
|
@@ -24,12 +25,12 @@ router = APIRouter(
|
|
|
24
25
|
)
|
|
25
26
|
|
|
26
27
|
|
|
27
|
-
@router.post("/get_archive_by_hash")
|
|
28
|
+
@router.post("/get_archive_by_hash", response_model=FileArchive)
|
|
28
29
|
async def get_archive_by_hash(
|
|
29
30
|
body: GetFileArchiveByHashRequest,
|
|
30
31
|
session: Annotated[AsyncSession, Depends(get_session)],
|
|
31
32
|
user: Annotated[UserModel, Depends(Authenticated())],
|
|
32
|
-
)
|
|
33
|
+
):
|
|
33
34
|
archive = await files.get_archive_by_hash(
|
|
34
35
|
session=session,
|
|
35
36
|
user=user,
|
|
@@ -37,16 +38,16 @@ async def get_archive_by_hash(
|
|
|
37
38
|
)
|
|
38
39
|
if archive is None:
|
|
39
40
|
raise ResourceNotExistsError()
|
|
40
|
-
return archive
|
|
41
|
+
return CustomORJSONResponse(archive)
|
|
41
42
|
|
|
42
43
|
|
|
43
|
-
@router.post("/upload_archive")
|
|
44
|
+
@router.post("/upload_archive", response_model=FileArchive)
|
|
44
45
|
async def upload_archive(
|
|
45
46
|
request: Request,
|
|
46
47
|
file: UploadFile,
|
|
47
48
|
session: Annotated[AsyncSession, Depends(get_session)],
|
|
48
49
|
user: Annotated[UserModel, Depends(Authenticated())],
|
|
49
|
-
)
|
|
50
|
+
):
|
|
50
51
|
request_size = get_request_size(request)
|
|
51
52
|
if SERVER_CODE_UPLOAD_LIMIT > 0 and request_size > SERVER_CODE_UPLOAD_LIMIT:
|
|
52
53
|
diff_size_fmt = sizeof_fmt(request_size)
|
|
@@ -64,4 +65,4 @@ async def upload_archive(
|
|
|
64
65
|
user=user,
|
|
65
66
|
file=file,
|
|
66
67
|
)
|
|
67
|
-
return archive
|
|
68
|
+
return CustomORJSONResponse(archive)
|
|
@@ -18,7 +18,10 @@ from dstack._internal.server.schemas.fleets import (
|
|
|
18
18
|
ListFleetsRequest,
|
|
19
19
|
)
|
|
20
20
|
from dstack._internal.server.security.permissions import Authenticated, ProjectMember
|
|
21
|
-
from dstack._internal.server.utils.routers import
|
|
21
|
+
from dstack._internal.server.utils.routers import (
|
|
22
|
+
CustomORJSONResponse,
|
|
23
|
+
get_base_api_additional_responses,
|
|
24
|
+
)
|
|
22
25
|
|
|
23
26
|
root_router = APIRouter(
|
|
24
27
|
prefix="/api/fleets",
|
|
@@ -32,12 +35,12 @@ project_router = APIRouter(
|
|
|
32
35
|
)
|
|
33
36
|
|
|
34
37
|
|
|
35
|
-
@root_router.post("/list")
|
|
38
|
+
@root_router.post("/list", response_model=List[Fleet])
|
|
36
39
|
async def list_fleets(
|
|
37
40
|
body: ListFleetsRequest,
|
|
38
41
|
session: AsyncSession = Depends(get_session),
|
|
39
42
|
user: UserModel = Depends(Authenticated()),
|
|
40
|
-
)
|
|
43
|
+
):
|
|
41
44
|
"""
|
|
42
45
|
Returns all fleets and instances within them visible to user sorted by descending `created_at`.
|
|
43
46
|
`project_name` and `only_active` can be specified as filters.
|
|
@@ -45,36 +48,40 @@ async def list_fleets(
|
|
|
45
48
|
The results are paginated. To get the next page, pass `created_at` and `id` of
|
|
46
49
|
the last fleet from the previous page as `prev_created_at` and `prev_id`.
|
|
47
50
|
"""
|
|
48
|
-
return
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
return CustomORJSONResponse(
|
|
52
|
+
await fleets_services.list_fleets(
|
|
53
|
+
session=session,
|
|
54
|
+
user=user,
|
|
55
|
+
project_name=body.project_name,
|
|
56
|
+
only_active=body.only_active,
|
|
57
|
+
prev_created_at=body.prev_created_at,
|
|
58
|
+
prev_id=body.prev_id,
|
|
59
|
+
limit=body.limit,
|
|
60
|
+
ascending=body.ascending,
|
|
61
|
+
)
|
|
57
62
|
)
|
|
58
63
|
|
|
59
64
|
|
|
60
|
-
@project_router.post("/list")
|
|
65
|
+
@project_router.post("/list", response_model=List[Fleet])
|
|
61
66
|
async def list_project_fleets(
|
|
62
67
|
session: AsyncSession = Depends(get_session),
|
|
63
68
|
user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectMember()),
|
|
64
|
-
)
|
|
69
|
+
):
|
|
65
70
|
"""
|
|
66
71
|
Returns all fleets in the project.
|
|
67
72
|
"""
|
|
68
73
|
_, project = user_project
|
|
69
|
-
return
|
|
74
|
+
return CustomORJSONResponse(
|
|
75
|
+
await fleets_services.list_project_fleets(session=session, project=project)
|
|
76
|
+
)
|
|
70
77
|
|
|
71
78
|
|
|
72
|
-
@project_router.post("/get")
|
|
79
|
+
@project_router.post("/get", response_model=Fleet)
|
|
73
80
|
async def get_fleet(
|
|
74
81
|
body: GetFleetRequest,
|
|
75
82
|
session: AsyncSession = Depends(get_session),
|
|
76
83
|
user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectMember()),
|
|
77
|
-
)
|
|
84
|
+
):
|
|
78
85
|
"""
|
|
79
86
|
Returns a fleet given `name` or `id`.
|
|
80
87
|
If given `name`, does not return deleted fleets.
|
|
@@ -86,15 +93,15 @@ async def get_fleet(
|
|
|
86
93
|
)
|
|
87
94
|
if fleet is None:
|
|
88
95
|
raise ResourceNotExistsError()
|
|
89
|
-
return fleet
|
|
96
|
+
return CustomORJSONResponse(fleet)
|
|
90
97
|
|
|
91
98
|
|
|
92
|
-
@project_router.post("/get_plan")
|
|
99
|
+
@project_router.post("/get_plan", response_model=FleetPlan)
|
|
93
100
|
async def get_plan(
|
|
94
101
|
body: GetFleetPlanRequest,
|
|
95
102
|
session: AsyncSession = Depends(get_session),
|
|
96
103
|
user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectMember()),
|
|
97
|
-
)
|
|
104
|
+
):
|
|
98
105
|
"""
|
|
99
106
|
Returns a fleet plan for the given fleet configuration.
|
|
100
107
|
"""
|
|
@@ -105,45 +112,49 @@ async def get_plan(
|
|
|
105
112
|
user=user,
|
|
106
113
|
spec=body.spec,
|
|
107
114
|
)
|
|
108
|
-
return plan
|
|
115
|
+
return CustomORJSONResponse(plan)
|
|
109
116
|
|
|
110
117
|
|
|
111
|
-
@project_router.post("/apply")
|
|
118
|
+
@project_router.post("/apply", response_model=Fleet)
|
|
112
119
|
async def apply_plan(
|
|
113
120
|
body: ApplyFleetPlanRequest,
|
|
114
121
|
session: AsyncSession = Depends(get_session),
|
|
115
122
|
user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectMember()),
|
|
116
|
-
)
|
|
123
|
+
):
|
|
117
124
|
"""
|
|
118
125
|
Creates a new fleet or updates an existing fleet.
|
|
119
126
|
Errors if the expected current resource from the plan does not match the current resource.
|
|
120
127
|
Use `force: true` to apply even if the current resource does not match.
|
|
121
128
|
"""
|
|
122
129
|
user, project = user_project
|
|
123
|
-
return
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
130
|
+
return CustomORJSONResponse(
|
|
131
|
+
await fleets_services.apply_plan(
|
|
132
|
+
session=session,
|
|
133
|
+
user=user,
|
|
134
|
+
project=project,
|
|
135
|
+
plan=body.plan,
|
|
136
|
+
force=body.force,
|
|
137
|
+
)
|
|
129
138
|
)
|
|
130
139
|
|
|
131
140
|
|
|
132
|
-
@project_router.post("/create")
|
|
141
|
+
@project_router.post("/create", response_model=Fleet)
|
|
133
142
|
async def create_fleet(
|
|
134
143
|
body: CreateFleetRequest,
|
|
135
144
|
session: AsyncSession = Depends(get_session),
|
|
136
145
|
user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectMember()),
|
|
137
|
-
)
|
|
146
|
+
):
|
|
138
147
|
"""
|
|
139
148
|
Creates a fleet given a fleet configuration.
|
|
140
149
|
"""
|
|
141
150
|
user, project = user_project
|
|
142
|
-
return
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
151
|
+
return CustomORJSONResponse(
|
|
152
|
+
await fleets_services.create_fleet(
|
|
153
|
+
session=session,
|
|
154
|
+
project=project,
|
|
155
|
+
user=user,
|
|
156
|
+
spec=body.spec,
|
|
157
|
+
)
|
|
147
158
|
)
|
|
148
159
|
|
|
149
160
|
|
|
@@ -13,7 +13,10 @@ from dstack._internal.server.security.permissions import (
|
|
|
13
13
|
ProjectAdmin,
|
|
14
14
|
ProjectMemberOrPublicAccess,
|
|
15
15
|
)
|
|
16
|
-
from dstack._internal.server.utils.routers import
|
|
16
|
+
from dstack._internal.server.utils.routers import (
|
|
17
|
+
CustomORJSONResponse,
|
|
18
|
+
get_base_api_additional_responses,
|
|
19
|
+
)
|
|
17
20
|
|
|
18
21
|
router = APIRouter(
|
|
19
22
|
prefix="/api/project/{project_name}/gateways",
|
|
@@ -22,40 +25,44 @@ router = APIRouter(
|
|
|
22
25
|
)
|
|
23
26
|
|
|
24
27
|
|
|
25
|
-
@router.post("/list")
|
|
28
|
+
@router.post("/list", response_model=List[models.Gateway])
|
|
26
29
|
async def list_gateways(
|
|
27
30
|
session: AsyncSession = Depends(get_session),
|
|
28
31
|
user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectMemberOrPublicAccess()),
|
|
29
|
-
)
|
|
32
|
+
):
|
|
30
33
|
_, project = user_project
|
|
31
|
-
return
|
|
34
|
+
return CustomORJSONResponse(
|
|
35
|
+
await gateways.list_project_gateways(session=session, project=project)
|
|
36
|
+
)
|
|
32
37
|
|
|
33
38
|
|
|
34
|
-
@router.post("/get")
|
|
39
|
+
@router.post("/get", response_model=models.Gateway)
|
|
35
40
|
async def get_gateway(
|
|
36
41
|
body: schemas.GetGatewayRequest,
|
|
37
42
|
session: AsyncSession = Depends(get_session),
|
|
38
43
|
user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectMemberOrPublicAccess()),
|
|
39
|
-
)
|
|
44
|
+
):
|
|
40
45
|
_, project = user_project
|
|
41
46
|
gateway = await gateways.get_gateway_by_name(session=session, project=project, name=body.name)
|
|
42
47
|
if gateway is None:
|
|
43
48
|
raise ResourceNotExistsError()
|
|
44
|
-
return gateway
|
|
49
|
+
return CustomORJSONResponse(gateway)
|
|
45
50
|
|
|
46
51
|
|
|
47
|
-
@router.post("/create")
|
|
52
|
+
@router.post("/create", response_model=models.Gateway)
|
|
48
53
|
async def create_gateway(
|
|
49
54
|
body: schemas.CreateGatewayRequest,
|
|
50
55
|
session: AsyncSession = Depends(get_session),
|
|
51
56
|
user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectAdmin()),
|
|
52
|
-
)
|
|
57
|
+
):
|
|
53
58
|
user, project = user_project
|
|
54
|
-
return
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
return CustomORJSONResponse(
|
|
60
|
+
await gateways.create_gateway(
|
|
61
|
+
session=session,
|
|
62
|
+
user=user,
|
|
63
|
+
project=project,
|
|
64
|
+
configuration=body.configuration,
|
|
65
|
+
)
|
|
59
66
|
)
|
|
60
67
|
|
|
61
68
|
|
|
@@ -83,13 +90,15 @@ async def set_default_gateway(
|
|
|
83
90
|
await gateways.set_default_gateway(session=session, project=project, name=body.name)
|
|
84
91
|
|
|
85
92
|
|
|
86
|
-
@router.post("/set_wildcard_domain")
|
|
93
|
+
@router.post("/set_wildcard_domain", response_model=models.Gateway)
|
|
87
94
|
async def set_gateway_wildcard_domain(
|
|
88
95
|
body: schemas.SetWildcardDomainRequest,
|
|
89
96
|
session: AsyncSession = Depends(get_session),
|
|
90
97
|
user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectAdmin()),
|
|
91
|
-
)
|
|
98
|
+
):
|
|
92
99
|
_, project = user_project
|
|
93
|
-
return
|
|
94
|
-
|
|
100
|
+
return CustomORJSONResponse(
|
|
101
|
+
await gateways.set_gateway_wildcard_domain(
|
|
102
|
+
session=session, project=project, name=body.name, wildcard_domain=body.wildcard_domain
|
|
103
|
+
)
|
|
95
104
|
)
|
|
@@ -9,7 +9,10 @@ from dstack._internal.server.db import get_session
|
|
|
9
9
|
from dstack._internal.server.models import UserModel
|
|
10
10
|
from dstack._internal.server.schemas.instances import ListInstancesRequest
|
|
11
11
|
from dstack._internal.server.security.permissions import Authenticated
|
|
12
|
-
from dstack._internal.server.utils.routers import
|
|
12
|
+
from dstack._internal.server.utils.routers import (
|
|
13
|
+
CustomORJSONResponse,
|
|
14
|
+
get_base_api_additional_responses,
|
|
15
|
+
)
|
|
13
16
|
|
|
14
17
|
root_router = APIRouter(
|
|
15
18
|
prefix="/api/instances",
|
|
@@ -18,12 +21,12 @@ root_router = APIRouter(
|
|
|
18
21
|
)
|
|
19
22
|
|
|
20
23
|
|
|
21
|
-
@root_router.post("/list")
|
|
24
|
+
@root_router.post("/list", response_model=List[Instance])
|
|
22
25
|
async def list_instances(
|
|
23
26
|
body: ListInstancesRequest,
|
|
24
27
|
session: AsyncSession = Depends(get_session),
|
|
25
28
|
user: UserModel = Depends(Authenticated()),
|
|
26
|
-
)
|
|
29
|
+
):
|
|
27
30
|
"""
|
|
28
31
|
Returns all instances visible to user sorted by descending `created_at`.
|
|
29
32
|
`project_names` and `fleet_ids` can be specified as filters.
|
|
@@ -31,14 +34,16 @@ async def list_instances(
|
|
|
31
34
|
The results are paginated. To get the next page, pass `created_at` and `id` of
|
|
32
35
|
the last instance from the previous page as `prev_created_at` and `prev_id`.
|
|
33
36
|
"""
|
|
34
|
-
return
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
return CustomORJSONResponse(
|
|
38
|
+
await instances.list_user_instances(
|
|
39
|
+
session=session,
|
|
40
|
+
user=user,
|
|
41
|
+
project_names=body.project_names,
|
|
42
|
+
fleet_ids=body.fleet_ids,
|
|
43
|
+
only_active=body.only_active,
|
|
44
|
+
prev_created_at=body.prev_created_at,
|
|
45
|
+
prev_id=body.prev_id,
|
|
46
|
+
limit=body.limit,
|
|
47
|
+
ascending=body.ascending,
|
|
48
|
+
)
|
|
44
49
|
)
|
|
@@ -7,7 +7,10 @@ from dstack._internal.server.models import ProjectModel, UserModel
|
|
|
7
7
|
from dstack._internal.server.schemas.logs import PollLogsRequest
|
|
8
8
|
from dstack._internal.server.security.permissions import ProjectMember
|
|
9
9
|
from dstack._internal.server.services import logs
|
|
10
|
-
from dstack._internal.server.utils.routers import
|
|
10
|
+
from dstack._internal.server.utils.routers import (
|
|
11
|
+
CustomORJSONResponse,
|
|
12
|
+
get_base_api_additional_responses,
|
|
13
|
+
)
|
|
11
14
|
|
|
12
15
|
router = APIRouter(
|
|
13
16
|
prefix="/api/project/{project_name}/logs",
|
|
@@ -18,13 +21,14 @@ router = APIRouter(
|
|
|
18
21
|
|
|
19
22
|
@router.post(
|
|
20
23
|
"/poll",
|
|
24
|
+
response_model=JobSubmissionLogs,
|
|
21
25
|
)
|
|
22
26
|
async def poll_logs(
|
|
23
27
|
body: PollLogsRequest,
|
|
24
28
|
user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectMember()),
|
|
25
|
-
)
|
|
29
|
+
):
|
|
26
30
|
_, project = user_project
|
|
27
31
|
# The runner guarantees logs have different timestamps if throughput < 1k logs / sec.
|
|
28
32
|
# Otherwise, some logs with duplicated timestamps may be filtered out.
|
|
29
33
|
# This limitation is imposed by cloud log services that support up to millisecond timestamp resolution.
|
|
30
|
-
return await logs.poll_logs_async(project=project, request=body)
|
|
34
|
+
return CustomORJSONResponse(await logs.poll_logs_async(project=project, request=body))
|
|
@@ -11,7 +11,10 @@ from dstack._internal.server.models import ProjectModel, UserModel
|
|
|
11
11
|
from dstack._internal.server.security.permissions import ProjectMember
|
|
12
12
|
from dstack._internal.server.services import metrics
|
|
13
13
|
from dstack._internal.server.services.jobs import get_run_job_model
|
|
14
|
-
from dstack._internal.server.utils.routers import
|
|
14
|
+
from dstack._internal.server.utils.routers import (
|
|
15
|
+
CustomORJSONResponse,
|
|
16
|
+
get_base_api_additional_responses,
|
|
17
|
+
)
|
|
15
18
|
|
|
16
19
|
router = APIRouter(
|
|
17
20
|
prefix="/api/project/{project_name}/metrics",
|
|
@@ -22,6 +25,7 @@ router = APIRouter(
|
|
|
22
25
|
|
|
23
26
|
@router.get(
|
|
24
27
|
"/job/{run_name}",
|
|
28
|
+
response_model=JobMetrics,
|
|
25
29
|
)
|
|
26
30
|
async def get_job_metrics(
|
|
27
31
|
run_name: str,
|
|
@@ -32,7 +36,7 @@ async def get_job_metrics(
|
|
|
32
36
|
before: Optional[datetime] = None,
|
|
33
37
|
session: AsyncSession = Depends(get_session),
|
|
34
38
|
user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectMember()),
|
|
35
|
-
)
|
|
39
|
+
):
|
|
36
40
|
"""
|
|
37
41
|
Returns job-level metrics such as hardware utilization
|
|
38
42
|
given `run_name`, `replica_num`, and `job_num`.
|
|
@@ -63,10 +67,12 @@ async def get_job_metrics(
|
|
|
63
67
|
if job_model is None:
|
|
64
68
|
raise ResourceNotExistsError("Found no job with given parameters")
|
|
65
69
|
|
|
66
|
-
return
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
return CustomORJSONResponse(
|
|
71
|
+
await metrics.get_job_metrics(
|
|
72
|
+
session=session,
|
|
73
|
+
job_model=job_model,
|
|
74
|
+
limit=limit,
|
|
75
|
+
after=after,
|
|
76
|
+
before=before,
|
|
77
|
+
)
|
|
72
78
|
)
|
|
@@ -23,7 +23,10 @@ from dstack._internal.server.security.permissions import (
|
|
|
23
23
|
ProjectMemberOrPublicAccess,
|
|
24
24
|
)
|
|
25
25
|
from dstack._internal.server.services import projects
|
|
26
|
-
from dstack._internal.server.utils.routers import
|
|
26
|
+
from dstack._internal.server.utils.routers import (
|
|
27
|
+
CustomORJSONResponse,
|
|
28
|
+
get_base_api_additional_responses,
|
|
29
|
+
)
|
|
27
30
|
|
|
28
31
|
router = APIRouter(
|
|
29
32
|
prefix="/api/projects",
|
|
@@ -32,30 +35,34 @@ router = APIRouter(
|
|
|
32
35
|
)
|
|
33
36
|
|
|
34
37
|
|
|
35
|
-
@router.post("/list")
|
|
38
|
+
@router.post("/list", response_model=List[Project])
|
|
36
39
|
async def list_projects(
|
|
37
40
|
session: AsyncSession = Depends(get_session),
|
|
38
41
|
user: UserModel = Depends(Authenticated()),
|
|
39
|
-
)
|
|
42
|
+
):
|
|
40
43
|
"""
|
|
41
44
|
Returns all projects visible to user sorted by descending `created_at`.
|
|
42
45
|
|
|
43
46
|
`members` and `backends` are always empty - call `/api/projects/{project_name}/get` to retrieve them.
|
|
44
47
|
"""
|
|
45
|
-
return
|
|
48
|
+
return CustomORJSONResponse(
|
|
49
|
+
await projects.list_user_accessible_projects(session=session, user=user)
|
|
50
|
+
)
|
|
46
51
|
|
|
47
52
|
|
|
48
|
-
@router.post("/create")
|
|
53
|
+
@router.post("/create", response_model=Project)
|
|
49
54
|
async def create_project(
|
|
50
55
|
body: CreateProjectRequest,
|
|
51
56
|
session: AsyncSession = Depends(get_session),
|
|
52
57
|
user: UserModel = Depends(Authenticated()),
|
|
53
|
-
)
|
|
54
|
-
return
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
):
|
|
59
|
+
return CustomORJSONResponse(
|
|
60
|
+
await projects.create_project(
|
|
61
|
+
session=session,
|
|
62
|
+
user=user,
|
|
63
|
+
project_name=body.project_name,
|
|
64
|
+
is_public=body.is_public,
|
|
65
|
+
)
|
|
59
66
|
)
|
|
60
67
|
|
|
61
68
|
|
|
@@ -72,23 +79,24 @@ async def delete_projects(
|
|
|
72
79
|
)
|
|
73
80
|
|
|
74
81
|
|
|
75
|
-
@router.post("/{project_name}/get")
|
|
82
|
+
@router.post("/{project_name}/get", response_model=Project)
|
|
76
83
|
async def get_project(
|
|
77
84
|
session: AsyncSession = Depends(get_session),
|
|
78
85
|
user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectMemberOrPublicAccess()),
|
|
79
|
-
)
|
|
86
|
+
):
|
|
80
87
|
_, project = user_project
|
|
81
|
-
return projects.project_model_to_project(project)
|
|
88
|
+
return CustomORJSONResponse(projects.project_model_to_project(project))
|
|
82
89
|
|
|
83
90
|
|
|
84
91
|
@router.post(
|
|
85
92
|
"/{project_name}/set_members",
|
|
93
|
+
response_model=Project,
|
|
86
94
|
)
|
|
87
95
|
async def set_project_members(
|
|
88
96
|
body: SetProjectMembersRequest,
|
|
89
97
|
session: AsyncSession = Depends(get_session),
|
|
90
98
|
user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectManager()),
|
|
91
|
-
)
|
|
99
|
+
):
|
|
92
100
|
user, project = user_project
|
|
93
101
|
await projects.set_project_members(
|
|
94
102
|
session=session,
|
|
@@ -97,17 +105,18 @@ async def set_project_members(
|
|
|
97
105
|
members=body.members,
|
|
98
106
|
)
|
|
99
107
|
await session.refresh(project)
|
|
100
|
-
return projects.project_model_to_project(project)
|
|
108
|
+
return CustomORJSONResponse(projects.project_model_to_project(project))
|
|
101
109
|
|
|
102
110
|
|
|
103
111
|
@router.post(
|
|
104
112
|
"/{project_name}/add_members",
|
|
113
|
+
response_model=Project,
|
|
105
114
|
)
|
|
106
115
|
async def add_project_members(
|
|
107
116
|
body: AddProjectMemberRequest,
|
|
108
117
|
session: AsyncSession = Depends(get_session),
|
|
109
118
|
user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectManagerOrPublicProject()),
|
|
110
|
-
)
|
|
119
|
+
):
|
|
111
120
|
user, project = user_project
|
|
112
121
|
await projects.add_project_members(
|
|
113
122
|
session=session,
|
|
@@ -116,17 +125,18 @@ async def add_project_members(
|
|
|
116
125
|
members=body.members,
|
|
117
126
|
)
|
|
118
127
|
await session.refresh(project)
|
|
119
|
-
return projects.project_model_to_project(project)
|
|
128
|
+
return CustomORJSONResponse(projects.project_model_to_project(project))
|
|
120
129
|
|
|
121
130
|
|
|
122
131
|
@router.post(
|
|
123
132
|
"/{project_name}/remove_members",
|
|
133
|
+
response_model=Project,
|
|
124
134
|
)
|
|
125
135
|
async def remove_project_members(
|
|
126
136
|
body: RemoveProjectMemberRequest,
|
|
127
137
|
session: AsyncSession = Depends(get_session),
|
|
128
138
|
user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectManagerOrSelfLeave()),
|
|
129
|
-
)
|
|
139
|
+
):
|
|
130
140
|
user, project = user_project
|
|
131
141
|
await projects.remove_project_members(
|
|
132
142
|
session=session,
|
|
@@ -135,17 +145,18 @@ async def remove_project_members(
|
|
|
135
145
|
usernames=body.usernames,
|
|
136
146
|
)
|
|
137
147
|
await session.refresh(project)
|
|
138
|
-
return projects.project_model_to_project(project)
|
|
148
|
+
return CustomORJSONResponse(projects.project_model_to_project(project))
|
|
139
149
|
|
|
140
150
|
|
|
141
151
|
@router.post(
|
|
142
152
|
"/{project_name}/update",
|
|
153
|
+
response_model=Project,
|
|
143
154
|
)
|
|
144
155
|
async def update_project(
|
|
145
156
|
body: UpdateProjectRequest,
|
|
146
157
|
session: AsyncSession = Depends(get_session),
|
|
147
158
|
user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectAdmin()),
|
|
148
|
-
)
|
|
159
|
+
):
|
|
149
160
|
user, project = user_project
|
|
150
161
|
await projects.update_project(
|
|
151
162
|
session=session,
|
|
@@ -154,4 +165,4 @@ async def update_project(
|
|
|
154
165
|
is_public=body.is_public,
|
|
155
166
|
)
|
|
156
167
|
await session.refresh(project)
|
|
157
|
-
return projects.project_model_to_project(project)
|
|
168
|
+
return CustomORJSONResponse(projects.project_model_to_project(project))
|
|
@@ -16,6 +16,7 @@ from dstack._internal.server.security.permissions import ProjectMember
|
|
|
16
16
|
from dstack._internal.server.services import repos
|
|
17
17
|
from dstack._internal.server.settings import SERVER_CODE_UPLOAD_LIMIT
|
|
18
18
|
from dstack._internal.server.utils.routers import (
|
|
19
|
+
CustomORJSONResponse,
|
|
19
20
|
get_base_api_additional_responses,
|
|
20
21
|
get_request_size,
|
|
21
22
|
)
|
|
@@ -28,21 +29,21 @@ router = APIRouter(
|
|
|
28
29
|
)
|
|
29
30
|
|
|
30
31
|
|
|
31
|
-
@router.post("/list")
|
|
32
|
+
@router.post("/list", response_model=List[RepoHead])
|
|
32
33
|
async def list_repos(
|
|
33
34
|
session: AsyncSession = Depends(get_session),
|
|
34
35
|
user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectMember()),
|
|
35
|
-
)
|
|
36
|
+
):
|
|
36
37
|
_, project = user_project
|
|
37
|
-
return await repos.list_repos(session=session, project=project)
|
|
38
|
+
return CustomORJSONResponse(await repos.list_repos(session=session, project=project))
|
|
38
39
|
|
|
39
40
|
|
|
40
|
-
@router.post("/get")
|
|
41
|
+
@router.post("/get", response_model=RepoHeadWithCreds)
|
|
41
42
|
async def get_repo(
|
|
42
43
|
body: GetRepoRequest,
|
|
43
44
|
session: AsyncSession = Depends(get_session),
|
|
44
45
|
user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectMember()),
|
|
45
|
-
)
|
|
46
|
+
):
|
|
46
47
|
user, project = user_project
|
|
47
48
|
repo = await repos.get_repo(
|
|
48
49
|
session=session,
|
|
@@ -53,7 +54,7 @@ async def get_repo(
|
|
|
53
54
|
)
|
|
54
55
|
if repo is None:
|
|
55
56
|
raise ResourceNotExistsError()
|
|
56
|
-
return repo
|
|
57
|
+
return CustomORJSONResponse(repo)
|
|
57
58
|
|
|
58
59
|
|
|
59
60
|
@router.post("/init")
|