dstack 0.19.6rc1__py3-none-any.whl → 0.19.7__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 (31) hide show
  1. dstack/_internal/cli/services/configurators/fleet.py +3 -2
  2. dstack/_internal/cli/services/configurators/run.py +12 -2
  3. dstack/_internal/cli/utils/fleet.py +3 -1
  4. dstack/_internal/cli/utils/run.py +25 -28
  5. dstack/_internal/core/backends/gcp/resources.py +6 -1
  6. dstack/_internal/core/backends/vastai/compute.py +2 -1
  7. dstack/_internal/core/errors.py +4 -0
  8. dstack/_internal/core/models/fleets.py +2 -0
  9. dstack/_internal/core/models/instances.py +2 -2
  10. dstack/_internal/core/models/resources.py +2 -0
  11. dstack/_internal/core/models/runs.py +3 -1
  12. dstack/_internal/server/background/tasks/process_submitted_jobs.py +1 -1
  13. dstack/_internal/server/routers/gateways.py +2 -1
  14. dstack/_internal/server/services/config.py +7 -2
  15. dstack/_internal/server/services/fleets.py +15 -0
  16. dstack/_internal/server/services/gateways/__init__.py +17 -2
  17. dstack/_internal/server/services/plugins.py +77 -0
  18. dstack/_internal/server/services/runs.py +25 -11
  19. dstack/_internal/server/services/volumes.py +10 -1
  20. dstack/_internal/utils/common.py +10 -9
  21. dstack/api/server/__init__.py +8 -1
  22. dstack/plugins/__init__.py +8 -0
  23. dstack/plugins/_base.py +72 -0
  24. dstack/plugins/_models.py +8 -0
  25. dstack/plugins/_utils.py +19 -0
  26. dstack/version.py +1 -1
  27. {dstack-0.19.6rc1.dist-info → dstack-0.19.7.dist-info}/METADATA +12 -2
  28. {dstack-0.19.6rc1.dist-info → dstack-0.19.7.dist-info}/RECORD +31 -26
  29. {dstack-0.19.6rc1.dist-info → dstack-0.19.7.dist-info}/WHEEL +0 -0
  30. {dstack-0.19.6rc1.dist-info → dstack-0.19.7.dist-info}/entry_points.txt +0 -0
  31. {dstack-0.19.6rc1.dist-info → dstack-0.19.7.dist-info}/licenses/LICENSE.md +0 -0
@@ -20,6 +20,7 @@ from dstack._internal.cli.utils.rich import MultiItemStatus
20
20
  from dstack._internal.core.errors import (
21
21
  CLIError,
22
22
  ConfigurationError,
23
+ MethodNotAllowedError,
23
24
  ResourceNotExistsError,
24
25
  ServerClientError,
25
26
  URLNotFoundError,
@@ -321,7 +322,7 @@ def _print_plan_header(plan: FleetPlan):
321
322
  offer.instance.name,
322
323
  resources.pretty_format(),
323
324
  "yes" if resources.spot else "no",
324
- f"${offer.price:g}",
325
+ f"${offer.price:3f}".rstrip("0").rstrip("."),
325
326
  availability,
326
327
  style=None if index == 1 else "secondary",
327
328
  )
@@ -367,7 +368,7 @@ def _apply_plan(api: Client, plan: FleetPlan) -> Fleet:
367
368
  project_name=api.project,
368
369
  plan=plan,
369
370
  )
370
- except URLNotFoundError:
371
+ except (URLNotFoundError, MethodNotAllowedError):
371
372
  # TODO: Remove in 0.20
