skypilot-nightly 1.0.0.dev20250408__py3-none-any.whl → 1.0.0.dev20250411__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.
Files changed (39) hide show
  1. sky/__init__.py +2 -2
  2. sky/adaptors/azure.py +1 -1
  3. sky/adaptors/nebius.py +5 -27
  4. sky/backends/backend.py +9 -7
  5. sky/backends/cloud_vm_ray_backend.py +7 -7
  6. sky/backends/local_docker_backend.py +3 -3
  7. sky/client/common.py +4 -2
  8. sky/client/sdk.py +58 -26
  9. sky/cloud_stores.py +0 -4
  10. sky/clouds/do.py +4 -5
  11. sky/clouds/gcp.py +5 -3
  12. sky/clouds/nebius.py +22 -12
  13. sky/clouds/service_catalog/data_fetchers/fetch_ibm.py +1 -2
  14. sky/clouds/service_catalog/gcp_catalog.py +37 -10
  15. sky/core.py +6 -6
  16. sky/data/data_utils.py +5 -9
  17. sky/data/mounting_utils.py +1 -1
  18. sky/data/storage.py +25 -31
  19. sky/data/storage_utils.py +27 -18
  20. sky/execution.py +11 -4
  21. sky/jobs/client/sdk.py +5 -0
  22. sky/jobs/server/server.py +5 -1
  23. sky/optimizer.py +1 -2
  24. sky/provision/do/utils.py +19 -16
  25. sky/provision/gcp/config.py +30 -20
  26. sky/serve/client/sdk.py +6 -0
  27. sky/server/common.py +16 -1
  28. sky/server/constants.py +5 -0
  29. sky/setup_files/dependencies.py +1 -1
  30. sky/skylet/log_lib.py +4 -0
  31. sky/skypilot_config.py +19 -30
  32. sky/task.py +27 -7
  33. sky/utils/schemas.py +25 -7
  34. {skypilot_nightly-1.0.0.dev20250408.dist-info → skypilot_nightly-1.0.0.dev20250411.dist-info}/METADATA +2 -2
  35. {skypilot_nightly-1.0.0.dev20250408.dist-info → skypilot_nightly-1.0.0.dev20250411.dist-info}/RECORD +39 -39
  36. {skypilot_nightly-1.0.0.dev20250408.dist-info → skypilot_nightly-1.0.0.dev20250411.dist-info}/WHEEL +0 -0
  37. {skypilot_nightly-1.0.0.dev20250408.dist-info → skypilot_nightly-1.0.0.dev20250411.dist-info}/entry_points.txt +0 -0
  38. {skypilot_nightly-1.0.0.dev20250408.dist-info → skypilot_nightly-1.0.0.dev20250411.dist-info}/licenses/LICENSE +0 -0
  39. {skypilot_nightly-1.0.0.dev20250408.dist-info → skypilot_nightly-1.0.0.dev20250411.dist-info}/top_level.txt +0 -0
sky/provision/do/utils.py CHANGED
@@ -15,6 +15,7 @@ from sky.adaptors import do
15
15
  from sky.provision import common
16
16
  from sky.provision import constants as provision_constants
17
17
  from sky.provision.do import constants
18
+ from sky.utils import annotations
18
19
  from sky.utils import common_utils
19
20
 
20
21
  logger = sky_logging.init_logger(__name__)
@@ -31,7 +32,6 @@ MAX_BACKOFF_FACTOR = 10
31
32
  MAX_ATTEMPTS = 6
32
33
  SSH_KEY_NAME_ON_DO = f'sky-key-{common_utils.get_user_hash()}'
33
34
 
34
- CREDENTIALS_PATH = '~/.config/doctl/config.yaml'
35
35
  _client = None
36
36
  _ssh_key_id = None
37
37
 
@@ -40,31 +40,34 @@ class DigitalOceanError(Exception):
40
40
  pass
41
41
 
42
42
 
43
- def _init_client():
44
- global _client, CREDENTIALS_PATH
45
- assert _client is None
46
- CREDENTIALS_PATH = None
43
+ @annotations.lru_cache(scope='request')
44
+ def get_credentials_path():
45
+ credentials_path = None
47
46
  credentials_found = 0
48
47
  for path in POSSIBLE_CREDENTIALS_PATHS:
49
48
  if os.path.exists(path):
