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.

Files changed (69) hide show
  1. dstack/_internal/cli/services/configurators/fleet.py +99 -1
  2. dstack/_internal/cli/services/profile.py +1 -1
  3. dstack/_internal/core/compatibility/runs.py +12 -1
  4. dstack/_internal/core/compatibility/volumes.py +2 -0
  5. dstack/_internal/core/models/common.py +38 -2
  6. dstack/_internal/core/models/configurations.py +9 -1
  7. dstack/_internal/core/models/fleets.py +2 -1
  8. dstack/_internal/core/models/profiles.py +8 -5
  9. dstack/_internal/core/models/resources.py +15 -8
  10. dstack/_internal/core/models/runs.py +41 -138
  11. dstack/_internal/core/models/volumes.py +14 -0
  12. dstack/_internal/core/services/diff.py +30 -10
  13. dstack/_internal/core/services/ssh/attach.py +2 -0
  14. dstack/_internal/server/app.py +17 -9
  15. dstack/_internal/server/background/__init__.py +5 -3
  16. dstack/_internal/server/background/tasks/process_gateways.py +46 -28
  17. dstack/_internal/server/background/tasks/process_idle_volumes.py +139 -0
  18. dstack/_internal/server/background/tasks/process_submitted_jobs.py +2 -0
  19. dstack/_internal/server/migrations/versions/35e90e1b0d3e_add_rolling_deployment_fields.py +6 -6
  20. dstack/_internal/server/migrations/versions/d5863798bf41_add_volumemodel_last_job_processed_at.py +40 -0
  21. dstack/_internal/server/models.py +1 -0
  22. dstack/_internal/server/routers/backends.py +23 -16
  23. dstack/_internal/server/routers/files.py +7 -6
  24. dstack/_internal/server/routers/fleets.py +47 -36
  25. dstack/_internal/server/routers/gateways.py +27 -18
  26. dstack/_internal/server/routers/instances.py +18 -13
  27. dstack/_internal/server/routers/logs.py +7 -3
  28. dstack/_internal/server/routers/metrics.py +14 -8
  29. dstack/_internal/server/routers/projects.py +33 -22
  30. dstack/_internal/server/routers/repos.py +7 -6
  31. dstack/_internal/server/routers/runs.py +49 -28
  32. dstack/_internal/server/routers/secrets.py +20 -15
  33. dstack/_internal/server/routers/server.py +7 -4
  34. dstack/_internal/server/routers/users.py +22 -19
  35. dstack/_internal/server/routers/volumes.py +34 -25
  36. dstack/_internal/server/schemas/logs.py +2 -2
  37. dstack/_internal/server/schemas/runs.py +17 -5
  38. dstack/_internal/server/services/fleets.py +354 -72
  39. dstack/_internal/server/services/gateways/__init__.py +13 -4
  40. dstack/_internal/server/services/gateways/client.py +5 -3
  41. dstack/_internal/server/services/instances.py +8 -0
  42. dstack/_internal/server/services/jobs/__init__.py +45 -0
  43. dstack/_internal/server/services/jobs/configurators/base.py +7 -0
  44. dstack/_internal/server/services/locking.py +3 -1
  45. dstack/_internal/server/services/logging.py +4 -2
  46. dstack/_internal/server/services/logs/__init__.py +15 -2
  47. dstack/_internal/server/services/logs/aws.py +2 -4
  48. dstack/_internal/server/services/logs/filelog.py +33 -27
  49. dstack/_internal/server/services/logs/gcp.py +3 -5
  50. dstack/_internal/server/services/proxy/repo.py +4 -1
  51. dstack/_internal/server/services/runs.py +115 -32
  52. dstack/_internal/server/services/services/__init__.py +2 -1
  53. dstack/_internal/server/services/users.py +3 -1
  54. dstack/_internal/server/services/volumes.py +13 -0
  55. dstack/_internal/server/settings.py +7 -2
  56. dstack/_internal/server/statics/index.html +1 -1
  57. dstack/_internal/server/statics/{main-d1ac2e8c38ed5f08a114.js → main-64f8273740c4b52c18f5.js} +6 -6
  58. dstack/_internal/server/statics/{main-d1ac2e8c38ed5f08a114.js.map → main-64f8273740c4b52c18f5.js.map} +1 -1
  59. dstack/_internal/server/testing/common.py +41 -5
  60. dstack/_internal/server/utils/routers.py +31 -8
  61. dstack/_internal/utils/json_utils.py +54 -0
  62. dstack/api/_public/runs.py +13 -2
  63. dstack/api/server/_runs.py +12 -2
  64. dstack/version.py +1 -1
  65. {dstack-0.19.18.dist-info → dstack-0.19.19.dist-info}/METADATA +7 -5
  66. {dstack-0.19.18.dist-info → dstack-0.19.19.dist-info}/RECORD +69 -66
  67. {dstack-0.19.18.dist-info → dstack-0.19.19.dist-info}/WHEEL +0 -0
  68. {dstack-0.19.18.dist-info → dstack-0.19.19.dist-info}/entry_points.txt +0 -0
  69. {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
- ) -> FileArchive:
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
- ) -> FileArchive:
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 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/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
- ) -> List[Fleet]:
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 await fleets_services.list_fleets(
49
- session=session,
50
- user=user,
51
- project_name=body.project_name,
52
- only_active=body.only_active,
53
- prev_created_at=body.prev_created_at,
54
- prev_id=body.prev_id,
55
- limit=body.limit,
56
- ascending=body.ascending,
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
- ) -> List[Fleet]:
69
+ ):
65
70
  """
66
71
  Returns all fleets in the project.
67
72
  """
