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
@@ -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)
@@ -9,12 +9,24 @@ from dstack._internal.core.models.runs import ApplyRunPlanInput, RunSpec
9
9
 
10
10
 
11
11
  class ListRunsRequest(CoreModel):
12
- project_name: Optional[str]
13
- repo_id: Optional[str]
14
- username: Optional[str]
12
+ project_name: Optional[str] = None
13
+ repo_id: Optional[str] = None
14
+ username: Optional[str] = None
15
15
  only_active: bool = False
16
- prev_submitted_at: Optional[datetime]
17
- prev_run_id: Optional[UUID]
16
+ include_jobs: bool = Field(
17
+ True,
18
+ description=("Whether to include `jobs` in the response"),
19
+ )
20
+ job_submissions_limit: Optional[int] = Field(
21
+ None,
22
+ ge=0,
23
+ description=(
24
+ "Limit number of job submissions returned per job to avoid large responses."
25
+ "Drops older job submissions. No effect with `include_jobs: false`"
26
+ ),
27
+ )
28
+ prev_submitted_at: Optional[datetime] = None
29
+ prev_run_id: Optional[UUID] = None
18
30
  limit: int = Field(100, ge=0, le=100)
19
31
  ascending: bool = False
20
32