dstack 0.19.9__py3-none-any.whl → 0.19.11__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 (53) hide show
  1. dstack/_internal/cli/commands/config.py +1 -1
  2. dstack/_internal/cli/commands/metrics.py +25 -10
  3. dstack/_internal/cli/commands/offer.py +2 -0
  4. dstack/_internal/cli/commands/project.py +161 -0
  5. dstack/_internal/cli/commands/ps.py +9 -2
  6. dstack/_internal/cli/main.py +2 -0
  7. dstack/_internal/cli/services/configurators/run.py +1 -1
  8. dstack/_internal/cli/utils/updates.py +13 -1
  9. dstack/_internal/core/backends/aws/compute.py +21 -9
  10. dstack/_internal/core/backends/azure/compute.py +8 -3
  11. dstack/_internal/core/backends/base/compute.py +9 -4
  12. dstack/_internal/core/backends/gcp/compute.py +43 -20
  13. dstack/_internal/core/backends/gcp/resources.py +18 -2
  14. dstack/_internal/core/backends/local/compute.py +4 -2
  15. dstack/_internal/core/models/configurations.py +21 -4
  16. dstack/_internal/core/models/runs.py +2 -1
  17. dstack/_internal/proxy/gateway/resources/nginx/00-log-format.conf +11 -1
  18. dstack/_internal/proxy/gateway/resources/nginx/service.jinja2 +12 -6
  19. dstack/_internal/proxy/gateway/services/stats.py +17 -3
  20. dstack/_internal/server/background/tasks/process_metrics.py +23 -21
  21. dstack/_internal/server/background/tasks/process_submitted_jobs.py +24 -15
  22. dstack/_internal/server/migrations/versions/bca2fdf130bf_add_runmodel_priority.py +34 -0
  23. dstack/_internal/server/models.py +1 -0
  24. dstack/_internal/server/routers/repos.py +13 -4
  25. dstack/_internal/server/services/fleets.py +2 -2
  26. dstack/_internal/server/services/gateways/__init__.py +1 -1
  27. dstack/_internal/server/services/instances.py +6 -2
  28. dstack/_internal/server/services/jobs/__init__.py +4 -4
  29. dstack/_internal/server/services/jobs/configurators/base.py +18 -4
  30. dstack/_internal/server/services/jobs/configurators/extensions/cursor.py +3 -1
  31. dstack/_internal/server/services/jobs/configurators/extensions/vscode.py +3 -1
  32. dstack/_internal/server/services/plugins.py +64 -32
  33. dstack/_internal/server/services/runs.py +33 -20
  34. dstack/_internal/server/services/volumes.py +1 -1
  35. dstack/_internal/server/settings.py +1 -0
  36. dstack/_internal/server/statics/index.html +1 -1
  37. dstack/_internal/server/statics/{main-b4f65323f5df007e1664.js → main-5b9786c955b42bf93581.js} +8 -8
  38. dstack/_internal/server/statics/{main-b4f65323f5df007e1664.js.map → main-5b9786c955b42bf93581.js.map} +1 -1
  39. dstack/_internal/server/testing/common.py +2 -0
  40. dstack/_internal/server/utils/routers.py +3 -6
  41. dstack/_internal/settings.py +4 -0
  42. dstack/api/_public/runs.py +6 -3
  43. dstack/api/server/_runs.py +2 -0
  44. dstack/plugins/builtin/__init__.py +0 -0
  45. dstack/plugins/builtin/rest_plugin/__init__.py +18 -0
  46. dstack/plugins/builtin/rest_plugin/_models.py +48 -0
  47. dstack/plugins/builtin/rest_plugin/_plugin.py +127 -0
  48. dstack/version.py +2 -2
  49. {dstack-0.19.9.dist-info → dstack-0.19.11.dist-info}/METADATA +10 -6
  50. {dstack-0.19.9.dist-info → dstack-0.19.11.dist-info}/RECORD +53 -47
  51. {dstack-0.19.9.dist-info → dstack-0.19.11.dist-info}/WHEEL +0 -0
  52. {dstack-0.19.9.dist-info → dstack-0.19.11.dist-info}/entry_points.txt +0 -0
  53. {dstack-0.19.9.dist-info → dstack-0.19.11.dist-info}/licenses/LICENSE.md +0 -0
