dstack 0.19.17__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.

Files changed (86) hide show
  1. dstack/_internal/cli/services/configurators/fleet.py +111 -1
  2. dstack/_internal/cli/services/profile.py +1 -1
  3. dstack/_internal/core/backends/aws/compute.py +237 -18
  4. dstack/_internal/core/backends/base/compute.py +20 -2
  5. dstack/_internal/core/backends/cudo/compute.py +23 -9
  6. dstack/_internal/core/backends/gcp/compute.py +13 -7
  7. dstack/_internal/core/backends/lambdalabs/compute.py +2 -1
  8. dstack/_internal/core/compatibility/fleets.py +12 -11
  9. dstack/_internal/core/compatibility/gateways.py +9 -8
  10. dstack/_internal/core/compatibility/logs.py +4 -3
  11. dstack/_internal/core/compatibility/runs.py +29 -21
  12. dstack/_internal/core/compatibility/volumes.py +11 -8
  13. dstack/_internal/core/errors.py +4 -0
  14. dstack/_internal/core/models/common.py +45 -2
  15. dstack/_internal/core/models/configurations.py +9 -1
  16. dstack/_internal/core/models/fleets.py +2 -1
  17. dstack/_internal/core/models/profiles.py +8 -5
  18. dstack/_internal/core/models/resources.py +15 -8
  19. dstack/_internal/core/models/runs.py +41 -138
  20. dstack/_internal/core/models/volumes.py +14 -0
  21. dstack/_internal/core/services/diff.py +56 -3
  22. dstack/_internal/core/services/ssh/attach.py +2 -0
  23. dstack/_internal/server/app.py +37 -9
  24. dstack/_internal/server/background/__init__.py +66 -40
  25. dstack/_internal/server/background/tasks/process_fleets.py +19 -3
  26. dstack/_internal/server/background/tasks/process_gateways.py +47 -29
  27. dstack/_internal/server/background/tasks/process_idle_volumes.py +139 -0
  28. dstack/_internal/server/background/tasks/process_instances.py +13 -2
  29. dstack/_internal/server/background/tasks/process_placement_groups.py +4 -2
  30. dstack/_internal/server/background/tasks/process_running_jobs.py +14 -3
  31. dstack/_internal/server/background/tasks/process_runs.py +8 -4
  32. dstack/_internal/server/background/tasks/process_submitted_jobs.py +38 -7
  33. dstack/_internal/server/background/tasks/process_terminating_jobs.py +5 -3
  34. dstack/_internal/server/background/tasks/process_volumes.py +2 -2
  35. dstack/_internal/server/migrations/versions/35e90e1b0d3e_add_rolling_deployment_fields.py +6 -6
  36. dstack/_internal/server/migrations/versions/d5863798bf41_add_volumemodel_last_job_processed_at.py +40 -0
  37. dstack/_internal/server/models.py +1 -0
  38. dstack/_internal/server/routers/backends.py +23 -16
  39. dstack/_internal/server/routers/files.py +7 -6
  40. dstack/_internal/server/routers/fleets.py +47 -36
  41. dstack/_internal/server/routers/gateways.py +27 -18
  42. dstack/_internal/server/routers/instances.py +18 -13
  43. dstack/_internal/server/routers/logs.py +7 -3
  44. dstack/_internal/server/routers/metrics.py +14 -8
  45. dstack/_internal/server/routers/projects.py +33 -22
  46. dstack/_internal/server/routers/repos.py +7 -6
  47. dstack/_internal/server/routers/runs.py +49 -28
  48. dstack/_internal/server/routers/secrets.py +20 -15
  49. dstack/_internal/server/routers/server.py +7 -4
  50. dstack/_internal/server/routers/users.py +22 -19
  51. dstack/_internal/server/routers/volumes.py +34 -25
  52. dstack/_internal/server/schemas/logs.py +2 -2
  53. dstack/_internal/server/schemas/runs.py +17 -5
  54. dstack/_internal/server/services/fleets.py +358 -75
  55. dstack/_internal/server/services/gateways/__init__.py +17 -6
  56. dstack/_internal/server/services/gateways/client.py +5 -3
  57. dstack/_internal/server/services/instances.py +8 -0
  58. dstack/_internal/server/services/jobs/__init__.py +45 -0
  59. dstack/_internal/server/services/jobs/configurators/base.py +12 -1
  60. dstack/_internal/server/services/locking.py +104 -13
  61. dstack/_internal/server/services/logging.py +4 -2
  62. dstack/_internal/server/services/logs/__init__.py +15 -2
  63. dstack/_internal/server/services/logs/aws.py +2 -4
  64. dstack/_internal/server/services/logs/filelog.py +33 -27
  65. dstack/_internal/server/services/logs/gcp.py +3 -5
  66. dstack/_internal/server/services/proxy/repo.py +4 -1
  67. dstack/_internal/server/services/runs.py +139 -72
  68. dstack/_internal/server/services/services/__init__.py +2 -1
  69. dstack/_internal/server/services/users.py +3 -1
  70. dstack/_internal/server/services/volumes.py +15 -2
  71. dstack/_internal/server/settings.py +25 -6
  72. dstack/_internal/server/statics/index.html +1 -1
  73. dstack/_internal/server/statics/{main-d151637af20f70b2e796.js → main-64f8273740c4b52c18f5.js} +71 -67
  74. dstack/_internal/server/statics/{main-d151637af20f70b2e796.js.map → main-64f8273740c4b52c18f5.js.map} +1 -1
  75. dstack/_internal/server/statics/{main-d48635d8fe670d53961c.css → main-d58fc0460cb0eae7cb5c.css} +1 -1
  76. dstack/_internal/server/testing/common.py +48 -8
  77. dstack/_internal/server/utils/routers.py +31 -8
  78. dstack/_internal/utils/json_utils.py +54 -0
  79. dstack/api/_public/runs.py +13 -2
  80. dstack/api/server/_runs.py +12 -2
  81. dstack/version.py +1 -1
  82. {dstack-0.19.17.dist-info → dstack-0.19.19.dist-info}/METADATA +17 -14
  83. {dstack-0.19.17.dist-info → dstack-0.19.19.dist-info}/RECORD +86 -83
  84. {dstack-0.19.17.dist-info → dstack-0.19.19.dist-info}/WHEEL +0 -0
  85. {dstack-0.19.17.dist-info → dstack-0.19.19.dist-info}/entry_points.txt +0 -0
  86. {dstack-0.19.17.dist-info → dstack-0.19.19.dist-info}/licenses/LICENSE.md +0 -0