68
73
  _, project = user_project
69
- return await fleets_services.list_project_fleets(session=session, project=project)
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
- ) -> Fleet:
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
- ) -> FleetPlan:
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
- ) -> Fleet:
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 await fleets_services.apply_plan(
124
- session=session,
125
- user=user,
126
- project=project,
127
- plan=body.plan,
128
- force=body.force,
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
- ) -> Fleet:
146
+ ):
138
147
  """
139
148
  Creates a fleet given a fleet configuration.
140
149
  """
141
150
  user, project = user_project
142
- return await fleets_services.create_fleet(
143
- session=session,
144
- project=project,
145
- user=user,
146
- spec=body.spec,
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 get_base_api_additional_responses
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
- ) -> List[models.Gateway]:
32
+ ):
30
33
  _, project = user_project
31
- return await gateways.list_project_gateways(session=session, project=project)
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
- ) -> models.Gateway:
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
- ) -> models.Gateway:
57
+ ):
53
58
  user, project = user_project
54
- return await gateways.create_gateway(
55
- session=session,
56
- user=user,
57
- project=project,
58
- configuration=body.configuration,
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
- ) -> models.Gateway:
98
+ ):
92
99
  _, project = user_project
93
- return await gateways.set_gateway_wildcard_domain(
94
- session=session, project=project, name=body.name, wildcard_domain=body.wildcard_domain
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 get_base_api_additional_responses
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
- ) -> List[Instance]:
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 await instances.list_user_instances(
35
- session=session,
36
- user=user,
37
- project_names=body.project_names,
38
- fleet_ids=body.fleet_ids,
39
- only_active=body.only_active,
40
- prev_created_at=body.prev_created_at,
41
- prev_id=body.prev_id,
42
- limit=body.limit,
43
- ascending=body.ascending,
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 get_base_api_additional_responses
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
- ) -> JobSubmissionLogs:
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 get_base_api_additional_responses
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
- ) -> JobMetrics:
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 await metrics.get_job_metrics(
67
- session=session,
68
- job_model=job_model,
69
- limit=limit,
70
- after=after,
71
- before=before,
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 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")