@@ -262,6 +262,7 @@ async def create_run(
262
262
  run_spec: Optional[RunSpec] = None,
263
263
  run_id: Optional[UUID] = None,
264
264
  deleted: bool = False,
265
+ priority: int = 0,
265
266
  ) -> RunModel:
266
267
  if run_spec is None:
267
268
  run_spec = get_run_spec(
@@ -282,6 +283,7 @@ async def create_run(
282
283
  run_spec=run_spec.json(),
283
284
  last_processed_at=submitted_at,
284
285
  jobs=[],
286
+ priority=priority,
285
287
  )
286
288
  session.add(run)
287
289
  await session.commit()
@@ -93,13 +93,10 @@ def get_server_client_error_details(error: ServerClientError) -> List[Dict]:
93
93
  return details
94
94
 
95
95
 
96
- def request_size_exceeded(request: Request, limit: int) -> bool:
96
+ def get_request_size(request: Request) -> int:
97
97
  if "content-length" not in request.headers:
98
- return True
99
- content_length = int(request.headers["content-length"])
100
- if content_length > limit:
101
- return True
102
- return False
98
+ return 0
99
+ return int(request.headers["content-length"])
103
100
 
104
101
 
105
102
  def check_client_server_compatibility(
@@ -12,6 +12,10 @@ DSTACK_RELEASE = os.getenv("DSTACK_RELEASE") is not None or version.__is_release
12
12
  DSTACK_USE_LATEST_FROM_BRANCH = os.getenv("DSTACK_USE_LATEST_FROM_BRANCH") is not None
13
13
 
14
14
 
15
+ DSTACK_BASE_IMAGE = os.getenv("DSTACK_BASE_IMAGE", "dstackai/base")
16
+ DSTACK_BASE_IMAGE_VERSION = os.getenv("DSTACK_BASE_IMAGE_VERSION", version.base_image)
17
+
18
+
15
19
  class FeatureFlags:
16
20
  """
17
21
  dstack feature flags. Feature flags are temporary and can be used when developing
@@ -691,29 +691,32 @@ class RunCollection:
691
691
  logger.warning("The exec_plan() method is deprecated in favor of apply_plan().")
692
692
  return self.apply_plan(run_plan=run_plan, repo=repo, reserve_ports=reserve_ports)
693
693
 
694
- def list(self, all: bool = False) -> List[Run]:
694
+ def list(self, all: bool = False, limit: Optional[int] = None) -> List[Run]:
695
695
  """
696
696
  List runs.
697
697
 
698
698
  Args:
699
699
  all: Show all runs (active and finished) if `True`.
700
+ limit: Limit the number of runs to return. Must be less than 100.
700
701
 
701
702
  Returns:
702
703
  List of runs.
703
704
  """
704
705
  # Return only one page of latest runs (<=100). Returning all the pages may be costly.
705
706
  # TODO: Consider introducing `since` filter with a reasonable default.
706
- only_active = not all
707
+ only_active = not all and limit is None
707
708
  runs = self._api_client.runs.list(
708
709
  project_name=self._project,
709
710
  repo_id=None,
710
711
  only_active=only_active,
712
+ limit=limit or 100,
711
713
  )
712
714
  if only_active and len(runs) == 0:
713
715
  runs = self._api_client.runs.list(
714
716
  project_name=self._project,
715
717
  repo_id=None,
716
- )[:1]
718
+ limit=1,
719
+ )
717
720
  return [self._model_to_run(run) for run in runs]
718
721
 
719
722
  def get(self, run_name: str) -> Optional[Run]:
@@ -186,6 +186,8 @@ def _get_run_spec_excludes(run_spec: RunSpec) -> Optional[Dict]:
186
186
  configuration_excludes["rate_limits"] = True
187
187
  if configuration.shell is None:
188
188
  configuration_excludes["shell"] = True
189
+ if configuration.priority is None:
190
+ configuration_excludes["priority"] = True
189
191
 
190
192
  if configuration_excludes:
191
193
  spec_excludes["configuration"] = configuration_excludes
File without changes
@@ -0,0 +1,18 @@
1
+ # ruff: noqa: F401
2
+ from dstack.plugins.builtin.rest_plugin._models import (
3
+ FleetSpecRequest,
4
+ FleetSpecResponse,
5
+ GatewaySpecRequest,
6
+ GatewaySpecResponse,
7
+ RunSpecRequest,
8
+ RunSpecResponse,
9
+ SpecApplyRequest,
10
+ SpecApplyResponse,
11
+ VolumeSpecRequest,
12
+ VolumeSpecResponse,
13
+ )
14
+ from dstack.plugins.builtin.rest_plugin._plugin import (
15
+ PLUGIN_SERVICE_URI_ENV_VAR_NAME,
16
+ CustomApplyPolicy,
17
+ RESTPlugin,
18
+ )
@@ -0,0 +1,48 @@
1
+ from typing import Generic, Optional, TypeVar
2
+
3
+ from pydantic import BaseModel, Field
4
+ from typing_extensions import Annotated
5
+
6
+ from dstack._internal.core.models.fleets import FleetSpec
7
+ from dstack._internal.core.models.gateways import GatewaySpec
8
+ from dstack._internal.core.models.runs import RunSpec
9
+ from dstack._internal.core.models.volumes import VolumeSpec
10
+
11
+ SpecType = TypeVar("SpecType", RunSpec, FleetSpec, VolumeSpec, GatewaySpec)
12
+
13
+
14
+ class SpecApplyRequest(BaseModel, Generic[SpecType]):
15
+ user: Annotated[str, Field(description="The name of the user making the apply request")]
16
+ project: Annotated[str, Field(description="The name of the project the request is for")]
17
+ spec: Annotated[SpecType, Field(description="The spec to be applied")]
18
+
19
+ # Override dict() to remove __orig_class__ attribute and avoid "TypeError: Object of type _GenericAlias is not JSON serializable"
20
+ # error. This issue doesn't happen though when running the code in pytest, only when running the server.
21
+ def dict(self, *args, **kwargs):
22
+ d = super().dict(*args, **kwargs)
23
+ d.pop("__orig_class__", None)
24
+ return d
25
+
26
+
27
+ RunSpecRequest = SpecApplyRequest[RunSpec]
28
+ FleetSpecRequest = SpecApplyRequest[FleetSpec]
29
+ VolumeSpecRequest = SpecApplyRequest[VolumeSpec]
30
+ GatewaySpecRequest = SpecApplyRequest[GatewaySpec]
31
+
32
+
33
+ class SpecApplyResponse(BaseModel, Generic[SpecType]):
34
+ spec: Annotated[
35
+ SpecType,
36
+ Field(
37
+ description="The spec to apply, original spec if error otherwise original or mutated by plugin service if approved"
38
+ ),
39
+ ]
40
+ error: Annotated[
41
+ Optional[str], Field(description="Error message if request is rejected", min_length=1)
42
+ ] = None
43
+
44
+
45
+ RunSpecResponse = SpecApplyResponse[RunSpec]
46
+ FleetSpecResponse = SpecApplyResponse[FleetSpec]
47
+ VolumeSpecResponse = SpecApplyResponse[VolumeSpec]
48
+ GatewaySpecResponse = SpecApplyResponse[GatewaySpec]
@@ -0,0 +1,127 @@
1
+ import json
2
+ import os
3
+ from typing import Type
4
+
5
+ import requests
6
+ from pydantic import ValidationError
7
+
8
+ from dstack._internal.core.errors import ServerClientError
9
+ from dstack._internal.core.models.fleets import FleetSpec
10
+ from dstack._internal.core.models.gateways import GatewaySpec
11
+ from dstack._internal.core.models.volumes import VolumeSpec
12
+ from dstack.plugins import ApplyPolicy, ApplySpec, Plugin, RunSpec, get_plugin_logger
13
+ from dstack.plugins.builtin.rest_plugin import (
14
+ FleetSpecRequest,
15
+ FleetSpecResponse,
16
+ GatewaySpecRequest,
17
+ GatewaySpecResponse,
18
+ RunSpecRequest,
19
+ RunSpecResponse,
20
+ SpecApplyRequest,
21
+ SpecApplyResponse,
22
+ VolumeSpecRequest,
23
+ VolumeSpecResponse,
24
+ )
25
+
26
+ logger = get_plugin_logger(__name__)
27
+
28
+ PLUGIN_SERVICE_URI_ENV_VAR_NAME = "DSTACK_PLUGIN_SERVICE_URI"
29
+ PLUGIN_REQUEST_TIMEOUT_SEC = 8
30
+
31
+
32
+ class CustomApplyPolicy(ApplyPolicy):
33
+ def __init__(self):
34
+ self._plugin_service_uri = os.getenv(PLUGIN_SERVICE_URI_ENV_VAR_NAME)
35
+ logger.info(f"Found plugin service at {self._plugin_service_uri}")
36
+ if not self._plugin_service_uri:
37
+ logger.error(
38
+ f"Cannot create policy because {PLUGIN_SERVICE_URI_ENV_VAR_NAME} is not set"
39
+ )
40
+ raise ServerClientError(f"{PLUGIN_SERVICE_URI_ENV_VAR_NAME} is not set")
41
+
42
+ def _check_request_rejected(self, response: SpecApplyResponse):
43
+ if response.error is not None:
44
+ logger.error(f"Plugin service rejected apply request: {response.error}")
45
+ raise ServerClientError(f"Apply request rejected: {response.error}")
46
+
47
+ def _call_plugin_service(self, spec_request: SpecApplyRequest, endpoint: str) -> ApplySpec:
48
+ response = None
49
+ try:
50
+ response = requests.post(
51
+ f"{self._plugin_service_uri}{endpoint}",
52
+ json=spec_request.dict(),
53
+ headers={"accept": "application/json", "Content-Type": "application/json"},
54
+ timeout=PLUGIN_REQUEST_TIMEOUT_SEC,
55
+ )
56
+ response.raise_for_status()
57
+ spec_json = json.loads(response.text)
58
+ return spec_json
59
+ except requests.exceptions.ConnectionError as e:
60
+ logger.error(
61
+ f"Could not connect to plugin service at {self._plugin_service_uri}: %s", e
62
+ )
63
+ raise ServerClientError(
64
+ f"Could not connect to plugin service at {self._plugin_service_uri}"
65
+ )
66
+ except requests.RequestException as e:
67
+ logger.error("Request to the plugin service failed: %s", e)
68
+ raise ServerClientError("Request to the plugin service failed")
69
+
70
+ def _on_apply(
71
+ self,
72
+ request_cls: Type[SpecApplyRequest],
73
+ response_cls: Type[SpecApplyResponse],
74
+ endpoint: str,
75
+ user: str,
76
+ project: str,
77
+ spec: ApplySpec,
78
+ ) -> ApplySpec:
79
+ try:
80
+ spec_request = request_cls(user=user, project=project, spec=spec)
81
+ spec_json = self._call_plugin_service(spec_request, endpoint)
82
+ response = response_cls(**spec_json)
83
+ self._check_request_rejected(response)
84
+ return response.spec
85
+ except ValidationError:
86
+ logger.error(f"Plugin service returned invalid response:\n{spec_json}")
87
+ raise ServerClientError("Plugin service returned an invalid response")
88
+
89
+ def on_run_apply(self, user: str, project: str, spec: RunSpec) -> RunSpec:
90
+ return self._on_apply(
91
+ RunSpecRequest, RunSpecResponse, "/apply_policies/on_run_apply", user, project, spec
92
+ )
93
+
94
+ def on_fleet_apply(self, user: str, project: str, spec: FleetSpec) -> FleetSpec:
95
+ return self._on_apply(
96
+ FleetSpecRequest,
97
+ FleetSpecResponse,
98
+ "/apply_policies/on_fleet_apply",
99
+ user,
100
+ project,
101
+ spec,
102
+ )
103
+
104
+ def on_volume_apply(self, user: str, project: str, spec: VolumeSpec) -> VolumeSpec:
105
+ return self._on_apply(
106
+ VolumeSpecRequest,
107
+ VolumeSpecResponse,
108
+ "/apply_policies/on_volume_apply",
109
+ user,
110
+ project,
111
+ spec,
112
+ )
113
+
114
+ def on_gateway_apply(self, user: str, project: str, spec: GatewaySpec) -> GatewaySpec:
115
+ return self._on_apply(
116
+ GatewaySpecRequest,
117
+ GatewaySpecResponse,
118
+ "/apply_policies/on_gateway_apply",
119
+ user,
120
+ project,
121
+ spec,
122
+ )
123
+
124
+
125
+ class RESTPlugin(Plugin):
126
+ def get_apply_policies(self) -> list[ApplyPolicy]:
127
+ return [CustomApplyPolicy()]
dstack/version.py CHANGED
@@ -1,3 +1,3 @@
1
- __version__ = "0.19.9"
1
+ __version__ = "0.19.11"
2
2
  __is_release__ = True
3
- base_image = "0.7"
3
+ base_image = "0.8"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dstack
3
- Version: 0.19.9
3
+ Version: 0.19.11
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
@@ -21,7 +21,7 @@ Requires-Dist: cryptography
21
21
  Requires-Dist: cursor
22
22
  Requires-Dist: filelock
23
23
  Requires-Dist: gitpython
24
- Requires-Dist: gpuhunt==0.1.5
24
+ Requires-Dist: gpuhunt==0.1.6
25
25
  Requires-Dist: jsonschema
26
26
  Requires-Dist: packaging
27
27
  Requires-Dist: paramiko>=3.2.0
@@ -56,6 +56,7 @@ Requires-Dist: azure-mgmt-subscription>=3.1.1; extra == 'all'
56
56
  Requires-Dist: backports-entry-points-selectable; extra == 'all'
57
57
  Requires-Dist: boto3>=1.38.13; extra == 'all'
58
58
  Requires-Dist: botocore; extra == 'all'
59
+ Requires-Dist: cryptography>=44.0.3; extra == 'all'
59
60
  Requires-Dist: datacrunch; extra == 'all'
60
61
  Requires-Dist: docker>=6.0.0; extra == 'all'
61
62
  Requires-Dist: fastapi; extra == 'all'
@@ -71,8 +72,9 @@ Requires-Dist: httpx; extra == 'all'
71
72
  Requires-Dist: jinja2; extra == 'all'
72
73
  Requires-Dist: kubernetes; extra == 'all'
73
74
  Requires-Dist: nebius<0.3,>=0.2.19; (python_version >= '3.10') and extra == 'all'
74
- Requires-Dist: oci; extra == 'all'
75
+ Requires-Dist: oci>=2.150.0; extra == 'all'
75
76
  Requires-Dist: prometheus-client; extra == 'all'
77
+ Requires-Dist: pyopenssl>=23.2.0; extra == 'all'
76
78
  Requires-Dist: python-dxf==12.1.0; extra == 'all'
77
79
  Requires-Dist: python-json-logger>=3.1.0; extra == 'all'
78
80
  Requires-Dist: sentry-sdk[fastapi]; extra == 'all'
@@ -280,13 +282,15 @@ Requires-Dist: alembic>=1.10.2; extra == 'oci'
280
282
  Requires-Dist: apscheduler<4; extra == 'oci'
281
283
  Requires-Dist: asyncpg; extra == 'oci'
282
284
  Requires-Dist: backports-entry-points-selectable; extra == 'oci'
285
+ Requires-Dist: cryptography>=44.0.3; extra == 'oci'
283
286
  Requires-Dist: docker>=6.0.0; extra == 'oci'
284
287
  Requires-Dist: fastapi; extra == 'oci'
285
288
  Requires-Dist: grpcio>=1.50; extra == 'oci'
286
289
  Requires-Dist: httpx; extra == 'oci'
287
290
  Requires-Dist: jinja2; extra == 'oci'
288
- Requires-Dist: oci; extra == 'oci'
291
+ Requires-Dist: oci>=2.150.0; extra == 'oci'
289
292
  Requires-Dist: prometheus-client; extra == 'oci'
293
+ Requires-Dist: pyopenssl>=23.2.0; extra == 'oci'
290
294
  Requires-Dist: python-dxf==12.1.0; extra == 'oci'
291
295
  Requires-Dist: python-json-logger>=3.1.0; extra == 'oci'
292
296
  Requires-Dist: sentry-sdk[fastapi]; extra == 'oci'
@@ -423,9 +427,9 @@ To point the CLI to the `dstack` server, configure it
423
427
  with the server address, user token, and project name:
424
428
 
425
429
  ```shell
426
- $ dstack config \
430
+ $ dstack project add \
431
+ --name main \
427
432
  --url http://127.0.0.1:3000 \
428
- --project main \
429
433
  --token bbae0f28-d3dd-4820-bf61-8f4bb40815da
430
434
 
431
435
  Configuration is updated at ~/.dstack/config.yml