anyscale 0.26.17__py3-none-any.whl → 0.26.20__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 (61) hide show
  1. anyscale/_private/docgen/models.md +3 -3
  2. anyscale/anyscale-cloud-setup.yaml +0 -4
  3. anyscale/client/README.md +18 -38
  4. anyscale/client/openapi_client/__init__.py +14 -20
  5. anyscale/client/openapi_client/api/default_api.py +460 -2097
  6. anyscale/client/openapi_client/models/__init__.py +14 -20
  7. anyscale/client/openapi_client/models/aws_config.py +402 -0
  8. anyscale/client/openapi_client/models/baseimagesenum.py +68 -1
  9. anyscale/client/openapi_client/models/cloud_deployment.py +397 -0
  10. anyscale/client/openapi_client/models/{webterminal_list_response.py → clouddeployment_list_response.py} +15 -15
  11. anyscale/client/openapi_client/models/decorated_production_job_state_transition.py +2 -2
  12. anyscale/client/openapi_client/models/file_storage.py +206 -0
  13. anyscale/client/openapi_client/models/gcp_config.py +402 -0
  14. anyscale/client/openapi_client/models/{session_details.py → job_queue_sort_directive.py} +39 -39
  15. anyscale/client/openapi_client/models/{sessiondescribe_response.py → job_queue_sort_field.py} +20 -34
  16. anyscale/client/openapi_client/models/job_queues_query.py +31 -3
  17. anyscale/client/openapi_client/models/kubernetes_config.py +150 -0
  18. anyscale/client/openapi_client/models/{monitor_logs_extension.py → networking_mode.py} +7 -7
  19. anyscale/client/openapi_client/models/object_storage.py +178 -0
  20. anyscale/client/openapi_client/models/{sessiondetails_response.py → pcp_config.py} +23 -22
  21. anyscale/client/openapi_client/models/production_job_state_transition.py +2 -2
  22. anyscale/client/openapi_client/models/supportedbaseimagesenum.py +68 -1
  23. anyscale/client/openapi_client/models/{external_service_status.py → update_job_queue_request.py} +39 -36
  24. anyscale/client/openapi_client/models/workspace_template_readme.py +181 -0
  25. anyscale/client/openapi_client/models/{archivedlogsinfo_response.py → workspacetemplatereadme_response.py} +11 -11
  26. anyscale/commands/cloud_commands.py +55 -7
  27. anyscale/commands/command_examples.py +58 -0
  28. anyscale/commands/job_commands.py +2 -2
  29. anyscale/commands/job_queue_commands.py +172 -0
  30. anyscale/connect_utils/prepare_cluster.py +19 -14
  31. anyscale/controllers/cloud_controller.py +60 -3
  32. anyscale/controllers/job_controller.py +215 -3
  33. anyscale/scripts.py +3 -0
  34. anyscale/sdk/anyscale_client/models/baseimagesenum.py +68 -1
  35. anyscale/sdk/anyscale_client/models/production_job_state_transition.py +2 -2
  36. anyscale/sdk/anyscale_client/models/supportedbaseimagesenum.py +68 -1
  37. anyscale/shared_anyscale_utils/latest_ray_version.py +1 -1
  38. anyscale/util.py +3 -1
  39. anyscale/utils/connect_helpers.py +34 -0
  40. anyscale/version.py +1 -1
  41. anyscale/workspace/_private/workspace_sdk.py +19 -6
  42. {anyscale-0.26.17.dist-info → anyscale-0.26.20.dist-info}/METADATA +1 -1
  43. {anyscale-0.26.17.dist-info → anyscale-0.26.20.dist-info}/RECORD +48 -53
  44. anyscale/client/openapi_client/models/archived_logs_info.py +0 -164
  45. anyscale/client/openapi_client/models/create_experimental_workspace_from_job.py +0 -123
  46. anyscale/client/openapi_client/models/create_session_from_snapshot_options.py +0 -538
  47. anyscale/client/openapi_client/models/create_session_in_db.py +0 -434
  48. anyscale/client/openapi_client/models/create_session_response.py +0 -174
  49. anyscale/client/openapi_client/models/createsessionresponse_response.py +0 -121
  50. anyscale/client/openapi_client/models/external_service_status_response.py +0 -250
  51. anyscale/client/openapi_client/models/externalservicestatusresponse_response.py +0 -121
  52. anyscale/client/openapi_client/models/session_describe.py +0 -175
  53. anyscale/client/openapi_client/models/session_history_item.py +0 -146
  54. anyscale/client/openapi_client/models/sessionhistoryitem_list_response.py +0 -147
  55. anyscale/client/openapi_client/models/update_compute_template.py +0 -146
  56. anyscale/client/openapi_client/models/update_compute_template_config.py +0 -464
  57. {anyscale-0.26.17.dist-info → anyscale-0.26.20.dist-info}/LICENSE +0 -0
  58. {anyscale-0.26.17.dist-info → anyscale-0.26.20.dist-info}/NOTICE +0 -0
  59. {anyscale-0.26.17.dist-info → anyscale-0.26.20.dist-info}/WHEEL +0 -0
  60. {anyscale-0.26.17.dist-info → anyscale-0.26.20.dist-info}/entry_points.txt +0 -0
  61. {anyscale-0.26.17.dist-info → anyscale-0.26.20.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,181 @@
1
+ # coding: utf-8
2
+
3
+ """
4
+ Managed Ray API
5
+
6
+ No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) # noqa: E501
7
+
8
+ The version of the OpenAPI document: 0.1.0
9
+ Generated by: https://openapi-generator.tech
10
+ """
11
+
12
+
13
+ import pprint
14
+ import re # noqa: F401
15
+
16
+ import six
17
+
18
+ from openapi_client.configuration import Configuration
19
+
20
+
21
+ class WorkspaceTemplateReadme(object):
22
+ """NOTE: This class is auto generated by OpenAPI Generator.
23
+ Ref: https://openapi-generator.tech
24
+
25
+ Do not edit the class manually.
26
+ """
27
+
28
+ """
29
+ Attributes:
30
+ openapi_types (dict): The key is attribute name
31
+ and the value is attribute type.
32
+ attribute_map (dict): The key is attribute name
33
+ and the value is json key in definition.
34
+ """
35
+ openapi_types = {
36
+ 'content': 'str',
37
+ 'title': 'str',
38
+ 'description': 'str'
39
+ }
40
+
41
+ attribute_map = {
42
+ 'content': 'content',
43
+ 'title': 'title',
44
+ 'description': 'description'
45
+ }
46
+
47
+ def __init__(self, content=None, title=None, description=None, local_vars_configuration=None): # noqa: E501
48
+ """WorkspaceTemplateReadme - a model defined in OpenAPI""" # noqa: E501
49
+ if local_vars_configuration is None:
50
+ local_vars_configuration = Configuration()
51
+ self.local_vars_configuration = local_vars_configuration
52
+
53
+ self._content = None
54
+ self._title = None
55
+ self._description = None
56
+ self.discriminator = None
57
+
58
+ self.content = content
59
+ self.title = title
60
+ self.description = description
61
+
62
+ @property
63
+ def content(self):
64
+ """Gets the content of this WorkspaceTemplateReadme. # noqa: E501
65
+
66
+ The content of the readme. # noqa: E501
67
+
68
+ :return: The content of this WorkspaceTemplateReadme. # noqa: E501
69
+ :rtype: str
70
+ """
71
+ return self._content
72
+
73
+ @content.setter
74
+ def content(self, content):
75
+ """Sets the content of this WorkspaceTemplateReadme.
76
+
77
+ The content of the readme. # noqa: E501
78
+
79
+ :param content: The content of this WorkspaceTemplateReadme. # noqa: E501
80
+ :type: str
81
+ """
82
+ if self.local_vars_configuration.client_side_validation and content is None: # noqa: E501
83
+ raise ValueError("Invalid value for `content`, must not be `None`") # noqa: E501
84
+
85
+ self._content = content
86
+
87
+ @property
88
+ def title(self):
89
+ """Gets the title of this WorkspaceTemplateReadme. # noqa: E501
90
+
91
+ The title of the workspace template # noqa: E501
92
+
93
+ :return: The title of this WorkspaceTemplateReadme. # noqa: E501
94
+ :rtype: str
95
+ """
96
+ return self._title
97
+
98
+ @title.setter
99
+ def title(self, title):
100
+ """Sets the title of this WorkspaceTemplateReadme.
101
+
102
+ The title of the workspace template # noqa: E501
103
+
104
+ :param title: The title of this WorkspaceTemplateReadme. # noqa: E501
105
+ :type: str
106
+ """
107
+ if self.local_vars_configuration.client_side_validation and title is None: # noqa: E501
108
+ raise ValueError("Invalid value for `title`, must not be `None`") # noqa: E501
109
+
110
+ self._title = title
111
+
112
+ @property
113
+ def description(self):
114
+ """Gets the description of this WorkspaceTemplateReadme. # noqa: E501
115
+
116
+ The description of the workspace template # noqa: E501
117
+
118
+ :return: The description of this WorkspaceTemplateReadme. # noqa: E501
119
+ :rtype: str
120
+ """
121
+ return self._description
122
+
123
+ @description.setter
124
+ def description(self, description):
125
+ """Sets the description of this WorkspaceTemplateReadme.
126
+
127
+ The description of the workspace template # noqa: E501
128
+
129
+ :param description: The description of this WorkspaceTemplateReadme. # noqa: E501
130
+ :type: str
131
+ """
132
+ if self.local_vars_configuration.client_side_validation and description is None: # noqa: E501
133
+ raise ValueError("Invalid value for `description`, must not be `None`") # noqa: E501
134
+
135
+ self._description = description
136
+
137
+ def to_dict(self):
138
+ """Returns the model properties as a dict"""
139
+ result = {}
140
+
141
+ for attr, _ in six.iteritems(self.openapi_types):
142
+ value = getattr(self, attr)
143
+ if isinstance(value, list):
144
+ result[attr] = list(map(
145
+ lambda x: x.to_dict() if hasattr(x, "to_dict") else x,
146
+ value
147
+ ))
148
+ elif hasattr(value, "to_dict"):
149
+ result[attr] = value.to_dict()
150
+ elif isinstance(value, dict):
151
+ result[attr] = dict(map(
152
+ lambda item: (item[0], item[1].to_dict())
153
+ if hasattr(item[1], "to_dict") else item,
154
+ value.items()
155
+ ))
156
+ else:
157
+ result[attr] = value
158
+
159
+ return result
160
+
161
+ def to_str(self):
162
+ """Returns the string representation of the model"""
163
+ return pprint.pformat(self.to_dict())
164
+
165
+ def __repr__(self):
166
+ """For `print` and `pprint`"""
167
+ return self.to_str()
168
+
169
+ def __eq__(self, other):
170
+ """Returns true if both objects are equal"""
171
+ if not isinstance(other, WorkspaceTemplateReadme):
172
+ return False
173
+
174
+ return self.to_dict() == other.to_dict()
175
+
176
+ def __ne__(self, other):
177
+ """Returns true if both objects are not equal"""
178
+ if not isinstance(other, WorkspaceTemplateReadme):
179
+ return True
180
+
181
+ return self.to_dict() != other.to_dict()
@@ -18,7 +18,7 @@ import six
18
18
  from openapi_client.configuration import Configuration
19
19
 
20
20
 
21
- class ArchivedlogsinfoResponse(object):
21
+ class WorkspacetemplatereadmeResponse(object):
22
22
  """NOTE: This class is auto generated by OpenAPI Generator.
23
23
  Ref: https://openapi-generator.tech
24
24
 
@@ -33,7 +33,7 @@ class ArchivedlogsinfoResponse(object):
33
33
  and the value is json key in definition.
34
34
  """
35
35
  openapi_types = {
36
- 'result': 'ArchivedLogsInfo'
36
+ 'result': 'WorkspaceTemplateReadme'
37
37
  }
38
38
 
39
39
  attribute_map = {
@@ -41,7 +41,7 @@ class ArchivedlogsinfoResponse(object):
41
41
  }
42
42
 
43
43
  def __init__(self, result=None, local_vars_configuration=None): # noqa: E501
44
- """ArchivedlogsinfoResponse - a model defined in OpenAPI""" # noqa: E501
44
+ """WorkspacetemplatereadmeResponse - a model defined in OpenAPI""" # noqa: E501
45
45
  if local_vars_configuration is None:
46
46
  local_vars_configuration = Configuration()
47
47
  self.local_vars_configuration = local_vars_configuration
@@ -53,21 +53,21 @@ class ArchivedlogsinfoResponse(object):
53
53
 
54
54
  @property
55
55
  def result(self):
56
- """Gets the result of this ArchivedlogsinfoResponse. # noqa: E501
56
+ """Gets the result of this WorkspacetemplatereadmeResponse. # noqa: E501
57
57
 
58
58
 
59
- :return: The result of this ArchivedlogsinfoResponse. # noqa: E501
60
- :rtype: ArchivedLogsInfo
59
+ :return: The result of this WorkspacetemplatereadmeResponse. # noqa: E501
60
+ :rtype: WorkspaceTemplateReadme
61
61
  """
62
62
  return self._result
63
63
 
64
64
  @result.setter
65
65
  def result(self, result):
66
- """Sets the result of this ArchivedlogsinfoResponse.
66
+ """Sets the result of this WorkspacetemplatereadmeResponse.
67
67
 
68
68
 
69
- :param result: The result of this ArchivedlogsinfoResponse. # noqa: E501
70
- :type: ArchivedLogsInfo
69
+ :param result: The result of this WorkspacetemplatereadmeResponse. # noqa: E501
70
+ :type: WorkspaceTemplateReadme
71
71
  """
72
72
  if self.local_vars_configuration.client_side_validation and result is None: # noqa: E501
73
73
  raise ValueError("Invalid value for `result`, must not be `None`") # noqa: E501
@@ -108,14 +108,14 @@ class ArchivedlogsinfoResponse(object):
108
108
 
109
109
  def __eq__(self, other):
110
110
  """Returns true if both objects are equal"""
111
- if not isinstance(other, ArchivedlogsinfoResponse):
111
+ if not isinstance(other, WorkspacetemplatereadmeResponse):
112
112
  return False
113
113
 
114
114
  return self.to_dict() == other.to_dict()
115
115
 
116
116
  def __ne__(self, other):
117
117
  """Returns true if both objects are not equal"""
118
- if not isinstance(other, ArchivedlogsinfoResponse):
118
+ if not isinstance(other, WorkspacetemplatereadmeResponse):
119
119
  return True
120
120
 
121
121
  return self.to_dict() != other.to_dict()
@@ -1112,7 +1112,16 @@ def add_collaborators(cloud: str, users_file: str,) -> None:
1112
1112
  type=str,
1113
1113
  required=False,
1114
1114
  )
