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
@@ -10,12 +10,23 @@ from pydantic import Field, ValidationError, conint, constr, root_validator, val
10
10
  from typing_extensions import Self
11
11
 
12
12
  from dstack._internal.core.errors import ConfigurationError
13
- from dstack._internal.core.models.common import CoreModel, Duration, RegistryAuth
13
+ from dstack._internal.core.models.common import (
14
+ CoreConfig,
15
+ CoreModel,
16
+ Duration,
17
+ RegistryAuth,
18
+ generate_dual_core_model,
19
+ )
14
20
  from dstack._internal.core.models.envs import Env
15
21
  from dstack._internal.core.models.files import FilePathMapping
16
22
  from dstack._internal.core.models.fleets import FleetConfiguration
17
23
  from dstack._internal.core.models.gateways import GatewayConfiguration
18
- from dstack._internal.core.models.profiles import ProfileParams, parse_duration, parse_off_duration
24
+ from dstack._internal.core.models.profiles import (
25
+ ProfileParams,
26
+ ProfileParamsConfig,
27
+ parse_duration,
28
+ parse_off_duration,
29
+ )
19
30
  from dstack._internal.core.models.resources import Range, ResourcesSpec
20
31
  from dstack._internal.core.models.services import AnyModel, OpenAIChatModel
21
32
  from dstack._internal.core.models.unix import UnixUser
@@ -34,7 +45,7 @@ STRIP_PREFIX_DEFAULT = True
34
45
  RUN_PRIOTIRY_MIN = 0
35
46
  RUN_PRIOTIRY_MAX = 100
36
47
  RUN_PRIORITY_DEFAULT = 0
37
- DEFAULT_REPO_DIR = "/workflow"
48
+ LEGACY_REPO_DIR = "/workflow"
38
49
  MIN_PROBE_TIMEOUT = 1
39
50
  MIN_PROBE_INTERVAL = 1
40
51
  DEFAULT_PROBE_URL = "/"
@@ -112,8 +123,15 @@ class RepoSpec(CoreModel):
112
123
  Optional[str],
113
124
  Field(description="The commit hash"),
114
125
  ] = None
115
- # Not implemented, has no effect, hidden in the docs
116
- path: str = DEFAULT_REPO_DIR
126
+ path: Annotated[
127
+ Optional[str],
128
+ Field(
129
+ description=(
130
+ "The repo path inside the run container. Relative paths are resolved"
131
+ f" relative to the working directory. Defaults to `{LEGACY_REPO_DIR}`"
132
+ )
133
+ ),
134
+ ] = None
117
135
 
118
136
  @classmethod
119
137
  def parse(cls, v: str) -> Self:
@@ -149,6 +167,14 @@ class RepoSpec(CoreModel):
149
167
  raise ValueError("Either `local_path` or `url` must be specified")
150
168
  return values
151
169
 
170
+ @validator("path")
171
+ def validate_path(cls, v: Optional[str]) -> Optional[str]:
172
+ if v is None:
173
+ return v
174
+ if v.startswith("~") and PurePosixPath(v).parts[0] != "~":
175
+ raise ValueError("`~username` syntax is not supported")
176
+ return v
177
+
152
178
 
153
179
  class ScalingSpec(CoreModel):
154
180
  metric: Annotated[
@@ -261,7 +287,20 @@ class HTTPHeaderSpec(CoreModel):
261
287
  ]
262
288
 
263
289
 
264
- class ProbeConfig(CoreModel):
290
+ class ProbeConfigConfig(CoreConfig):
291
+ @staticmethod
292
+ def schema_extra(schema: Dict[str, Any]):
293
+ add_extra_schema_types(
294
+ schema["properties"]["timeout"],
295
+ extra_types=[{"type": "string"}],
296
+ )
297
+ add_extra_schema_types(
298
+ schema["properties"]["interval"],
299
+ extra_types=[{"type": "string"}],
300
+ )
301
+
302
+
303
+ class ProbeConfig(generate_dual_core_model(ProbeConfigConfig)):
265
304
  type: Literal["http"] # expect other probe types in the future, namely `exec`
