dstack 0.19.26__py3-none-any.whl → 0.19.28__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 (93) hide show
  1. dstack/_internal/cli/commands/__init__.py +11 -8
  2. dstack/_internal/cli/commands/apply.py +6 -3
  3. dstack/_internal/cli/commands/completion.py +3 -1
  4. dstack/_internal/cli/commands/config.py +1 -0
  5. dstack/_internal/cli/commands/init.py +4 -4
  6. dstack/_internal/cli/commands/offer.py +1 -1
  7. dstack/_internal/cli/commands/project.py +1 -0
  8. dstack/_internal/cli/commands/server.py +2 -2
  9. dstack/_internal/cli/main.py +1 -1
  10. dstack/_internal/cli/services/configurators/base.py +2 -4
  11. dstack/_internal/cli/services/configurators/fleet.py +4 -5
  12. dstack/_internal/cli/services/configurators/gateway.py +3 -5
  13. dstack/_internal/cli/services/configurators/run.py +165 -43
  14. dstack/_internal/cli/services/configurators/volume.py +3 -5
  15. dstack/_internal/cli/services/repos.py +1 -18
  16. dstack/_internal/core/backends/amddevcloud/__init__.py +1 -0
  17. dstack/_internal/core/backends/amddevcloud/backend.py +16 -0
  18. dstack/_internal/core/backends/amddevcloud/compute.py +5 -0
  19. dstack/_internal/core/backends/amddevcloud/configurator.py +29 -0
  20. dstack/_internal/core/backends/aws/compute.py +6 -1
  21. dstack/_internal/core/backends/base/compute.py +33 -5
  22. dstack/_internal/core/backends/base/offers.py +2 -0
  23. dstack/_internal/core/backends/configurators.py +15 -0
  24. dstack/_internal/core/backends/digitalocean/__init__.py +1 -0
  25. dstack/_internal/core/backends/digitalocean/backend.py +16 -0
  26. dstack/_internal/core/backends/digitalocean/compute.py +5 -0
  27. dstack/_internal/core/backends/digitalocean/configurator.py +31 -0
  28. dstack/_internal/core/backends/digitalocean_base/__init__.py +1 -0
  29. dstack/_internal/core/backends/digitalocean_base/api_client.py +104 -0
  30. dstack/_internal/core/backends/digitalocean_base/backend.py +5 -0
  31. dstack/_internal/core/backends/digitalocean_base/compute.py +173 -0
  32. dstack/_internal/core/backends/digitalocean_base/configurator.py +57 -0
  33. dstack/_internal/core/backends/digitalocean_base/models.py +43 -0
  34. dstack/_internal/core/backends/gcp/compute.py +32 -8
  35. dstack/_internal/core/backends/hotaisle/api_client.py +25 -33
  36. dstack/_internal/core/backends/hotaisle/compute.py +1 -6
  37. dstack/_internal/core/backends/models.py +7 -0
  38. dstack/_internal/core/backends/nebius/compute.py +0 -7
  39. dstack/_internal/core/backends/oci/compute.py +4 -5
  40. dstack/_internal/core/backends/vultr/compute.py +1 -5
  41. dstack/_internal/core/compatibility/fleets.py +5 -0
  42. dstack/_internal/core/compatibility/runs.py +10 -1
  43. dstack/_internal/core/models/backends/base.py +5 -1
  44. dstack/_internal/core/models/common.py +67 -43
  45. dstack/_internal/core/models/configurations.py +109 -69
  46. dstack/_internal/core/models/files.py +1 -1
  47. dstack/_internal/core/models/fleets.py +115 -25
  48. dstack/_internal/core/models/instances.py +5 -5
  49. dstack/_internal/core/models/profiles.py +66 -47
  50. dstack/_internal/core/models/repos/remote.py +21 -16
  51. dstack/_internal/core/models/resources.py +69 -65
  52. dstack/_internal/core/models/runs.py +41 -14
  53. dstack/_internal/core/services/repos.py +85 -80
  54. dstack/_internal/server/app.py +5 -0
  55. dstack/_internal/server/background/tasks/process_fleets.py +117 -13
  56. dstack/_internal/server/background/tasks/process_instances.py +12 -71
  57. dstack/_internal/server/background/tasks/process_running_jobs.py +2 -0
  58. dstack/_internal/server/background/tasks/process_runs.py +2 -0
  59. dstack/_internal/server/background/tasks/process_submitted_jobs.py +48 -16
  60. dstack/_internal/server/migrations/versions/2498ab323443_add_fleetmodel_consolidation_attempt_.py +44 -0
  61. dstack/_internal/server/models.py +11 -7
  62. dstack/_internal/server/schemas/gateways.py +10 -9
  63. dstack/_internal/server/schemas/runner.py +1 -0
  64. dstack/_internal/server/services/backends/handlers.py +2 -0
  65. dstack/_internal/server/services/docker.py +8 -7
  66. dstack/_internal/server/services/fleets.py +23 -25
  67. dstack/_internal/server/services/instances.py +3 -3
  68. dstack/_internal/server/services/jobs/configurators/base.py +46 -6
  69. dstack/_internal/server/services/jobs/configurators/dev.py +4 -4
  70. dstack/_internal/server/services/jobs/configurators/extensions/cursor.py +3 -5
  71. dstack/_internal/server/services/jobs/configurators/extensions/vscode.py +4 -6
  72. dstack/_internal/server/services/jobs/configurators/service.py +0 -3
  73. dstack/_internal/server/services/jobs/configurators/task.py +0 -3
  74. dstack/_internal/server/services/projects.py +52 -1
  75. dstack/_internal/server/services/runs.py +16 -0
  76. dstack/_internal/server/settings.py +46 -0
  77. dstack/_internal/server/statics/index.html +1 -1
  78. dstack/_internal/server/statics/{main-aec4762350e34d6fbff9.css → main-5e0d56245c4bd241ec27.css} +1 -1
  79. dstack/_internal/server/statics/{main-d151b300fcac3933213d.js → main-a2a16772fbf11a14d191.js} +1215 -998
  80. dstack/_internal/server/statics/{main-d151b300fcac3933213d.js.map → main-a2a16772fbf11a14d191.js.map} +1 -1
  81. dstack/_internal/server/testing/common.py +6 -3
  82. dstack/_internal/utils/env.py +85 -11
  83. dstack/_internal/utils/path.py +8 -1
  84. dstack/_internal/utils/ssh.py +7 -0
  85. dstack/api/_public/repos.py +41 -6
  86. dstack/api/_public/runs.py +14 -1
  87. dstack/version.py +1 -1
  88. {dstack-0.19.26.dist-info → dstack-0.19.28.dist-info}/METADATA +2 -2
  89. {dstack-0.19.26.dist-info → dstack-0.19.28.dist-info}/RECORD +92 -78
  90. dstack/_internal/server/statics/static/media/github.1f7102513534c83a9d8d735d2b8c12a2.svg +0 -3
  91. {dstack-0.19.26.dist-info → dstack-0.19.28.dist-info}/WHEEL +0 -0
  92. {dstack-0.19.26.dist-info → dstack-0.19.28.dist-info}/entry_points.txt +0 -0
  93. {dstack-0.19.26.dist-info → dstack-0.19.28.dist-info}/licenses/LICENSE.md +0 -0