@@ -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 get_base_api_additional_responses
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
- ) -> List[Project]:
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 await projects.list_user_accessible_projects(session=session, user=user)
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
- ) -> Project:
54
- return await projects.create_project(
55
- session=session,
56
- user=user,
57
- project_name=body.project_name,
58
- is_public=body.is_public,
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
- ) -> Project:
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
- ) -> Project:
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
- ) -> Project:
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
- ) -> Project:
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
- ) -> Project:
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
- ) -> List[RepoHead]:
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
- ) -> RepoHeadWithCreds:
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")
@@ -18,7 +18,10 @@ from dstack._internal.server.schemas.runs import (
18
18
  )
19
19
  from dstack._internal.server.security.permissions import Authenticated, ProjectMember
20
20
  from dstack._internal.server.services import runs
21
- from dstack._internal.server.utils.routers import get_base_api_additional_responses
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/runs",
@@ -32,12 +35,15 @@ project_router = APIRouter(
32
35
  )
33
36
 
34
37
 
35
- @root_router.post("/list")
38
+ @root_router.post(
39
+ "/list",
40
+ response_model=List[Run],
41
+ )
36
42
  async def list_runs(
37
43
  body: ListRunsRequest,
38
44
  session: AsyncSession = Depends(get_session),
39
45
  user: UserModel = Depends(Authenticated()),
40
- ) -> List[Run]:
46
+ ):
41
47
  """
42
48
  Returns all runs visible to user sorted by descending `submitted_at`.
43
49
  `project_name`, `repo_id`, `username`, and `only_active` can be specified as filters.
@@ -47,26 +53,33 @@ async def list_runs(
47
53
  The results are paginated. To get the next page, pass `submitted_at` and `id` of
48
54
  the last run from the previous page as `prev_submitted_at` and `prev_run_id`.
49
55
  """
