dstack 0.19.33__py3-none-any.whl → 0.19.35__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 (56) hide show
  1. dstack/_internal/cli/services/configurators/run.py +1 -1
  2. dstack/_internal/core/backends/base/compute.py +20 -1
  3. dstack/_internal/core/backends/base/models.py +10 -0
  4. dstack/_internal/core/backends/base/offers.py +4 -1
  5. dstack/_internal/core/backends/features.py +5 -0
  6. dstack/_internal/core/backends/gcp/compute.py +24 -9
  7. dstack/_internal/core/backends/gcp/models.py +4 -1
  8. dstack/_internal/core/backends/nebius/compute.py +28 -16
  9. dstack/_internal/core/backends/nebius/configurator.py +1 -1
  10. dstack/_internal/core/backends/nebius/models.py +4 -0
  11. dstack/_internal/core/backends/nebius/resources.py +41 -20
  12. dstack/_internal/core/backends/runpod/api_client.py +245 -59
  13. dstack/_internal/core/backends/runpod/compute.py +161 -14
  14. dstack/_internal/core/compatibility/runs.py +25 -4
  15. dstack/_internal/core/models/compute_groups.py +39 -0
  16. dstack/_internal/core/models/fleets.py +6 -1
  17. dstack/_internal/core/models/instances.py +2 -1
  18. dstack/_internal/core/models/profiles.py +3 -1
  19. dstack/_internal/core/models/runs.py +4 -0
  20. dstack/_internal/core/services/ssh/key_manager.py +56 -0
  21. dstack/_internal/server/app.py +14 -2
  22. dstack/_internal/server/background/__init__.py +7 -0
  23. dstack/_internal/server/background/tasks/process_compute_groups.py +164 -0
  24. dstack/_internal/server/background/tasks/process_instances.py +81 -49
  25. dstack/_internal/server/background/tasks/process_submitted_jobs.py +179 -84
  26. dstack/_internal/server/migrations/env.py +20 -2
  27. dstack/_internal/server/migrations/versions/7d1ec2b920ac_add_computegroupmodel.py +93 -0
  28. dstack/_internal/server/models.py +42 -0
  29. dstack/_internal/server/routers/metrics.py +6 -2
  30. dstack/_internal/server/routers/runs.py +15 -6
  31. dstack/_internal/server/routers/users.py +7 -0
  32. dstack/_internal/server/services/compute_groups.py +22 -0
  33. dstack/_internal/server/services/fleets.py +1 -0
  34. dstack/_internal/server/services/jobs/__init__.py +31 -9
  35. dstack/_internal/server/services/jobs/configurators/base.py +3 -2
  36. dstack/_internal/server/services/offers.py +1 -0
  37. dstack/_internal/server/services/requirements/combine.py +1 -0
  38. dstack/_internal/server/services/runs.py +21 -3
  39. dstack/_internal/server/services/users.py +3 -3
  40. dstack/_internal/server/statics/index.html +1 -1
  41. dstack/_internal/server/statics/{main-97c7e184573ca23f9fe4.js → main-e79754c136f1d8e4e7e6.js} +11 -11
  42. dstack/_internal/server/statics/{main-97c7e184573ca23f9fe4.js.map → main-e79754c136f1d8e4e7e6.js.map} +1 -1
  43. dstack/_internal/server/testing/common.py +55 -0
  44. dstack/_internal/server/utils/routers.py +18 -20
  45. dstack/_internal/settings.py +4 -1
  46. dstack/_internal/utils/version.py +22 -0
  47. dstack/api/_public/__init__.py +2 -2
  48. dstack/api/_public/runs.py +36 -39
  49. dstack/api/server/__init__.py +4 -0
  50. dstack/version.py +1 -1
  51. {dstack-0.19.33.dist-info → dstack-0.19.35.dist-info}/METADATA +4 -4
  52. {dstack-0.19.33.dist-info → dstack-0.19.35.dist-info}/RECORD +55 -50
  53. dstack/_internal/core/backends/nebius/fabrics.py +0 -49
  54. {dstack-0.19.33.dist-info → dstack-0.19.35.dist-info}/WHEEL +0 -0
  55. {dstack-0.19.33.dist-info → dstack-0.19.35.dist-info}/entry_points.txt +0 -0
  56. {dstack-0.19.33.dist-info → dstack-0.19.35.dist-info}/licenses/LICENSE.md +0 -0
