dstack 0.19.12rc1__py3-none-any.whl → 0.19.13__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 (44) hide show
  1. dstack/_internal/cli/services/configurators/run.py +43 -47
  2. dstack/_internal/cli/utils/run.py +15 -27
  3. dstack/_internal/core/backends/aws/compute.py +22 -9
  4. dstack/_internal/core/backends/aws/resources.py +26 -0
  5. dstack/_internal/core/backends/base/offers.py +0 -1
  6. dstack/_internal/core/backends/template/configurator.py.jinja +1 -6
  7. dstack/_internal/core/backends/template/models.py.jinja +4 -0
  8. dstack/_internal/core/compatibility/__init__.py +0 -0
  9. dstack/_internal/core/compatibility/fleets.py +72 -0
  10. dstack/_internal/core/compatibility/gateways.py +34 -0
  11. dstack/_internal/core/compatibility/runs.py +125 -0
  12. dstack/_internal/core/compatibility/volumes.py +32 -0
  13. dstack/_internal/core/models/configurations.py +1 -1
  14. dstack/_internal/core/models/fleets.py +6 -1
  15. dstack/_internal/core/models/instances.py +51 -12
  16. dstack/_internal/core/models/profiles.py +43 -3
  17. dstack/_internal/core/models/repos/local.py +3 -3
  18. dstack/_internal/core/models/runs.py +118 -44
  19. dstack/_internal/server/app.py +1 -1
  20. dstack/_internal/server/background/tasks/process_running_jobs.py +47 -12
  21. dstack/_internal/server/background/tasks/process_runs.py +14 -1
  22. dstack/_internal/server/services/runner/client.py +4 -1
  23. dstack/_internal/server/services/storage/__init__.py +38 -0
  24. dstack/_internal/server/services/storage/base.py +27 -0
  25. dstack/_internal/server/services/storage/gcs.py +44 -0
  26. dstack/_internal/server/services/{storage.py → storage/s3.py} +4 -27
  27. dstack/_internal/server/settings.py +7 -3
  28. dstack/_internal/server/statics/index.html +1 -1
  29. dstack/_internal/server/statics/{main-5b9786c955b42bf93581.js → main-2066f1f22ddb4557bcde.js} +1677 -46
  30. dstack/_internal/server/statics/{main-5b9786c955b42bf93581.js.map → main-2066f1f22ddb4557bcde.js.map} +1 -1
  31. dstack/_internal/server/statics/{main-8f9c66f404e9c7e7e020.css → main-f39c418b05fe14772dd8.css} +1 -1
  32. dstack/_internal/server/testing/common.py +2 -1
  33. dstack/_internal/utils/common.py +4 -0
  34. dstack/api/server/_fleets.py +9 -69
  35. dstack/api/server/_gateways.py +3 -14
  36. dstack/api/server/_runs.py +4 -116
  37. dstack/api/server/_volumes.py +3 -14
  38. dstack/plugins/builtin/rest_plugin/_plugin.py +24 -5
  39. dstack/version.py +2 -2
  40. {dstack-0.19.12rc1.dist-info → dstack-0.19.13.dist-info}/METADATA +1 -1
  41. {dstack-0.19.12rc1.dist-info → dstack-0.19.13.dist-info}/RECORD +44 -36
  42. {dstack-0.19.12rc1.dist-info → dstack-0.19.13.dist-info}/WHEEL +0 -0
  43. {dstack-0.19.12rc1.dist-info → dstack-0.19.13.dist-info}/entry_points.txt +0 -0
  44. {dstack-0.19.12rc1.dist-info → dstack-0.19.13.dist-info}/licenses/LICENSE.md +0 -0
@@ -308,12 +308,13 @@ async def create_job(
308
308
  ) -> JobModel:
309
309
  run_spec = RunSpec.parse_raw(run.run_spec)
310
310
  job_spec = (await get_job_specs_from_run_spec(run_spec, replica_num=replica_num))[0]
311
+ job_spec.job_num = job_num
311
312
  job = JobModel(
312
313
  project_id=run.project_id,
313
314
  run_id=run.id,
314
315
  run_name=run.run_name,
315
316
  job_num=job_num,
316
- job_name=run.run_name + f"-0-{replica_num}",
317
+ job_name=run.run_name + f"-{job_num}-{replica_num}",
317
318
  replica_num=replica_num,
318
319
  submission_num=submission_num,
319
320
  submitted_at=submitted_at,
@@ -314,3 +314,7 @@ def make_proxy_url(server_url: str, proxy_url: str) -> str:
314
314
  path=concat_url_path(server.path, proxy.path),
315
315
  )