50
- return await runs.list_user_runs(
51
- session=session,
52
- user=user,
53
- project_name=body.project_name,
54
- repo_id=body.repo_id,
55
- username=body.username,
56
- only_active=body.only_active,
57
- prev_submitted_at=body.prev_submitted_at,
58
- prev_run_id=body.prev_run_id,
59
- limit=body.limit,
60
- ascending=body.ascending,
56
+ return CustomORJSONResponse(
57
+ await runs.list_user_runs(
58
+ session=session,
59
+ user=user,
60
+ project_name=body.project_name,
61
+ repo_id=body.repo_id,
62
+ username=body.username,
63
+ only_active=body.only_active,
64
+ include_jobs=body.include_jobs,
65
+ job_submissions_limit=body.job_submissions_limit,
66
+ prev_submitted_at=body.prev_submitted_at,
67
+ prev_run_id=body.prev_run_id,
68
+ limit=body.limit,
69
+ ascending=body.ascending,
70
+ )
61
71
  )
62
72
 
63
73
 
64
- @project_router.post("/get")
74
+ @project_router.post(
75
+ "/get",
76
+ response_model=Run,
77
+ )
65
78
  async def get_run(
66
79
  body: GetRunRequest,
67
80
  session: AsyncSession = Depends(get_session),
68
81
  user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectMember()),
69
- ) -> Run:
82
+ ):
70
83
  """
71
84
  Returns a run given `run_name` or `id`.
72
85
  If given `run_name`, does not return deleted runs.
@@ -81,15 +94,18 @@ async def get_run(
81
94
  )
82
95
  if run is None:
83
96
  raise ResourceNotExistsError("Run not found")
84
- return run
97
+ return CustomORJSONResponse(run)
85
98
 
86
99
 
87
- @project_router.post("/get_plan")
100
+ @project_router.post(
101
+ "/get_plan",
102
+ response_model=RunPlan,
103
+ )
88
104
  async def get_plan(
89
105
  body: GetRunPlanRequest,
90
106
  session: AsyncSession = Depends(get_session),
91
107
  user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectMember()),
92
- ) -> RunPlan:
108
+ ):
93
109
  """
94
110
  Returns a run plan for the given run spec.
95
111
  This is an optional step before calling `/apply`.
@@ -102,15 +118,18 @@ async def get_plan(
102
118
  run_spec=body.run_spec,
103
119
  max_offers=body.max_offers,
104
120
  )
105
- return run_plan
121
+ return CustomORJSONResponse(run_plan)
106
122
 
107
123
 
108
- @project_router.post("/apply")
124
+ @project_router.post(
125
+ "/apply",
126
+ response_model=Run,
127
+ )
109
128
  async def apply_plan(
110
129
  body: ApplyRunPlanRequest,
111
130
  session: AsyncSession = Depends(get_session),
112
131
  user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectMember()),
113
- ) -> Run:
132
+ ):
114
133
  """
115
134
  Creates a new run or updates an existing run.
116
135
  Errors if the expected current resource from the plan does not match the current resource.
@@ -118,12 +137,14 @@ async def apply_plan(
118
137
  If the existing run is active and cannot be updated, it must be stopped first.
119
138
  """
120
139
  user, project = user_project
121
- return await runs.apply_plan(
122
- session=session,
123
- user=user,
124
- project=project,
125
- plan=body.plan,
126
- force=body.force,
140
+ return CustomORJSONResponse(
141
+ await runs.apply_plan(
142
+ session=session,
143
+ user=user,
144
+ project=project,
145
+ plan=body.plan,
146
+ force=body.force,
147
+ )
127
148
  )
128
149
 
129
150
 
@@ -14,6 +14,7 @@ from dstack._internal.server.schemas.secrets import (
14
14
  )
15
15
  from dstack._internal.server.security.permissions import ProjectAdmin
16
16
  from dstack._internal.server.services import secrets as secrets_services
17
+ from dstack._internal.server.utils.routers import CustomORJSONResponse
17
18
 
18
19
  router = APIRouter(
19
20
  prefix="/api/project/{project_name}/secrets",
@@ -21,24 +22,26 @@ router = APIRouter(
21
22
  )
22
23
 
23
24
 
24
- @router.post("/list")
25
+ @router.post("/list", response_model=List[Secret])
25
26
  async def list_secrets(
26
27
  session: AsyncSession = Depends(get_session),
27
28
  user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectAdmin()),
28
- ) -> List[Secret]:
29
+ ):
29
30
  _, project = user_project