266
305
  url: Annotated[
267
306
  Optional[str], Field(description=f"The URL to request. Defaults to `{DEFAULT_PROBE_URL}`")
@@ -316,18 +355,6 @@ class ProbeConfig(CoreModel):
316
355
  ),
317
356
  ] = None
318
357
 
319
- class Config(CoreModel.Config):
320
- @staticmethod
321
- def schema_extra(schema: Dict[str, Any]):
322
- add_extra_schema_types(
323
- schema["properties"]["timeout"],
324
- extra_types=[{"type": "string"}],
325
- )
326
- add_extra_schema_types(
327
- schema["properties"]["interval"],
328
- extra_types=[{"type": "string"}],
329
- )
330
-
331
358
  @validator("timeout", pre=True)
332
359
  def parse_timeout(cls, v: Optional[Union[int, str]]) -> Optional[int]:
333
360
  if v is None:
@@ -366,6 +393,19 @@ class ProbeConfig(CoreModel):
366
393
  return values
367
394
 
368
395
 
396
+ class BaseRunConfigurationConfig(CoreConfig):
397
+ @staticmethod
398
+ def schema_extra(schema: Dict[str, Any]):
399
+ add_extra_schema_types(
400
+ schema["properties"]["volumes"]["items"],
401
+ extra_types=[{"type": "string"}],
402
+ )
403
+ add_extra_schema_types(
404
+ schema["properties"]["files"]["items"],
405
+ extra_types=[{"type": "string"}],
406
+ )
407
+
408
+
369
409
  class BaseRunConfiguration(CoreModel):
370
410
  type: Literal["none"]
371
411
  name: Annotated[
@@ -380,7 +420,7 @@ class BaseRunConfiguration(CoreModel):
380
420
  Field(
381
421
  description=(
382
422
  "The user inside the container, `user_name_or_id[:group_name_or_id]`"
383
- " (e.g., `ubuntu`, `1000:1000`). Defaults to the default `image` user"
423
+ " (e.g., `ubuntu`, `1000:1000`). Defaults to the default user from the `image`"
384
424
  )
385
425
  ),
386
426
  ] = None
@@ -390,9 +430,8 @@ class BaseRunConfiguration(CoreModel):
390
430
  Optional[str],
391
431
  Field(
392
432
  description=(
393
- "The path to the working directory inside the container."
394
- f" It's specified relative to the repository directory (`{DEFAULT_REPO_DIR}`) and should be inside it."
395
- ' Defaults to `"."` '
433
+ "The absolute path to the working directory inside the container."
434
+ f" Defaults to `{LEGACY_REPO_DIR}`"
396
435
  )
397
436
  ),
398
437
  ] = None
@@ -470,18 +509,6 @@ class BaseRunConfiguration(CoreModel):
470
509
  # deprecated since 0.18.31; task, service -- no effect; dev-environment -- executed right before `init`
471
510
  setup: CommandsList = []
472
511
 
473
- class Config(CoreModel.Config):
474
- @staticmethod
475
- def schema_extra(schema: Dict[str, Any]):
476
- add_extra_schema_types(
477
- schema["properties"]["volumes"]["items"],
478
- extra_types=[{"type": "string"}],
479
- )
480
- add_extra_schema_types(
481
- schema["properties"]["files"]["items"],
482
- extra_types=[{"type": "string"}],
483
- )
484
-
485
512
  @validator("python", pre=True, always=True)
486
513
  def convert_python(cls, v, values) -> Optional[PythonVersion]:
487
514
  if v is not None and values.get("image"):
@@ -607,20 +634,25 @@ class DevEnvironmentConfigurationParams(CoreModel):
607
634
  return None
608
635
 
609
636
 