50
- CREDENTIALS_PATH = path
51
- credentials_found += 1
52
49
  logger.debug(f'Digital Ocean credential path found at {path}')
53
- if not credentials_found > 1:
54
- logger.debug('more than 1 credential file found')
55
- if CREDENTIALS_PATH is None:
56
- raise DigitalOceanError(
57
- 'no credentials file found from '
58
- f'the following paths {POSSIBLE_CREDENTIALS_PATHS}')
50
+ credentials_path = path
51
+ credentials_found += 1
52
+ if credentials_found > 1:
53
+ logger.debug('More than 1 credential file found')
54
+ return credentials_path
59
55
 
56
+
57
+ def _init_client():
58
+ global _client
59
+ assert _client is None
60
60
  # attempt default context
61
- credentials = common_utils.read_yaml(CREDENTIALS_PATH)
61
+ if get_credentials_path() is None:
62
+ raise DigitalOceanError(
63
+ 'No credentials found, please run `doctl auth init`')
64
+ credentials = common_utils.read_yaml(get_credentials_path())
62
65
  default_token = credentials.get('access-token', None)
63
66
  if default_token is not None:
64
67
  try:
65
68
  test_client = do.pydo.Client(token=default_token)
66
69
  test_client.droplets.list()
67
- logger.debug('trying `default` context')
70
+ logger.debug('Trying `default` context')
68
71
  _client = test_client
69
72
  return _client
70
73
  except do.exceptions().HttpResponseError:
@@ -76,7 +79,7 @@ def _init_client():
76
79
  try:
77
80
  test_client = do.pydo.Client(token=api_token)
78
81
  test_client.droplets.list()
79
- logger.debug(f'using {context} context')
82
+ logger.debug(f'Using "{context}" context')
80
83
  _client = test_client
81
84
  break
82
85
  except do.exceptions().HttpResponseError:
@@ -571,35 +571,45 @@ def get_usable_vpc_and_subnet(
571
571
 
572
572
  specific_vpc_to_use = config.provider_config.get('vpc_name', None)
573
573
  if specific_vpc_to_use is not None:
574
+ if '/' in specific_vpc_to_use:
575
+ # VPC can also be specified in the format PROJECT_ID/VPC_NAME.
576
+ # This enables use of shared VPCs.
577
+ split_vpc_value = specific_vpc_to_use.split('/')
578
+ if len(split_vpc_value) != 2:
579
+ raise ValueError(f'Invalid VPC name: {specific_vpc_to_use}. '
580
+ 'Please specify the VPC name in the format '
581
+ 'PROJECT_ID/VPC_NAME.')
582
+ project_id = split_vpc_value[0]
583
+ specific_vpc_to_use = split_vpc_value[1]
584
+
574
585
  vpcnets_all = _list_vpcnets(project_id,
575
586
  compute,
576
587
  filter=f'name={specific_vpc_to_use}')
577
- # On GCP, VPC names are unique, so it'd be 0 or 1 VPC found.
578
- assert (len(vpcnets_all) <=
579
- 1), (f'{len(vpcnets_all)} VPCs found with the same name '
580
- f'{specific_vpc_to_use}')
581
- if len(vpcnets_all) == 1:
582
- # Skip checking any firewall rules if the user has specified a VPC.
583
- logger.info(f'Using user-specified VPC {specific_vpc_to_use!r}.')
584
- subnets = _list_subnets(project_id,
585
- region,
586
- compute,
587
- network=specific_vpc_to_use)
588
- if not subnets:
589
- _skypilot_log_error_and_exit_for_failover(
590
- 'SUBNET_NOT_FOUND_FOR_VPC',
591
- f'No subnet for region {region} found for specified VPC '
592
- f'{specific_vpc_to_use!r}. '
593
- f'Check the subnets of VPC {specific_vpc_to_use!r} at '
594
- 'https://console.cloud.google.com/networking/networks')
595
- return specific_vpc_to_use, subnets[0]
596
- else:
588
+ if not vpcnets_all:
597
589
  # VPC with this name not found. Error out and let SkyPilot failover.
598
590
  _skypilot_log_error_and_exit_for_failover(
599
591
  'VPC_NOT_FOUND',
600
592
  f'No VPC with name {specific_vpc_to_use!r} is found. '
601
593
  'To fix: specify a correct VPC name.')
602
594
  # Should not reach here.
595
+ assert False
596
+
597
+ # On GCP, VPC names are unique within a project.
598
+ assert len(vpcnets_all) == 1, (vpcnets_all, specific_vpc_to_use)
599
+ # Skip checking any firewall rules if the user has specified a VPC.
600
+ logger.info(f'Using user-specified VPC {specific_vpc_to_use!r}.')
601
+ subnets = _list_subnets(project_id,
602
+ region,
603
+ compute,
604
+ network=specific_vpc_to_use)
605
+ if not subnets:
606
+ _skypilot_log_error_and_exit_for_failover(
607
+ 'SUBNET_NOT_FOUND_FOR_VPC',
608
+ f'No subnet for region {region} found for specified VPC '
609
+ f'{specific_vpc_to_use!r}. '
610
+ f'Check the subnets of VPC {specific_vpc_to_use!r} at '
611
+ 'https://console.cloud.google.com/networking/networks')
612
+ return specific_vpc_to_use, subnets[0]
603
613
 
604
614
  subnets_all = _list_subnets(project_id, region, compute)
605
615
 
sky/serve/client/sdk.py CHANGED
@@ -74,6 +74,7 @@ def up(
74
74
  f'{server_common.get_server_url()}/serve/up',
75
75
  json=json.loads(body.model_dump_json()),
76
76
  timeout=(5, None),
77
+ cookies=server_common.get_api_cookie_jar(),
77
78
  )
78
79
  return server_common.get_request_id(response)
79
80
 
@@ -132,6 +133,7 @@ def update(
132
133
  f'{server_common.get_server_url()}/serve/update',
133
134
  json=json.loads(body.model_dump_json()),
134
135
  timeout=(5, None),
136
+ cookies=server_common.get_api_cookie_jar(),
135
137
  )
136
138
  return server_common.get_request_id(response)
137
139
 
@@ -173,6 +175,7 @@ def down(
173
175
  f'{server_common.get_server_url()}/serve/down',
174
176
  json=json.loads(body.model_dump_json()),
175
177
  timeout=(5, None),
178
+ cookies=server_common.get_api_cookie_jar(),
176
179
  )
177
180
  return server_common.get_request_id(response)
178
181
 
@@ -207,6 +210,7 @@ def terminate_replica(service_name: str, replica_id: int,
207
210
  f'{server_common.get_server_url()}/serve/terminate-replica',
208
211
  json=json.loads(body.model_dump_json()),
209
212
  timeout=(5, None),
213
+ cookies=server_common.get_api_cookie_jar(),
210
214
  )
211
215
  return server_common.get_request_id(response)
212
216
 
@@ -279,6 +283,7 @@ def status(
279
283
  f'{server_common.get_server_url()}/serve/status',
280
284
  json=json.loads(body.model_dump_json()),
281
285
  timeout=(5, None),
286
+ cookies=server_common.get_api_cookie_jar(),
282
287
  )
283
288
  return server_common.get_request_id(response)
284
289
 
@@ -365,6 +370,7 @@ def tail_logs(service_name: str,
365
370
  json=json.loads(body.model_dump_json()),
366
371
  timeout=(5, None),
367
372
  stream=True,
373
+ cookies=server_common.get_api_cookie_jar(),
368
374
  )
369
375
  request_id = server_common.get_request_id(response)
370
376
  sdk.stream_response(request_id, response, output_stream)
sky/server/common.py CHANGED
@@ -3,6 +3,7 @@
3
3
  import dataclasses
4
4
  import enum
5
5
  import functools
6
+ from http.cookiejar import MozillaCookieJar
6
7
  import json
7
8
  import os
8
9
  import pathlib
@@ -80,6 +81,18 @@ class ApiServerInfo:
80
81
  api_version: ApiVersion
81
82
 
82
83
 
84
+ def get_api_cookie_jar() -> requests.cookies.RequestsCookieJar:
85
+ """Returns the cookie jar used by the client to access the API server."""
86
+ cookie_file = os.environ.get(server_constants.API_COOKIE_FILE_ENV_VAR)
87
+ cookie_jar = requests.cookies.RequestsCookieJar()
88
+ if cookie_file and os.path.exists(cookie_file):
89
+ cookie_path = pathlib.Path(cookie_file).expanduser().resolve()
90
+ file_cookie_jar = MozillaCookieJar(cookie_path)
91
+ file_cookie_jar.load()
92
+ cookie_jar.update(file_cookie_jar)
93
+ return cookie_jar
94
+
95
+
83
96
  @annotations.lru_cache(scope='global')
84
97
  def get_server_url(host: Optional[str] = None) -> str:
85
98
  endpoint = DEFAULT_SERVER_URL
@@ -117,7 +130,9 @@ def get_api_server_status(endpoint: Optional[str] = None) -> ApiServerInfo:
117
130
  server_url = endpoint if endpoint is not None else get_server_url()
118
131
  while time_out_try_count <= RETRY_COUNT_ON_TIMEOUT:
119
132
  try:
120
- response = requests.get(f'{server_url}/api/health', timeout=2.5)
133
+ response = requests.get(f'{server_url}/api/health',
134
+ timeout=2.5,
135
+ cookies=get_api_cookie_jar())
121
136
  if response.status_code == 200:
122
137
  try:
123
138
  result = response.json()
sky/server/constants.py CHANGED
@@ -1,5 +1,7 @@
1
1
  """Constants for the API servers."""
2
2
 
3
+ from sky.skylet import constants
4
+
3
5
  # API server version, whenever there is a change in API server that requires a
4
6
  # restart of the local API server or error out when the client does not match
5
7
  # the server version.
@@ -19,3 +21,6 @@ API_SERVER_REQUEST_DB_PATH = '~/.sky/api_server/requests.db'
19
21
  # The interval (seconds) for the cluster status to be refreshed in the
20
22
  # background.
21
23
  CLUSTER_REFRESH_DAEMON_INTERVAL_SECONDS = 60
24
+
25
+ # Environment variable for a file path to the API cookie file.
26
+ API_COOKIE_FILE_ENV_VAR = f'{constants.SKYPILOT_ENV_VAR_PREFIX}API_COOKIE_FILE'
@@ -9,7 +9,7 @@ import sys
9
9
  from typing import Dict, List
10
10
 
11
11
  install_requires = [
12
- 'wheel',
12
+ 'wheel<0.46.0', # https://github.com/skypilot-org/skypilot/issues/5153
13
13
  'cachetools',
14
14
  # NOTE: ray requires click>=7.0.
15
15
  'click >= 7.0',
sky/skylet/log_lib.py CHANGED
@@ -149,6 +149,7 @@ def run_with_log(
149
149
  process_stream: bool = True,
150
150
  line_processor: Optional[log_utils.LineProcessor] = None,
151
151
  streaming_prefix: Optional[str] = None,
152
+ log_cmd: bool = False,
152
153
  **kwargs,
153
154
  ) -> Union[int, Tuple[int, str, str]]:
154
155
  """Runs a command and logs its output to a file.
@@ -182,6 +183,9 @@ def run_with_log(
182
183
  # the terminal output when typing in the terminal that starts the API
183
184
  # server.
184
185
  stdin = kwargs.pop('stdin', subprocess.DEVNULL)
186
+ if log_cmd:
187
+ with open(log_path, 'a', encoding='utf-8') as f:
188
+ print(f'Running command: {cmd}', file=f)
185
189
  with subprocess.Popen(cmd,
186
190
  stdout=stdout_arg,
187
191
  stderr=stderr_arg,
sky/skypilot_config.py CHANGED
@@ -52,7 +52,6 @@ import contextlib
52
52
  import copy
53
53
  import os
54
54
  import pprint
55
- import tempfile
56
55
  import typing
57
56
  from typing import Any, Dict, Iterator, Optional, Tuple
58
57
 
@@ -92,6 +91,7 @@ CONFIG_PATH = '~/.sky/config.yaml'
92
91
  # The loaded config.
93
92
  _dict = config_utils.Config()
94
93
  _loaded_config_path: Optional[str] = None
94
+ _config_overridden: bool = False
95
95
 
96
96
 
97
97
  def get_nested(keys: Tuple[str, ...],
@@ -178,7 +178,10 @@ def _reload_config() -> None:
178
178
 
179
179
 
180
180
  def loaded_config_path() -> Optional[str]:
181
- """Returns the path to the loaded config file."""
181
+ """Returns the path to the loaded config file, or
182
+ '<overridden>' if the config is overridden."""
183
+ if _config_overridden:
184
+ return '<overridden>'
182
185
  return _loaded_config_path
183
186
 
184
187
 
@@ -195,31 +198,30 @@ def loaded() -> bool:
195
198
  def override_skypilot_config(
196
199
  override_configs: Optional[Dict[str, Any]]) -> Iterator[None]:
197
200
  """Overrides the user configurations."""
201
+ global _dict, _config_overridden
198
202
  # TODO(SKY-1215): allow admin user to extend the disallowed keys or specify
199
203
  # allowed keys.
200
204
  if not override_configs:
201
205
  # If no override configs (None or empty dict), do nothing.
202
206
  yield
203
207
  return
204
- original_env_config_path = _loaded_config_path
205
- original_config = dict(_dict)
208
+ original_config = _dict
206
209
  config = _dict.get_nested(
207
210
  keys=tuple(),
208
211
  default_value=None,
209
212
  override_configs=override_configs,
210
213
  allowed_override_keys=None,
211
214
  disallowed_override_keys=constants.SKIPPED_CLIENT_OVERRIDE_KEYS)
212
- with tempfile.NamedTemporaryFile(
213
- mode='w',
214
- prefix='skypilot_config',
215
- # Have to avoid deleting the file as the underlying function needs
216
- # to read the config file, and we need to close the file mode='w'
217
- # to enable reading.
218
- delete=False) as f:
219
- common_utils.dump_yaml(f.name, dict(config))
220
- os.environ[ENV_VAR_SKYPILOT_CONFIG] = f.name
221
215
  try:
222
- _reload_config()
216
+ common_utils.validate_schema(
217
+ config,
218
+ schemas.get_config_schema(),
219
+ f'Invalid config {config}. See: '
220
+ 'https://docs.skypilot.co/en/latest/reference/config.html. ' # pylint: disable=line-too-long
221
+ 'Error: ',
222
+ skip_none=False)
223
+ _config_overridden = True
224
+ _dict = config
223
225
  yield
224
226
  except exceptions.InvalidSkyPilotConfigError as e:
225
227
  with ux_utils.print_exception_no_traceback():
@@ -227,23 +229,10 @@ def override_skypilot_config(
227
229
  'Failed to override the SkyPilot config on API '
228
230
  'server with your local SkyPilot config:\n'
229
231
  '=== SkyPilot config on API server ===\n'
230
- f'{common_utils.dump_yaml_str(original_config)}\n'
232
+ f'{common_utils.dump_yaml_str(dict(original_config))}\n'
231
233
  '=== Your local SkyPilot config ===\n'
232
234
  f'{common_utils.dump_yaml_str(override_configs)}\n'
233
235
  f'Details: {e}') from e
234
-
235
236
  finally:
236
- if original_env_config_path is not None:
237
- os.environ[ENV_VAR_SKYPILOT_CONFIG] = original_env_config_path
238
- else:
239
- os.environ.pop(ENV_VAR_SKYPILOT_CONFIG, None)
240
- # Reload the config to restore the original config to avoid the next
241
- # request reusing the same process to use the config for the current
242
- # request.
243
- _reload_config()
244
-
245
- try:
246
- os.remove(f.name)
247
- except Exception: # pylint: disable=broad-except
248
- # Failing to delete the file is not critical.
249
- pass
237
+ _dict = original_config
238
+ _config_overridden = False
sky/task.py CHANGED
@@ -552,15 +552,35 @@ class Task:
552
552
  estimated_size_gigabytes=estimated_size_gigabytes)
553
553
 
554
554
  # Experimental configs.
555
- experimnetal_configs = config.pop('experimental', None)
556
- cluster_config_override = None
557
- if experimnetal_configs is not None:
558
- cluster_config_override = experimnetal_configs.pop(
555
+ experimental_configs = config.pop('experimental', None)
556
+
557
+ # Handle the top-level config field
558
+ config_override = config.pop('config', None)
559
+
560
+ # Handle backward compatibility with experimental.config_overrides
561
+ # TODO: Remove experimental.config_overrides in 0.11.0.
562
+ if experimental_configs is not None:
563
+ exp_config_override = experimental_configs.pop(
559
564
  'config_overrides', None)
565
+ if exp_config_override is not None:
566
+ logger.warning(
567
+ f'{colorama.Fore.YELLOW}`experimental.config_overrides` '
568
+ 'field is deprecated in the task YAML. Use the `config` '
569
+ f'field to set config overrides.{colorama.Style.RESET_ALL}')
570
+ if config_override is not None:
571
+ logger.warning(
572
+ f'{colorama.Fore.YELLOW}Both top-level `config` and '
573
+ f'`experimental.config_overrides` are specified. '
574
+ f'Using top-level `config`.{colorama.Style.RESET_ALL}')
575
+ else:
576
+ config_override = exp_config_override
560
577
  logger.debug('Overriding skypilot config with task-level config: '
561
- f'{cluster_config_override}')
562
- assert not experimnetal_configs, ('Invalid task args: '
563
- f'{experimnetal_configs.keys()}')
578
+ f'{config_override}')
579
+ assert not experimental_configs, ('Invalid task args: '
580
+ f'{experimental_configs.keys()}')
581
+
582
+ # Store the final config override for use in resource setup
583
+ cluster_config_override = config_override
564
584
 
565
585
  # Parse resources field.
566
586
  resources_config = config.pop('resources', {})
sky/utils/schemas.py CHANGED
@@ -473,6 +473,8 @@ def _filter_schema(schema: dict, keys_to_keep: List[Tuple[str, ...]]) -> dict:
473
473
 
474
474
 
475
475
  def _experimental_task_schema() -> dict:
476
+ # TODO: experimental.config_overrides has been deprecated in favor of the
477
+ # top-level `config` field. Remove in v0.11.0.
476
478
  config_override_schema = _filter_schema(
477
479
  get_config_schema(), constants.OVERRIDEABLE_CONFIG_KEYS_IN_TASK)
478
480
  return {
@@ -555,6 +557,9 @@ def get_task_schema():
555
557
  'file_mounts_mapping': {
556
558
  'type': 'object',
557
559
  },
560
+ 'config': _filter_schema(
561
+ get_config_schema(),
562
+ constants.OVERRIDEABLE_CONFIG_KEYS_IN_TASK),
558
563
  **_experimental_task_schema(),
559
564
  }
560
565
  }
@@ -604,13 +609,6 @@ def get_cluster_schema():
604
609
 
605
610
 
606
611
  _NETWORK_CONFIG_SCHEMA = {
607
- 'vpc_name': {
608
- 'oneOf': [{
609
- 'type': 'string',
610
- }, {
611
- 'type': 'null',
612
- }],
613
- },
614
612
  'use_internal_ips': {
615
613
  'type': 'boolean',
616
614
  },
@@ -767,6 +765,13 @@ def get_config_schema():
767
765
  },
768
766
  'security_group_name':
769
767
  (_PRORPERTY_NAME_OR_CLUSTER_NAME_TO_PROPERTY),
768
+ 'vpc_name': {
769
+ 'oneOf': [{
770
+ 'type': 'string',
771
+ }, {
772
+ 'type': 'null',
773
+ }],
774
+ },
770
775
  **_LABELS_SCHEMA,
771
776
  **_NETWORK_CONFIG_SCHEMA,
772
777
  },
@@ -805,6 +810,19 @@ def get_config_schema():
805
810
  'enable_gvnic': {
806
811
  'type': 'boolean'
807
812
  },
813
+ 'vpc_name': {
814
+ 'oneOf': [
815
+ {
816
+ 'type': 'string',
817
+ # vpc-name or project-id/vpc-name
818
+ # VPC name and Project ID have -, a-z, and 0-9.
819
+ 'pattern': '^(?:[-a-z0-9]+/)?[-a-z0-9]+$'
820
+ },
821
+ {
822
+ 'type': 'null',
823
+ }
824
+ ],
825
+ },
808
826
  **_LABELS_SCHEMA,
809
827
  **_NETWORK_CONFIG_SCHEMA,
810
828
  },
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: skypilot-nightly
3
- Version: 1.0.0.dev20250408
3
+ Version: 1.0.0.dev20250411
4
4
  Summary: SkyPilot: An intercloud broker for the clouds
5
5
  Author: SkyPilot Team
6
6
  License: Apache 2.0
@@ -19,7 +19,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
19
  Classifier: Topic :: System :: Distributed Computing
20
20
  Description-Content-Type: text/markdown
21
21
  License-File: LICENSE
22
- Requires-Dist: wheel
22
+ Requires-Dist: wheel<0.46.0
23
23
  Requires-Dist: cachetools
24
24
  Requires-Dist: click>=7.0
25
25
  Requires-Dist: colorama