316
316
  return proxy.geturl()
317
+
318
+
319
+ def list_enum_values_for_annotation(enum_class: type[enum.Enum]) -> str:
320
+ return ", ".join(f"`{e.value}`" for e in enum_class)
@@ -1,9 +1,13 @@
1
- from typing import Any, Dict, List, Optional, Union
1
+ from typing import List, Union
2
2
 
3
3
  from pydantic import parse_obj_as
4
4
 
5
+ from dstack._internal.core.compatibility.fleets import (
6
+ get_apply_plan_excludes,
7
+ get_create_fleet_excludes,
8
+ get_get_plan_excludes,
9
+ )
5
10
  from dstack._internal.core.models.fleets import ApplyFleetPlanInput, Fleet, FleetPlan, FleetSpec
6
- from dstack._internal.core.models.instances import Instance
7
11
  from dstack._internal.server.schemas.fleets import (
8
12
  ApplyFleetPlanRequest,
9
13
  CreateFleetRequest,
@@ -34,7 +38,7 @@ class FleetsAPIClient(APIClientGroup):
34
38
  spec: FleetSpec,
35
39
  ) -> FleetPlan:
36
40
  body = GetFleetPlanRequest(spec=spec)
37
- body_json = body.json(exclude=_get_get_plan_excludes(spec))
41
+ body_json = body.json(exclude=get_get_plan_excludes(spec))
38
42
  resp = self._request(f"/api/project/{project_name}/fleets/get_plan", body=body_json)
39
43
  return parse_obj_as(FleetPlan.__response__, resp.json())
40
44
 
@@ -46,7 +50,7 @@ class FleetsAPIClient(APIClientGroup):
46
50
  ) -> Fleet:
47
51
  plan_input = ApplyFleetPlanInput.__response__.parse_obj(plan)
48
52
  body = ApplyFleetPlanRequest(plan=plan_input, force=force)
49
- body_json = body.json(exclude=_get_apply_plan_excludes(plan_input))
53
+ body_json = body.json(exclude=get_apply_plan_excludes(plan_input))
50
54
  resp = self._request(f"/api/project/{project_name}/fleets/apply", body=body_json)
51
55
  return parse_obj_as(Fleet.__response__, resp.json())
52
56
 
@@ -66,70 +70,6 @@ class FleetsAPIClient(APIClientGroup):
66
70
  spec: FleetSpec,
67
71
  ) -> Fleet:
68
72
  body = CreateFleetRequest(spec=spec)
69
- body_json = body.json(exclude=_get_create_fleet_excludes(spec))
73
+ body_json = body.json(exclude=get_create_fleet_excludes(spec))
70
74
  resp = self._request(f"/api/project/{project_name}/fleets/create", body=body_json)
71
75
  return parse_obj_as(Fleet.__response__, resp.json())