637
+ class DevEnvironmentConfigurationConfig(
638
+ ProfileParamsConfig,
639
+ BaseRunConfigurationConfig,
640
+ ):
641
+ @staticmethod
642
+ def schema_extra(schema: Dict[str, Any]):
643
+ ProfileParamsConfig.schema_extra(schema)
644
+ BaseRunConfigurationConfig.schema_extra(schema)
645
+
646
+
610
647
  class DevEnvironmentConfiguration(
611
648
  ProfileParams,
612
649
  BaseRunConfiguration,
613
650
  ConfigurationWithPortsParams,
614
651
  DevEnvironmentConfigurationParams,
652
+ generate_dual_core_model(DevEnvironmentConfigurationConfig),
615
653
  ):
616
654
  type: Literal["dev-environment"] = "dev-environment"
617
655
 
618
- class Config(ProfileParams.Config, BaseRunConfiguration.Config):
619
- @staticmethod
620
- def schema_extra(schema: Dict[str, Any]):
621
- ProfileParams.Config.schema_extra(schema)
622
- BaseRunConfiguration.Config.schema_extra(schema)
623
-
624
656
  @validator("entrypoint")
625
657
  def validate_entrypoint(cls, v: Optional[str]) -> Optional[str]:
626
658
  if v is not None:
@@ -632,20 +664,38 @@ class TaskConfigurationParams(CoreModel):
632
664
  nodes: Annotated[int, Field(description="Number of nodes", ge=1)] = 1
633
665
 
634
666
 
667
+ class TaskConfigurationConfig(
668
+ ProfileParamsConfig,
669
+ BaseRunConfigurationConfig,
670
+ ):
671
+ @staticmethod
672
+ def schema_extra(schema: Dict[str, Any]):
673
+ ProfileParamsConfig.schema_extra(schema)
674
+ BaseRunConfigurationConfig.schema_extra(schema)
675
+
676
+
635
677
  class TaskConfiguration(
636
678
  ProfileParams,
637
679
  BaseRunConfiguration,
638
680
  ConfigurationWithCommandsParams,
639
681
  ConfigurationWithPortsParams,
640
682
  TaskConfigurationParams,
683
+ generate_dual_core_model(TaskConfigurationConfig),
641
684
  ):
642
685
  type: Literal["task"] = "task"
643
686
 
644
- class Config(ProfileParams.Config, BaseRunConfiguration.Config):
645
- @staticmethod
646
- def schema_extra(schema: Dict[str, Any]):
647
- ProfileParams.Config.schema_extra(schema)
648
- BaseRunConfiguration.Config.schema_extra(schema)
687
+
688
+ class ServiceConfigurationParamsConfig(CoreConfig):
689
+ @staticmethod
690
+ def schema_extra(schema: Dict[str, Any]):
691
+ add_extra_schema_types(
692
+ schema["properties"]["replicas"],
693
+ extra_types=[{"type": "integer"}, {"type": "string"}],
694
+ )
695
+ add_extra_schema_types(
696
+ schema["properties"]["model"],
697
+ extra_types=[{"type": "string"}],
698
+ )
649
699
 
650
700
 
651
701
  class ServiceConfigurationParams(CoreModel):
@@ -705,18 +755,6 @@ class ServiceConfigurationParams(CoreModel):
705
755
  Field(description="List of probes used to determine job health"),
706
756
  ] = []
707
757
 
708
- class Config(CoreModel.Config):
709
- @staticmethod
710
- def schema_extra(schema: Dict[str, Any]):
711
- add_extra_schema_types(
712
- schema["properties"]["replicas"],
713
- extra_types=[{"type": "integer"}, {"type": "string"}],
714
- )
715
- add_extra_schema_types(
716
- schema["properties"]["model"],
717
- extra_types=[{"type": "string"}],
718
- )
719
-
720
758
  @validator("port")
721
759
  def convert_port(cls, v) -> PortMapping:
722
760
  if isinstance(v, int):
@@ -783,25 +821,27 @@ class ServiceConfigurationParams(CoreModel):
783
821
  return v
784
822
 
785
823
 