1115
- def get_cloud(cloud_id: Optional[str], name: Optional[str]) -> None:
1115
+ @click.option(
1116
+ "--output",
1117
+ "-o",
1118
+ help="File to write the full cloud YAML to.",
1119
+ type=str,
1120
+ required=False,
1121
+ )
1122
+ def get_cloud(
1123
+ cloud_id: Optional[str], name: Optional[str], output: Optional[str]
1124
+ ) -> None:
1116
1125
  """
1117
1126
  Retrieve a cloud by its name or ID and display its details.
1118
1127
 
@@ -1131,9 +1140,20 @@ def get_cloud(cloud_id: Optional[str], name: Optional[str]) -> None:
1131
1140
  log.error("Cloud not found.")
1132
1141
  return
1133
1142
 
1134
- cloud_dict = cloud.to_dict() if hasattr(cloud, "to_dict") else cloud.__dict__
1143
+ if output:
1144
+ # Include all cloud deployments for the cloud.
1145
+ result = CloudController().get_cloud_deployments(
1146
+ cloud_id=cloud.id, cloud_name=cloud.name
1147
+ )
1135
1148
 
1136
- print(yaml.dump(cloud_dict, sort_keys=False))
1149
+ with open(output, "w") as f:
1150
+ yaml.dump(result, f, sort_keys=False)
1151
+
1152
+ else:
1153
+ cloud_dict = (
1154
+ cloud.to_dict() if hasattr(cloud, "to_dict") else cloud.__dict__
1155
+ )
1156
+ print(yaml.dump(cloud_dict, sort_keys=False))
1137
1157
 
1138
1158
  except ValueError as e:
1139
1159
  log.error(f"Error retrieving cloud: {e}")
@@ -1189,15 +1209,38 @@ def get_default_cloud() -> None:
1189
1209
  type=bool,
1190
1210
  required=False,
1191
1211
  default=False,
1212
+ is_flag=True,
1192
1213
  )
1193
1214
  @click.option(
1194
1215
  "--out",
1195
- help="Output file name for the report.",
1216
+ help="Output file name for the report. (Default jobs_report.html)",
1196
1217
  type=str,
1197
1218
  required=False,
1198
- default="jobs_report.html",
1219
+ default=None,
1220
+ )
1221
+ @click.option(
1222
+ "--sort-by",
1223
+ help=(
1224
+ "Column to sort by. (Default created_at). "
1225
+ "created_at: Job creation time. "
1226
+ "gpu: Unused GPU hours. "
1227
+ "cpu: Unused CPU hours. "
1228
+ "instances: Number of instances."
1229
+ ),
1230
+ type=click.Choice(["created_at", "gpu", "cpu", "instances"], case_sensitive=False),
1231
+ required=False,
1232
+ default="created_at",
1199
1233
  )
1200
- def generate_jobs_report(cloud_id: str, csv: bool, out: str) -> None:
1234
+ @click.option(
1235
+ "--sort-order",
1236
+ help="Sort order. (Default desc)",
1237
+ type=click.Choice(["asc", "desc"], case_sensitive=False),
1238
+ required=False,
1239
+ default="desc",
1240
+ )
1241
+ def generate_jobs_report(
1242
+ cloud_id: str, csv: bool, out: Optional[str], sort_by: str, sort_order: str
1243
+ ) -> None:
1201
1244
  """
