dstack 0.19.25rc1__py3-none-any.whl → 0.19.27__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 (161) hide show
  1. dstack/_internal/cli/commands/__init__.py +2 -2
  2. dstack/_internal/cli/commands/apply.py +3 -61
  3. dstack/_internal/cli/commands/attach.py +1 -1
  4. dstack/_internal/cli/commands/completion.py +1 -1
  5. dstack/_internal/cli/commands/delete.py +2 -2
  6. dstack/_internal/cli/commands/fleet.py +1 -1
  7. dstack/_internal/cli/commands/gateway.py +2 -2
  8. dstack/_internal/cli/commands/init.py +56 -24
  9. dstack/_internal/cli/commands/logs.py +1 -1
  10. dstack/_internal/cli/commands/metrics.py +1 -1
  11. dstack/_internal/cli/commands/offer.py +45 -7
  12. dstack/_internal/cli/commands/project.py +2 -2
  13. dstack/_internal/cli/commands/secrets.py +2 -2
  14. dstack/_internal/cli/commands/server.py +1 -1
  15. dstack/_internal/cli/commands/stop.py +1 -1
  16. dstack/_internal/cli/commands/volume.py +1 -1
  17. dstack/_internal/cli/main.py +2 -2
  18. dstack/_internal/cli/services/completion.py +2 -2
  19. dstack/_internal/cli/services/configurators/__init__.py +6 -2
  20. dstack/_internal/cli/services/configurators/base.py +6 -7
  21. dstack/_internal/cli/services/configurators/fleet.py +1 -3
  22. dstack/_internal/cli/services/configurators/gateway.py +2 -4
  23. dstack/_internal/cli/services/configurators/run.py +293 -58
  24. dstack/_internal/cli/services/configurators/volume.py +2 -4
  25. dstack/_internal/cli/services/profile.py +1 -1
  26. dstack/_internal/cli/services/repos.py +35 -48
  27. dstack/_internal/core/backends/amddevcloud/__init__.py +1 -0
  28. dstack/_internal/core/backends/amddevcloud/backend.py +16 -0
  29. dstack/_internal/core/backends/amddevcloud/compute.py +5 -0
  30. dstack/_internal/core/backends/amddevcloud/configurator.py +29 -0
  31. dstack/_internal/core/backends/aws/compute.py +6 -1
  32. dstack/_internal/core/backends/aws/configurator.py +11 -7
  33. dstack/_internal/core/backends/azure/configurator.py +11 -7
  34. dstack/_internal/core/backends/base/compute.py +33 -5
  35. dstack/_internal/core/backends/base/configurator.py +25 -13
  36. dstack/_internal/core/backends/base/offers.py +2 -0
  37. dstack/_internal/core/backends/cloudrift/configurator.py +13 -7
  38. dstack/_internal/core/backends/configurators.py +15 -0
  39. dstack/_internal/core/backends/cudo/configurator.py +11 -7
  40. dstack/_internal/core/backends/datacrunch/compute.py +5 -1
  41. dstack/_internal/core/backends/datacrunch/configurator.py +13 -7
  42. dstack/_internal/core/backends/digitalocean/__init__.py +1 -0
  43. dstack/_internal/core/backends/digitalocean/backend.py +16 -0
  44. dstack/_internal/core/backends/digitalocean/compute.py +5 -0
  45. dstack/_internal/core/backends/digitalocean/configurator.py +31 -0
  46. dstack/_internal/core/backends/digitalocean_base/__init__.py +1 -0
  47. dstack/_internal/core/backends/digitalocean_base/api_client.py +104 -0
  48. dstack/_internal/core/backends/digitalocean_base/backend.py +5 -0
  49. dstack/_internal/core/backends/digitalocean_base/compute.py +173 -0
  50. dstack/_internal/core/backends/digitalocean_base/configurator.py +57 -0
  51. dstack/_internal/core/backends/digitalocean_base/models.py +43 -0
  52. dstack/_internal/core/backends/gcp/compute.py +32 -8
  53. dstack/_internal/core/backends/gcp/configurator.py +11 -7
  54. dstack/_internal/core/backends/hotaisle/api_client.py +25 -33
  55. dstack/_internal/core/backends/hotaisle/compute.py +1 -6
  56. dstack/_internal/core/backends/hotaisle/configurator.py +13 -7
  57. dstack/_internal/core/backends/kubernetes/configurator.py +13 -7
  58. dstack/_internal/core/backends/lambdalabs/configurator.py +11 -7
  59. dstack/_internal/core/backends/models.py +7 -0
  60. dstack/_internal/core/backends/nebius/compute.py +1 -8
  61. dstack/_internal/core/backends/nebius/configurator.py +11 -7
  62. dstack/_internal/core/backends/nebius/resources.py +21 -11
  63. dstack/_internal/core/backends/oci/compute.py +4 -5
  64. dstack/_internal/core/backends/oci/configurator.py +11 -7
  65. dstack/_internal/core/backends/runpod/configurator.py +11 -7
  66. dstack/_internal/core/backends/template/configurator.py.jinja +11 -7
  67. dstack/_internal/core/backends/tensordock/configurator.py +13 -7
  68. dstack/_internal/core/backends/vastai/configurator.py +11 -7
  69. dstack/_internal/core/backends/vultr/compute.py +1 -5
  70. dstack/_internal/core/backends/vultr/configurator.py +11 -4
  71. dstack/_internal/core/compatibility/fleets.py +5 -0
  72. dstack/_internal/core/compatibility/gpus.py +13 -0
  73. dstack/_internal/core/compatibility/runs.py +9 -1
  74. dstack/_internal/core/models/backends/base.py +5 -1
  75. dstack/_internal/core/models/common.py +3 -3
  76. dstack/_internal/core/models/configurations.py +191 -32
  77. dstack/_internal/core/models/files.py +1 -1
  78. dstack/_internal/core/models/fleets.py +80 -3
  79. dstack/_internal/core/models/profiles.py +41 -11
  80. dstack/_internal/core/models/resources.py +46 -42
  81. dstack/_internal/core/models/runs.py +28 -5
  82. dstack/_internal/core/services/configs/__init__.py +6 -3
  83. dstack/_internal/core/services/profiles.py +2 -2
  84. dstack/_internal/core/services/repos.py +86 -79
  85. dstack/_internal/core/services/ssh/ports.py +1 -1
  86. dstack/_internal/proxy/lib/deps.py +6 -2
  87. dstack/_internal/server/app.py +22 -17
  88. dstack/_internal/server/background/tasks/process_fleets.py +109 -13
  89. dstack/_internal/server/background/tasks/process_gateways.py +4 -1
  90. dstack/_internal/server/background/tasks/process_instances.py +22 -73
  91. dstack/_internal/server/background/tasks/process_probes.py +1 -1
  92. dstack/_internal/server/background/tasks/process_running_jobs.py +12 -4
  93. dstack/_internal/server/background/tasks/process_runs.py +3 -1
  94. dstack/_internal/server/background/tasks/process_submitted_jobs.py +67 -44
  95. dstack/_internal/server/background/tasks/process_terminating_jobs.py +2 -2
  96. dstack/_internal/server/background/tasks/process_volumes.py +1 -1
  97. dstack/_internal/server/db.py +8 -4
  98. dstack/_internal/server/migrations/versions/2498ab323443_add_fleetmodel_consolidation_attempt_.py +44 -0
  99. dstack/_internal/server/models.py +6 -2
  100. dstack/_internal/server/routers/gpus.py +1 -6
  101. dstack/_internal/server/schemas/runner.py +11 -0
  102. dstack/_internal/server/services/backends/__init__.py +14 -8
  103. dstack/_internal/server/services/backends/handlers.py +6 -1
  104. dstack/_internal/server/services/docker.py +5 -5
  105. dstack/_internal/server/services/fleets.py +37 -38
  106. dstack/_internal/server/services/gateways/__init__.py +2 -0
  107. dstack/_internal/server/services/gateways/client.py +5 -2
  108. dstack/_internal/server/services/gateways/connection.py +1 -1
  109. dstack/_internal/server/services/gpus.py +50 -49
  110. dstack/_internal/server/services/instances.py +44 -4
  111. dstack/_internal/server/services/jobs/__init__.py +15 -4
  112. dstack/_internal/server/services/jobs/configurators/base.py +53 -17
  113. dstack/_internal/server/services/jobs/configurators/dev.py +9 -4
  114. dstack/_internal/server/services/jobs/configurators/extensions/cursor.py +6 -8
  115. dstack/_internal/server/services/jobs/configurators/extensions/vscode.py +7 -9
  116. dstack/_internal/server/services/jobs/configurators/service.py +1 -3
  117. dstack/_internal/server/services/jobs/configurators/task.py +3 -3
  118. dstack/_internal/server/services/locking.py +5 -5
  119. dstack/_internal/server/services/logging.py +10 -2
  120. dstack/_internal/server/services/logs/__init__.py +8 -6
  121. dstack/_internal/server/services/logs/aws.py +330 -327
  122. dstack/_internal/server/services/logs/filelog.py +7 -6
  123. dstack/_internal/server/services/logs/gcp.py +141 -139
  124. dstack/_internal/server/services/plugins.py +1 -1
  125. dstack/_internal/server/services/projects.py +2 -5
  126. dstack/_internal/server/services/proxy/repo.py +5 -1
  127. dstack/_internal/server/services/requirements/__init__.py +0 -0
  128. dstack/_internal/server/services/requirements/combine.py +259 -0
  129. dstack/_internal/server/services/runner/client.py +7 -0
  130. dstack/_internal/server/services/runs.py +17 -1
  131. dstack/_internal/server/services/services/__init__.py +8 -2
  132. dstack/_internal/server/services/services/autoscalers.py +2 -0
  133. dstack/_internal/server/services/ssh.py +2 -1
  134. dstack/_internal/server/services/storage/__init__.py +5 -6
  135. dstack/_internal/server/services/storage/gcs.py +49 -49
  136. dstack/_internal/server/services/storage/s3.py +52 -52
  137. dstack/_internal/server/statics/index.html +1 -1
  138. dstack/_internal/server/statics/{main-d151b300fcac3933213d.js → main-4eecc75fbe64067eb1bc.js} +1146 -899
  139. dstack/_internal/server/statics/{main-d151b300fcac3933213d.js.map → main-4eecc75fbe64067eb1bc.js.map} +1 -1
  140. dstack/_internal/server/statics/{main-aec4762350e34d6fbff9.css → main-56191c63d516fd0041c4.css} +1 -1
  141. dstack/_internal/server/testing/common.py +7 -4
  142. dstack/_internal/server/utils/logging.py +3 -3
  143. dstack/_internal/server/utils/provisioning.py +3 -3
  144. dstack/_internal/utils/json_schema.py +3 -1
  145. dstack/_internal/utils/path.py +8 -1
  146. dstack/_internal/utils/ssh.py +7 -0
  147. dstack/_internal/utils/typing.py +14 -0
  148. dstack/api/_public/repos.py +62 -8
  149. dstack/api/_public/runs.py +19 -8
  150. dstack/api/server/__init__.py +17 -19
  151. dstack/api/server/_gpus.py +2 -1
  152. dstack/api/server/_group.py +4 -3
  153. dstack/api/server/_repos.py +20 -3
  154. dstack/plugins/builtin/rest_plugin/_plugin.py +1 -0
  155. dstack/version.py +1 -1
  156. {dstack-0.19.25rc1.dist-info → dstack-0.19.27.dist-info}/METADATA +2 -2
  157. {dstack-0.19.25rc1.dist-info → dstack-0.19.27.dist-info}/RECORD +160 -142
  158. dstack/api/huggingface/__init__.py +0 -73
  159. {dstack-0.19.25rc1.dist-info → dstack-0.19.27.dist-info}/WHEEL +0 -0
  160. {dstack-0.19.25rc1.dist-info → dstack-0.19.27.dist-info}/entry_points.txt +0 -0
  161. {dstack-0.19.25rc1.dist-info → dstack-0.19.27.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,
@@ -573,13 +576,13 @@ def get_fleet_spec(conf: Optional[FleetConfiguration] = None) -> FleetSpec:
573
576
  return FleetSpec(
574
577
  configuration=conf,
575
578
  configuration_path="fleet.dstack.yml",
576
- profile=Profile(name=""),
579
+ profile=Profile(),
577
580
  )
578
581
 
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(
@@ -31,15 +31,15 @@ def configure_logging():
31
31
  rename_fields={"name": "logger", "asctime": "timestamp", "levelname": "level"},
32
32
  ),
33
33
  }