@@ -13,6 +13,7 @@ from dstack._internal.core.backends.base.compute import (
13
13
  Compute,
14
14
  ComputeWithCreateInstanceSupport,
15
15
  ComputeWithGatewaySupport,
16
+ ComputeWithGroupProvisioningSupport,
16
17
  ComputeWithMultinodeSupport,
17
18
  ComputeWithPlacementGroupSupport,
18
19
  ComputeWithPrivateGatewaySupport,
@@ -22,6 +23,10 @@ from dstack._internal.core.backends.base.compute import (
22
23
  )
23
24
  from dstack._internal.core.models.backends.base import BackendType
24
25
  from dstack._internal.core.models.common import NetworkMode
26
+ from dstack._internal.core.models.compute_groups import (
27
+ ComputeGroupProvisioningData,
28
+ ComputeGroupStatus,
29
+ )
25
30
  from dstack._internal.core.models.configurations import (
26
31
  AnyRunConfiguration,
27
32
  DevEnvironmentConfiguration,
@@ -83,6 +88,7 @@ from dstack._internal.core.models.volumes import (
83
88
  )
84
89
  from dstack._internal.server.models import (
85
90
  BackendModel,
91
+ ComputeGroupModel,
86
92
  DecryptedString,
87
93
  FileArchiveModel,
88
94
  FleetModel,
@@ -126,6 +132,8 @@ async def create_user(
126
132
  global_role: GlobalRole = GlobalRole.ADMIN,
127
133
  token: Optional[str] = None,
128
134
  email: Optional[str] = None,
135
+ ssh_public_key: Optional[str] = None,
136
+ ssh_private_key: Optional[str] = None,
129
137
  active: bool = True,
130
138
  ) -> UserModel:
131
139
  if token is None:
@@ -137,6 +145,8 @@ async def create_user(
137
145
  token=DecryptedString(plaintext=token),
138
146
  token_hash=get_token_hash(token),
139
147
  email=email,
148
+ ssh_public_key=ssh_public_key,
149
+ ssh_private_key=ssh_private_key,
140
150
  active=active,
141
151
  )
142
152
  session.add(user)
@@ -349,6 +359,7 @@ async def create_job(
349
359
  instance_assigned: bool = False,
350
360
  disconnected_at: Optional[datetime] = None,
351
361
  registered: bool = False,
362
+ waiting_master_job: Optional[bool] = None,
352
363
  ) -> JobModel:
353
364
  if deployment_num is None:
354
365
  deployment_num = run.deployment_num
@@ -380,6 +391,7 @@ async def create_job(
380
391
  disconnected_at=disconnected_at,
381
392
  probes=[],
382
393
  registered=registered,
394
+ waiting_master_job=waiting_master_job,
383
395
  )
384
396
  session.add(job)
385
397
  await session.commit()
@@ -451,6 +463,48 @@ def get_job_runtime_data(
451
463
  )
452
464
 
453
465
 
466
+ def get_compute_group_provisioning_data(
467
+ compute_group_id: str = "test_compute_group",
468
+ compute_group_name: str = "test_compute_group",
469
+ backend: BackendType = BackendType.RUNPOD,
470
+ region: str = "US",
471
+ job_provisioning_datas: Optional[list[JobProvisioningData]] = None,
472
+ backend_data: Optional[str] = None,
473
+ ) -> ComputeGroupProvisioningData:
474
+ if job_provisioning_datas is None:
475
+ job_provisioning_datas = []
476
+ return ComputeGroupProvisioningData(
477
+ compute_group_id=compute_group_id,
478
+ compute_group_name=compute_group_name,
479
+ backend=backend,
480
+ region=region,
481
+ job_provisioning_datas=job_provisioning_datas,
482
+ backend_data=backend_data,
483
+ )
484
+
485
+
486
+ async def create_compute_group(
487
+ session: AsyncSession,
488
+ project: ProjectModel,
489
+ fleet: FleetModel,
490
+ status: ComputeGroupStatus = ComputeGroupStatus.RUNNING,
491
+ provisioning_data: Optional[ComputeGroupProvisioningData] = None,
492
+ last_processed_at: datetime = datetime(2023, 1, 2, 3, 4, tzinfo=timezone.utc),
493
+ ):
494
+ if provisioning_data is None:
495
+ provisioning_data = get_compute_group_provisioning_data()
496
+ compute_group = ComputeGroupModel(
497
+ project=project,
498
+ fleet=fleet,
499
+ status=status,
500
+ provisioning_data=provisioning_data.json(),
501
+ last_processed_at=last_processed_at,
502
+ )
503
+ session.add(compute_group)
504
+ await session.commit()
505
+ return compute_group
506
+
507
+
454
508
  async def create_probe(
455
509
  session: AsyncSession,
456
510
  job: JobModel,
@@ -1132,6 +1186,7 @@ class AsyncContextManager:
1132
1186
  class ComputeMockSpec(
1133
1187
  Compute,
1134
1188
  ComputeWithCreateInstanceSupport,
1189
+ ComputeWithGroupProvisioningSupport,
1135
1190
  ComputeWithPrivilegedSupport,
1136
1191
  ComputeWithMultinodeSupport,
1137
1192
  ComputeWithReservationSupport,
@@ -1,12 +1,13 @@
1
1
  from typing import Any, Dict, List, Optional
2
2
 
3
3
  import orjson
4
+ import packaging.version
4
5
  from fastapi import HTTPException, Request, Response, status
5
- from packaging import version
6
6
 
7
7
  from dstack._internal.core.errors import ServerClientError, ServerClientErrorCode
8
8
  from dstack._internal.core.models.common import CoreModel
9
9
  from dstack._internal.utils.json_utils import get_orjson_default_options, orjson_default
10
+ from dstack._internal.utils.version import parse_version
10
11
 
11
12
 
12
13
  class CustomORJSONResponse(Response):
@@ -122,8 +123,15 @@ def get_request_size(request: Request) -> int:
122
123
  return int(request.headers["content-length"])
123
124
 
124
125
 
126
+ def get_client_version(request: Request) -> Optional[packaging.version.Version]:
127
+ version = request.headers.get("x-api-version")
128
+ if version is None:
129
+ return None
130
+ return parse_version(version)
131
+
132
+
125
133
  def check_client_server_compatibility(
126
- client_version: Optional[str],
134
+ client_version: Optional[packaging.version.Version],
127
135
  server_version: Optional[str],
128
136
  ) -> Optional[CustomORJSONResponse]:
129
137
  """
@@ -132,28 +140,18 @@ def check_client_server_compatibility(
132
140
  """
133
141
  if client_version is None or server_version is None:
134
142
  return None
135
- parsed_server_version = version.parse(server_version)
136
- # latest allows client to bypass compatibility check (e.g. frontend)
137
- if client_version == "latest":
143
+ parsed_server_version = parse_version(server_version)
144
+ if parsed_server_version is None:
138
145
  return None
139
- try:
140
- parsed_client_version = version.parse(client_version)
141
- except version.InvalidVersion:
142
- return CustomORJSONResponse(
143
- status_code=status.HTTP_400_BAD_REQUEST,
144
- content={
145
- "detail": get_server_client_error_details(
146
- ServerClientError("Bad API version specified")
147
- )
148
- },
149
- )
150
146
  # We preserve full client backward compatibility across patch releases.
151
147
  # Server is always partially backward-compatible (so no check).
152
- if parsed_client_version > parsed_server_version and (
153
- parsed_client_version.major > parsed_server_version.major
154
- or parsed_client_version.minor > parsed_server_version.minor
148
+ if client_version > parsed_server_version and (
149
+ client_version.major > parsed_server_version.major
150
+ or client_version.minor > parsed_server_version.minor
155
151
  ):
156
- return error_incompatible_versions(client_version, server_version, ask_cli_update=False)
152
+ return error_incompatible_versions(
153
+ str(client_version), server_version, ask_cli_update=False
154
+ )
157
155
  return None
158
156
 
159
157
 
@@ -1,9 +1,10 @@
1
1
  import os
2
2
 
3
3
  from dstack import version
4
+ from dstack._internal.utils.version import parse_version
4
5
 
5
6
  DSTACK_VERSION = os.getenv("DSTACK_VERSION", version.__version__)
6
- if DSTACK_VERSION == "0.0.0":
7
+ if parse_version(DSTACK_VERSION) is None:
7
8
  # The build backend (hatching) requires not None for versions,
8
9
  # but the code currently treats None as dev version.
9
10
  # TODO: update the code to treat 0.0.0 as dev version.
@@ -33,3 +34,5 @@ class FeatureFlags:
33
34
  large features. This class may be empty if there are no such features in
34
35
  development. Feature flags are environment variables of the form DSTACK_FF_*
35
36
  """
37
+
38
+ pass
@@ -0,0 +1,22 @@
1
+ from typing import Optional
2
+
3
+ import packaging.version
4
+
5
+
6
+ def parse_version(version_string: str) -> Optional[packaging.version.Version]:
7
+ """
8
+ Returns a `packaging.version.Version` instance or `None` if the version is dev/latest.
9
+
10
+ Values parsed as the dev/latest version:
11
+ * the "latest" literal
12
+ * any "0.0.0" release, e.g., "0.0.0", "0.0.0a1", "0.0.0.dev0"
13
+ """
14
+ if version_string == "latest":
15
+ return None
16
+ try:
17
+ version = packaging.version.parse(version_string)
18
+ except packaging.version.InvalidVersion as e:
19
+ raise ValueError(f"Invalid version: {version_string}") from e
20
+ if version.release == (0, 0, 0):
21
+ return None
22
+ return version
@@ -6,7 +6,7 @@ from dstack._internal.utils.logging import get_logger
6
6
  from dstack._internal.utils.path import PathLike as PathLike
7
7
  from dstack.api._public.backends import BackendCollection
8
8
  from dstack.api._public.repos import RepoCollection
9
- from dstack.api._public.runs import RunCollection, warn
9
+ from dstack.api._public.runs import RunCollection
10
10
  from dstack.api.server import APIClient
11
11
 
12
12
  logger = get_logger(__name__)
@@ -42,7 +42,7 @@ class Client:
42
42
  self._backends = BackendCollection(api_client, project_name)
43
43
  self._runs = RunCollection(api_client, project_name, self)
44
44
  if ssh_identity_file is not None:
45
- warn(
45
+ logger.warning(
46
46
  "[code]ssh_identity_file[/code] in [code]Client[/code] is deprecated and ignored; will be removed"
47
47
  " since 0.19.40"
48
48
  )
@@ -1,6 +1,4 @@
1
1
  import base64
2
- import hashlib
3
- import os
4
2
  import queue
5
3
  import tempfile
6
4
  import threading
@@ -17,7 +15,6 @@ from urllib.parse import urlparse
17
15
  from websocket import WebSocketApp
18
16
 
19
17
  import dstack.api as api
20
- from dstack._internal.cli.utils.common import warn
21
18
  from dstack._internal.core.consts import DSTACK_RUNNER_HTTP_PORT, DSTACK_RUNNER_SSH_PORT
22
19
  from dstack._internal.core.errors import ClientError, ConfigurationError, ResourceNotExistsError
23
20
  from dstack._internal.core.models.backends.base import BackendType
@@ -48,10 +45,10 @@ from dstack._internal.core.models.runs import (
48
45
  get_service_port,
49
46
  )
50
47
  from dstack._internal.core.models.runs import Run as RunModel
51
- from dstack._internal.core.models.users import UserWithCreds
52
48
  from dstack._internal.core.services.configs import ConfigManager
53
49
  from dstack._internal.core.services.logs import URLReplacer
54
50
  from dstack._internal.core.services.ssh.attach import SSHAttach
51
+ from dstack._internal.core.services.ssh.key_manager import UserSSHKeyManager
55
52
  from dstack._internal.core.services.ssh.ports import PortsLock
56
53
  from dstack._internal.server.schemas.logs import PollLogsRequest
57
54
  from dstack._internal.utils.common import get_or_error, make_proxy_url
@@ -88,7 +85,7 @@ class Run(ABC):
88
85
  self._ports_lock: Optional[PortsLock] = ports_lock
89
86
  self._ssh_attach: Optional[SSHAttach] = None
90
87
  if ssh_identity_file is not None:
91
- warn(
88
+ logger.warning(
92
89
  "[code]ssh_identity_file[/code] in [code]Run[/code] is deprecated and ignored; will be removed"
93
90
  " since 0.19.40"
94
91
  )
@@ -281,31 +278,20 @@ class Run(ABC):
281
278
  dstack.api.PortUsedError: If ports are in use or the run is attached by another process.
282
279
  """
283
280
  if not ssh_identity_file:
284
- user = self._api_client.users.get_my_user()
285
- run_ssh_key_pub = self._run.run_spec.ssh_key_pub
286
281
  config_manager = ConfigManager()
287
- if isinstance(user, UserWithCreds) and user.ssh_public_key == run_ssh_key_pub:
288
- token_hash = hashlib.sha1(user.creds.token.encode()).hexdigest()[:8]
289
- config_manager.dstack_ssh_dir.mkdir(parents=True, exist_ok=True)
290
- ssh_identity_file = config_manager.dstack_ssh_dir / token_hash
291
-
292
- def key_opener(path, flags):
293
- return os.open(path, flags, 0o600)
294
-
295
- with open(ssh_identity_file, "wb", opener=key_opener) as f:
296
- assert user.ssh_private_key
297
- f.write(user.ssh_private_key.encode())
282
+ key_manager = UserSSHKeyManager(self._api_client, config_manager.dstack_ssh_dir)
283
+ if (
284
+ user_key := key_manager.get_user_key()
285
+ ) and user_key.public_key == self._run.run_spec.ssh_key_pub:
286
+ ssh_identity_file = user_key.private_key_path
298
287
  else:
299
288
  if config_manager.dstack_key_path.exists():
300
289
  # TODO: Remove since 0.19.40
301
- warn(
302
- f"Using legacy [code]{config_manager.dstack_key_path}[/code]."
303
- " Future versions will use the user SSH key from the server.",
304
- )
290
+ logger.debug(f"Using legacy [code]{config_manager.dstack_key_path}[/code].")
305
291
  ssh_identity_file = config_manager.dstack_key_path
306
292
  else:
307
293
  raise ConfigurationError(
308
- f"User SSH key doen't match; default SSH key ({config_manager.dstack_key_path}) doesn't exist"
294
+ f"User SSH key doesn't match; default SSH key ({config_manager.dstack_key_path}) doesn't exist"
309
295
  )
310
296
  ssh_identity_file = str(ssh_identity_file)
311
297
 
@@ -504,15 +490,19 @@ class RunCollection:
504
490
  ssh_key_pub = Path(ssh_identity_file).with_suffix(".pub").read_text()
505
491
  else:
506
492
  config_manager = ConfigManager()
507
- if not config_manager.dstack_key_path.exists():
508
- generate_rsa_key_pair(private_key_path=config_manager.dstack_key_path)
509
- warn(
510
- f"Using legacy [code]{config_manager.dstack_key_path.with_suffix('.pub')}[/code]."
511
- " Future versions will use the user SSH key from the server.",
512
- )
513
- ssh_key_pub = config_manager.dstack_key_path.with_suffix(".pub").read_text()
514
- # TODO: Uncomment after 0.19.40
515
- # ssh_key_pub = None
493
+ key_manager = UserSSHKeyManager(self._api_client, config_manager.dstack_ssh_dir)
494
+ if key_manager.get_user_key():
495
+ ssh_key_pub = None # using the server-managed user key
496
+ else:
497
+ if not config_manager.dstack_key_path.exists():
498
+ generate_rsa_key_pair(private_key_path=config_manager.dstack_key_path)
499
+ logger.warning(
500
+ f"Using legacy [code]{config_manager.dstack_key_path.with_suffix('.pub')}[/code]."
501
+ " You will only be able to attach to the run from this client."
502
+ " Update the [code]dstack[/] server to [code]0.19.34[/]+ to switch to user keys"
503
+ " automatically replicated to all clients.",
504
+ )
505
+ ssh_key_pub = config_manager.dstack_key_path.with_suffix(".pub").read_text()
516
506
  run_spec = RunSpec(
517
507
  run_name=configuration.name,
518
508
  repo_id=repo.repo_id,
@@ -760,12 +750,19 @@ class RunCollection:
760
750
  idle_duration=idle_duration, # type: ignore[assignment]
761
751
  )
762
752
  config_manager = ConfigManager()
763
- if not config_manager.dstack_key_path.exists():
764
- generate_rsa_key_pair(private_key_path=config_manager.dstack_key_path)
765
- warn(
766
- f"Using legacy [code]{config_manager.dstack_key_path.with_suffix('.pub')}[/code]."
767
- " Future versions will use the user SSH key from the server.",
768
- )
753
+ key_manager = UserSSHKeyManager(self._api_client, config_manager.dstack_ssh_dir)
754
+ if key_manager.get_user_key():
755
+ ssh_key_pub = None # using the server-managed user key
756
+ else:
757
+ if not config_manager.dstack_key_path.exists():
758
+ generate_rsa_key_pair(private_key_path=config_manager.dstack_key_path)
759
+ logger.warning(
760
+ f"Using legacy [code]{config_manager.dstack_key_path.with_suffix('.pub')}[/code]."
761
+ " You will only be able to attach to the run from this client."
762
+ " Update the [code]dstack[/] server to [code]0.19.34[/]+ to switch to user keys"
763
+ " automatically replicated to all clients.",
764
+ )
765
+ ssh_key_pub = config_manager.dstack_key_path.with_suffix(".pub").read_text()
769
766
  run_spec = RunSpec(
770
767
  run_name=run_name,
771
768
  repo_id=repo.repo_id,
@@ -775,7 +772,7 @@ class RunCollection:
775
772
  configuration_path=configuration_path,
776
773
  configuration=configuration,
777
774
  profile=profile,
778
- ssh_key_pub=config_manager.dstack_key_path.with_suffix(".pub").read_text(),
775
+ ssh_key_pub=ssh_key_pub,
779
776
  )
780
777
  logger.debug("Getting run plan")
781
778
  run_plan = self._api_client.runs.get_plan(self._project, run_spec)
@@ -1,3 +1,4 @@
1
+ import hashlib
1
2
  import os
2
3
  import pprint
3
4
  import time
@@ -121,6 +122,9 @@ class APIClient:
121
122
  def files(self) -> FilesAPIClient:
122
123
  return FilesAPIClient(self._request, self._logger)
123
124
 
125
+ def get_token_hash(self) -> str:
126
+ return hashlib.sha1(self._token.encode()).hexdigest()[:8]
127
+
124
128
  def _request(
125
129
  self,
126
130
  path: str,
dstack/version.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "0.19.33"
1
+ __version__ = "0.19.35"
2
2
  __is_release__ = True
3
3
  base_image = "0.11"
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.33
3
+ Version: 0.19.35
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.10
25
+ Requires-Dist: gpuhunt==0.1.11
26
26
  Requires-Dist: ignore-python>=0.2.0
27
27
  Requires-Dist: jsonschema
28
28
  Requires-Dist: orjson
@@ -73,7 +73,7 @@ Requires-Dist: grpcio>=1.50; extra == 'all'
73
73
  Requires-Dist: httpx; extra == 'all'
74
74
  Requires-Dist: jinja2; extra == 'all'
75
75
  Requires-Dist: kubernetes; extra == 'all'
76
- Requires-Dist: nebius<=0.2.72,>=0.2.40; (python_version >= '3.10') and extra == 'all'
76
+ Requires-Dist: nebius<0.4,>=0.3.4; (python_version >= '3.10') and extra == 'all'
77
77
  Requires-Dist: oci>=2.150.0; extra == 'all'
78
78
  Requires-Dist: prometheus-client; extra == 'all'
79
79
  Requires-Dist: pyopenssl>=23.2.0; extra == 'all'
@@ -259,7 +259,7 @@ Requires-Dist: fastapi; extra == 'nebius'
259
259
  Requires-Dist: grpcio>=1.50; extra == 'nebius'
260
260
  Requires-Dist: httpx; extra == 'nebius'
261
261
  Requires-Dist: jinja2; extra == 'nebius'
262
- Requires-Dist: nebius<=0.2.72,>=0.2.40; (python_version >= '3.10') and extra == 'nebius'
262
+ Requires-Dist: nebius<0.4,>=0.3.4; (python_version >= '3.10') and extra == 'nebius'
263
263
  Requires-Dist: prometheus-client; extra == 'nebius'
264
264
  Requires-Dist: python-dxf==12.1.0; extra == 'nebius'
265
265
  Requires-Dist: python-json-logger>=3.1.0; extra == 'nebius'