30
- return await secrets_services.list_secrets(
31
- session=session,
32
- project=project,
31
+ return CustomORJSONResponse(
32
+ await secrets_services.list_secrets(
33
+ session=session,
34
+ project=project,
35
+ )
33
36
  )
34
37
 
35
38
 
36
- @router.post("/get")
39
+ @router.post("/get", response_model=Secret)
37
40
  async def get_secret(
38
41
  body: GetSecretRequest,
39
42
  session: AsyncSession = Depends(get_session),
40
43
  user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectAdmin()),
41
- ) -> Secret:
44
+ ):
42
45
  _, project = user_project
43
46
  secret = await secrets_services.get_secret(
44
47
  session=session,
@@ -47,21 +50,23 @@ async def get_secret(
47
50
  )
48
51
  if secret is None:
49
52
  raise ResourceNotExistsError()
50
- return secret
53
+ return CustomORJSONResponse(secret)
51
54
 
52
55
 
53
- @router.post("/create_or_update")
56
+ @router.post("/create_or_update", response_model=Secret)
54
57
  async def create_or_update_secret(
55
58
  body: CreateOrUpdateSecretRequest,
56
59
  session: AsyncSession = Depends(get_session),
57
60
  user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectAdmin()),
58
- ) -> Secret:
61
+ ):
59
62
  _, project = user_project