72
-
73
-
74
- def _get_get_plan_excludes(fleet_spec: FleetSpec) -> Dict:
75
- get_plan_excludes = {}
76
- spec_excludes = _get_fleet_spec_excludes(fleet_spec)
77
- if spec_excludes:
78
- get_plan_excludes["spec"] = spec_excludes
79
- return get_plan_excludes
80
-
81
-
82
- def _get_apply_plan_excludes(plan_input: ApplyFleetPlanInput) -> Dict:
83
- apply_plan_excludes = {}
84
- spec_excludes = _get_fleet_spec_excludes(plan_input.spec)
85
- if spec_excludes:
86
- apply_plan_excludes["spec"] = apply_plan_excludes
87
- current_resource = plan_input.current_resource
88
- if current_resource is not None:
89
- current_resource_excludes = {}
90
- apply_plan_excludes["current_resource"] = current_resource_excludes
91
- if all(map(_should_exclude_instance_cpu_arch, current_resource.instances)):
92
- current_resource_excludes["instances"] = {
93
- "__all__": {"instance_type": {"resources": {"cpu_arch"}}}
94
- }
95
- return {"plan": apply_plan_excludes}
96
-
97
-
98
- def _should_exclude_instance_cpu_arch(instance: Instance) -> bool:
99
- try:
100
- return instance.instance_type.resources.cpu_arch is None
101
- except AttributeError:
102
- return True
103
-
104
-
105
- def _get_create_fleet_excludes(fleet_spec: FleetSpec) -> Dict:
106
- create_fleet_excludes = {}
107
- spec_excludes = _get_fleet_spec_excludes(fleet_spec)
108
- if spec_excludes:
109
- create_fleet_excludes["spec"] = spec_excludes
110
- return create_fleet_excludes
111
-
112
-
113
- def _get_fleet_spec_excludes(fleet_spec: FleetSpec) -> Optional[Dict]:
114
- """
115
- Returns `fleet_spec` exclude mapping to exclude certain fields from the request.
116
- Use this method to exclude new fields when they are not set to keep
117
- clients backward-compatibility with older servers.
118
- """
119
- spec_excludes: Dict[str, Any] = {}
120
- configuration_excludes: Dict[str, Any] = {}
121
- profile_excludes: set[str] = set()
122
- profile = fleet_spec.profile
123
- if profile.fleets is None:
124
- profile_excludes.add("fleets")
125
- if fleet_spec.configuration.tags is None:
126
- configuration_excludes["tags"] = True
127
- if profile.tags is None:
128
- profile_excludes.add("tags")
129
- if configuration_excludes:
130
- spec_excludes["configuration"] = configuration_excludes
131
- if profile_excludes:
132
- spec_excludes["profile"] = profile_excludes
133
- if spec_excludes:
134
- return spec_excludes
135
- return None
@@ -1,7 +1,8 @@
1
- from typing import Dict, List
1
+ from typing import List
2
2
 
3
3
  from pydantic import parse_obj_as
4
4
 
5
+ from dstack._internal.core.compatibility.gateways import get_create_gateway_excludes
5
6
  from dstack._internal.core.models.gateways import Gateway, GatewayConfiguration
6
7
  from dstack._internal.server.schemas.gateways import (
7
8
  CreateGatewayRequest,
@@ -31,7 +32,7 @@ class GatewaysAPIClient(APIClientGroup):
31
32
  body = CreateGatewayRequest(configuration=configuration)
32
33
  resp = self._request(
33
34
  f"/api/project/{project_name}/gateways/create",
34
- body=body.json(exclude=_get_gateway_configuration_excludes(configuration)),
35
+ body=body.json(exclude=get_create_gateway_excludes(configuration)),
35
36
  )
36
37
  return parse_obj_as(Gateway.__response__, resp.json())
37
38
 
@@ -51,15 +52,3 @@ class GatewaysAPIClient(APIClientGroup):
51
52
  f"/api/project/{project_name}/gateways/set_wildcard_domain", body=body.json()
52
53
  )
53
54
  return parse_obj_as(Gateway.__response__, resp.json())
54
-
55
-
56
- def _get_gateway_configuration_excludes(configuration: GatewayConfiguration) -> Dict:
57
- """
58
- Returns `configuration` exclude mapping to exclude certain fields from the request.
59
- Use this method to exclude new fields when they are not set to keep
60
- clients backward-compatibility with older servers.
61
- """
62
- configuration_excludes = {}
63
- if configuration.tags is None:
64
- configuration_excludes["tags"] = True
65
- return {"configuration": configuration_excludes}
@@ -1,13 +1,12 @@
1
1
  from datetime import datetime
2
- from typing import Any, Dict, List, Optional, Union
2
+ from typing import List, Optional, Union
3
3
  from uuid import UUID
4
4
 
5
5
  from pydantic import parse_obj_as
6
6
 