824
+ class ServiceConfigurationConfig(
825
+ ProfileParamsConfig,
826
+ BaseRunConfigurationConfig,
827
+ ServiceConfigurationParamsConfig,
828
+ ):
829
+ @staticmethod
830
+ def schema_extra(schema: Dict[str, Any]):
831
+ ProfileParamsConfig.schema_extra(schema)
832
+ BaseRunConfigurationConfig.schema_extra(schema)
833
+ ServiceConfigurationParamsConfig.schema_extra(schema)
834
+
835
+
786
836
  class ServiceConfiguration(
787
837
  ProfileParams,
788
838
  BaseRunConfiguration,
789
839
  ConfigurationWithCommandsParams,
790
840
  ServiceConfigurationParams,
841
+ generate_dual_core_model(ServiceConfigurationConfig),
791
842
  ):
792
843
  type: Literal["service"] = "service"
793
844
 
794
- class Config(
795
- ProfileParams.Config,
796
- BaseRunConfiguration.Config,
797
- ServiceConfigurationParams.Config,
798
- ):
799
- @staticmethod
800
- def schema_extra(schema: Dict[str, Any]):
801
- ProfileParams.Config.schema_extra(schema)
802
- BaseRunConfiguration.Config.schema_extra(schema)
803
- ServiceConfigurationParams.Config.schema_extra(schema)
804
-
805
845
 
806
846
  AnyRunConfiguration = Union[DevEnvironmentConfiguration, TaskConfiguration, ServiceConfiguration]
807
847
 
@@ -862,7 +902,7 @@ class DstackConfiguration(CoreModel):
862
902
  Field(discriminator="type"),
863
903
  ]
864
904
 
865
- class Config(CoreModel.Config):
905
+ class Config(CoreConfig):
866
906
  json_loads = orjson.loads
867
907
  json_dumps = pydantic_orjson_dumps_with_indent
868
908
 
@@ -28,7 +28,7 @@ class FilePathMapping(CoreModel):
28
28
  Field(
29
29
  description=(
30
30
  "The path in the container. Relative paths are resolved relative to"
31
- " the repo directory"
31
+ " the working directory"
32
32
  )
33
33
  ),
34
34
  ]
@@ -2,13 +2,18 @@ import ipaddress
2
2
  import uuid
3
3
  from datetime import datetime
4
4
  from enum import Enum
5
- from typing import Any, Dict, List, Optional, Type, Union
5
+ from typing import Any, Dict, List, Optional, Union
6
6
 
7
7
  from pydantic import Field, root_validator, validator
8
8
  from typing_extensions import Annotated, Literal
9
9
 
10
10
  from dstack._internal.core.models.backends.base import BackendType
11
- from dstack._internal.core.models.common import ApplyAction, CoreModel
11
+ from dstack._internal.core.models.common import (
12
+ ApplyAction,
13
+ CoreConfig,
14
+ CoreModel,
15
+ generate_dual_core_model,
16
+ )
12
17
  from dstack._internal.core.models.envs import Env
13
18
  from dstack._internal.core.models.instances import Instance, InstanceOfferWithAvailability, SSHKey