60
- return await secrets_services.create_or_update_secret(
61
- session=session,
62
- project=project,
63
- name=body.name,
64
- value=body.value,
63
+ return CustomORJSONResponse(
64
+ await secrets_services.create_or_update_secret(
65
+ session=session,
66
+ project=project,
67
+ name=body.name,
68
+ value=body.value,
69
+ )
65
70
  )
66
71
 
67
72
 
@@ -2,6 +2,7 @@ from fastapi import APIRouter
2
2
 
3
3
  from dstack._internal import settings
4
4
  from dstack._internal.core.models.server import ServerInfo
5
+ from dstack._internal.server.utils.routers import CustomORJSONResponse
5
6
 
6
7
  router = APIRouter(
7
8
  prefix="/api/server",
@@ -9,8 +10,10 @@ router = APIRouter(
9
10
  )
10
11
 
11
12
 
12
- @router.post("/get_info")
13
- async def get_server_info() -> ServerInfo:
14
- return ServerInfo(
15
- server_version=settings.DSTACK_VERSION,
13
+ @router.post("/get_info", response_model=ServerInfo)
14
+ async def get_server_info():
15
+ return CustomORJSONResponse(
16
+ ServerInfo(
17
+ server_version=settings.DSTACK_VERSION,
18
+ )
16
19
  )
@@ -16,7 +16,10 @@ from dstack._internal.server.schemas.users import (
16
16
  )
17
17
  from dstack._internal.server.security.permissions import Authenticated, GlobalAdmin
18
18
  from dstack._internal.server.services import users
19
- from dstack._internal.server.utils.routers import get_base_api_additional_responses
19
+ from dstack._internal.server.utils.routers import (
20
+ CustomORJSONResponse,
21
+ get_base_api_additional_responses,
22
+ )
20
23
 
21
24
  router = APIRouter(
22
25
  prefix="/api/users",
@@ -25,41 +28,41 @@ router = APIRouter(
25
28
  )
26
29
 
27
30
 
28
- @router.post("/list")
31
+ @router.post("/list", response_model=List[User])
29
32
  async def list_users(
30
33
  session: AsyncSession = Depends(get_session),
31
34
  user: UserModel = Depends(Authenticated()),
32
- ) -> List[User]:
33
- return await users.list_users_for_user(session=session, user=user)
35
+ ):
36
+ return CustomORJSONResponse(await users.list_users_for_user(session=session, user=user))
34
37
 
35
38
 
36
- @router.post("/get_my_user")
39
+ @router.post("/get_my_user", response_model=User)
37
40
  async def get_my_user(
38
41
  user: UserModel = Depends(Authenticated()),
39
- ) -> User:
40
- return users.user_model_to_user(user)
42
+ ):
43
+ return CustomORJSONResponse(users.user_model_to_user(user))
41
44
 
42
45
 
43
- @router.post("/get_user")
46
+ @router.post("/get_user", response_model=UserWithCreds)
44
47
  async def get_user(
45
48
  body: GetUserRequest,
46
49
  session: AsyncSession = Depends(get_session),
47
50
  user: UserModel = Depends(Authenticated()),
48
- ) -> UserWithCreds:
51
+ ):
49
52
  res = await users.get_user_with_creds_by_name(
50
53
  session=session, current_user=user, username=body.username
51
54
  )
52
55
  if res is None:
53
56
  raise ResourceNotExistsError()
54
- return res
57
+ return CustomORJSONResponse(res)
55
58
 
56
59
 
57
- @router.post("/create")
60
+ @router.post("/create", response_model=User)
58
61
  async def create_user(
59
62
  body: CreateUserRequest,
60
63
  session: AsyncSession = Depends(get_session),
61
64
  user: UserModel = Depends(GlobalAdmin()),
62
- ) -> User:
65
+ ):
63
66
  res = await users.create_user(
64
67
  session=session,
65
68
  username=body.username,
@@ -67,15 +70,15 @@ async def create_user(
67
70
  email=body.email,
68
71
  active=body.active,
69
72
  )
70
- return users.user_model_to_user(res)
73
+ return CustomORJSONResponse(users.user_model_to_user(res))
71
74
 
72
75
 
73
- @router.post("/update")
76
+ @router.post("/update", response_model=User)
74
77
  async def update_user(
75
78
  body: UpdateUserRequest,
76
79
  session: AsyncSession = Depends(get_session),
77
80
  user: UserModel = Depends(GlobalAdmin()),
78
- ) -> User:
81
+ ):
79
82
  res = await users.update_user(
80
83
  session=session,
81
84
  username=body.username,
@@ -85,19 +88,19 @@ async def update_user(
85
88
  )
86
89
  if res is None:
87
90
  raise ResourceNotExistsError()
88
- return users.user_model_to_user(res)
91
+ return CustomORJSONResponse(users.user_model_to_user(res))
89
92
 
90
93
 
91
- @router.post("/refresh_token")
94
+ @router.post("/refresh_token", response_model=UserWithCreds)
92
95
  async def refresh_token(
93
96
  body: RefreshTokenRequest,
94
97
  session: AsyncSession = Depends(get_session),
95
98
  user: UserModel = Depends(Authenticated()),
96
- ) -> UserWithCreds:
99
+ ):
97
100
  res = await users.refresh_user_token(session=session, user=user, username=body.username)
98
101
  if res is None:
99
102
  raise ResourceNotExistsError()
100
- return users.user_model_to_user_with_creds(res)
103
+ return CustomORJSONResponse(users.user_model_to_user_with_creds(res))
101
104
 
102
105
 
103
106
  @router.post("/delete")
@@ -15,7 +15,10 @@ from dstack._internal.server.schemas.volumes import (
15
15
  ListVolumesRequest,
16
16
  )
17
17
  from dstack._internal.server.security.permissions import Authenticated, ProjectMember
18
- from dstack._internal.server.utils.routers import get_base_api_additional_responses
18
+ from dstack._internal.server.utils.routers import (
19
+ CustomORJSONResponse,
20
+ get_base_api_additional_responses,
21
+ )
19
22
 
