anyscale 0.26.16__py3-none-any.whl → 0.26.18__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 (40) hide show
  1. anyscale/anyscale-cloud-setup-gcp.yaml +2 -0
  2. anyscale/anyscale-cloud-setup.yaml +0 -4
  3. anyscale/client/README.md +7 -37
  4. anyscale/client/openapi_client/__init__.py +5 -20
  5. anyscale/client/openapi_client/api/default_api.py +410 -2163
  6. anyscale/client/openapi_client/models/__init__.py +5 -20
  7. anyscale/client/openapi_client/models/{create_session_response.py → i_know_response.py} +51 -51
  8. anyscale/client/openapi_client/models/{session_details.py → i_know_time_series_event.py} +35 -35
  9. anyscale/client/openapi_client/models/job_report.py +199 -0
  10. anyscale/client/openapi_client/models/job_with_report.py +254 -0
  11. anyscale/client/openapi_client/models/{webterminal_list_response.py → jobwithreport_list_response.py} +15 -15
  12. anyscale/commands/cloud_commands.py +71 -0
  13. anyscale/connect_utils/prepare_cluster.py +19 -14
  14. anyscale/controllers/cloud_controller.py +164 -1
  15. anyscale/job/_private/job_sdk.py +22 -24
  16. anyscale/version.py +1 -1
  17. {anyscale-0.26.16.dist-info → anyscale-0.26.18.dist-info}/METADATA +1 -1
  18. {anyscale-0.26.16.dist-info → anyscale-0.26.18.dist-info}/RECORD +23 -38
  19. anyscale/client/openapi_client/models/archived_logs_info.py +0 -164
  20. anyscale/client/openapi_client/models/archivedlogsinfo_response.py +0 -121
  21. anyscale/client/openapi_client/models/create_experimental_workspace_from_job.py +0 -123
  22. anyscale/client/openapi_client/models/create_session_from_snapshot_options.py +0 -538
  23. anyscale/client/openapi_client/models/create_session_in_db.py +0 -434
  24. anyscale/client/openapi_client/models/createsessionresponse_response.py +0 -121
  25. anyscale/client/openapi_client/models/external_service_status.py +0 -147
  26. anyscale/client/openapi_client/models/external_service_status_response.py +0 -250
  27. anyscale/client/openapi_client/models/externalservicestatusresponse_response.py +0 -121
  28. anyscale/client/openapi_client/models/monitor_logs_extension.py +0 -100
  29. anyscale/client/openapi_client/models/session_describe.py +0 -175
  30. anyscale/client/openapi_client/models/session_history_item.py +0 -146
  31. anyscale/client/openapi_client/models/sessiondescribe_response.py +0 -121
  32. anyscale/client/openapi_client/models/sessiondetails_response.py +0 -121
  33. anyscale/client/openapi_client/models/sessionhistoryitem_list_response.py +0 -147
  34. anyscale/client/openapi_client/models/update_compute_template.py +0 -146
  35. anyscale/client/openapi_client/models/update_compute_template_config.py +0 -464
  36. {anyscale-0.26.16.dist-info → anyscale-0.26.18.dist-info}/LICENSE +0 -0
  37. {anyscale-0.26.16.dist-info → anyscale-0.26.18.dist-info}/NOTICE +0 -0
  38. {anyscale-0.26.16.dist-info → anyscale-0.26.18.dist-info}/WHEEL +0 -0
  39. {anyscale-0.26.16.dist-info → anyscale-0.26.18.dist-info}/entry_points.txt +0 -0
  40. {anyscale-0.26.16.dist-info → anyscale-0.26.18.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,254 @@
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 JobWithReport(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
+ 'job_id': 'str',
37
+ 'job_name': 'str',
38
+ 'job_state': 'HaJobStates',
39
+ 'job_report': 'JobReport',
40
+ 'created_at': 'datetime',
41
+ 'finished_at': 'datetime'
42
+ }
43
+
44
+ attribute_map = {
45
+ 'job_id': 'job_id',
46
+ 'job_name': 'job_name',
47
+ 'job_state': 'job_state',
48
+ 'job_report': 'job_report',
49
+ 'created_at': 'created_at',
50
+ 'finished_at': 'finished_at'
51
+ }
52
+
53
+ def __init__(self, job_id=None, job_name=None, job_state=None, job_report=None, created_at=None, finished_at=None, local_vars_configuration=None): # noqa: E501
54
+ """JobWithReport - a model defined in OpenAPI""" # noqa: E501
55
+ if local_vars_configuration is None:
56
+ local_vars_configuration = Configuration()
57
+ self.local_vars_configuration = local_vars_configuration
58
+
59
+ self._job_id = None
60
+ self._job_name = None
61
+ self._job_state = None
62
+ self._job_report = None
63
+ self._created_at = None
64
+ self._finished_at = None
65
+ self.discriminator = None
66
+
67
+ self.job_id = job_id
68
+ self.job_name = job_name
69
+ self.job_state = job_state
70
+ if job_report is not None:
71
+ self.job_report = job_report
72
+ self.created_at = created_at
73
+ if finished_at is not None:
74
+ self.finished_at = finished_at
75
+
76
+ @property
77
+ def job_id(self):
78
+ """Gets the job_id of this JobWithReport. # noqa: E501
79
+
80
+
81
+ :return: The job_id of this JobWithReport. # noqa: E501
82
+ :rtype: str
83
+ """
84
+ return self._job_id
85
+
86
+ @job_id.setter
87
+ def job_id(self, job_id):
88
+ """Sets the job_id of this JobWithReport.
89
+
90
+
91
+ :param job_id: The job_id of this JobWithReport. # noqa: E501
92
+ :type: str
93
+ """
94
+ if self.local_vars_configuration.client_side_validation and job_id is None: # noqa: E501
95
+ raise ValueError("Invalid value for `job_id`, must not be `None`") # noqa: E501
96
+
97
+ self._job_id = job_id
98
+
99
+ @property
100
+ def job_name(self):
101
+ """Gets the job_name of this JobWithReport. # noqa: E501
102
+
103
+
104
+ :return: The job_name of this JobWithReport. # noqa: E501
105
+ :rtype: str
106
+ """
107
+ return self._job_name
108
+
109
+ @job_name.setter
110
+ def job_name(self, job_name):
111
+ """Sets the job_name of this JobWithReport.
112
+
113
+
114
+ :param job_name: The job_name of this JobWithReport. # noqa: E501
115
+ :type: str
116
+ """
117
+ if self.local_vars_configuration.client_side_validation and job_name is None: # noqa: E501
118
+ raise ValueError("Invalid value for `job_name`, must not be `None`") # noqa: E501
119
+
120
+ self._job_name = job_name
121
+
122
+ @property
123
+ def job_state(self):
124
+ """Gets the job_state of this JobWithReport. # noqa: E501
125
+
126
+
127
+ :return: The job_state of this JobWithReport. # noqa: E501
128
+ :rtype: HaJobStates
129
+ """
130
+ return self._job_state
131
+
132
+ @job_state.setter
133
+ def job_state(self, job_state):
134
+ """Sets the job_state of this JobWithReport.
135
+
136
+
137
+ :param job_state: The job_state of this JobWithReport. # noqa: E501
138
+ :type: HaJobStates
139
+ """
140
+ if self.local_vars_configuration.client_side_validation and job_state is None: # noqa: E501
141
+ raise ValueError("Invalid value for `job_state`, must not be `None`") # noqa: E501
142
+
143
+ self._job_state = job_state
144
+
145
+ @property
146
+ def job_report(self):
147
+ """Gets the job_report of this JobWithReport. # noqa: E501
148
+
149
+
150
+ :return: The job_report of this JobWithReport. # noqa: E501
151
+ :rtype: JobReport
152
+ """
153
+ return self._job_report
154
+
155
+ @job_report.setter
156
+ def job_report(self, job_report):
157
+ """Sets the job_report of this JobWithReport.
158
+
159
+
160
+ :param job_report: The job_report of this JobWithReport. # noqa: E501
161
+ :type: JobReport
162
+ """
163
+
164
+ self._job_report = job_report
165
+
166
+ @property
167
+ def created_at(self):
168
+ """Gets the created_at of this JobWithReport. # noqa: E501
169
+
170
+
171
+ :return: The created_at of this JobWithReport. # noqa: E501
172
+ :rtype: datetime
173
+ """
174
+ return self._created_at
175
+
176
+ @created_at.setter
177
+ def created_at(self, created_at):
178
+ """Sets the created_at of this JobWithReport.
179
+
180
+
181
+ :param created_at: The created_at of this JobWithReport. # noqa: E501
182
+ :type: datetime
183
+ """
184
+ if self.local_vars_configuration.client_side_validation and created_at is None: # noqa: E501
185
+ raise ValueError("Invalid value for `created_at`, must not be `None`") # noqa: E501
186
+
187
+ self._created_at = created_at
188
+
189
+ @property
190
+ def finished_at(self):
191
+ """Gets the finished_at of this JobWithReport. # noqa: E501
192
+
193
+
194
+ :return: The finished_at of this JobWithReport. # noqa: E501
195
+ :rtype: datetime
196
+ """
197
+ return self._finished_at
198
+
199
+ @finished_at.setter
200
+ def finished_at(self, finished_at):
201
+ """Sets the finished_at of this JobWithReport.
202
+
203
+
204
+ :param finished_at: The finished_at of this JobWithReport. # noqa: E501
205
+ :type: datetime
206
+ """
207
+
208
+ self._finished_at = finished_at
209
+
210
+ def to_dict(self):
211
+ """Returns the model properties as a dict"""
212
+ result = {}
213
+
214
+ for attr, _ in six.iteritems(self.openapi_types):
215
+ value = getattr(self, attr)
216
+ if isinstance(value, list):
217
+ result[attr] = list(map(
218
+ lambda x: x.to_dict() if hasattr(x, "to_dict") else x,
219
+ value
220
+ ))
221
+ elif hasattr(value, "to_dict"):
222
+ result[attr] = value.to_dict()
223
+ elif isinstance(value, dict):
224
+ result[attr] = dict(map(
225
+ lambda item: (item[0], item[1].to_dict())
226
+ if hasattr(item[1], "to_dict") else item,
227
+ value.items()
228
+ ))
229
+ else:
230
+ result[attr] = value
231
+
232
+ return result
233
+
234
+ def to_str(self):
235
+ """Returns the string representation of the model"""
236
+ return pprint.pformat(self.to_dict())
237
+
238
+ def __repr__(self):
239
+ """For `print` and `pprint`"""
240
+ return self.to_str()
241
+
242
+ def __eq__(self, other):
243
+ """Returns true if both objects are equal"""
244
+ if not isinstance(other, JobWithReport):
245
+ return False
246
+
247
+ return self.to_dict() == other.to_dict()
248
+
249
+ def __ne__(self, other):
250
+ """Returns true if both objects are not equal"""
251
+ if not isinstance(other, JobWithReport):
252
+ return True
253
+
254
+ 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 WebterminalListResponse(object):
21
+ class JobwithreportListResponse(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 WebterminalListResponse(object):
33
33
  and the value is json key in definition.
34
34
  """
35
35
  openapi_types = {
36
- 'results': 'list[WebTerminal]',
36
+ 'results': 'list[JobWithReport]',
37
37
  'metadata': 'ListResponseMetadata'
38
38
  }
39
39
 
@@ -43,7 +43,7 @@ class WebterminalListResponse(object):
43
43
  }
44
44
 
45
45
  def __init__(self, results=None, metadata=None, local_vars_configuration=None): # noqa: E501
46
- """WebterminalListResponse - a model defined in OpenAPI""" # noqa: E501
46
+ """JobwithreportListResponse - a model defined in OpenAPI""" # noqa: E501
47
47
  if local_vars_configuration is None:
48
48
  local_vars_configuration = Configuration()
49
49
  self.local_vars_configuration = local_vars_configuration
@@ -58,21 +58,21 @@ class WebterminalListResponse(object):
58
58
 
59
59
  @property
60
60
  def results(self):
61
- """Gets the results of this WebterminalListResponse. # noqa: E501
61
+ """Gets the results of this JobwithreportListResponse. # noqa: E501
62
62
 
63
63
 
64
- :return: The results of this WebterminalListResponse. # noqa: E501
65
- :rtype: list[WebTerminal]
64
+ :return: The results of this JobwithreportListResponse. # noqa: E501
65
+ :rtype: list[JobWithReport]
66
66
  """
67
67
  return self._results
68
68
 
69
69
  @results.setter
70
70
  def results(self, results):
71
- """Sets the results of this WebterminalListResponse.
71
+ """Sets the results of this JobwithreportListResponse.
72
72
 
73
73
 
74
- :param results: The results of this WebterminalListResponse. # noqa: E501
75
- :type: list[WebTerminal]
74
+ :param results: The results of this JobwithreportListResponse. # noqa: E501
75
+ :type: list[JobWithReport]
76
76
  """
77
77
  if self.local_vars_configuration.client_side_validation and results is None: # noqa: E501
78
78
  raise ValueError("Invalid value for `results`, must not be `None`") # noqa: E501
@@ -81,20 +81,20 @@ class WebterminalListResponse(object):
81
81
 
82
82
  @property
83
83
  def metadata(self):
84
- """Gets the metadata of this WebterminalListResponse. # noqa: E501
84
+ """Gets the metadata of this JobwithreportListResponse. # noqa: E501
85
85
 
86
86
 
87
- :return: The metadata of this WebterminalListResponse. # noqa: E501
87
+ :return: The metadata of this JobwithreportListResponse. # noqa: E501
88
88
  :rtype: ListResponseMetadata
89
89
  """
90
90
  return self._metadata
91
91
 
92
92
  @metadata.setter
93
93
  def metadata(self, metadata):
94
- """Sets the metadata of this WebterminalListResponse.
94
+ """Sets the metadata of this JobwithreportListResponse.
95
95
 
96
96
 
97
- :param metadata: The metadata of this WebterminalListResponse. # noqa: E501
97
+ :param metadata: The metadata of this JobwithreportListResponse. # noqa: E501
98
98
  :type: ListResponseMetadata
99
99
  """
100
100
 
@@ -134,14 +134,14 @@ class WebterminalListResponse(object):
134
134
 
135
135
  def __eq__(self, other):
136
136
  """Returns true if both objects are equal"""
137
- if not isinstance(other, WebterminalListResponse):
137
+ if not isinstance(other, JobwithreportListResponse):
138
138
  return False
139
139
 
140
140
  return self.to_dict() == other.to_dict()
141
141
 
142
142
  def __ne__(self, other):
143
143
  """Returns true if both objects are not equal"""
144
- if not isinstance(other, WebterminalListResponse):
144
+ if not isinstance(other, JobwithreportListResponse):
145
145
  return True
146
146
 
147
147
  return self.to_dict() != other.to_dict()
@@ -1166,3 +1166,74 @@ def get_default_cloud() -> None:
1166
1166
 
1167
1167
  except ValueError as e:
1168
1168
  log.error(f"Error retrieving default cloud: {e}")
1169
+
1170
+
1171
+ @cloud_cli.command(
1172
+ name="jobs-report",
1173
+ help=(
1174
+ "Generate a report of the jobs created in the last 7 days in HTML format. "
1175
+ "Shows unused CPU-hours, unused GPU-hours, and other data."
1176
+ ),
1177
+ cls=AnyscaleCommand,
1178
+ hidden=True,
1179
+ )
1180
+ @click.option(
1181
+ "--cloud-id",
1182
+ help="ID of the cloud to generate a report on.",
1183
+ type=str,
1184
+ required=True,
1185
+ )
1186
+ @click.option(
1187
+ "--csv",
1188
+ help="Outputs the report in CSV format.",
1189
+ type=bool,
1190
+ required=False,
1191
+ default=False,
1192
+ is_flag=True,
1193
+ )
1194
+ @click.option(
1195
+ "--out",
1196
+ help="Output file name for the report. (Default jobs_report.html)",
1197
+ type=str,
1198
+ required=False,
1199
+ default=None,
1200
+ )
1201
+ @click.option(
1202
+ "--sort-by",
1203
+ help=(
1204
+ "Column to sort by. (Default created_at). "
1205
+ "created_at: Job creation time. "
1206
+ "gpu: Unused GPU hours. "
1207
+ "cpu: Unused CPU hours. "
1208
+ "instances: Number of instances."
1209
+ ),
1210
+ type=click.Choice(["created_at", "gpu", "cpu", "instances"], case_sensitive=False),
1211
+ required=False,
1212
+ default="created_at",
1213
+ )
1214
+ @click.option(
1215
+ "--sort-order",
1216
+ help="Sort order. (Default desc)",
1217
+ type=click.Choice(["asc", "desc"], case_sensitive=False),
1218
+ required=False,
1219
+ default="desc",
1220
+ )
1221
+ def generate_jobs_report(
1222
+ cloud_id: str, csv: bool, out: Optional[str], sort_by: str, sort_order: str
1223
+ ) -> None:
1224
+ """
1225
+ Generate a report of the jobs created in the last 7 days in HTML format.
1226
+ Shows unused CPU-hours, unused GPU-hours, and other data.
1227
+ :param cloud_id: The ID of the cloud to generate a report on.
1228
+ :param csv: Outputs the report in CSV format.
1229
+ :param out: Output file name for the report.
1230
+ """
1231
+ if out is None:
1232
+ out = "jobs_report.html" if not csv else "jobs_report.csv"
1233
+
1234
+ try:
1235
+ CloudController().generate_jobs_report(
1236
+ cloud_id, csv, out, sort_by, sort_order == "asc"
1237
+ )
1238
+ except ValueError as e:
1239
+ log.error(f"Error generating jobs report: {e}")
@@ -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
  )
@@ -3,6 +3,7 @@ Fetches data required and formats output for `anyscale cloud` commands.
3
3
  """
4
4
 
5
5
  import copy
6
+ from datetime import datetime, timedelta
6
7
  import json
7
8
  from os import getenv
8
9
  import pathlib
@@ -16,6 +17,7 @@ import boto3
16
17
  from botocore.exceptions import ClientError, NoCredentialsError
17
18
  import click
18
19
  from click import Abort, ClickException
20
+ from rich.progress import Progress, track
19
21
  import yaml
20
22
 
21
23
  from anyscale import __version__ as anyscale_version
@@ -72,8 +74,12 @@ from anyscale.controllers.cloud_functional_verification_controller import (
72
74
  CloudFunctionalVerificationType,
73
75
  )
74
76
  from anyscale.formatters import clouds_formatter
77
+ from anyscale.job._private.job_sdk import (
78
+ HA_JOB_STATE_TO_JOB_STATE,
79
+ TERMINAL_HA_JOB_STATES,
80
+ )
75
81
  from anyscale.shared_anyscale_utils.aws import AwsRoleArn
76
- from anyscale.shared_anyscale_utils.conf import ANYSCALE_ENV
82
+ from anyscale.shared_anyscale_utils.conf import ANYSCALE_ENV, ANYSCALE_HOST
77
83
  from anyscale.util import ( # pylint:disable=private-import
78
84
  _client,
79
85
  _get_aws_efs_mount_target_ip,
@@ -3643,3 +3649,160 @@ class CloudController(BaseController):
3643
3649
  )
3644
3650
 
3645
3651
  ### End of edit cloud ###
3652
+
3653
+ def generate_jobs_report(
3654
+ self, cloud_id: str, csv: bool, out_path: str, sort: str, sort_order_asc: bool
3655
+ ) -> None:
3656
+ end_time = datetime.now()
3657
+ start_time = end_time - timedelta(days=7)
3658
+
3659
+ full_results = []
3660
+ paging_token: Optional[str] = None
3661
+ count_per_page = 20
3662
+ curr_page_results = None
3663
+ total_jobs: Optional[int] = None
3664
+
3665
+ with Progress() as progress:
3666
+ download_task = progress.add_task("Downloading jobs...", total=None)
3667
+
3668
+ while curr_page_results is None or len(curr_page_results) == count_per_page:
3669
+ response = self.api_client.list_job_reports_api_v2_job_reports_get(
3670
+ cloud_id,
3671
+ start_time=start_time,
3672
+ end_time=end_time,
3673
+ paging_token=paging_token,
3674
+ count=count_per_page,
3675
+ )
3676
+ curr_page_results = response.results
3677
+ full_results.extend(curr_page_results)
3678
+ paging_token = response.metadata.next_paging_token
3679
+ total_jobs = response.metadata.total
3680
+ progress.update(
3681
+ download_task, total=total_jobs, advance=len(curr_page_results)
3682
+ )
3683
+
3684
+ progress.update(download_task, completed=total_jobs)
3685
+
3686
+ if not full_results:
3687
+ self.log.info("No jobs found in the last 7 days.")
3688
+ return
3689
+
3690
+ filtered_results = [
3691
+ job
3692
+ for job in full_results
3693
+ if job.job_state in TERMINAL_HA_JOB_STATES and job.job_report is not None
3694
+ ]
3695
+ if sort == "created_at":
3696
+ filtered_results.sort(
3697
+ key=lambda x: x.created_at, reverse=not sort_order_asc
3698
+ )
3699
+ elif sort == "gpu":
3700
+ filtered_results.sort(
3701
+ key=lambda x: x.job_report.unused_gpu_hours or 0,
3702
+ reverse=not sort_order_asc,
3703
+ )
3704
+ elif sort == "cpu":
3705
+ filtered_results.sort(
3706
+ key=lambda x: x.job_report.unused_cpu_hours or 0,
3707
+ reverse=not sort_order_asc,
3708
+ )
3709
+ elif sort == "instances":
3710
+ filtered_results.sort(
3711
+ key=lambda x: x.job_report.max_instances_launched or 0,
3712
+ reverse=not sort_order_asc,
3713
+ )
3714
+
3715
+ with open(out_path, "w") as out_file:
3716
+ if csv:
3717
+ out_file.write(
3718
+ "Job ID,Job name,Job state,Created at,Finished at,Duration,Unused CPU hours,Unused GPU hours,Max concurrent instances\n"
3719
+ )
3720
+ for job in track(filtered_results, description="Generating report..."):
3721
+ job_state = HA_JOB_STATE_TO_JOB_STATE[job.job_state]
3722
+ if job.finished_at is not None:
3723
+ duration = str(job.finished_at - job.created_at)
3724
+ finished_at = str(job.finished_at)
3725
+ else:
3726
+ duration = ""
3727
+ finished_at = ""
3728
+ unused_cpu_hours = job.job_report.unused_cpu_hours or ""
3729
+ unused_gpu_hours = job.job_report.unused_gpu_hours or ""
3730
+ max_instances_launched = job.job_report.max_instances_launched or ""
3731
+
3732
+ out_file.write(
3733
+ f'"{job.job_id}","{job.job_name}","{job_state}","{str(job.created_at)}","{finished_at}","{duration}","{unused_cpu_hours}","{unused_gpu_hours}","{max_instances_launched}"\n'
3734
+ )
3735
+ else:
3736
+ out_file.write(
3737
+ f"""
3738
+ <html>
3739
+ <head>
3740
+ <title>Jobs Report - {str(end_time)}</title>
3741
+ <style>
3742
+ table {{
3743
+ border: 1px solid black;
3744
+ border-collapse: collapse;
3745
+ }}
3746
+ th, td {{
3747
+ border: 1px solid black;
3748
+ border-collapse: collapse;
3749
+ padding: 8px;
3750
+ }}
3751
+ </style>
3752
+ </head>
3753
+ <body>
3754
+ <h1>Job Report - {str(end_time)}</h1>
3755
+ <p>Total jobs reported (finished jobs): {len(filtered_results)}</p>
3756
+ <p>Total jobs in the last 7 days: {total_jobs}</p>
3757
+ <table>
3758
+ <thead>
3759
+ <tr>
3760
+ <th>Job ID</th>
3761
+ <th>Job name</th>
3762
+ <th>Job state</th>
3763
+ <th>Created at</th>
3764
+ <th>Finished at</th>
3765
+ <th>Duration</th>
3766
+ <th>Unused CPU hours</th>
3767
+ <th>Unused GPU hours</th>
3768
+ <th>Max concurrent instances</th>
3769
+ </tr>
3770
+ </thead>
3771
+ <tbody>
3772
+ """
3773
+ )
3774
+
3775
+ for job in track(filtered_results, description="Generating report..."):
3776
+ job_state = HA_JOB_STATE_TO_JOB_STATE[job.job_state]
3777
+ if job.finished_at is not None:
3778
+ duration = str(job.finished_at - job.created_at)
3779
+ finished_at = str(job.finished_at)
3780
+ else:
3781
+ duration = ""
3782
+ finished_at = ""
3783
+ unused_cpu_hours = job.job_report.unused_cpu_hours or ""
3784
+ unused_gpu_hours = job.job_report.unused_gpu_hours or ""
3785
+ max_instances_launched = job.job_report.max_instances_launched or ""
3786
+
3787
+ out_file.write(
3788
+ f"""
3789
+ <tr>
3790
+ <td><a target="_blank" rel="noreferrer" href="{ANYSCALE_HOST}/jobs/{job.job_id}">{job.job_id}</a></td>
3791
+ <td>{job.job_name}</td>
3792
+ <td>{job_state}</td>
3793
+ <td>{str(job.created_at)}</td>
3794
+ <td>{finished_at}</td>
3795
+ <td>{duration}</td>
3796
+ <td>{unused_cpu_hours}</td>
3797
+ <td>{unused_gpu_hours}</td>
3798
+ <td>{max_instances_launched}</td>
3799
+ </tr>
3800
+ """
3801
+ )
3802
+
3803
+ out_file.write(
3804
+ """
3805
+ </tbody>
3806
+ </table>
3807
+ """
3808
+ )