1202
1245
  Generate a report of the jobs created in the last 7 days in HTML format.
1203
1246
  Shows unused CPU-hours, unused GPU-hours, and other data.
@@ -1205,7 +1248,12 @@ def generate_jobs_report(cloud_id: str, csv: bool, out: str) -> None:
1205
1248
  :param csv: Outputs the report in CSV format.
1206
1249
  :param out: Output file name for the report.
1207
1250
  """
1251
+ if out is None:
1252
+ out = "jobs_report.html" if not csv else "jobs_report.csv"
1253
+
1208
1254
  try:
1209
- CloudController().generate_jobs_report(cloud_id, csv, out)
1255
+ CloudController().generate_jobs_report(
1256
+ cloud_id, csv, out, sort_by, sort_order == "asc"
1257
+ )
1210
1258
  except ValueError as e:
1211
1259
  log.error(f"Error generating jobs report: {e}")
@@ -77,6 +77,64 @@ NAME ID COST PROJECT NAME CLUSTER NAME
77
77
  my-job prodjob_s9x4uzc5jnkt5z53g4tujb3y2e 0 default cluster_for_prodjob_s9x4uzc5jnkt5z53g4tujb3y2e SUCCESS doc@anyscale.com python main.py
78
78
  """
79
79
 
80
+ JOB_QUEUE_LIST = """\
81
+ $ anyscale job-queue list
82
+ Output
83
+ JOB QUEUES:
84
+ ID NAME CLUSTER ID CREATOR ID MAX CONCURRENCY IDLE TIMEOUT SEC CURRENT CLUSTER STATE
85
+ jq_h8fcze2qkr8wttuuvapi1hvyuc queue_3 ses_cjr7uaf1yh2ue5uzvd11p24p4u usr_we8x7d7u8hq8mj2488ed9x47n6 3 5000 Terminated
86
+ jq_v5bx9z1sd4pbxasxhdms37j4gi queue_2 ses_k86raeu6k1t6z1bvyejn3vblad usr_we8x7d7u8hq8mj2488ed9x47n6 10 5000 Terminated
87
+ jq_ni6hk66nt3194msr7hzzj9daun queue_1 ses_uhb8a9gamtarz68kcurpjh86sa usr_we8x7d7u8hq8mj2488ed9x47n6 10 5000 Terminated
88
+ """
89
+
90
+ JOB_QUEUE_INFO = """\
91
+ $ anyscale job-queue info --id jq_h8fcze2qkr8wttuuvapi1hvyuc
92
+ Output
93
+ ID : jq_h8fcze2qkr8wttuuvapi1hvyuc
94
+ USER PROVIDED ID : queue_3
95
+ NAME : queue_3
96
+ CURRENT JOB QUEUE STATE : ACTIVE
97
+ EXECUTION MODE : PRIORITY
98
+ MAX CONCURRENCY : 3
99
+ IDLE TIMEOUT SEC : 5000
100
+ CREATED AT : 2025-04-15 20:40:44
101
+ CREATOR ID : usr_we8x7d7u8hq8mj2488ed9x47n6
102
+ CREATOR EMAIL : test@anyscale.com
103
+ COMPUTE CONFIG ID : cpt_8hzsv1t4jvb6kwjhfqbfjw5i6b
104
+ CURRENT CLUSTER STATE : Terminated
105
+ CLUSTER ID : ses_cjr7uaf1yh2ue5uzvd11p24p4u
106
+ PROJECT ID : prj_7FWKGPGPaD3Q5mvk9zK2viBD
107
+ CLOUD ID : cld_kvedZWag2qA8i5BjxUevf5i7
108
+ TOTAL JOBS : 6
109
+ SUCCESSFUL JOBS : 6
110
+ FAILED JOBS : 0
111
+ ACTIVE JOBS : 0
112
+ """
113
+
114
+ JOB_QUEUE_UPDATE = """\
115
+ $ anyscale job-queue update --id jq_h8fcze2qkr8wttuuvapi1hvyuc --max-concurrency 5
116
+ Output
117
+ ID : jq_h8fcze2qkr8wttuuvapi1hvyuc
118
+ USER PROVIDED ID : queue_3
119
+ NAME : queue_3
120
+ CURRENT JOB QUEUE STATE : ACTIVE
121
+ EXECUTION MODE : PRIORITY
122
+ MAX CONCURRENCY : 5
123
+ IDLE TIMEOUT SEC : 5000
124
+ CREATED AT : 2025-04-15 20:40:44
125
+ CREATOR ID : usr_we8x7d7u8hq8mj2488ed9x47n6
126
+ CREATOR EMAIL : test@anyscale.com
127
+ COMPUTE CONFIG ID : cpt_8hzsv1t4jvb6kwjhfqbfjw5i6b
128
+ CURRENT CLUSTER STATE : Terminated
129
+ CLUSTER ID : ses_cjr7uaf1yh2ue5uzvd11p24p4u
130
+ PROJECT ID : prj_7FWKGPGPaD3Q5mvk9zK2viBD
131
+ CLOUD ID : cld_kvedZWag2qA8i5BjxUevf5i7
132
+ TOTAL JOBS : 6
133
+ SUCCESSFUL JOBS : 6
134
+ FAILED JOBS : 0
135
+ ACTIVE JOBS : 0
136
+ """
137
+
80
138
  SCHEDULE_APPLY_EXAMPLE = """\
81
139
  $ anyscale schedule apply -n my-schedule -f my-schedule.yaml
82
140
  (anyscale +0.5s) Applying schedule with config ScheduleConfig(job_config=JobConfig(name='my-schedule', image_uri=None, compute_config=None, env_vars=None, py_modules=None, cloud=None, project=None, ray_version=None, job_queue_config=None), cron_expression='0 0 * * * *', timezone='UTC').
@@ -396,14 +396,14 @@ and override the entrypoint with `python main.py`.
396
396
  help=f"Filter jobs by state. Accepts one or more states. Allowed states: {', '.join(HaJobStates.allowable_values)}",
397
397
  callback=validate_list_jobs_state_filter,
398
398
  )
399
- def list( # noqa: A001
399
+ def list( # noqa: A001 PLR0913
400
400
  name: Optional[str],
401
401
  id: Optional[str], # noqa: A002
402
402
  project_id: Optional[str],
403
403
  include_all_users: bool,
404
404
  include_archived: bool,
405
405
  max_items: int,
406
- states: List[str],
406
+ states: List[HaJobStates],
407
407
  ) -> None:
408
408
  job_controller = JobController()
409
409
  job_controller.list(
@@ -0,0 +1,172 @@
1
+ from typing import List
2
+
3
+ import click
4
+
5
+ from anyscale.client.openapi_client.models.job_queue_sort_directive import (
6
+ JobQueueSortDirective,
7
+ )
8
+ from anyscale.client.openapi_client.models.job_queue_sort_field import JobQueueSortField
9
+ from anyscale.client.openapi_client.models.sort_order import SortOrder
10
+ from anyscale.commands import command_examples
11
+ from anyscale.commands.util import AnyscaleCommand
12
+ from anyscale.controllers.job_controller import JobController, JobQueueView
13
+ from anyscale.util import validate_non_negative_arg
14
+
15
+
16
+ @click.group(
17
+ "job-queues", help="Interact with production job queues running on Anyscale."
18
+ )
19
+ def job_queue_cli() -> None:
20
+ pass
21
+
22
+
23
+ def parse_sort_fields(
24
+ param: str, sort_fields: List[str],
25
+ ) -> List[JobQueueSortDirective]:
26
+ sort_directives = []
27
+
28
+ for field_str in sort_fields:
29
+ descending = field_str.startswith("-")
30
+ raw_field = field_str.lstrip("-").upper()
31
+
32
+ if raw_field not in JobQueueSortField.allowable_values:
33
+ raise click.UsageError(
34
+ f"{param} must be one of {', '.join([v.lower() for v in JobQueueSortField.allowable_values])}"
35
+ )
36
+
37
+ sort_directives.append(
38
+ JobQueueSortDirective(
39
+ sort_field=raw_field,
40
+ sort_order=SortOrder.DESC if descending else SortOrder.ASC,
41
+ )
42
+ )
43
+
44
+ return sort_directives
45
+
46
+
47
+ @job_queue_cli.command(
48
+ name="list",
49
+ short_help="List job queues.",
50
+ cls=AnyscaleCommand,
51
+ example=command_examples.JOB_QUEUE_LIST,
52
+ )
53
+ @click.option(
54
+ "--include-all-users",
55
+ is_flag=True,
56
+ default=False,
57
+ help="Include job queues not created by current user.",
58
+ )
59
+ @click.option(
60
+ "--view",
61
+ type=click.Choice([v.name.lower() for v in JobQueueView], case_sensitive=False),
62
+ default=JobQueueView.DEFAULT.name,
63
+ help="Select which view to display.",
64
+ callback=lambda _, __, value: JobQueueView[value.upper()],
65
+ )
66
+ @click.option(
67
+ "--page",
68
+ default=100,
69
+ type=int,
70
+ help="Page size (default 100).",
71
+ callback=validate_non_negative_arg,
72
+ )
73
+ @click.option(
74
+ "--max-items",
75
+ required=False,
76
+ type=int,
77
+ help="Max items to show in list (only valid in interactive mode).",
78
+ callback=lambda ctx, param, value: validate_non_negative_arg(ctx, param, value)
79
+ if value
80
+ else None,
81
+ )
82
+ @click.option(
83
+ "--sort",
84
+ "sorting_directives",
85
+ multiple=True,
86
+ default=[JobQueueSortField.CREATED_AT],
87
+ help=f"""
88
+ Sort by column(s). Prefix column with - to sort in descending order.
89
+ Supported columns: {', '.join([v.lower() for v in JobQueueSortField.allowable_values])}.
90
+ """,
91
+ callback=lambda _, __, value: parse_sort_fields("sort", list(value)),
92
+ )
93
+ @click.option(
94
+ "--interactive/--no-interactive",
95
+ default=True,
96
+ help="--no-interactive disables the default interactive mode.",
97
+ )
98
+ def list_job_queues(
99
+ include_all_users: bool,
100
+ view: JobQueueView,
101
+ page: int,
102
+ max_items: int,
103
+ sorting_directives: List[JobQueueSortDirective],
104
+ interactive: bool,
105
+ ):
106
+ if max_items is not None and interactive:
107
+ raise click.UsageError("--max-items can only be used in non interactive mode.")
108
+ job_controller = JobController()
109
+ job_controller.list_job_queues(
110
+ max_items=max_items,
111
+ page_size=page,
112
+ include_all_users=include_all_users,
113
+ view=view,
114
+ sorting_directives=sorting_directives,
115
+ interactive=interactive,
116
+ )
117
+
118
+
119
+ @job_queue_cli.command(
120
+ name="update",
121
+ short_help="Update job queue.",
122
+ cls=AnyscaleCommand,
123
+ example=command_examples.JOB_QUEUE_UPDATE,
124
+ )
125
+ @click.option(
126
+ "--id", "job_queue_id", required=False, default=None, help="ID of the job queue."
127
+ )
128
+ @click.option(
129
+ "--name",
130
+ "job_queue_name",
131
+ required=False,
132
+ default=None,
133
+ help="Name of the job queue.",
134
+ )
135
+ @click.option(
136
+ "--max-concurrency",
137
+ required=False,
138
+ default=None,
139
+ help="Maximum concurrency of the job queue",
140
+ )
141
+ @click.option(
142
+ "--idle-timeout-s",
143
+ required=False,
144
+ default=None,
145
+ help="Idle timeout of the job queue",
146
+ )
147
+ def update_job_queue(
148
+ job_queue_id: str, job_queue_name: str, max_concurrency: int, idle_timeout_s: int
149
+ ):
150
+ if job_queue_id is None and job_queue_name is None:
151
+ raise click.ClickException("ID or name of job queue is required")
152
+ job_controller = JobController()
153
+ job_controller.update_job_queue(
154
+ job_queue_id=job_queue_id,
155
+ job_queue_name=job_queue_name,
156
+ max_concurrency=max_concurrency,
157
+ idle_timeout_s=idle_timeout_s,
158
+ )
159
+
160
+
161
+ @job_queue_cli.command(
162
+ name="info",
163
+ short_help="Info of a job queue.",
164
+ cls=AnyscaleCommand,
165
+ example=command_examples.JOB_QUEUE_INFO,
166
+ )
167
+ @click.option(
168
+ "--id", "job_queue_id", required=True, default=None, help="ID of the job."
169
+ )
170
+ def get_job_queue(job_queue_id: str):
171
+ job_controller = JobController()
172
+ job_controller.get_job_queue(job_queue_id=job_queue_id)
@@ -56,6 +56,7 @@ BUILD_STEPS = [
56
56
  # Default minutes for autosuspend.
57
57
  DEFAULT_AUTOSUSPEND_TIMEOUT = 120
58
58
 
59
+
59
60
  # Default docker images to use for connect clusters.
60
61
  def _get_base_image(image: str, ray_version: str, cpu_or_gpu: str) -> str:
61
62
  py_version = "".join(str(x) for x in sys.version_info[0:2])
@@ -557,34 +558,36 @@ class PrepareClusterBlock:
557
558
  """