372
373
  return api.client.fleets.create(
373
374
  project_name=api.project,
@@ -52,7 +52,7 @@ from dstack.api.utils import load_profile
52
52
  _KNOWN_AMD_GPUS = {gpu.name.lower() for gpu in gpuhunt.KNOWN_AMD_GPUS}
53
53
  _KNOWN_NVIDIA_GPUS = {gpu.name.lower() for gpu in gpuhunt.KNOWN_NVIDIA_GPUS}
54
54
  _KNOWN_TPU_VERSIONS = {gpu.name.lower() for gpu in gpuhunt.KNOWN_TPUS}
55
-
55
+ _KNOWN_TENSTORRENT_GPUS = {gpu.name.lower() for gpu in gpuhunt.KNOWN_TENSTORRENT_ACCELERATORS}
56
56
  _BIND_ADDRESS_ARG = "bind_address"
57
57
 
58
58
  logger = get_logger(__name__)
@@ -350,6 +350,7 @@ class BaseRunConfigurator(ApplyEnvVarsConfiguratorMixin, BaseApplyConfigurator):
350
350
  if gpu_spec.count.max == 0:
351
351
  return
352
352
  has_amd_gpu: bool
353
+ has_tt_gpu: bool
353
354
  vendor = gpu_spec.vendor
354
355
  if vendor is None:
355
356
  names = gpu_spec.name
@@ -362,6 +363,8 @@ class BaseRunConfigurator(ApplyEnvVarsConfiguratorMixin, BaseApplyConfigurator):
362
363
  vendors.add(gpuhunt.AcceleratorVendor.NVIDIA)
363
364
  elif name in _KNOWN_AMD_GPUS:
364
365
  vendors.add(gpuhunt.AcceleratorVendor.AMD)
366
+ elif name in _KNOWN_TENSTORRENT_GPUS:
367
+ vendors.add(gpuhunt.AcceleratorVendor.TENSTORRENT)
365
368
  else:
366
369
  maybe_tpu_version, _, maybe_tpu_cores = name.partition("-")
367
370
  if maybe_tpu_version in _KNOWN_TPU_VERSIONS and maybe_tpu_cores.isdigit():
@@ -380,15 +383,22 @@ class BaseRunConfigurator(ApplyEnvVarsConfiguratorMixin, BaseApplyConfigurator):
380
383
  # to execute a run on an instance with an AMD accelerator with a default
381
384
  # CUDA image, not a big deal.
382
385
  has_amd_gpu = gpuhunt.AcceleratorVendor.AMD in vendors
386
+ has_tt_gpu = gpuhunt.AcceleratorVendor.TENSTORRENT in vendors
383
387
  else:
384
388
  # If neither gpu.vendor nor gpu.name is set, assume Nvidia.
385
389
  vendor = gpuhunt.AcceleratorVendor.NVIDIA
386
390
  has_amd_gpu = False
391
+ has_tt_gpu = False
387
392
  gpu_spec.vendor = vendor
388
393
  else:
389
394
  has_amd_gpu = vendor == gpuhunt.AcceleratorVendor.AMD
395
+ has_tt_gpu = vendor == gpuhunt.AcceleratorVendor.TENSTORRENT
390
396
  if has_amd_gpu and conf.image is None:
391
- raise ConfigurationError("`image` is required if `resources.gpu.vendor` is AMD.")
397
+ raise ConfigurationError("`image` is required if `resources.gpu.vendor` is `amd`")
398
+ if has_tt_gpu and conf.image is None:
399
+ raise ConfigurationError(
400
+ "`image` is required if `resources.gpu.vendor` is `tenstorrent`"
401
+ )
392
402
 
393
403
 
394
404
  class RunWithPortsConfigurator(BaseRunConfigurator):
@@ -79,7 +79,9 @@ def get_fleets_table(
79
79
  "BACKEND": backend,
80
80
  "REGION": region,
81
81
  "RESOURCES": resources,
82
- "PRICE": f"${instance.price:.4}" if instance.price is not None else "",
82
+ "PRICE": f"${instance.price:.4f}".rstrip("0").rstrip(".")
83
+ if instance.price is not None
84
+ else "",
83
85
  "STATUS": status,
84
86
  "CREATED": format_date(instance.created),
85
87
  "ERROR": error,
@@ -1,3 +1,4 @@
1
+ import os
1
2
  from typing import Any, Dict, List, Optional, Union
2
3
 
3
4
  from rich.markup import escape
@@ -36,7 +37,7 @@ def print_run_plan(
36
37
 
37
38
  req = job_plan.job_spec.requirements
38
39
  pretty_req = req.pretty_format(resources_only=True)
39
- max_price = f"${req.max_price:g}" if req.max_price else "-"
40
+ max_price = f"${req.max_price:3f}".rstrip("0").rstrip(".") if req.max_price else "-"
40
41
  max_duration = (
41
42
  format_pretty_duration(job_plan.job_spec.max_duration)
42
43
  if job_plan.job_spec.max_duration
@@ -94,14 +95,12 @@ def print_run_plan(
94
95
  props.add_row(th("Inactivity duration"), inactivity_duration)
95
96
  props.add_row(th("Reservation"), run_spec.configuration.reservation or "-")
96
97
 
97
- offers = Table(box=None)
98
+ offers = Table(box=None, expand=os.get_terminal_size()[0] <= 110)
98
99
  offers.add_column("#")
99
- offers.add_column("BACKEND")
100
- offers.add_column("REGION")
101
- offers.add_column("INSTANCE TYPE")
102
- offers.add_column("RESOURCES")
103
- offers.add_column("SPOT")
104
- offers.add_column("PRICE")
100
+ offers.add_column("BACKEND", style="grey58", ratio=2)
101
+ offers.add_column("RESOURCES", ratio=4)
102
+ offers.add_column("INSTANCE TYPE", style="grey58", no_wrap=True, ratio=2)
103
+ offers.add_column("PRICE", style="grey58", ratio=1)
105
104
  offers.add_column()
106
105
 
107
106
  job_plan.offers = job_plan.offers[:max_offers] if max_offers else job_plan.offers
@@ -122,14 +121,12 @@ def print_run_plan(
122
121
  instance += f" ({offer.blocks}/{offer.total_blocks})"
123
122
  offers.add_row(
124
123
  f"{i}",
125
- offer.backend.replace("remote", "ssh"),
126
- offer.region,
124
+ offer.backend.replace("remote", "ssh") + " (" + offer.region + ")",
125
+ r.pretty_format(include_spot=True),
127
126
  instance,
128
- r.pretty_format(),
129
- "yes" if r.spot else "no",
130
- f"${offer.price:g}",
127
+ f"${offer.price:.4f}".rstrip("0").rstrip("."),
131
128
  availability,
132
- style=None if i == 1 else "secondary",
129
+ style=None if i == 1 or not include_run_properties else "secondary",
133
130
  )
134
131
  if job_plan.total_offers > len(job_plan.offers):
135
132
  offers.add_row("", "...", style="secondary")
@@ -141,7 +138,8 @@ def print_run_plan(
141
138
  if job_plan.total_offers > len(job_plan.offers):
142
139
  console.print(
143
140
  f"[secondary] Shown {len(job_plan.offers)} of {job_plan.total_offers} offers, "
144
- f"${job_plan.max_price:g} max[/]"
141
+ f"${job_plan.max_price:3f}".rstrip("0").rstrip(".")
142
+ + "max[/]"
145
143
  )
146
144
  console.print()
147
145
  else:
@@ -151,19 +149,18 @@ def print_run_plan(
151
149
  def get_runs_table(
152
150
  runs: List[Run], verbose: bool = False, format_date: DateFormatter = pretty_date
153
151
  ) -> Table:
154
- table = Table(box=None)
155
- table.add_column("NAME", style="bold", no_wrap=True)
156
- table.add_column("BACKEND", style="grey58")
152
+ table = Table(box=None, expand=os.get_terminal_size()[0] <= 110)
153
+ table.add_column("NAME", style="bold", no_wrap=True, ratio=2)
154
+ table.add_column("BACKEND", style="grey58", ratio=2)
155
+ table.add_column("RESOURCES", ratio=3 if not verbose else 2)
157
156
  if verbose:
158
- table.add_column("INSTANCE", no_wrap=True)
159
- table.add_column("RESOURCES")
157
+ table.add_column("INSTANCE", no_wrap=True, ratio=1)
158
+ table.add_column("RESERVATION", no_wrap=True, ratio=1)
159
+ table.add_column("PRICE", style="grey58", ratio=1)
160
+ table.add_column("STATUS", no_wrap=True, ratio=1)
161
+ table.add_column("SUBMITTED", style="grey58", no_wrap=True, ratio=1)
160
162
  if verbose:
161
- table.add_column("RESERVATION", no_wrap=True)
162
- table.add_column("PRICE", no_wrap=True)
163
- table.add_column("STATUS", no_wrap=True)
164
- table.add_column("SUBMITTED", style="grey58", no_wrap=True)
165
- if verbose:
166
- table.add_column("ERROR", no_wrap=True)
163
+ table.add_column("ERROR", no_wrap=True, ratio=2)
167
164
 
168
165
  for run in runs:
169
166
  run_error = _get_run_error(run)
@@ -202,10 +199,10 @@ def get_runs_table(
202
199
  job_row.update(
203
200
  {
204
201
  "BACKEND": f"{jpd.backend.value.replace('remote', 'ssh')} ({jpd.region})",
205
- "INSTANCE": instance,
206
202
  "RESOURCES": resources.pretty_format(include_spot=True),
203
+ "INSTANCE": instance,
207
204
  "RESERVATION": jpd.reservation,
208
- "PRICE": f"${jpd.price:.4}",
205
+ "PRICE": f"${jpd.price:.4f}".rstrip("0").rstrip("."),
209
206
  }
210
207
  )
211
208
  if len(run.jobs) == 1:
@@ -205,12 +205,17 @@ def _get_network_interfaces(
205
205
  else:
206
206
  network_interface.access_configs = []
207
207
 
208
+ if extra_subnetworks:
209
+ # Multiple interfaces are set only for GPU VM that require gVNIC for best performance
210
+ network_interface.nic_type = compute_v1.NetworkInterface.NicType.GVNIC.name
211
+
208
212
  network_interfaces = [network_interface]
209
213
  for network, subnetwork in extra_subnetworks or []:
210
214
  network_interfaces.append(
211
215
  compute_v1.NetworkInterface(
212
216
  network=network,
213
217
  subnetwork=subnetwork,
218
+ nic_type=compute_v1.NetworkInterface.NicType.GVNIC.name,
214
219
  )
215
220
  )
216
221
  return network_interfaces
@@ -437,7 +442,7 @@ def wait_for_operation(operation: Operation, verbose_name: str = "operation", ti
437
442
  raise
438
443
  except Exception as e:
439
444
  # Write only debug logs here.
440
- # The unexpected errors will be propagated and logged appropriatly by the caller.
445
+ # The unexpected errors will be propagated and logged appropriately by the caller.
441
446
  logger.debug("Error during %s: %s", verbose_name, e)
442
447
  raise operation.exception() or e
443
448
  return result
@@ -43,7 +43,8 @@ class VastAICompute(Compute):
43
43
  "reliability2": {"gte": 0.9},
44
44
  "inet_down": {"gt": 128},
45
45
  "verified": {"eq": True},
46
- "cuda_max_good": {"gte": 11.8},
46
+ "cuda_max_good": {"gte": 12.1},
47
+ "compute_cap": {"gte": 600},
47
48
  }
48
49
  )
49
50
  )
@@ -22,6 +22,10 @@ class URLNotFoundError(ClientError):
22
22
  pass
23
23
 
24
24
 
25
+ class MethodNotAllowedError(ClientError):
26
+ pass
27
+
28
+
25
29
  class ServerClientErrorCode(str, enum.Enum):
26
30
  UNSPECIFIED_ERROR = "error"
27
31
  RESOURCE_EXISTS = "resource_exists"
@@ -269,6 +269,8 @@ class FleetSpec(CoreModel):
269
269
  configuration_path: Optional[str] = None
270
270
  profile: Profile
271
271
  autocreated: bool = False
272
+ # merged_profile stores profile parameters merged from profile and configuration.
273
+ # Read profile parameters from merged_profile instead of profile directly.
272
274
  # TODO: make merged_profile a computed field after migrating to pydanticV2
273
275
  merged_profile: Annotated[Profile, Field(exclude=True)] = None
274
276
 
@@ -57,7 +57,7 @@ class Resources(CoreModel):
57
57
  if self.memory_mib > 0:
58
58
  resources["memory"] = f"{self.memory_mib / 1024:.0f}GB"
59
59
  if self.disk.size_mib > 0:
60
- resources["disk_size"] = f"{self.disk.size_mib / 1024:.1f}GB"
60
+ resources["disk_size"] = f"{self.disk.size_mib / 1024:.0f}GB"
61
61
  if self.gpus:
62
62
  gpu = self.gpus[0]
63
63
  resources["gpu_name"] = gpu.name
@@ -66,7 +66,7 @@ class Resources(CoreModel):
66
66
  resources["gpu_memory"] = f"{gpu.memory_mib / 1024:.0f}GB"
67
67
  output = pretty_resources(**resources)
68
68
  if include_spot and self.spot:
69
- output += ", SPOT"
69
+ output += " (spot)"
70
70
  return output
71
71
 
72
72
 
@@ -246,6 +246,8 @@ class GPUSpec(CoreModel):
246
246
  v = v.lower()
247
247
  if v == "tpu":
248
248
  return gpuhunt.AcceleratorVendor.GOOGLE
249
+ if v == "tt":
250
+ return gpuhunt.AcceleratorVendor.TENSTORRENT
249
251
  return gpuhunt.AcceleratorVendor.cast(v)
250
252
 
251
253
 
@@ -162,7 +162,7 @@ class Requirements(CoreModel):
162
162
  if self.spot is not None:
163
163
  res += f", {'spot' if self.spot else 'on-demand'}"
164
164
  if self.max_price is not None:
165
- res += f" under ${self.max_price:g} per hour"
165
+ res += f" under ${self.max_price:3f}".rstrip("0").rstrip(".") + " per hour"
166
166
  return res
167
167
 
168
168
 
@@ -357,6 +357,8 @@ class RunSpec(CoreModel):
357
357
  description="The contents of the SSH public key that will be used to connect to the run."
358
358
  ),
359
359
  ]
360
+ # merged_profile stores profile parameters merged from profile and configuration.
361
+ # Read profile parameters from merged_profile instead of profile directly.
360
362
  # TODO: make merged_profile a computed field after migrating to pydanticV2
361
363
  merged_profile: Annotated[Profile, Field(exclude=True)] = None
362
364
 
@@ -197,7 +197,7 @@ async def _process_submitted_job(session: AsyncSession, job_model: JobModel):
197
197
  pool_instances = list(res.unique().scalars().all())
198
198
  instances_ids = sorted([i.id for i in pool_instances])
199
199
  if get_db().dialect_name == "sqlite":
200
- # Start new transaction to see commited changes after lock
200
+ # Start new transaction to see committed changes after lock
201
201
  await session.commit()
202
202
  async with get_locker().lock_ctx(InstanceModel.__tablename__, instances_ids):
203
203
  # If another job freed the instance but is still trying to detach volumes,
@@ -47,9 +47,10 @@ async def create_gateway(
47
47
  session: AsyncSession = Depends(get_session),
48
48
  user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectAdmin()),
49
49
  ) -> models.Gateway:
50
- _, project = user_project
50
+ user, project = user_project
51
51
  return await gateways.create_gateway(
52
52
  session=session,
53
+ user=user,
53
54
  project=project,
54
55
  configuration=body.configuration,
55
56
  )
@@ -29,6 +29,7 @@ from dstack._internal.server.services.permissions import (
29
29
  DefaultPermissions,
30
30
  set_default_permissions,
31
31
  )
32
+ from dstack._internal.server.services.plugins import load_plugins
32
33
  from dstack._internal.utils.logging import get_logger
33
34
 
34
35
  logger = get_logger(__name__)
@@ -38,7 +39,7 @@ logger = get_logger(__name__)
38
39
  # If a collection has nested collections, it will be assigned the block style. Otherwise it will have the flow style.
39
40
  #
40
41
  # We want mapping to always be displayed in block-style but lists without nested objects in flow-style.
41
- # So we define a custom representeter
42
+ # So we define a custom representer.
42
43
 
43
44
 
44
45
  def seq_representer(dumper, sequence):
@@ -75,7 +76,10 @@ class ServerConfig(CoreModel):
75
76
  ] = None
76
77
  default_permissions: Annotated[
77
78
  Optional[DefaultPermissions], Field(description="The default user permissions")
78
- ]
79
+ ] = None
80
+ plugins: Annotated[
81
+ Optional[List[str]], Field(description="The server-side plugins to enable")
82
+ ] = None
79
83
 
80
84
 
81
85
  class ServerConfigManager:
@@ -112,6 +116,7 @@ class ServerConfigManager:
112
116
  await self._apply_project_config(
113
117
  session=session, owner=owner, project_config=project_config
114
118
  )
119
+ load_plugins(enabled_plugins=self.config.plugins or [])
115
120
 
116
121
  async def _apply_project_config(
117
122
  self,
@@ -55,6 +55,7 @@ from dstack._internal.server.services.locking import (
55
55
  get_locker,
56
56
  string_to_lock_id,
57
57
  )
58
+ from dstack._internal.server.services.plugins import apply_plugin_policies
58
59
  from dstack._internal.server.services.projects import (
59
60
  get_member,
60
61
  get_member_permissions,
@@ -234,7 +235,14 @@ async def get_plan(
234
235
  user: UserModel,
235
236
  spec: FleetSpec,
236
237
  ) -> FleetPlan:
238
+ # Spec must be copied by parsing to calculate merged_profile
237
239
  effective_spec = FleetSpec.parse_obj(spec.dict())
240
+ effective_spec = apply_plugin_policies(
241
+ user=user.name,
242
+ project=project.name,
243
+ spec=effective_spec,
244
+ )
245
+ effective_spec = FleetSpec.parse_obj(effective_spec.dict())
238
246
  current_fleet: Optional[Fleet] = None
239
247
  current_fleet_id: Optional[uuid.UUID] = None
240
248
  if effective_spec.configuration.name is not None:
@@ -330,6 +338,13 @@ async def create_fleet(
330
338
  user: UserModel,
331
339
  spec: FleetSpec,
332
340
  ) -> Fleet:
341
+ # Spec must be copied by parsing to calculate merged_profile
342
+ spec = apply_plugin_policies(
343
+ user=user.name,
344
+ project=project.name,
345
+ spec=spec,
346
+ )
347
+ spec = FleetSpec.parse_obj(spec.dict())
333
348
  _validate_fleet_spec(spec)
334
349
 
335
350
  if spec.configuration.ssh_config is not None:
@@ -31,13 +31,19 @@ from dstack._internal.core.models.gateways import (
31
31
  Gateway,
32
32
  GatewayComputeConfiguration,
33
33
  GatewayConfiguration,
34
+ GatewaySpec,
34
35
  GatewayStatus,
35
36
  LetsEncryptGatewayCertificate,
36
37
  )
37
38
  from dstack._internal.core.services import validate_dstack_resource_name
38
39
  from dstack._internal.server import settings
39
40
  from dstack._internal.server.db import get_db
40
- from dstack._internal.server.models import GatewayComputeModel, GatewayModel, ProjectModel
41
+ from dstack._internal.server.models import (
42
+ GatewayComputeModel,
43
+ GatewayModel,
44
+ ProjectModel,
45
+ UserModel,
46
+ )
41
47
  from dstack._internal.server.services.backends import (
42
48
  check_backend_type_available,
43
49
  get_project_backend_by_type_or_error,
@@ -50,6 +56,7 @@ from dstack._internal.server.services.locking import (
50
56
  get_locker,
51
57
  string_to_lock_id,
52
58
  )
59
+ from dstack._internal.server.services.plugins import apply_plugin_policies
53
60
  from dstack._internal.server.utils.common import gather_map_async
54
61
  from dstack._internal.utils.common import get_current_datetime, run_async
55
62
  from dstack._internal.utils.crypto import generate_rsa_key_pair_bytes
@@ -129,9 +136,17 @@ async def create_gateway_compute(
129
136
 
130
137
  async def create_gateway(
131
138
  session: AsyncSession,
139
+ user: UserModel,
132
140
  project: ProjectModel,
133
141
  configuration: GatewayConfiguration,
134
142
  ) -> Gateway:
143
+ spec = apply_plugin_policies(
144
+ user=user.name,
145
+ project=project.name,
146
+ # Create pseudo spec until the gateway API is updated to accept spec
147
+ spec=GatewaySpec(configuration=configuration),
148
+ )
149
+ configuration = spec.configuration
135
150
  _validate_gateway_configuration(configuration)
136
151
 
137
152
  backend_model, _ = await get_project_backend_with_model_by_type_or_error(
@@ -140,7 +155,7 @@ async def create_gateway(
140
155
 
141
156
  lock_namespace = f"gateway_names_{project.name}"
142
157
  if get_db().dialect_name == "sqlite":
143
- # Start new transaction to see commited changes after lock
158
+ # Start new transaction to see committed changes after lock
144
159
  await session.commit()
145
160
  elif get_db().dialect_name == "postgresql":
146
161
  await session.execute(
@@ -0,0 +1,77 @@
1
+ import itertools
2
+ from importlib import import_module
3
+
4
+ from backports.entry_points_selectable import entry_points # backport for Python 3.9
5
+
6
+ from dstack._internal.core.errors import ServerClientError
7
+ from dstack._internal.utils.logging import get_logger
8
+ from dstack.plugins import ApplyPolicy, ApplySpec, Plugin
9
+
10
+ logger = get_logger(__name__)
11
+
12
+
13
+ _PLUGINS: list[Plugin] = []
14
+
15
+
16
+ def load_plugins(enabled_plugins: list[str]):
17
+ _PLUGINS.clear()
18
+ plugins_entrypoints = entry_points(group="dstack.plugins")
19
+ plugins_to_load = enabled_plugins.copy()
20
+ for entrypoint in plugins_entrypoints:
21
+ if entrypoint.name not in enabled_plugins:
22
+ logger.info(
23
+ ("Found not enabled plugin %s. Plugin will not be loaded."),
24
+ entrypoint.name,
25
+ )
26
+ continue
27
+ try:
28
+ module_path, _, class_name = entrypoint.value.partition(":")
29
+ module = import_module(module_path)
30
+ except ImportError:
31
+ logger.warning(
32
+ (
33
+ "Failed to load plugin %s when importing %s."
34
+ " Ensure the module is on the import path."
35
+ ),
36
+ entrypoint.name,
37
+ entrypoint.value,
38
+ )
39
+ continue
40
+ plugin_class = getattr(module, class_name, None)
41
+ if plugin_class is None:
42
+ logger.warning(
43
+ ("Failed to load plugin %s: plugin class %s not found in module %s."),
44
+ entrypoint.name,
45
+ class_name,
46
+ module_path,
47
+ )
48
+ continue
49
+ if not issubclass(plugin_class, Plugin):
50
+ logger.warning(
51
+ ("Failed to load plugin %s: plugin class %s is not a subclass of Plugin."),
52
+ entrypoint.name,
53
+ class_name,
54
+ )
55
+ continue
56
+ plugins_to_load.remove(entrypoint.name)
57
+ _PLUGINS.append(plugin_class())
58
+ logger.info("Loaded plugin %s", entrypoint.name)
59
+ if plugins_to_load:
60
+ logger.warning("Enabled plugins not found: %s", plugins_to_load)
61
+
62
+
63
+ def apply_plugin_policies(user: str, project: str, spec: ApplySpec) -> ApplySpec:
64
+ policies = _get_apply_policies()
65
+ for policy in policies:
66
+ try:
67
+ spec = policy.on_apply(user=user, project=project, spec=spec)
68
+ except ValueError as e:
69
+ msg = None
70
+ if len(e.args) > 0:
71
+ msg = e.args[0]
72
+ raise ServerClientError(msg)
73
+ return spec
74
+
75
+
76
+ def _get_apply_policies() -> list[ApplyPolicy]:
77
+ return list(itertools.chain(*[p.get_apply_policies() for p in _PLUGINS]))
@@ -79,6 +79,7 @@ from dstack._internal.server.services.jobs import (
79
79
  from dstack._internal.server.services.locking import get_locker, string_to_lock_id
80
80
  from dstack._internal.server.services.logging import fmt
81
81
  from dstack._internal.server.services.offers import get_offers_by_requirements
82
+ from dstack._internal.server.services.plugins import apply_plugin_policies
82
83
  from dstack._internal.server.services.projects import list_project_models, list_user_project_models
83
84
  from dstack._internal.server.services.users import get_user_model_by_name
84
85
  from dstack._internal.utils.logging import get_logger
@@ -279,7 +280,14 @@ async def get_plan(
279
280
  run_spec: RunSpec,
280
281
  max_offers: Optional[int],
281
282
  ) -> RunPlan:
283
+ # Spec must be copied by parsing to calculate merged_profile
282
284
  effective_run_spec = RunSpec.parse_obj(run_spec.dict())
285
+ effective_run_spec = apply_plugin_policies(
286
+ user=user.name,
287
+ project=project.name,
288
+ spec=effective_run_spec,
289
+ )
290
+ effective_run_spec = RunSpec.parse_obj(effective_run_spec.dict())
283
291
  _validate_run_spec_and_set_defaults(effective_run_spec)
284
292
 
285
293
  profile = effective_run_spec.merged_profile
@@ -370,28 +378,36 @@ async def apply_plan(
370
378
  plan: ApplyRunPlanInput,
371
379
  force: bool,
372
380
  ) -> Run:
373
- _validate_run_spec_and_set_defaults(plan.run_spec)
374
- if plan.run_spec.run_name is None:
381
+ run_spec = plan.run_spec
382
+ run_spec = apply_plugin_policies(
383
+ user=user.name,
384
+ project=project.name,
385
+ spec=run_spec,
386
+ )
387
+ # Spec must be copied by parsing to calculate merged_profile
388
+ run_spec = RunSpec.parse_obj(run_spec.dict())
389
+ _validate_run_spec_and_set_defaults(run_spec)
390
+ if run_spec.run_name is None:
375
391
  return await submit_run(
376
392
  session=session,
377
393
  user=user,
378
394
  project=project,
379
- run_spec=plan.run_spec,
395
+ run_spec=run_spec,
380
396
  )
381
397
  current_resource = await get_run_by_name(
382
398
  session=session,
383
399
  project=project,
384
- run_name=plan.run_spec.run_name,
400
+ run_name=run_spec.run_name,
385
401
  )
386
402
  if current_resource is None or current_resource.status.is_finished():
387
403
  return await submit_run(
388
404
  session=session,
389
405
  user=user,
390
406
  project=project,
391
- run_spec=plan.run_spec,
407
+ run_spec=run_spec,
392
408
  )
393
409
  try:
394
- _check_can_update_run_spec(current_resource.run_spec, plan.run_spec)
410
+ _check_can_update_run_spec(current_resource.run_spec, run_spec)
395
411
  except ServerClientError:
396
412
  # The except is only needed to raise an appropriate error if run is active
397
413
  if not current_resource.status.is_finished():
@@ -409,14 +425,12 @@ async def apply_plan(
409
425
  # FIXME: potentially long write transaction
410
426
  # Avoid getting run_model after update
411
427
  await session.execute(
412
- update(RunModel)
413
- .where(RunModel.id == current_resource.id)
414
- .values(run_spec=plan.run_spec.json())
428
+ update(RunModel).where(RunModel.id == current_resource.id).values(run_spec=run_spec.json())
415
429
  )
416
430
  run = await get_run_by_name(
417
431
  session=session,
418
432
  project=project,
419
- run_name=plan.run_spec.run_name,
433
+ run_name=run_spec.run_name,
420
434
  )
421
435
  return common_utils.get_or_error(run)
422
436
 
@@ -436,7 +450,7 @@ async def submit_run(
436
450
 
437
451
  lock_namespace = f"run_names_{project.name}"
438
452
  if get_db().dialect_name == "sqlite":
439
- # Start new transaction to see commited changes after lock
453
+ # Start new transaction to see committed changes after lock
440
454
  await session.commit()
441
455
  elif get_db().dialect_name == "postgresql":
442
456
  await session.execute(
@@ -21,6 +21,7 @@ from dstack._internal.core.models.volumes import (
21
21
  VolumeConfiguration,
22
22
  VolumeInstance,
23
23
  VolumeProvisioningData,
24
+ VolumeSpec,
24
25
  VolumeStatus,
25
26
  )
26
27
  from dstack._internal.core.services import validate_dstack_resource_name
@@ -38,6 +39,7 @@ from dstack._internal.server.services.locking import (
38
39
  get_locker,
39
40
  string_to_lock_id,
40
41
  )
42
+ from dstack._internal.server.services.plugins import apply_plugin_policies
41
43
  from dstack._internal.server.services.projects import list_project_models, list_user_project_models
42
44
  from dstack._internal.utils import common, random_names
43
45
  from dstack._internal.utils.logging import get_logger
@@ -203,11 +205,18 @@ async def create_volume(
203
205
  user: UserModel,
204
206
  configuration: VolumeConfiguration,
205
207
  ) -> Volume:
208
+ spec = apply_plugin_policies(
209
+ user=user.name,
210
+ project=project.name,
211
+ # Create pseudo spec until the volume API is updated to accept spec
212
+ spec=VolumeSpec(configuration=configuration),
213
+ )
214
+ configuration = spec.configuration
206
215
  _validate_volume_configuration(configuration)
207
216
 
208
217
  lock_namespace = f"volume_names_{project.name}"
209
218
  if get_db().dialect_name == "sqlite":
210
- # Start new transaction to see commited changes after lock
219
+ # Start new transaction to see committed changes after lock
211
220
  await session.commit()
212
221
  elif get_db().dialect_name == "postgresql":
213
222
  await session.execute(
@@ -110,25 +110,26 @@ def pretty_resources(
110
110
  """
111
111
  parts = []
112
112
  if cpus is not None:
113
- parts.append(f"{cpus}xCPU")
113
+ parts.append(f"cpu={cpus}")
114
114
  if memory is not None:
115
- parts.append(f"{memory}")
115
+ parts.append(f"mem={memory}")
116
+ if disk_size:
117
+ parts.append(f"disk={disk_size}")
116
118
  if gpu_count:
117
119
  gpu_parts = []
120
+ gpu_parts.append(f"{gpu_name or 'gpu'}")
118
121
  if gpu_memory is not None:
119
122
  gpu_parts.append(f"{gpu_memory}")
123
+ if gpu_count is not None:
124
+ gpu_parts.append(f"{gpu_count}")
120
125
  if total_gpu_memory is not None:
121
- gpu_parts.append(f"total {total_gpu_memory}")
126
+ gpu_parts.append(f"{total_gpu_memory}")
122
127
  if compute_capability is not None:
123
128
  gpu_parts.append(f"{compute_capability}")
124
129
 
125
- gpu = f"{gpu_count}x{gpu_name or 'GPU'}"
126
- if gpu_parts:
127
- gpu += f" ({', '.join(gpu_parts)})"
130
+ gpu = ":".join(gpu_parts)
128
131
  parts.append(gpu)
129
- if disk_size:
130
- parts.append(f"{disk_size} (disk)")
131
- return ", ".join(parts)
132
+ return " ".join(parts)
132
133
 
133
134
 
134
135
  def since(timestamp: str) -> datetime:
@@ -6,7 +6,12 @@ from typing import Dict, List, Optional, Type
6
6
  import requests
7
7
 
8
8
  from dstack import version
9
- from dstack._internal.core.errors import ClientError, ServerClientError, URLNotFoundError
9
+ from dstack._internal.core.errors import (
10
+ ClientError,
11
+ MethodNotAllowedError,
12
+ ServerClientError,
13
+ URLNotFoundError,
14
+ )
10
15
  from dstack._internal.utils.logging import get_logger
11
16
  from dstack.api.server._backends import BackendsAPIClient
12
17
  from dstack.api.server._fleets import FleetsAPIClient
@@ -156,6 +161,8 @@ class APIClient:
156
161
  )
157
162
  if resp.status_code == 404:
158
163
  raise URLNotFoundError(f"Status code 404 when requesting {resp.request.url}")
164
+ if resp.status_code == 405:
165
+ raise MethodNotAllowedError(f"Status code 405 when requesting {resp.request.url}")
159
166
  if 400 <= resp.status_code < 600:
160
167
  raise ClientError(
161
168
  f"Unexpected error: status code {resp.status_code}"
@@ -0,0 +1,8 @@
1
+ # ruff: noqa: F401
2
+ from dstack._internal.core.models.fleets import FleetSpec
3
+ from dstack._internal.core.models.gateways import GatewaySpec
4
+ from dstack._internal.core.models.runs import RunSpec
5
+ from dstack._internal.core.models.volumes import VolumeSpec
6
+ from dstack.plugins._base import ApplyPolicy, Plugin
7
+ from dstack.plugins._models import ApplySpec
8
+ from dstack.plugins._utils import get_plugin_logger
@@ -0,0 +1,72 @@
1
+ from dstack._internal.core.models.fleets import FleetSpec
2
+ from dstack._internal.core.models.gateways import GatewaySpec
3
+ from dstack._internal.core.models.runs import RunSpec
4
+ from dstack._internal.core.models.volumes import VolumeSpec
5
+ from dstack.plugins._models import ApplySpec
6
+
7
+
8
+ class ApplyPolicy:
9
+ """
10
+ A base apply policy class to modify specs on `dstack apply`.
11
+ Subclass it and return the subclass instance in `Plugin.get_apply_policies()`.
12
+ """
13
+
14
+ def on_apply(self, user: str, project: str, spec: ApplySpec) -> ApplySpec:
15
+ """
16
+ Modify `spec` before it's applied.
17
+ Raise `ValueError` for `spec` to be rejected as invalid.
18
+
19
+ This method can be called twice:
20
+ * first when a user gets a plan
21
+ * second when a user applies a plan
22
+
23
+ In both cases, the original spec is passed, so the method does not
24
+ need to check if it modified the spec before.
25
+
26
+ It's safe to modify and return `spec` without copying.
27
+ """
28
+ if isinstance(spec, RunSpec):
29
+ return self.on_run_apply(user=user, project=project, spec=spec)
30
+ if isinstance(spec, FleetSpec):
31
+ return self.on_fleet_apply(user=user, project=project, spec=spec)
32
+ if isinstance(spec, VolumeSpec):
33
+ return self.on_volume_apply(user=user, project=project, spec=spec)
34
+ if isinstance(spec, GatewaySpec):
35
+ return self.on_gateway_apply(user=user, project=project, spec=spec)
36
+ raise ValueError(f"Unknown spec type {type(spec)}")
37
+
38
+ def on_run_apply(self, user: str, project: str, spec: RunSpec) -> RunSpec:
39
+ """
40
+ Called by the default `on_apply()` implementation for runs.
41
+ """
42
+ return spec
43
+
44
+ def on_fleet_apply(self, user: str, project: str, spec: FleetSpec) -> FleetSpec:
45
+ """
46
+ Called by the default `on_apply()` implementation for fleets.
47
+ """
48
+ return spec
49
+
50
+ def on_volume_apply(self, user: str, project: str, spec: VolumeSpec) -> VolumeSpec:
51
+ """
52
+ Called by the default `on_apply()` implementation for volumes.
53
+ """
54
+ return spec
55
+
56
+ def on_gateway_apply(self, user: str, project: str, spec: GatewaySpec) -> GatewaySpec:
57
+ """
58
+ Called by the default `on_apply()` implementation for gateways.
59
+ """
60
+ return spec
61
+
62
+
63
+ class Plugin:
64
+ """
65
+ A base plugin class.
66
+ Plugins must subclass it, implement public methods,
67
+ and register the subclass as an entrypoint of the package
68
+ (https://packaging.python.org/en/latest/specifications/entry-points/).
69
+ """
70
+
71
+ def get_apply_policies(self) -> list[ApplyPolicy]:
72
+ return []
@@ -0,0 +1,8 @@
1
+ from typing import TypeVar
2
+
3
+ from dstack._internal.core.models.fleets import FleetSpec
4
+ from dstack._internal.core.models.gateways import GatewaySpec
5
+ from dstack._internal.core.models.runs import RunSpec
6
+ from dstack._internal.core.models.volumes import VolumeSpec
7
+
8
+ ApplySpec = TypeVar("ApplySpec", RunSpec, FleetSpec, VolumeSpec, GatewaySpec)
@@ -0,0 +1,19 @@
1
+ import logging
2
+
3
+ from dstack._internal.utils.logging import get_logger
4
+
5
+
6
+ def get_plugin_logger(name: str) -> logging.Logger:
7
+ """
8
+ Use this function to set up loggers in plugins.
9
+
10
+ Put at the top of the plugin modules:
11
+
12
+ ```
13
+ from dstack.plugins import get_plugin_logger
14
+
15
+ logger = get_plugin_logger(__name__)
16
+ ```
17
+
18
+ """
19
+ return get_logger(f"dstack.plugins.{name}")
dstack/version.py CHANGED
@@ -1,3 +1,3 @@
1
- __version__ = "0.19.6rc1"
1
+ __version__ = "0.19.7"
2
2
  __is_release__ = True
3
3
  base_image = "0.7"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dstack
3
- Version: 0.19.6rc1
3
+ Version: 0.19.7
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.2.0,>=0.1.3
24
+ Requires-Dist: gpuhunt==0.1.5
25
25
  Requires-Dist: jsonschema
26
26
  Requires-Dist: packaging
27
27
  Requires-Dist: paramiko>=3.2.0
@@ -52,6 +52,7 @@ Requires-Dist: azure-mgmt-compute>=29.1.0; extra == 'all'
52
52
  Requires-Dist: azure-mgmt-network<28.0.0,>=23.0.0; extra == 'all'
53
53
  Requires-Dist: azure-mgmt-resource>=22.0.0; extra == 'all'
54
54
  Requires-Dist: azure-mgmt-subscription>=3.1.1; extra == 'all'
55
+ Requires-Dist: backports-entry-points-selectable; extra == 'all'
55
56
  Requires-Dist: boto3; extra == 'all'
56
57
  Requires-Dist: botocore; extra == 'all'
57
58
  Requires-Dist: datacrunch; extra == 'all'
@@ -87,6 +88,7 @@ Requires-Dist: alembic-postgresql-enum; extra == 'aws'
87
88
  Requires-Dist: alembic>=1.10.2; extra == 'aws'
88
89
  Requires-Dist: apscheduler<4; extra == 'aws'
89
90
  Requires-Dist: asyncpg; extra == 'aws'
91
+ Requires-Dist: backports-entry-points-selectable; extra == 'aws'
90
92
  Requires-Dist: boto3; extra == 'aws'
91
93
  Requires-Dist: botocore; extra == 'aws'
92
94
  Requires-Dist: docker>=6.0.0; extra == 'aws'
@@ -117,6 +119,7 @@ Requires-Dist: azure-mgmt-compute>=29.1.0; extra == 'azure'
117
119
  Requires-Dist: azure-mgmt-network<28.0.0,>=23.0.0; extra == 'azure'
118
120
  Requires-Dist: azure-mgmt-resource>=22.0.0; extra == 'azure'
119
121
  Requires-Dist: azure-mgmt-subscription>=3.1.1; extra == 'azure'
122
+ Requires-Dist: backports-entry-points-selectable; extra == 'azure'
120
123
  Requires-Dist: docker>=6.0.0; extra == 'azure'
121
124
  Requires-Dist: fastapi; extra == 'azure'
122
125
  Requires-Dist: grpcio>=1.50; extra == 'azure'
@@ -139,6 +142,7 @@ Requires-Dist: alembic-postgresql-enum; extra == 'datacrunch'
139
142
  Requires-Dist: alembic>=1.10.2; extra == 'datacrunch'
140
143
  Requires-Dist: apscheduler<4; extra == 'datacrunch'
141
144
  Requires-Dist: asyncpg; extra == 'datacrunch'
145
+ Requires-Dist: backports-entry-points-selectable; extra == 'datacrunch'
142
146
  Requires-Dist: datacrunch; extra == 'datacrunch'
143
147
  Requires-Dist: docker>=6.0.0; extra == 'datacrunch'
144
148
  Requires-Dist: fastapi; extra == 'datacrunch'
@@ -170,6 +174,7 @@ Requires-Dist: alembic-postgresql-enum; extra == 'gcp'
170
174
  Requires-Dist: alembic>=1.10.2; extra == 'gcp'
171
175
  Requires-Dist: apscheduler<4; extra == 'gcp'
172
176
  Requires-Dist: asyncpg; extra == 'gcp'
177
+ Requires-Dist: backports-entry-points-selectable; extra == 'gcp'
173
178
  Requires-Dist: docker>=6.0.0; extra == 'gcp'
174
179
  Requires-Dist: fastapi; extra == 'gcp'
175
180
  Requires-Dist: google-api-python-client>=2.80.0; extra == 'gcp'
@@ -199,6 +204,7 @@ Requires-Dist: alembic-postgresql-enum; extra == 'kubernetes'
199
204
  Requires-Dist: alembic>=1.10.2; extra == 'kubernetes'
200
205
  Requires-Dist: apscheduler<4; extra == 'kubernetes'
201
206
  Requires-Dist: asyncpg; extra == 'kubernetes'
207
+ Requires-Dist: backports-entry-points-selectable; extra == 'kubernetes'
202
208
  Requires-Dist: docker>=6.0.0; extra == 'kubernetes'
203
209
  Requires-Dist: fastapi; extra == 'kubernetes'
204
210
  Requires-Dist: grpcio>=1.50; extra == 'kubernetes'
@@ -222,6 +228,7 @@ Requires-Dist: alembic-postgresql-enum; extra == 'lambda'
222
228
  Requires-Dist: alembic>=1.10.2; extra == 'lambda'
223
229
  Requires-Dist: apscheduler<4; extra == 'lambda'
224
230
  Requires-Dist: asyncpg; extra == 'lambda'
231
+ Requires-Dist: backports-entry-points-selectable; extra == 'lambda'
225
232
  Requires-Dist: boto3; extra == 'lambda'
226
233
  Requires-Dist: botocore; extra == 'lambda'
227
234
  Requires-Dist: docker>=6.0.0; extra == 'lambda'
@@ -246,6 +253,7 @@ Requires-Dist: alembic-postgresql-enum; extra == 'nebius'
246
253
  Requires-Dist: alembic>=1.10.2; extra == 'nebius'
247
254
  Requires-Dist: apscheduler<4; extra == 'nebius'
248
255
  Requires-Dist: asyncpg; extra == 'nebius'
256
+ Requires-Dist: backports-entry-points-selectable; extra == 'nebius'
249
257
  Requires-Dist: docker>=6.0.0; extra == 'nebius'
250
258
  Requires-Dist: fastapi; extra == 'nebius'
251
259
  Requires-Dist: grpcio>=1.50; extra == 'nebius'
@@ -269,6 +277,7 @@ Requires-Dist: alembic-postgresql-enum; extra == 'oci'
269
277
  Requires-Dist: alembic>=1.10.2; extra == 'oci'
270
278
  Requires-Dist: apscheduler<4; extra == 'oci'
271
279
  Requires-Dist: asyncpg; extra == 'oci'
280
+ Requires-Dist: backports-entry-points-selectable; extra == 'oci'
272
281
  Requires-Dist: docker>=6.0.0; extra == 'oci'
273
282
  Requires-Dist: fastapi; extra == 'oci'
274
283
  Requires-Dist: grpcio>=1.50; extra == 'oci'
@@ -292,6 +301,7 @@ Requires-Dist: alembic-postgresql-enum; extra == 'server'
292
301
  Requires-Dist: alembic>=1.10.2; extra == 'server'
293
302
  Requires-Dist: apscheduler<4; extra == 'server'
294
303
  Requires-Dist: asyncpg; extra == 'server'
304
+ Requires-Dist: backports-entry-points-selectable; extra == 'server'
295
305
  Requires-Dist: docker>=6.0.0; extra == 'server'
296
306
  Requires-Dist: fastapi; extra == 'server'
297
307
  Requires-Dist: grpcio>=1.50; extra == 'server'
@@ -1,5 +1,5 @@
1
1
  dstack/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- dstack/version.py,sha256=UUpgUhdND2BCWQQIg6bu0Vl3VRKsFMFZ9MFjakM6j9w,67
2
+ dstack/version.py,sha256=ikdWwuUYtHtNaJ5eN6aIj36R0uhZ5NlYDVtxl3E_02M,64
3
3
  dstack/_internal/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  dstack/_internal/compat.py,sha256=bF9U9fTMfL8UVhCouedoUSTYFl7UAOiU0WXrnRoByxw,40
5
5
  dstack/_internal/settings.py,sha256=otvcNT0X5UnGZdoNIWNFZBohQRzLme9Zc6oiBzc1BEk,796
@@ -29,21 +29,21 @@ dstack/_internal/cli/services/profile.py,sha256=zfLRuesv5DSnLkbOU-TouvYmGGaKvw33
29
29
  dstack/_internal/cli/services/repos.py,sha256=ImJuElGjsR4OQ7dBquy4PCQUbBM4bR4vhyLwMrVBS7k,3307
30
30
  dstack/_internal/cli/services/configurators/__init__.py,sha256=z94VPBFqybP8Zpwy3CzYxmpPAqYBOvRRLpXoz2H4GKI,2697
31
31
  dstack/_internal/cli/services/configurators/base.py,sha256=bGfde2zoma28lLE8MUACO4-NKT1CdJJQJoXrzjpz0mQ,3360
32
- dstack/_internal/cli/services/configurators/fleet.py,sha256=gxGmAI-03jk1EXwQpABMWf4DedqWnYRqJWXmvrL6ty4,13972
32
+ dstack/_internal/cli/services/configurators/fleet.py,sha256=jm4tNH6QQVplLdboCTlvRYUee3nZ0UYb_qLTrvtYVYM,14049
33
33
  dstack/_internal/cli/services/configurators/gateway.py,sha256=czB2s89s7IowOmWnpDwWErPAUlW3FvFMizImhrkQiBM,8927
34
- dstack/_internal/cli/services/configurators/run.py,sha256=7oUFcoIj4aPfJoI-o5wd4JqYrm5zsdFgQ5OHPiP4wrA,23092
34
+ dstack/_internal/cli/services/configurators/run.py,sha256=arYV5pDAtA4ZJSwY1y4OOYeJpi4JLDteHM2ubI6dCaw,23710
35
35
  dstack/_internal/cli/services/configurators/volume.py,sha256=riMXLQbgvHIIFwLKdHfad-_0iE9wE3G_rUmXU5P3ZS8,8519
36
36
  dstack/_internal/cli/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
37
  dstack/_internal/cli/utils/common.py,sha256=rfmzqrsgR3rXW3wj0vxDdvrhUUg2aIy4A6E9MZbd55g,1763
38
- dstack/_internal/cli/utils/fleet.py,sha256=NpqTdcMVcuUGke9drroxDoNnmifbKV7iziqdGPLc3lk,3885
38
+ dstack/_internal/cli/utils/fleet.py,sha256=ch-LN1X9boSm-rFLW4mAJRmz0XliLhH0LvKD2DqSt2g,3942
39
39
  dstack/_internal/cli/utils/gateway.py,sha256=qMYa1NTAT_O98x2_mSyWDRbiHj5fqt6xUXFh9NIUwAM,1502
40
40
  dstack/_internal/cli/utils/rich.py,sha256=Gx1MJU929kMKsbdo9qF7XHARNta2426Ssb-xMLVhwbQ,5710
41
- dstack/_internal/cli/utils/run.py,sha256=N3S0GG0kQI8dCn5XQDNKNjxOaAFHZG6EJ-RLCYYhHow,8617
41
+ dstack/_internal/cli/utils/run.py,sha256=30K28Y8fnbcME7kVZv8iflpH4FyyC50XehrKLYvMYZ4,8965
42
42
  dstack/_internal/cli/utils/updates.py,sha256=sAPYYptkFzQnGaRjv7FV7HOj-Be3IXGe63xj-sVEpv4,2566
43
43
  dstack/_internal/cli/utils/volume.py,sha256=mU9I06dVMFbpjfkefxrZNoSWadKLoib3U14rHudNQN4,1975
44
44
  dstack/_internal/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
45
  dstack/_internal/core/consts.py,sha256=c1Yd5UY6Qx7KeuYgloXWncWhMsYj6TqwlElda7NtB98,254
46
- dstack/_internal/core/errors.py,sha256=zoYS3na_2qB33TRFYOKTv41XbeQsLqY6HCCpOkgAFVI,3260
46
+ dstack/_internal/core/errors.py,sha256=tJcZXwrvWvvtzIqkzLxJJMrhDuAQl8eOk0xpKCqqHzw,3313
47
47
  dstack/_internal/core/backends/__init__.py,sha256=fwgV8CN8Ap6MZmWklMGHHf0roliBtqne-ijjVOpWcgc,2467
48
48
  dstack/_internal/core/backends/configurators.py,sha256=JxGfZwcmL90akMFkAzzZ_fzPvU2No0pBaBqU_g0D-y0,3775
49
49
  dstack/_internal/core/backends/models.py,sha256=aKQOrDEStouuwY4MacSen7SkoyAa6HR6a6PFq5-cbNk,4088
@@ -87,7 +87,7 @@ dstack/_internal/core/backends/gcp/backend.py,sha256=OvTv1c7j4LTPCIEtkwD3-q6Eo1Q
87
87
  dstack/_internal/core/backends/gcp/compute.py,sha256=uDPoDoqx6S29bAZBQk-Bwoz_u6oMfQB8u2FpMLZ-uDg,40815
88
88
  dstack/_internal/core/backends/gcp/configurator.py,sha256=mvI7WMz8cC1YnN-0KFIIEqkfcEBehRJI0WgKnqILjv0,6730
89
89
  dstack/_internal/core/backends/gcp/models.py,sha256=biLA3rlFcoPatAZpKycuIl-8PdnNSAFiDCJjov65_zo,4612
90
- dstack/_internal/core/backends/gcp/resources.py,sha256=MueDEZwn7K-55Gbq9X9UZBPLd313XSFJwzT7htqfo6U,16208
90
+ dstack/_internal/core/backends/gcp/resources.py,sha256=gLoRo9Z1--n55JTEDVaofUsBo-8h5Nfbhsb_2ydvmK8,16487
91
91
  dstack/_internal/core/backends/gcp/features/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
92
92
  dstack/_internal/core/backends/gcp/features/tcpx.py,sha256=8bDR5kwF5qke5EWNdBscdbZQnC7oVXKSls3WPcoXgZI,2902
93
93
  dstack/_internal/core/backends/kubernetes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -142,7 +142,7 @@ dstack/_internal/core/backends/tensordock/models.py,sha256=oxbSWRCsD0_j4-2lQYo4L
142
142
  dstack/_internal/core/backends/vastai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
143
143
  dstack/_internal/core/backends/vastai/api_client.py,sha256=b-qgx352lnxhBXichJtURFGxR7Jd9JYx4WXaxYV-HH0,4940
144
144
  dstack/_internal/core/backends/vastai/backend.py,sha256=NawGHNYVmgNwL8HruEA84rimnpeSyXccdJXr6Dr95UY,566
145
- dstack/_internal/core/backends/vastai/compute.py,sha256=I0TMGGEh2J0LIIzO1hYRWXDgSELC4eV2F8zczSLHtno,4862
145
+ dstack/_internal/core/backends/vastai/compute.py,sha256=ZjeJF196lXTjO5gQ8fnQtvNEVZdYpcVODmZOOV3g96Q,4911
146
146
  dstack/_internal/core/backends/vastai/configurator.py,sha256=mn8Sx0ZFgTlBa2mf2xNjGymXhbfswgHTjYDUnTiI028,2267
147
147
  dstack/_internal/core/backends/vastai/models.py,sha256=2xpFE-ouYUBienxZXam2Mo4dy9enSv-FuVJXeYnjVLk,1036
148
148
  dstack/_internal/core/backends/vultr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -156,16 +156,16 @@ dstack/_internal/core/models/common.py,sha256=XWd79dmFGMrdpTcStH5fVmNXCKE0s7FsIo
156
156
  dstack/_internal/core/models/config.py,sha256=JJ7rT7dztzTWCY5TkoyxXxTvG5D4IFYhGe7EzwkLOWQ,581
157
157
  dstack/_internal/core/models/configurations.py,sha256=c2717pYpna6u2bmtmC3DlX67HtQa-Tud387mUVa4vEg,18498
158
158
  dstack/_internal/core/models/envs.py,sha256=yq84YRFBILOy4x3XnGcTgYpbZ69eFTCQPgBCr9Ndov4,4969
159
- dstack/_internal/core/models/fleets.py,sha256=o6Kgx7hh0CpGew1h0GIflWmTvSvW6vF1o53g2ccpDJQ,11894
159
+ dstack/_internal/core/models/fleets.py,sha256=gbP2rj1ODDuy4IfTbPNB-ae050thvzTtA8uDb5iTmdY,12059
160
160
  dstack/_internal/core/models/gateways.py,sha256=_O8EWwHWLdgNoWY4P4u71KM-uEr5DDp42LXfyv1qMDI,4054
161
- dstack/_internal/core/models/instances.py,sha256=nX48UuSK6LccFBZ0QSzKWGLuGJp5POmX5HyI93yBXpI,5392
161
+ dstack/_internal/core/models/instances.py,sha256=gwJFF0wJhPQCd0Y5ZiFD9e4_EADP3ktHMurxGV0QR18,5393
162
162
  dstack/_internal/core/models/logs.py,sha256=Lsmtd_NrnChMjBJahUZpFb1j8Xobix9FHWf1L47FOGs,443
163
163
  dstack/_internal/core/models/metrics.py,sha256=Xb8hCXUL-ncQ3PMsErIUAJTe9gwh5jyrQ4UQoZbibsc,269
164
164
  dstack/_internal/core/models/placement.py,sha256=WJVq5ENJykyRarQzL2EeYQag_9_jV7VSAtR_xoFvPVM,720
165
165
  dstack/_internal/core/models/profiles.py,sha256=seeysTuMv1vVUmpHAZgrMUGcbMtH7hSMFIvfx0Qk__0,10406
166
166
  dstack/_internal/core/models/projects.py,sha256=H5ZZRiyUEKifpTFAhl45KBi5ly7ooE0WmI329myK360,643
167
- dstack/_internal/core/models/resources.py,sha256=28xfFnTd5FHjI-WOtRlIj6L4Hmh0sQAMpIMo8nVe7LE,11301
168
- dstack/_internal/core/models/runs.py,sha256=sIuAkBegFsUZIHJPQHd6ZlYxi22bzpQzz_2jxtjfuLs,18196
167
+ dstack/_internal/core/models/resources.py,sha256=xvPuRlF4y3Pj_HNBJTmmzuroTggQQsaR87KERu6asN0,11380
168
+ dstack/_internal/core/models/runs.py,sha256=RvUJHIP1h_GAYiqvTwJxnW46AipEsM92pB33cv21Mn4,18391
169
169
  dstack/_internal/core/models/secrets.py,sha256=IQyemsNpSzqOCB-VlVTuc4gyPFmXXO4mhko0Ur0ey3I,221
170
170
  dstack/_internal/core/models/server.py,sha256=Hkc1v2s3KOiwslsWVmhUOAzcSeREoG-HD1SzSX9WUGg,152
171
171
  dstack/_internal/core/models/services.py,sha256=2Hpi7j0Q1shaf_0wd0C0044AJAmuYi-D3qx3PH849oI,3076
@@ -262,7 +262,7 @@ dstack/_internal/server/background/tasks/process_placement_groups.py,sha256=FqGf
262
262
  dstack/_internal/server/background/tasks/process_prometheus_metrics.py,sha256=u8hCXjOOek7VLEsmLy2VnDXFmIwTNjrJwcpWG7a1zW0,5093
263
263
  dstack/_internal/server/background/tasks/process_running_jobs.py,sha256=U6JdkEnpIApbiSRLKxqjNwA9WFAZY2zZNXujofhUd_g,34719
264
264
  dstack/_internal/server/background/tasks/process_runs.py,sha256=EI1W6HUyB-og3g8BDP_GsBrJjQ-Z3JvZHTuJf7CRKRM,17974
265
- dstack/_internal/server/background/tasks/process_submitted_jobs.py,sha256=ypZw75ZK6_vnp-434obnkFVj6Yll9UylOOydAUrWpc4,26602
265
+ dstack/_internal/server/background/tasks/process_submitted_jobs.py,sha256=-XOApBgmn9ZyCoeXgnbp6cnsFT3uxE_-xqLtn1ez5dc,26603
266
266
  dstack/_internal/server/background/tasks/process_terminating_jobs.py,sha256=0Z3Q409RwSxOL_pgK8JktBthjtESEUH3ahwTLsTdYPk,3800
267
267
  dstack/_internal/server/background/tasks/process_volumes.py,sha256=206rbT4ICeZtEmqh_94Rry_fgHfFLLaSEX9W-svwFk4,5089
268
268
  dstack/_internal/server/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -336,7 +336,7 @@ dstack/_internal/server/migrations/versions/ffa99edd1988_add_jobterminationreaso
336
336
  dstack/_internal/server/routers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
337
337
  dstack/_internal/server/routers/backends.py,sha256=vSCP-wbH-fsoKDR1TrMyQCz5UA-biiY0Lo7ERVz_L_g,4684
338
338
  dstack/_internal/server/routers/fleets.py,sha256=ikXeJ7phoVocU3i-_S6V5RIMygjh-KHuhEIAS5Cw1wo,5565
339
- dstack/_internal/server/routers/gateways.py,sha256=am5HnyWUydgUbEjIdjYBcob8CEIrx4bRtdj9kQhutkA,3149
339
+ dstack/_internal/server/routers/gateways.py,sha256=K_VG5Dt_2F_cykWjtFj4GBwtm_D3xfsE3Bf-lDgQx_w,3171
340
340
  dstack/_internal/server/routers/instances.py,sha256=XOogTC9My2Zv0ck37_PbHKoZI-j4QeGrP2sN5wpX7Ow,1579
341
341
  dstack/_internal/server/routers/logs.py,sha256=_Euk283LbhlwHibJTKM-7YcpbeQFtWBqMfbOry3PSkU,1159
342
342
  dstack/_internal/server/routers/metrics.py,sha256=VFgWhkOvxVFDLlRM_kXHYFylLcfCD6UjXInvcd7H4dY,2314
@@ -365,9 +365,9 @@ dstack/_internal/server/schemas/volumes.py,sha256=9iwaQLMhA6aj9XmtdU_9jWVhpzNOtF
365
365
  dstack/_internal/server/security/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
366
366
  dstack/_internal/server/security/permissions.py,sha256=FJ_8YPhjmebA4jQjtQoAGEaj1Hahb_po0tYRCQ18aaE,4940
367
367
  dstack/_internal/server/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
368
- dstack/_internal/server/services/config.py,sha256=X61ypyuBQqAuWi-mVFJ7Uk1DyGfHqk8_d2C6xIG0yxE,10434
368
+ dstack/_internal/server/services/config.py,sha256=yo8njslwfS7_blhbPPhOtzCMyg8N_mmFSw5aPvirSzw,10691
369
369
  dstack/_internal/server/services/docker.py,sha256=zAvjFHxIP03Td92NzbGEScz0piLjloE60tQ7vz0AeCA,5328
370
- dstack/_internal/server/services/fleets.py,sha256=za87tcoGYxQAMjfTPSf4cBsmNHjzMFyYDltsrtlXcK0,26517
370
+ dstack/_internal/server/services/fleets.py,sha256=JQuXwunED_m_gvLeAO6cUVxeeRKTFAlBHNqQKuvLUZ8,27076
371
371
  dstack/_internal/server/services/instances.py,sha256=omCzPWEIRJQx7WkwyJPmxMtkjas9KtgIFl-zLtnurBY,18610
372
372
  dstack/_internal/server/services/locking.py,sha256=7JUgNSplKRx7dxC4LIpmWw81agUtslEDTeDiNMPbAVg,3013
373
373
  dstack/_internal/server/services/logging.py,sha256=Nu1628kW2hqB__N0Eyr07wGWjVWxfyJnczonTJ72kSM,417
@@ -375,13 +375,14 @@ dstack/_internal/server/services/metrics.py,sha256=jKLy1jSCVR_crqVu_CmsOMbvMkucW
375
375
  dstack/_internal/server/services/offers.py,sha256=At_fRbFCEHBsiJiqIZMMowofO4o0pfs6tSOtFgZc8Nc,7086
376
376
  dstack/_internal/server/services/permissions.py,sha256=l7Ngdelmn65vjw13NcOdaC6lBYMRuSw6FbHzYwdK3nE,1005
377
377
  dstack/_internal/server/services/placement.py,sha256=DWZ8-iAE3o0J0xaHikuJYZzpuBiq7lj41LiAP1PfoEs,1773
378
+ dstack/_internal/server/services/plugins.py,sha256=e5ESJVw5EaxkmxLXdvZdIGaFNxy8k5QOiPQD2zN9w84,2650
378
379
  dstack/_internal/server/services/projects.py,sha256=Je1iWZ-ArmyFxK1yMUzod5WRXyiIDxtuVp6pHcdctTQ,14988
379
380
  dstack/_internal/server/services/prometheus.py,sha256=xq5G-Q2BJup9lS2F6__0wUVTs-k1Gr3dYclGzo2WoWo,12474
380
381
  dstack/_internal/server/services/repos.py,sha256=f9ztN7jz_2gvD9hXF5sJwWDVyG2-NHRfjIdSukowPh8,9342
381
- dstack/_internal/server/services/runs.py,sha256=7CcA41wCtso1Ao7d9g1OEs146FlB3kedCJXcRJU3Jqk,37265
382
+ dstack/_internal/server/services/runs.py,sha256=W8MuRchSGijmFZo7TCdm2ko0LnICUGc1jtOz0qt5p5E,37824
382
383
  dstack/_internal/server/services/storage.py,sha256=6I0xI_3_RpJNbKZwHjDnjrEwXGdHfiaeb5li15T-M1I,1884
383
384
  dstack/_internal/server/services/users.py,sha256=W-5xL7zsHNjeG7BBK54RWGvIrBOrw-FF0NcG_z9qhoE,7466
384
- dstack/_internal/server/services/volumes.py,sha256=sdjSobFdY7CpB5LzZmD6oG1FfmXoF-j283hLCqowFgU,15883
385
+ dstack/_internal/server/services/volumes.py,sha256=vfKY6eZp64I58Mfdvrk9Wig7deveD2Rw4ET1cbc1Sog,16238
385
386
  dstack/_internal/server/services/backends/__init__.py,sha256=Aqo1GoqhZ_FsLEkCcBrvReKSq6E5w4QbBLrDXfGjiKU,13154
386
387
  dstack/_internal/server/services/backends/handlers.py,sha256=j-MhBxrpdepoDG7f2tApjFnE23RVO5I15-hxHyOWnew,3251
387
388
  dstack/_internal/server/services/encryption/__init__.py,sha256=3kCw_cxC3-Un1OIofdW5Gqsm0ZCXXTlGz09cULBx_uc,3155
@@ -389,7 +390,7 @@ dstack/_internal/server/services/encryption/keys/__init__.py,sha256=47DEQpj8HBSa
389
390
  dstack/_internal/server/services/encryption/keys/aes.py,sha256=2He1p2_Rg6hnCeLIGJo-Yfdsij7so_338oY49RXuL3Q,2276
390
391
  dstack/_internal/server/services/encryption/keys/base.py,sha256=mqumJiidoexUPoqxhQG6J_SpC1WGYwkdjKm1MUWnXo8,352
391
392
  dstack/_internal/server/services/encryption/keys/identity.py,sha256=ryb_YSV6u4c7W1OsVfEpzJvZCrR4zZYlzLw_GpjpD2Q,741
392
- dstack/_internal/server/services/gateways/__init__.py,sha256=sFP2vyXvFrg1mqr399s30r8IFMymewXw4NrIjhMyExU,21119
393
+ dstack/_internal/server/services/gateways/__init__.py,sha256=Up8uFsEQDBE0yOXn7n5o7Q8MkY0XJfbMWViMFd2EIL4,21530
393
394
  dstack/_internal/server/services/gateways/client.py,sha256=XIJX3fGBbZ_AG8qZMTSE8KAB_ojq5YJFa0OXoD_dofg,7493
394
395
  dstack/_internal/server/services/gateways/connection.py,sha256=ot3lV85XdmCT45vBWeyj57nLPcLPNm316zu3jMyeWjA,5625
395
396
  dstack/_internal/server/services/gateways/pool.py,sha256=0LclTl1tyx-doS78LeaAKjr-SMp98zuwh5f9s06JSd0,1914
@@ -535,7 +536,7 @@ dstack/_internal/server/utils/common.py,sha256=PbjXtqYy1taKXpyG5ys8cIrz9MXqc9dBA
535
536
  dstack/_internal/server/utils/logging.py,sha256=bxUS2uWG5snypNRfL0d5sMLCDytyOZac81PSQlb7_rs,1907
536
537
  dstack/_internal/server/utils/routers.py,sha256=OzL9Oxy-1no7Txk1r-Pvf28l3S25CYJlyAscYY345Xg,4729
537
538
  dstack/_internal/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
538
- dstack/_internal/utils/common.py,sha256=z6ZCQnvxOfZoiXx7hRKljMsHt5ZDfhNdeyH9RyJ2v3I,8789
539
+ dstack/_internal/utils/common.py,sha256=1Ig2D3XUpYfiDHAsqC3JRKwx8PsWp1sWLRVHOlEKwwU,8827
539
540
  dstack/_internal/utils/crypto.py,sha256=2RTSyzePuwwqc1X2HO6lwcSFyZ2kujnqluoICQ2DLJQ,1462
540
541
  dstack/_internal/utils/dxf.py,sha256=wguK9s6-69kqSDZkxd1kFEr6VlH5ixvFRJxizyOuJ8I,3229
541
542
  dstack/_internal/utils/env.py,sha256=HRbIspHpKHh05fMZeV23-hrZoV6vVMuniefD08u6ey0,357
@@ -558,7 +559,7 @@ dstack/api/_public/backends.py,sha256=w_cIUVU3L1tN8VoPH6-ltq-oJewlfXWTiEFXfDvm-1
558
559
  dstack/api/_public/repos.py,sha256=2ufUtxY8WyPci9tCGYzsnyIFTXNJfTuPzDYvpTydZ6k,6133
559
560
  dstack/api/_public/runs.py,sha256=tILFldJb4EKMYYBMr5rUiD5aJGIPB4-eLXzwwcqPbVg,27828
560
561
  dstack/api/huggingface/__init__.py,sha256=oIrEij3wttLZ1yrywEGvCMd6zswMQrX5pPjrqdSi0UA,2201
561
- dstack/api/server/__init__.py,sha256=pkWTwAuR4SVx8zo6GNER0ERqEUcr67vljtr0ZbxV4tQ,5914
562
+ dstack/api/server/__init__.py,sha256=Zyl1M51tifn4pB150yIsh39N96qUgMjg5XplcElHDxg,6097
562
563
  dstack/api/server/_backends.py,sha256=tSvJ4j-yp-S-4IYo7pKHluDaSsx6Xbwo08Ff6Do85fo,1639
563
564
  dstack/api/server/_fleets.py,sha256=CMxBH49WF18urYx5kj4-B0D5a26QbaO2uDr5Xg-HH0M,4480
564
565
  dstack/api/server/_gateways.py,sha256=FAjSi1l6dj1H0mHuFwcCH-QbRxAxVzPI8xE2cmcvWqo,2688
@@ -573,8 +574,12 @@ dstack/api/server/_users.py,sha256=XzhgGKc5Tsr0-xkz3T6rGyWZ1tO7aYNhLux2eE7dAoY,1
573
574
  dstack/api/server/_volumes.py,sha256=xxOt8o5G-bhMh6wSvF4BDFNoqVEhlM4BXQr2KvX0pN0,1937
574
575
  dstack/api/server/utils.py,sha256=i1KX4CNXVeDj9CnytdzsJz0bxjvvfLRTb7xw8oqtEtQ,1040
575
576
  dstack/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
576
- dstack-0.19.6rc1.dist-info/METADATA,sha256=fXqtx7wlO0dEoAV7Aerh3ExEO9ZdwU0j15HT6Kio8Fw,19377
577
- dstack-0.19.6rc1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
578
- dstack-0.19.6rc1.dist-info/entry_points.txt,sha256=GnLrMS8hx3rWAySQjA7tPNhtixV6a-brRkmal1PKoHc,58
579
- dstack-0.19.6rc1.dist-info/licenses/LICENSE.md,sha256=qDABaRGjSKVOib1U8viw2P_96sIK7Puo426784oD9f8,15976
580
- dstack-0.19.6rc1.dist-info/RECORD,,
577
+ dstack/plugins/__init__.py,sha256=buT1pcyORLgVbl89ATkRWJPhvejriVz7sNBjvuZRCRE,403
578
+ dstack/plugins/_base.py,sha256=-etiB-EozaJCg2wtmONfj8ic-K03qXvXyl_TIDp-kNE,2662
579
+ dstack/plugins/_models.py,sha256=1Gw--mDQ1_0FFr9Zur9LE8UbMoWESUpTdHHt12AyIZo,341
580
+ dstack/plugins/_utils.py,sha256=FqeWYb7zOrgZkO9Bd8caL5I81_TUEsysIzvxsULrmzk,392
581
+ dstack-0.19.7.dist-info/METADATA,sha256=g67K4f_PhyA2NrwUmQ-SG1cb45HpHMoQUtd5YQwZ3d4,20042
582
+ dstack-0.19.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
583
+ dstack-0.19.7.dist-info/entry_points.txt,sha256=GnLrMS8hx3rWAySQjA7tPNhtixV6a-brRkmal1PKoHc,58
584
+ dstack-0.19.7.dist-info/licenses/LICENSE.md,sha256=qDABaRGjSKVOib1U8viw2P_96sIK7Puo426784oD9f8,15976
585
+ dstack-0.19.7.dist-info/RECORD,,