14
19
  from dstack._internal.core.models.profiles import (
@@ -19,7 +24,7 @@ from dstack._internal.core.models.profiles import (
19
24
  TerminationPolicy,
20
25
  parse_idle_duration,
21
26
  )
22
- from dstack._internal.core.models.resources import Range, ResourcesSpec
27
+ from dstack._internal.core.models.resources import ResourcesSpec
23
28
  from dstack._internal.utils.common import list_enum_values_for_annotation
24
29
  from dstack._internal.utils.json_schema import add_extra_schema_types
25
30
  from dstack._internal.utils.tags import tags_validator
@@ -141,6 +146,82 @@ class SSHParams(CoreModel):
141
146
  return value
142
147
 
143
148
 
149
+ class FleetNodesSpec(CoreModel):
150
+ min: Annotated[
151
+ int, Field(description=("The minimum number of instances to maintain in the fleet"))
152
+ ]
153
+ target: Annotated[
154
+ int,
155
+ Field(
156
+ description=(
157
+ "The number of instances to provision on fleet apply. `min` <= `target` <= `max`"
158
+ " Defaults to `min`"
159
+ )
160
+ ),
161
+ ]
162
+ max: Annotated[
163
+ Optional[int],
164
+ Field(
165
+ description=(
166
+ "The maximum number of instances allowed in the fleet. Unlimited if not specified"
167
+ )
168
+ ),
169
+ ] = None
170
+
171
+ def dict(self, *args, **kwargs) -> Dict:
172
+ # super() does not work with pydantic-duality
173
+ res = CoreModel.dict(self, *args, **kwargs)
174
+ # For backward compatibility with old clients
175
+ # that do not ignore extra fields due to https://github.com/dstackai/dstack/issues/3066
176
+ if "target" in res and res["target"] == res["min"]:
177
+ del res["target"]
178
+ return res
179
+
180
+ @root_validator(pre=True)
181
+ def set_min_and_target_defaults(cls, values):
182
+ min_ = values.get("min")
183
+ target = values.get("target")
184
+ if min_ is None:
185
+ values["min"] = 0
186
+ if target is None:
187
+ values["target"] = values["min"]
188
+ return values
189
+
190
+ @validator("min")
191
+ def validate_min(cls, v: int) -> int:
192
+ if v < 0:
193
+ raise ValueError("min cannot be negative")
194
+ return v
195
+
196
+ @root_validator(skip_on_failure=True)
197
+ def _post_validate_ranges(cls, values):
198
+ min_ = values["min"]
199
+ target = values["target"]
200
+ max_ = values.get("max")
201
+ if target < min_:
202
+ raise ValueError("target must not be be less than min")
203
+ if max_ is not None and max_ < min_:
204
+ raise ValueError("max must not be less than min")
205
+ if max_ is not None and max_ < target:
206
+ raise ValueError("max must not be less than target")
207
+ return values
208
+
209
+
210
+ class InstanceGroupParamsConfig(CoreConfig):
211
+ @staticmethod
212
+ def schema_extra(schema: Dict[str, Any]):
213
+ del schema["properties"]["termination_policy"]
214
+ del schema["properties"]["termination_idle_time"]
215
+ add_extra_schema_types(
216
+ schema["properties"]["nodes"],
217
+ extra_types=[{"type": "integer"}, {"type": "string"}],
218
+ )
219
+ add_extra_schema_types(
220
+ schema["properties"]["idle_duration"],
221
+ extra_types=[{"type": "string"}],
222
+ )
223
+
224
+
144
225
  class InstanceGroupParams(CoreModel):
145
226
  env: Annotated[
146
227
  Env,
@@ -151,7 +232,9 @@ class InstanceGroupParams(CoreModel):
151
232
  Field(description="The parameters for adding instances via SSH"),
152
233
  ] = None
153
234
 
154
- nodes: Annotated[Optional[Range[int]], Field(description="The number of instances")] = None
235
+ nodes: Annotated[
236
+ Optional[FleetNodesSpec], Field(description="The number of instances in cloud fleet")
237
+ ] = None
155
238
  placement: Annotated[
156
239
  Optional[InstanceGroupPlacement],
157
240
  Field(description="The placement of instances: `any` or `cluster`"),
@@ -234,19 +317,15 @@ class InstanceGroupParams(CoreModel):
234
317
  termination_policy: Annotated[Optional[TerminationPolicy], Field(exclude=True)] = None
235
318
  termination_idle_time: Annotated[Optional[Union[str, int]], Field(exclude=True)] = None
236
319
 
237
- class Config(CoreModel.Config):
238
- @staticmethod
239
- def schema_extra(schema: Dict[str, Any], model: Type):
240
- del schema["properties"]["termination_policy"]
241
- del schema["properties"]["termination_idle_time"]
242
- add_extra_schema_types(
243
- schema["properties"]["nodes"],
244
- extra_types=[{"type": "integer"}, {"type": "string"}],
245
- )
246
- add_extra_schema_types(
247
- schema["properties"]["idle_duration"],
248
- extra_types=[{"type": "string"}],
249
- )
320
+ @validator("nodes", pre=True)
321
+ def parse_nodes(cls, v: Optional[Union[dict, str]]) -> Optional[dict]:
322
+ if isinstance(v, str) and ".." in v:
323
+ v = v.replace(" ", "")
324
+ min, max = v.split("..")
325
+ return dict(min=min or None, max=max or None)
326
+ elif isinstance(v, str) or isinstance(v, int):
327
+ return dict(min=v, max=v)
328
+ return v
250
329
 
251
330
  _validate_idle_duration = validator("idle_duration", pre=True, allow_reuse=True)(
252
331
  parse_idle_duration
@@ -258,7 +337,17 @@ class FleetProps(CoreModel):
258
337
  name: Annotated[Optional[str], Field(description="The fleet name")] = None
259
338
 
260
339
 
261
- class FleetConfiguration(InstanceGroupParams, FleetProps):
340
+ class FleetConfigurationConfig(InstanceGroupParamsConfig):
341
+ @staticmethod
342
+ def schema_extra(schema: Dict[str, Any]):
343
+ InstanceGroupParamsConfig.schema_extra(schema)
344
+
345
+
346
+ class FleetConfiguration(
347
+ InstanceGroupParams,
348
+ FleetProps,
349
+ generate_dual_core_model(FleetConfigurationConfig),
350
+ ):
262
351
  tags: Annotated[
263
352
  Optional[Dict[str, str]],
264
353
  Field(
@@ -273,7 +362,14 @@ class FleetConfiguration(InstanceGroupParams, FleetProps):
273
362
  _validate_tags = validator("tags", pre=True, allow_reuse=True)(tags_validator)
274
363
 
275
364
 
276
- class FleetSpec(CoreModel):
365
+ class FleetSpecConfig(CoreConfig):
366
+ @staticmethod
367
+ def schema_extra(schema: Dict[str, Any]):
368
+ prop = schema.get("properties", {})
369
+ prop.pop("merged_profile", None)
370
+
371
+
372
+ class FleetSpec(generate_dual_core_model(FleetSpecConfig)):
277
373
  configuration: FleetConfiguration
278
374
  configuration_path: Optional[str] = None
279
375
  profile: Profile
@@ -283,12 +379,6 @@ class FleetSpec(CoreModel):
283
379
  # TODO: make merged_profile a computed field after migrating to pydanticV2
284
380
  merged_profile: Annotated[Profile, Field(exclude=True)] = None
285
381
 
286
- class Config(CoreModel.Config):
287
- @staticmethod
288
- def schema_extra(schema: Dict[str, Any], model: Type) -> None:
289
- prop = schema.get("properties", {})
290
- prop.pop("merged_profile", None)
291
-
292
382
  @root_validator
293
383
  def _merged_profile(cls, values) -> Dict:
294
384
  try:
@@ -7,7 +7,10 @@ import gpuhunt
7
7
  from pydantic import root_validator
8
8
 
9
9
  from dstack._internal.core.models.backends.base import BackendType
10
- from dstack._internal.core.models.common import CoreModel
10
+ from dstack._internal.core.models.common import (
11
+ CoreModel,
12
+ FrozenCoreModel,
13
+ )
11
14
  from dstack._internal.core.models.envs import Env
12
15
  from dstack._internal.core.models.health import HealthStatus
13
16
  from dstack._internal.core.models.volumes import Volume
@@ -117,14 +120,11 @@ class InstanceType(CoreModel):
117
120
  resources: Resources
118
121
 
119
122
 
120
- class SSHConnectionParams(CoreModel):
123
+ class SSHConnectionParams(FrozenCoreModel):
121
124
  hostname: str
122
125
  username: str
123
126
  port: int
124
127
 
125
- class Config(CoreModel.Config):
126
- frozen = True
127
-
128
128
 
129
129
  class SSHKey(CoreModel):
130
130
  public: str