34
- handlers = {
34
+ handlers: dict[str, logging.Handler] = {
35
35
  "rich": DstackRichHandler(console=console),
36
36
  "standard": logging.StreamHandler(stream=sys.stdout),
37
37
  "json": logging.StreamHandler(stream=sys.stdout),
38
38
  }
39
39
  if settings.LOG_FORMAT not in formatters:
40
40
  raise ValueError(f"Invalid settings.LOG_FORMAT: {settings.LOG_FORMAT}")
41
- formatter = formatters.get(settings.LOG_FORMAT)
42
- handler = handlers.get(settings.LOG_FORMAT)
41
+ formatter = formatters[settings.LOG_FORMAT]
42
+ handler = handlers[settings.LOG_FORMAT]
43
43
  handler.setFormatter(formatter)
44
44
  handler.addFilter(AsyncioCancelledErrorFilter())
45
45
  root_logger = logging.getLogger(None)
@@ -312,10 +312,10 @@ def get_paramiko_connection(
312
312
  with proxy_ctx as proxy_client, paramiko.SSHClient() as client:
313
313
  proxy_channel: Optional[paramiko.Channel] = None
314
314
  if proxy_client is not None:
315
+ transport = proxy_client.get_transport()
316
+ assert transport is not None
315
317
  try:
316
- proxy_channel = proxy_client.get_transport().open_channel(
317
- "direct-tcpip", (host, port), ("", 0)
318
- )
318
+ proxy_channel = transport.open_channel("direct-tcpip", (host, port), ("", 0))
319
319
  except (paramiko.SSHException, OSError) as e:
320
320
  raise ProvisioningError(f"Proxy channel failed: {e}") from e
321
321
  client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
@@ -3,7 +3,9 @@ def add_extra_schema_types(schema_property: dict, extra_types: list[dict]):
3
3
  refs = [schema_property.pop("allOf")[0]]
4
4
  elif "anyOf" in schema_property:
5
5
  refs = schema_property.pop("anyOf")
6
- else:
6
+ elif "type" in schema_property:
7
7
  refs = [{"type": schema_property.pop("type")}]
8
+ else:
9
+ refs = [{"$ref": schema_property.pop("$ref")}]
8
10
  refs.extend(extra_types)
9
11
  schema_property["anyOf"] = refs
@@ -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:
@@ -0,0 +1,14 @@
1
+ from typing import Any, Protocol, TypeVar, Union
2
+
3
+ _T_contra = TypeVar("_T_contra", contravariant=True)
4
+
5
+
6
+ class SupportsDunderLT(Protocol[_T_contra]):
7
+ def __lt__(self, other: _T_contra, /) -> bool: ...
8
+
9
+
10
+ class SupportsDunderGT(Protocol[_T_contra]):
11
+ def __gt__(self, other: _T_contra, /) -> bool: ...
12
+
13
+
14
+ SupportsRichComparison = Union[SupportsDunderLT[Any], SupportsDunderGT[Any]]
@@ -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,11 +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):
79
+ assert repo.repo_url is not None
71
80
  try:
72
- creds = get_local_repo_credentials(
81
+ creds, _ = get_repo_creds_and_default_branch(
73
82
  repo_url=repo.repo_url,
74
83
  identity_file=git_identity_file,
75
84
  oauth_token=oauth_token,
@@ -140,22 +149,67 @@ class RepoCollection:
140
149
  def is_initialized(
141
150
  self,
142
151
  repo: Repo,
152
+ by_user: bool = False,
143
153
  ) -> bool:
144
154
  """
145
- Checks if the remote repo is initialized in the project
155
+ Checks if the repo is initialized in the project
146
156
 
147
157
  Args:
148
158
  repo: The repo to check.
159
+ by_user: Require the remote repo to be initialized by the user, that is, to have
160
+ the user's credentials. Ignored for other repo types.
149
161
 
150
162
  Returns:
151
163
  Whether the repo is initialized or not.
152
164
  """
165
+ if isinstance(repo, RemoteRepo) and by_user:
166
+ return self._is_initialized_by_user(repo)
153
167
  try:
154
- self._api_client.repos.get(self._project, repo.repo_id, include_creds=False)
168
+ self._api_client.repos.get(self._project, repo.repo_id)
155
169
  return True
156
170
  except ResourceNotExistsError:
157
171
  return False
158
172
 
173
+ def _is_initialized_by_user(self, repo: RemoteRepo) -> bool:
174
+ try:
175
+ repo_head = self._api_client.repos.get_with_creds(self._project, repo.repo_id)
176
+ except ResourceNotExistsError:
177
+ return False
178
+ # This works because:
179
+ # - RepoCollection.init() always submits RemoteRepoCreds for remote repos, even if
180
+ # the repo is public
181
+ # - Server returns creds only if there is RepoCredsModel for the user (or legacy
182
+ # shared creds in RepoModel)
183
+ # TODO: add an API method with the same logic returning a bool value?
184
+ return repo_head.repo_creds is not None
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
+
159
213
 
160
214
  def get_ssh_keypair(key_path: Optional[PathLike], dstack_key_path: Path) -> str:
161
215
  """Returns a path to the private key"""
@@ -23,7 +23,7 @@ from dstack._internal.core.models.configurations import (
23
23
  PortMapping,
24
24
  ServiceConfiguration,
25
25
  )
26
- from dstack._internal.core.models.files import FileArchiveMapping, FilePathMapping
26
+ from dstack._internal.core.models.files import FileArchiveMapping
27
27
  from dstack._internal.core.models.profiles import (
28
28
  CreationPolicy,
29
29
  Profile,
@@ -433,21 +433,28 @@ 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.
439
- Use this method to see the run plan before applying the cofiguration.
440
+ Use this method to see the run plan before applying the configuration.
440
441
 
441
442
  Args:
442
443
  configuration (Union[Task, Service, DevEnvironment]): The run configuration.
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,
@@ -499,7 +512,6 @@ class RunCollection:
499
512
 
500
513
  self._validate_configuration_files(configuration, run_spec.configuration_path)
501
514
  for file_mapping in configuration.files:
502
- assert isinstance(file_mapping, FilePathMapping)
503
515
  with tempfile.TemporaryFile("w+b") as fp:
504
516
  try:
505
517
  archive_hash = create_file_archive(file_mapping.local_path, fp)
@@ -691,11 +703,11 @@ class RunCollection:
691
703
  spot_policy=spot_policy,
692
704
  retry=None,
693
705
  utilization_policy=utilization_policy,
694
- max_duration=max_duration,
695
- stop_duration=stop_duration,
706
+ max_duration=max_duration, # type: ignore[assignment]
707
+ stop_duration=stop_duration, # type: ignore[assignment]
696
708
  max_price=max_price,
697
709
  creation_policy=creation_policy,
698
- idle_duration=idle_duration,
710
+ idle_duration=idle_duration, # type: ignore[assignment]
699
711
  )
700
712
  run_spec = RunSpec(
701
713
  run_name=run_name,
@@ -812,7 +824,6 @@ class RunCollection:
812
824
  if configuration_path is not None:
813
825
  base_dir = Path(configuration_path).expanduser().resolve().parent
814
826
  for file_mapping in configuration.files:
815
- assert isinstance(file_mapping, FilePathMapping)
816
827
  path = Path(file_mapping.local_path).expanduser()
817
828
  if not path.is_absolute():
818
829
  if base_dir is None:
@@ -27,9 +27,6 @@ from dstack.api.server._secrets import SecretsAPIClient
27
27
  from dstack.api.server._users import UsersAPIClient
28
28
  from dstack.api.server._volumes import VolumesAPIClient
29
29
 
30
- logger = get_logger(__name__)
31
-
32
-
33
30
  _MAX_RETRIES = 3
34
31
  _RETRY_INTERVAL = 1
35
32
 
@@ -66,6 +63,7 @@ class APIClient:
66
63
  client_api_version = os.getenv("DSTACK_CLIENT_API_VERSION", version.__version__)
67
64
  if client_api_version is not None:
68
65
  self._s.headers.update({"X-API-VERSION": client_api_version})
66
+ self._logger = get_logger(__name__)
69
67
 
70
68
  @property
71
69
  def base_url(self) -> str:
@@ -73,55 +71,55 @@ class APIClient:
73
71
 
74
72
  @property
75
73
  def users(self) -> UsersAPIClient:
76
- return UsersAPIClient(self._request)
74
+ return UsersAPIClient(self._request, self._logger)
77
75
 
78
76
  @property
79
77
  def projects(self) -> ProjectsAPIClient:
80
- return ProjectsAPIClient(self._request)
78
+ return ProjectsAPIClient(self._request, self._logger)
81
79
 
82
80
  @property
83
81
  def backends(self) -> BackendsAPIClient:
84
- return BackendsAPIClient(self._request)
82
+ return BackendsAPIClient(self._request, self._logger)
85
83
 
86
84
  @property
87
85
  def fleets(self) -> FleetsAPIClient:
88
- return FleetsAPIClient(self._request)
86
+ return FleetsAPIClient(self._request, self._logger)
89
87
 
90
88
  @property
91
89
  def repos(self) -> ReposAPIClient:
92
- return ReposAPIClient(self._request)
90
+ return ReposAPIClient(self._request, self._logger)
93
91
 
94
92
  @property
95
93
  def runs(self) -> RunsAPIClient:
96
- return RunsAPIClient(self._request)
94
+ return RunsAPIClient(self._request, self._logger)
97
95
 
98
96
  @property
99
97
  def gpus(self) -> GpusAPIClient:
100
- return GpusAPIClient(self._request)
98
+ return GpusAPIClient(self._request, self._logger)
101
99
 
102
100
  @property
103
101
  def metrics(self) -> MetricsAPIClient:
104
- return MetricsAPIClient(self._request)
102
+ return MetricsAPIClient(self._request, self._logger)
105
103
 
106
104
  @property
107
105
  def logs(self) -> LogsAPIClient:
108
- return LogsAPIClient(self._request)
106
+ return LogsAPIClient(self._request, self._logger)
109
107
 
110
108
  @property
111
109
  def secrets(self) -> SecretsAPIClient:
112
- return SecretsAPIClient(self._request)
110
+ return SecretsAPIClient(self._request, self._logger)
113
111
 
114
112
  @property
115
113
  def gateways(self) -> GatewaysAPIClient:
116
- return GatewaysAPIClient(self._request)
114
+ return GatewaysAPIClient(self._request, self._logger)
117
115
 
118
116
  @property
119
117
  def volumes(self) -> VolumesAPIClient:
120
- return VolumesAPIClient(self._request)
118
+ return VolumesAPIClient(self._request, self._logger)
121
119
 
122
120
  @property
123
121
  def files(self) -> FilesAPIClient:
124
- return FilesAPIClient(self._request)
122
+ return FilesAPIClient(self._request, self._logger)
125
123
 
126
124
  def _request(
127
125
  self,
@@ -136,20 +134,20 @@ class APIClient:
136
134
  kwargs.setdefault("headers", {})["Content-Type"] = "application/json"
137
135
  kwargs["data"] = body
138
136
 
139
- logger.debug("POST /%s", path)
137
+ self._logger.debug("POST /%s", path)
140
138
  for _ in range(_MAX_RETRIES):
141
139
  try:
142
140
  # TODO: set adequate timeout here or everywhere the method is used
143
141
  resp = self._s.request(method, f"{self._base_url}/{path}", **kwargs)
144
142
  break
145
143
  except requests.exceptions.ConnectionError as e:
146
- logger.debug("Could not connect to server: %s", e)
144
+ self._logger.debug("Could not connect to server: %s", e)
147
145
  time.sleep(_RETRY_INTERVAL)
148
146
  else:
149
147
  raise ClientError(f"Failed to connect to dstack server {self._base_url}")
150
148
 
151
149
  if 400 <= resp.status_code < 600:
152
- logger.debug(
150
+ self._logger.debug(
153
151
  "Error requesting %s. Status: %s. Headers: %s. Body: %s",
154
152
  resp.request.url,
155
153
  resp.status_code,
@@ -2,6 +2,7 @@ from typing import List, Optional
2
2
 
3
3
  from pydantic import parse_obj_as
4
4
 
5
+ from dstack._internal.core.compatibility.gpus import get_list_gpus_excludes
5
6
  from dstack._internal.core.models.runs import RunSpec
6
7
  from dstack._internal.server.schemas.gpus import GpuGroup, ListGpusRequest, ListGpusResponse
7
8
  from dstack.api.server._group import APIClientGroup
@@ -17,6 +18,6 @@ class GpusAPIClient(APIClientGroup):
17
18
  body = ListGpusRequest(run_spec=run_spec, group_by=group_by)
18
19
  resp = self._request(
19
20
  f"/api/project/{project_name}/gpus/list",
20
- body=body.json(),
21
+ body=body.json(exclude=get_list_gpus_excludes(body)),
21
22
  )
22
23
  return parse_obj_as(ListGpusResponse, resp.json()).gpus
@@ -1,3 +1,4 @@
1
+ from logging import Logger
1
2
  from typing import Optional
2
3
 
3
4
  import requests
@@ -12,10 +13,10 @@ class APIRequest(Protocol):
12
13
  raise_for_status: bool = True,
13
14
  method: str = "POST",
14
15
  **kwargs,
15
- ) -> requests.Response:
16
- pass
16
+ ) -> requests.Response: ...
17
17
 
18
18
 
19
19
  class APIClientGroup:
20
- def __init__(self, _request: APIRequest):
20
+ def __init__(self, _request: APIRequest, _logger: Logger):
21
21
  self._request = _request
22
+ self._logger = _logger
@@ -2,7 +2,12 @@ from typing import BinaryIO, List, Optional
2
2
 
3
3
  from pydantic import parse_obj_as
4
4
 
5
- from dstack._internal.core.models.repos import AnyRepoInfo, RemoteRepoCreds, RepoHead
5
+ from dstack._internal.core.models.repos import (
6
+ AnyRepoInfo,
7
+ RemoteRepoCreds,
8
+ RepoHead,
9
+ RepoHeadWithCreds,
10
+ )
6
11
  from dstack._internal.server.schemas.repos import (
7
12
  DeleteReposRequest,
8
13
  GetRepoRequest,
@@ -16,11 +21,23 @@ class ReposAPIClient(APIClientGroup):
16
21
  resp = self._request(f"/api/project/{project_name}/repos/list")
17
22
  return parse_obj_as(List[RepoHead.__response__], resp.json())
18
23
 
19
- def get(self, project_name: str, repo_id: str, include_creds: bool) -> RepoHead:
20
- body = GetRepoRequest(repo_id=repo_id, include_creds=include_creds)
24
+ def get(
25
+ self, project_name: str, repo_id: str, include_creds: Optional[bool] = None
26
+ ) -> RepoHead:
27
+ if include_creds is not None:
28
+ self._logger.warning(
29
+ "`include_creds` argument is deprecated and has no effect, `get()` always returns"
30
+ " the repo without creds. Use `get_with_creds()` to get the repo with creds"
31
+ )
32
+ body = GetRepoRequest(repo_id=repo_id, include_creds=False)
21
33
  resp = self._request(f"/api/project/{project_name}/repos/get", body=body.json())
22
34
  return parse_obj_as(RepoHead.__response__, resp.json())
23
35
 
36
+ def get_with_creds(self, project_name: str, repo_id: str) -> RepoHeadWithCreds:
37
+ body = GetRepoRequest(repo_id=repo_id, include_creds=True)
38
+ resp = self._request(f"/api/project/{project_name}/repos/get", body=body.json())
39
+ return parse_obj_as(RepoHeadWithCreds.__response__, resp.json())
40
+
24
41
  def init(
25
42
  self,
26
43
  project_name: str,
@@ -86,6 +86,7 @@ class CustomApplyPolicy(ApplyPolicy):
86
86
  spec: ApplySpec,
87
87
  excludes: Optional[Dict] = None,
88
88
  ) -> ApplySpec:
89
+ spec_json = None
89
90
  try:
90
91
  spec_request = request_cls(user=user, project=project, spec=spec)
91
92
  spec_json = self._call_plugin_service(spec_request, endpoint, excludes)
dstack/version.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "0.19.25rc1"
1
+ __version__ = "0.19.27"
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.25rc1
3
+ Version: 0.19.27
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