20
23
  root_router = APIRouter(
21
24
  prefix="/api/volumes",
@@ -25,12 +28,12 @@ root_router = APIRouter(
25
28
  project_router = APIRouter(prefix="/api/project/{project_name}/volumes", tags=["volumes"])
26
29
 
27
30
 
28
- @root_router.post("/list")
31
+ @root_router.post("/list", response_model=List[Volume])
29
32
  async def list_volumes(
30
33
  body: ListVolumesRequest,
31
34
  session: AsyncSession = Depends(get_session),
32
35
  user: UserModel = Depends(Authenticated()),
33
- ) -> List[Volume]:
36
+ ):
34
37
  """
35
38
  Returns all volumes visible to user sorted by descending `created_at`.
36
39
  `project_name` and `only_active` can be specified as filters.
@@ -38,36 +41,40 @@ async def list_volumes(
38
41
  The results are paginated. To get the next page, pass `created_at` and `id` of
39
42
  the last fleet from the previous page as `prev_created_at` and `prev_id`.
40
43
  """
41
- return await volumes_services.list_volumes(
42
- session=session,
43
- user=user,
44
- project_name=body.project_name,
45
- only_active=body.only_active,
46
- prev_created_at=body.prev_created_at,
47
- prev_id=body.prev_id,
48
- limit=body.limit,
49
- ascending=body.ascending,
44
+ return CustomORJSONResponse(
45
+ await volumes_services.list_volumes(
46
+ session=session,
47
+ user=user,
48
+ project_name=body.project_name,
49
+ only_active=body.only_active,
50
+ prev_created_at=body.prev_created_at,
51
+ prev_id=body.prev_id,
52
+ limit=body.limit,
53
+ ascending=body.ascending,
54
+ )
50
55
  )
51
56
 
52
57
 
53
- @project_router.post("/list")
58
+ @project_router.post("/list", response_model=List[Volume])
54
59
  async def list_project_volumes(
55
60
  session: AsyncSession = Depends(get_session),
56
61
  user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectMember()),
57
- ) -> List[Volume]:
62
+ ):
58
63
  """
59
64
  Returns all volumes in the project.
60
65
  """
61
66
  _, project = user_project
62
- return await volumes_services.list_project_volumes(session=session, project=project)
67
+ return CustomORJSONResponse(
68
+ await volumes_services.list_project_volumes(session=session, project=project)
69
+ )
63
70
 
64
71
 
65
- @project_router.post("/get")
72
+ @project_router.post("/get", response_model=Volume)
66
73
  async def get_volume(
67
74
  body: GetVolumeRequest,
68
75
  session: AsyncSession = Depends(get_session),
69
76
  user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectMember()),
70
- ) -> Volume:
77
+ ):
71
78
  """
72
79
  Returns a volume given a volume name.
73
80
  """
@@ -77,24 +84,26 @@ async def get_volume(
77
84
  )
78
85
  if volume is None:
79
86
  raise ResourceNotExistsError()
80
- return volume
87
+ return CustomORJSONResponse(volume)
81
88
 
82
89
 
83
- @project_router.post("/create")
90
+ @project_router.post("/create", response_model=Volume)
84
91
  async def create_volume(
85
92
  body: CreateVolumeRequest,
86
93
  session: AsyncSession = Depends(get_session),
87
94
  user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectMember()),
88
- ) -> Volume:
95
+ ):
89
96
  """
90
97
  Creates a volume given a volume configuration.
91
98
  """
92
99
  user, project = user_project
93
- return await volumes_services.create_volume(
94
- session=session,
95
- project=project,
96
- user=user,
97
- configuration=body.configuration,
100
+ return CustomORJSONResponse(
101
+ await volumes_services.create_volume(
102
+ session=session,
103
+ project=project,
104
+ user=user,
105
+ configuration=body.configuration,
106
+ )
98
107
  )
99
108
 
100
109
 
@@ -9,8 +9,8 @@ from dstack._internal.core.models.common import CoreModel
9
9
  class PollLogsRequest(CoreModel):
10
10
  run_name: str
11
11
  job_submission_id: UUID4
12
- start_time: Optional[datetime]
13
- end_time: Optional[datetime]
12
+ start_time: Optional[datetime] = None
13
+ end_time: Optional[datetime] = None
14
14
  descending: bool = False
15
15
  next_token: Optional[str] = None
16
16
  limit: int = Field(100, ge=0, le=1000)