558
559
  Get the default cluster env build based on the local python and ray versions.
559
560
  """
560
- py_version = "".join(str(x) for x in sys.version_info[0:2])
561
- if sys.version_info.major == 3 and sys.version_info.minor == 10:
562
- py_version = "310"
563
- if py_version not in ["36", "37", "38", "39", "310"]:
561
+ major, minor = sys.version_info[:2]
562
+ MIN_PY_VER = (3, 8)
563
+ MAX_PY_VER = (3, 12)
564
+
565
+ if not (MIN_PY_VER <= (major, minor) <= MAX_PY_VER):
564
566
  raise ValueError(
565
- "No default cluster env for py{}. Please use a version of python between 3.6 and 3.8.".format(
566
- py_version
567
- )
567
+ f"No default container image for python version {major}.{minor}."
568
+ f"Please use a Python version between {MIN_PY_VER[0]}.{MIN_PY_VER[1]} "
569
+ f"and {MAX_PY_VER[0]}.{MAX_PY_VER[1]}."
568
570
  )
571
+
569
572
  ray_version = self._ray.__version__
570
573
  if version.parse(ray_version) < version.parse(MINIMUM_RAY_VERSION):
571
574
  raise ValueError(
572
- f"No default cluster env for Ray version {ray_version}. Please upgrade "
575
+ f"No default container image for Ray version {ray_version}. Please upgrade "
573
576
  f"to a version >= {MINIMUM_RAY_VERSION}."
574
577
  )
575
578
  if "dev0" in ray_version:
576
579
  raise ValueError(
577
580
  f"Your locally installed Ray version is {ray_version}. "
578
- "There is no default cluster environments for nightly versions of Ray."
581
+ "There is no default container image for nightly versions of Ray."
579
582
  )
580
583
  try:
581
584
  build = self.api_client.get_default_cluster_env_build_api_v2_builds_default_py_version_ray_version_get(
582
- f"py{py_version}", ray_version
585
+ f"py{major}{minor}", ray_version
583
586
  ).result
584
587
  return build
585
588
  except Exception: # noqa: BLE001
586
589
  raise RuntimeError(
587
- f"Failed to get default cluster env for Ray: {ray_version} on Python: py{py_version}"
590
+ f"Failed to get default container image for Ray: {ray_version} on Python: py{major}{minor}"
588
591
  )
589
592
 
590
593
  def _get_cluster_build(
@@ -815,14 +818,16 @@ class PrepareClusterBlock:
815
818
  return compute_template_id
816
819
 
817
820
  def _register_compute_template(
818
- self, project_id: str, config_object: ComputeTemplateConfig
821
+ self, project_id: str, config_object: ComputeTemplateConfig # noqa: ARG002
819
822
  ) -> str:
820
823
  """
821
- Register compute template with a default name and return the compute template id."""
824
+ Register compute template with a default name and return the compute template id.
825
+ """
822
826
  created_template = self.api_client.create_compute_template_api_v2_compute_templates_post(
823
827
  create_compute_template=CreateComputeTemplate(
824
828
  name=gen_valid_name("autogenerated-config"),
825
- project_id=project_id,
829
+ # project ID is deprecated in compute config
830
+ project_id=None,
826
831
  config=config_object,
827
832
  anonymous=True,
828
833
  )