7
- from dstack._internal.core.models.configurations import ServiceConfiguration
7
+ from dstack._internal.core.compatibility.runs import get_apply_plan_excludes, get_get_plan_excludes
8
8
  from dstack._internal.core.models.runs import (
9
9
  ApplyRunPlanInput,
10
- JobSubmission,
11
10
  Run,
12
11
  RunPlan,
13
12
  RunSpec,
@@ -60,7 +59,7 @@ class RunsAPIClient(APIClientGroup):
60
59
  body = GetRunPlanRequest(run_spec=run_spec, max_offers=max_offers)
61
60
  resp = self._request(
62
61
  f"/api/project/{project_name}/runs/get_plan",
63
- body=body.json(exclude=_get_get_plan_excludes(body)),
62
+ body=body.json(exclude=get_get_plan_excludes(body)),
64
63
  )
65
64
  return parse_obj_as(RunPlan.__response__, resp.json())
66
65
 
@@ -74,7 +73,7 @@ class RunsAPIClient(APIClientGroup):
74
73
  body = ApplyRunPlanRequest(plan=plan_input, force=force)
75
74
  resp = self._request(
76
75
  f"/api/project/{project_name}/runs/apply",
77
- body=body.json(exclude=_get_apply_plan_excludes(plan_input)),
76
+ body=body.json(exclude=get_apply_plan_excludes(plan_input)),
78
77
  )
79
78
  return parse_obj_as(Run.__response__, resp.json())
80
79
 
@@ -85,114 +84,3 @@ class RunsAPIClient(APIClientGroup):
85
84
  def delete(self, project_name: str, runs_names: List[str]):
86
85
  body = DeleteRunsRequest(runs_names=runs_names)
87
86
  self._request(f"/api/project/{project_name}/runs/delete", body=body.json())
88
-
89
-
90
- def _get_apply_plan_excludes(plan: ApplyRunPlanInput) -> Optional[Dict]:
91
- """
92
- Returns `plan` exclude mapping to exclude certain fields from the request.
93
- Use this method to exclude new fields when they are not set to keep
94
- clients backward-compatibility with older servers.
95
- """
96
- apply_plan_excludes = {}
97
- run_spec_excludes = _get_run_spec_excludes(plan.run_spec)
98
- if run_spec_excludes is not None:
99
- apply_plan_excludes["run_spec"] = run_spec_excludes
100
- current_resource = plan.current_resource
101
- if current_resource is not None:
102
- current_resource_excludes = {}
103
- apply_plan_excludes["current_resource"] = current_resource_excludes
104
- current_resource_excludes["run_spec"] = _get_run_spec_excludes(current_resource.run_spec)
105
- job_submissions_excludes = {}
106
- current_resource_excludes["jobs"] = {
107
- "__all__": {"job_submissions": {"__all__": job_submissions_excludes}}
108
- }
109
- job_submissions = [js for j in current_resource.jobs for js in j.job_submissions]
110
- if all(map(_should_exclude_job_submission_jpd_cpu_arch, job_submissions)):
111
- job_submissions_excludes["job_provisioning_data"] = {
112
- "instance_type": {"resources": {"cpu_arch"}}
113
- }
114
- if all(map(_should_exclude_job_submission_jrd_cpu_arch, job_submissions)):
115
- job_submissions_excludes["job_runtime_data"] = {
116
- "offer": {"instance": {"resources": {"cpu_arch"}}}
117
- }
118
- if all(js.exit_status is None for js in job_submissions):
119
- job_submissions_excludes["exit_status"] = True
120
- latest_job_submission = current_resource.latest_job_submission
121
- if latest_job_submission is not None:
122
- latest_job_submission_excludes = {}
123
- current_resource_excludes["latest_job_submission"] = latest_job_submission_excludes
124
- if _should_exclude_job_submission_jpd_cpu_arch(latest_job_submission):
125
- latest_job_submission_excludes["job_provisioning_data"] = {
126
- "instance_type": {"resources": {"cpu_arch"}}
127
- }
128
- if _should_exclude_job_submission_jrd_cpu_arch(latest_job_submission):
129
- latest_job_submission_excludes["job_runtime_data"] = {
130
- "offer": {"instance": {"resources": {"cpu_arch"}}}
131
- }
132
- if latest_job_submission.exit_status is None:
133
- latest_job_submission_excludes["exit_status"] = True
134
- return {"plan": apply_plan_excludes}
135
-
136
-
137
- def _should_exclude_job_submission_jpd_cpu_arch(job_submission: JobSubmission) -> bool:
138
- try:
139
- return job_submission.job_provisioning_data.instance_type.resources.cpu_arch is None
140
- except AttributeError:
141
- return True
142
-
143
-
144
- def _should_exclude_job_submission_jrd_cpu_arch(job_submission: JobSubmission) -> bool:
145
- try:
146
- return job_submission.job_runtime_data.offer.instance.resources.cpu_arch is None
147
- except AttributeError:
148
- return True
149
-
150
-
151
- def _get_get_plan_excludes(request: GetRunPlanRequest) -> Optional[Dict]:
152
- """
153
- Excludes new fields when they are not set to keep
154
- clients backward-compatibility with older servers.
155
- """
156
- get_plan_excludes = {}
157
- run_spec_excludes = _get_run_spec_excludes(request.run_spec)
158
- if run_spec_excludes is not None:
159
- get_plan_excludes["run_spec"] = run_spec_excludes
160
- if request.max_offers is None:
161
- get_plan_excludes["max_offers"] = True
162
- return get_plan_excludes
163
-
164
-
165
- def _get_run_spec_excludes(run_spec: RunSpec) -> Optional[Dict]:
166
- """
167
- Returns `run_spec` exclude mapping to exclude certain fields from the request.
168
- Use this method to exclude new fields when they are not set to keep
169
- clients backward-compatibility with older servers.
170
- """
171
- spec_excludes: dict[str, Any] = {}
172
- configuration_excludes: dict[str, Any] = {}
173
- profile_excludes: set[str] = set()
174
- configuration = run_spec.configuration
175
- profile = run_spec.profile
176
-
177
- if configuration.fleets is None:
178
- configuration_excludes["fleets"] = True
179
- if profile is not None and profile.fleets is None:
180
- profile_excludes.add("fleets")
181
- if configuration.tags is None:
182
- configuration_excludes["tags"] = True
183
- if profile is not None and profile.tags is None:
184
- profile_excludes.add("tags")
185
- if isinstance(configuration, ServiceConfiguration) and not configuration.rate_limits:
186
- configuration_excludes["rate_limits"] = True
187
- if configuration.shell is None:
188
- configuration_excludes["shell"] = True
189
- if configuration.priority is None:
190
- configuration_excludes["priority"] = True
191
-
192
- if configuration_excludes:
193
- spec_excludes["configuration"] = configuration_excludes
194
- if profile_excludes:
195
- spec_excludes["profile"] = profile_excludes
196
- if spec_excludes:
197
- return spec_excludes
198
- return None
@@ -1,7 +1,8 @@
1
- from typing import Dict, List
1
+ from typing import List
2
2
 
3
3
  from pydantic import parse_obj_as
4
4
 
5
+ from dstack._internal.core.compatibility.volumes import get_create_volume_excludes
5
6
  from dstack._internal.core.models.volumes import Volume, VolumeConfiguration
6
7
  from dstack._internal.server.schemas.volumes import (
7
8
  CreateVolumeRequest,
@@ -29,22 +30,10 @@ class VolumesAPIClient(APIClientGroup):
29
30
  body = CreateVolumeRequest(configuration=configuration)
30
31
  resp = self._request(
31
32
  f"/api/project/{project_name}/volumes/create",
32
- body=body.json(exclude=_get_volume_configuration_excludes(configuration)),
33
+ body=body.json(exclude=get_create_volume_excludes(configuration)),
33
34
  )
34
35
  return parse_obj_as(Volume.__response__, resp.json())
35
36
 
36
37
  def delete(self, project_name: str, names: List[str]) -> None:
37
38
  body = DeleteVolumesRequest(names=names)
38
39
  self._request(f"/api/project/{project_name}/volumes/delete", body=body.json())
39
-
40
-
41
- def _get_volume_configuration_excludes(configuration: VolumeConfiguration) -> Dict:
42
- """
43
- Returns `configuration` exclude mapping to exclude certain fields from the request.
44
- Use this method to exclude new fields when they are not set to keep
45
- clients backward-compatibility with older servers.
46
- """
47
- configuration_excludes = {}
48
- if configuration.tags is None:
49
- configuration_excludes["tags"] = True
50
- return {"configuration": configuration_excludes}
@@ -1,10 +1,14 @@
1
1
  import json
2
2
  import os
3
- from typing import Type
3
+ from typing import Dict, Optional, Type
4
4
 
5
5
  import requests
6
6
  from pydantic import ValidationError
7
7
 
8
+ from dstack._internal.core.compatibility.fleets import get_fleet_spec_excludes
9
+ from dstack._internal.core.compatibility.gateways import get_gateway_spec_excludes
10
+ from dstack._internal.core.compatibility.runs import get_run_spec_excludes
11
+ from dstack._internal.core.compatibility.volumes import get_volume_spec_excludes
8
12
  from dstack._internal.core.errors import ServerClientError
9
13
  from dstack._internal.core.models.fleets import FleetSpec
10
14
  from dstack._internal.core.models.gateways import GatewaySpec
@@ -44,12 +48,17 @@ class CustomApplyPolicy(ApplyPolicy):
44
48
  logger.error(f"Plugin service rejected apply request: {response.error}")
45
49
  raise ServerClientError(f"Apply request rejected: {response.error}")
46
50
 
47
- def _call_plugin_service(self, spec_request: SpecApplyRequest, endpoint: str) -> ApplySpec:
51
+ def _call_plugin_service(
52
+ self,
53
+ spec_request: SpecApplyRequest,
54
+ endpoint: str,
55
+ excludes: Optional[Dict],
56
+ ) -> ApplySpec:
48
57
  response = None
49
58
  try:
50
59
  response = requests.post(
51
60
  f"{self._plugin_service_uri}{endpoint}",
52
- json=spec_request.dict(),
61
+ json=spec_request.dict(exclude={"spec": excludes}),
53
62
  headers={"accept": "application/json", "Content-Type": "application/json"},
54
63
  timeout=PLUGIN_REQUEST_TIMEOUT_SEC,
55
64
  )
@@ -75,10 +84,11 @@ class CustomApplyPolicy(ApplyPolicy):
75
84
  user: str,
76
85
  project: str,
77
86
  spec: ApplySpec,
87
+ excludes: Optional[Dict] = None,
78
88
  ) -> ApplySpec:
79
89
  try:
80
90
  spec_request = request_cls(user=user, project=project, spec=spec)
81
- spec_json = self._call_plugin_service(spec_request, endpoint)
91
+ spec_json = self._call_plugin_service(spec_request, endpoint, excludes)
82
92
  response = response_cls(**spec_json)
83
93
  self._check_request_rejected(response)
84
94
  return response.spec
@@ -88,7 +98,13 @@ class CustomApplyPolicy(ApplyPolicy):
88
98
 
89
99
  def on_run_apply(self, user: str, project: str, spec: RunSpec) -> RunSpec:
90
100
  return self._on_apply(
91
- RunSpecRequest, RunSpecResponse, "/apply_policies/on_run_apply", user, project, spec
101
+ RunSpecRequest,
102
+ RunSpecResponse,
103
+ "/apply_policies/on_run_apply",
104
+ user,
105
+ project,
106
+ spec,
107
+ excludes=get_run_spec_excludes(spec),
92
108
  )
93
109
 
94
110
  def on_fleet_apply(self, user: str, project: str, spec: FleetSpec) -> FleetSpec:
@@ -99,6 +115,7 @@ class CustomApplyPolicy(ApplyPolicy):
99
115
  user,
100
116
  project,
101
117
  spec,
118
+ excludes=get_fleet_spec_excludes(spec),
102
119
  )