@@ -28,6 +28,7 @@ from dstack._internal.core.models.configurations import (
28
28
  from dstack._internal.core.models.envs import Env
29
29
  from dstack._internal.core.models.fleets import (
30
30
  FleetConfiguration,
31
+ FleetNodesSpec,
31
32
  FleetSpec,
32
33
  FleetStatus,
33
34
  InstanceGroupPlacement,
@@ -60,7 +61,7 @@ from dstack._internal.core.models.profiles import (
60
61
  )
61
62
  from dstack._internal.core.models.repos.base import RepoType
62
63
  from dstack._internal.core.models.repos.local import LocalRunRepoData
63
- from dstack._internal.core.models.resources import CPUSpec, Memory, Range, ResourcesSpec
64
+ from dstack._internal.core.models.resources import CPUSpec, Memory, ResourcesSpec
64
65
  from dstack._internal.core.models.runs import (
65
66
  JobProvisioningData,
66
67
  JobRuntimeData,
@@ -271,7 +272,7 @@ def get_run_spec(
271
272
  repo_id=repo_id,
272
273
  repo_data=LocalRunRepoData(repo_dir="/"),
273
274
  repo_code_hash=None,
274
- working_dir=".",
275
+ working_dir=None,
275
276
  configuration_path=configuration_path,
276
277
  configuration=configuration or DevEnvironmentConfiguration(ide="vscode"),
277
278
  profile=profile,
@@ -284,6 +285,7 @@ async def create_run(
284
285
  project: ProjectModel,
285
286
  repo: RepoModel,
286
287
  user: UserModel,
288
+ fleet: Optional[FleetModel] = None,
287
289
  run_name: str = "test-run",
288
290
  status: RunStatus = RunStatus.SUBMITTED,
289
291
  termination_reason: Optional[RunTerminationReason] = None,
@@ -309,6 +311,7 @@ async def create_run(
309
311
  project_id=project.id,
310
312
  repo_id=repo.id,
311
313
  user_id=user.id,
314
+ fleet_id=fleet.id if fleet else None,
312
315
  submitted_at=submitted_at,
313
316
  run_name=run_name,
314
317
  status=status,
@@ -579,7 +582,7 @@ def get_fleet_spec(conf: Optional[FleetConfiguration] = None) -> FleetSpec:
579
582
 
580
583
  def get_fleet_configuration(
581
584
  name: str = "test-fleet",
582
- nodes: Range[int] = Range(min=1, max=1),
585
+ nodes: FleetNodesSpec = FleetNodesSpec(min=1, target=1, max=1),
583
586
  placement: Optional[InstanceGroupPlacement] = None,
584
587
  ) -> FleetConfiguration:
585
588
  return FleetConfiguration(
@@ -1,14 +1,88 @@
1
1
  import os
2
+ from collections.abc import Mapping
3
+ from enum import Enum
4
+ from typing import Optional, TypeVar, Union, overload
2
5
 
6
+ _Value = Union[str, int]
7
+ _T = TypeVar("_T", bound=Enum)
3
8
 
4
- def get_bool(name: str, default: bool = False) -> bool:
5
- try:
6
- value = os.environ[name]
7
- except KeyError:
8
- return default
9
- value = value.lower()
10
- if value in ["0", "false", "off"]:
11
- return False
12
- if value in ["1", "true", "on"]:
13
- return True
14
- raise ValueError(f"Invalid bool value: {name}={value}")
9
+
10
+ class Environ:
11
+ def __init__(self, environ: Mapping[str, str]):
12
+ self._environ = environ
13
+
14
+ @overload
15
+ def get_bool(self, name: str, *, default: None = None) -> Optional[bool]: ...
16
+
17
+ @overload
18
+ def get_bool(self, name: str, *, default: bool) -> bool: ...
19
+
20
+ def get_bool(self, name: str, *, default: Optional[bool] = None) -> Optional[bool]:
21
+ try:
22
+ raw_value = self._environ[name]
23
+ except KeyError:
24
+ return default
25
+ value = raw_value.lower()
26
+ if value in ["0", "false", "off"]:
27
+ return False
28
+ if value in ["1", "true", "on"]:
29
+ return True
30
+ raise ValueError(f"Invalid bool value: {name}={raw_value}")
31
+
32
+ @overload
33
+ def get_int(self, name: str, *, default: None = None) -> Optional[int]: ...
34
+
35
+ @overload
36
+ def get_int(self, name: str, *, default: int) -> int: ...
37
+
38
+ def get_int(self, name: str, *, default: Optional[int] = None) -> Optional[int]:
39
+ try:
40
+ raw_value = self._environ[name]
41
+ except KeyError:
42
+ return default
43
+ try:
44
+ return int(raw_value)
45
+ except ValueError as e:
46
+ raise ValueError(f"Invalid int value: {e}: {name}={raw_value}") from e
47
+
48
+ @overload
49
+ def get_enum(
50
+ self,
51
+ name: str,
52
+ enum_cls: type[_T],
53
+ *,
54
+ value_type: Optional[type[_Value]] = None,
55
+ default: None = None,
56
+ ) -> Optional[_T]: ...
57
+
58
+ @overload
59
+ def get_enum(
60
+ self,
61
+ name: str,
62
+ enum_cls: type[_T],
63
+ *,
64
+ value_type: Optional[type[_Value]] = None,
65
+ default: _T,
66
+ ) -> _T: ...
67
+
68
+ def get_enum(
69
+ self,
70
+ name: str,
71
+ enum_cls: type[_T],
72
+ *,
73
+ value_type: Optional[type[_Value]] = None,
74
+ default: Optional[_T] = None,
75
+ ) -> Optional[_T]:
76
+ try:
77
+ raw_value = self._environ[name]
78
+ except KeyError:
79
+ return default
80
+ try:
81
+ if value_type is not None:
82
+ raw_value = value_type(raw_value)
83
+ return enum_cls(raw_value)
84
+ except (ValueError, TypeError) as e:
85
+ raise ValueError(f"Invalid {enum_cls.__name__} value: {e}: {name}={raw_value}") from e
86
+
87
+
88
+ environ = Environ(os.environ)
@@ -1,6 +1,6 @@
1
1
  import os
2
2
  from dataclasses import dataclass
3
- from pathlib import Path, PurePath
3
+ from pathlib import Path, PurePath, PurePosixPath
4
4
  from typing import Union
5
5
 
6
6
  PathLike = Union[str, os.PathLike]
@@ -48,3 +48,10 @@ def resolve_relative_path(path: PathLike) -> PurePath:
48
48
  return normalize_path(path)
49
49
  except ValueError:
50
50
  raise ValueError("Path is outside of the repo")
51
+
52
+
53
+ def is_absolute_posix_path(path: PathLike) -> bool:
54
+ # Passing Windows path leads to undefined behavior
55
+ if str(path).startswith("~"):
56
+ return True
57
+ return PurePosixPath(path).is_absolute()
@@ -50,6 +50,13 @@ def make_ssh_command_for_git(identity_file: PathLike) -> str:
50
50
  )
51
51
 
52
52
 
53
+ def make_git_env(*, identity_file: Optional[PathLike] = None) -> dict[str, str]:
54
+ env: dict[str, str] = {"GIT_TERMINAL_PROMPT": "0"}
55
+ if identity_file is not None:
56
+ env["GIT_SSH_COMMAND"] = make_ssh_command_for_git(identity_file)
57
+ return env
58
+
59
+
53
60
  def try_ssh_key_passphrase(identity_file: PathLike, passphrase: str = "") -> bool:
54
61
  ssh_keygen = find_ssh_util("ssh-keygen")
55
62
  if ssh_keygen is None:
@@ -1,15 +1,21 @@
1
1
  from pathlib import Path
2
- from typing import Optional, Union
2
+ from typing import Literal, Optional, Union, overload
3
3
 
4
4
  from git import InvalidGitRepositoryError
5
5
 
6
6
  from dstack._internal.core.errors import ConfigurationError, ResourceNotExistsError
7
- from dstack._internal.core.models.repos import LocalRepo, RemoteRepo
7
+ from dstack._internal.core.models.repos import (
8
+ LocalRepo,
9
+ RemoteRepo,
10
+ RemoteRepoCreds,
11
+ RepoHead,
12
+ RepoHeadWithCreds,
13
+ )
8
14
  from dstack._internal.core.models.repos.base import Repo, RepoType
9
15
  from dstack._internal.core.services.configs import ConfigManager
10
16
  from dstack._internal.core.services.repos import (
11
17
  InvalidRepoCredentialsError,
12
- get_local_repo_credentials,
18
+ get_repo_creds_and_default_branch,
13
19
  load_repo,
14
20
  )
15
21
  from dstack._internal.utils.crypto import generate_rsa_key_pair
@@ -34,6 +40,7 @@ class RepoCollection:
34
40
  repo: Repo,
35
41
  git_identity_file: Optional[PathLike] = None,
36
42
  oauth_token: Optional[str] = None,
43
+ creds: Optional[RemoteRepoCreds] = None,
37
44
  ):
38
45
  """
39
46
  Initializes the repo and configures its credentials in the project.
@@ -65,12 +72,13 @@ class RepoCollection:
65
72
  repo: The repo to initialize.
66
73
  git_identity_file: The private SSH key path for accessing the remote repo.
67
74
  oauth_token: The GitHub OAuth token to access the remote repo.
75
+ creds: Optional prepared repo credentials. If specified, both `git_identity_file`
76
+ and `oauth_token` are ignored.
68
77
  """
69
- creds = None
70
- if isinstance(repo, RemoteRepo):
78
+ if creds is None and isinstance(repo, RemoteRepo):
71
79
  assert repo.repo_url is not None
72
80
  try:
73
- creds = get_local_repo_credentials(
81
+ creds, _ = get_repo_creds_and_default_branch(
74
82
  repo_url=repo.repo_url,
75
83
  identity_file=git_identity_file,
76
84
  oauth_token=oauth_token,
@@ -175,6 +183,33 @@ class RepoCollection:
175
183
  # TODO: add an API method with the same logic returning a bool value?
176
184
  return repo_head.repo_creds is not None
177
185
 
186
+ @overload
187
+ def get(self, repo_id: str, *, with_creds: Literal[False] = False) -> Optional[RepoHead]: ...
188
+
189
+ @overload
190
+ def get(self, repo_id: str, *, with_creds: Literal[True]) -> Optional[RepoHeadWithCreds]: ...
191
+
192
+ def get(
193
+ self, repo_id: str, *, with_creds: bool = False
194
+ ) -> Optional[Union[RepoHead, RepoHeadWithCreds]]:
195
+ """
196
+ Returns the repo by `repo_id`
197
+
198
+ Args:
199
+ repo_id: The repo ID.
200
+ with_creds: include repo credentials in the response.
201
+
202
+ Returns:
203
+ The repo or `None` if the repo is not found.
204
+ """
205
+ method = self._api_client.repos.get
206
+ if with_creds:
207
+ method = self._api_client.repos.get_with_creds
208
+ try:
209
+ return method(self._project, repo_id)
210
+ except ResourceNotExistsError:
211
+ return None
212
+
178
213
 
179
214
  def get_ssh_keypair(key_path: Optional[PathLike], dstack_key_path: Path) -> str:
180
215
  """Returns a path to the private key"""
@@ -433,6 +433,7 @@ class RunCollection:
433
433
  repo: Optional[Repo] = None,
434
434
  profile: Optional[Profile] = None,
435
435
  configuration_path: Optional[str] = None,
436
+ repo_dir: Optional[str] = None,
436
437
  ) -> RunPlan:
437
438
  """
438
439
  Get a run plan.
@@ -443,11 +444,17 @@ class RunCollection:
443
444
  repo (Union[LocalRepo, RemoteRepo, VirtualRepo, None]):
444
445
  The repo to use for the run. Pass `None` if repo is not needed.
445
446
  profile: The profile to use for the run.
446
- configuration_path: The path to the configuration file. Omit if the configuration is not loaded from a file.
447
+ configuration_path: The path to the configuration file. Omit if the configuration
448
+ is not loaded from a file.
449
+ repo_dir: The path of the cloned repo inside the run container. If not set,
450
+ defaults first to the `repos[0].path` property of the configuration (for remote
451
+ repos only), then to `/workflow`.
447
452
 
448
453
  Returns:
449
454
  Run plan.
450
455
  """
456
+ # XXX: not using the LEGACY_REPO_DIR const in the docstring above, as the docs generator,
457
+ # apparently, doesn't support f-strings (f"""...""").
451
458
  if repo is None:
452
459
  repo = VirtualRepo()
453
460
  repo_code_hash = None
@@ -455,11 +462,17 @@ class RunCollection:
455
462
  with _prepare_code_file(repo) as (_, repo_code_hash):
456
463
  pass
457
464
 
465
+ if repo_dir is None and configuration.repos:
466
+ repo_dir = configuration.repos[0].path
467
+
458
468
  run_spec = RunSpec(
459
469
  run_name=configuration.name,
460
470
  repo_id=repo.repo_id,
461
471
  repo_data=repo.run_repo_data,
462
472
  repo_code_hash=repo_code_hash,
473
+ repo_dir=repo_dir,
474
+ # Server doesn't use this field since 0.19.27, but we still send it for compatibility
475
+ # with older servers
463
476
  working_dir=configuration.working_dir,
464
477
  configuration_path=configuration_path,
465
478
  configuration=configuration,
dstack/version.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "0.19.26"
1
+ __version__ = "0.19.28"
2
2
  __is_release__ = True
3
3
  base_image = "0.10"
4
4
  base_image_ubuntu_version = "22.04"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dstack
3
- Version: 0.19.26
3
+ Version: 0.19.28
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
@@ -22,7 +22,7 @@ Requires-Dist: cryptography
22
22
  Requires-Dist: cursor
23
23
  Requires-Dist: filelock
24
24
  Requires-Dist: gitpython
25
- Requires-Dist: gpuhunt==0.1.7
25
+ Requires-Dist: gpuhunt==0.1.8
26
26
  Requires-Dist: ignore-python>=0.2.0
27
27
  Requires-Dist: jsonschema
28
28
  Requires-Dist: orjson