103
120
 
104
121
  def on_volume_apply(self, user: str, project: str, spec: VolumeSpec) -> VolumeSpec:
@@ -109,6 +126,7 @@ class CustomApplyPolicy(ApplyPolicy):
109
126
  user,
110
127
  project,
111
128
  spec,
129
+ excludes=get_volume_spec_excludes(spec),
112
130
  )
113
131
 
114
132
  def on_gateway_apply(self, user: str, project: str, spec: GatewaySpec) -> GatewaySpec:
@@ -119,6 +137,7 @@ class CustomApplyPolicy(ApplyPolicy):
119
137
  user,
120
138
  project,
121
139
  spec,
140
+ excludes=get_gateway_spec_excludes(spec),
122
141
  )
123
142
 
124
143
 
dstack/version.py CHANGED
@@ -1,3 +1,3 @@
1
- __version__ = "0.19.12rc1"
1
+ __version__ = "0.19.13"
2
2
  __is_release__ = True
3
- base_image = "0.8"
3
+ base_image = "0.9"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dstack
3
- Version: 0.19.12rc1
3
+ Version: 0.19.13
4
4
  Summary: dstack is an open-source orchestration engine for running AI workloads on any cloud or on-premises.
5
5
  Project-URL: Homepage, https://dstack.ai
6
6
  Project-URL: Source, https://github.com/dstackai/dstack