ciocore 5.1.1__py2.py3-none-any.whl → 10.0.0b3__py2.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.
- ciocore/VERSION +1 -1
- ciocore/__init__.py +23 -1
- ciocore/api_client.py +655 -160
- ciocore/auth/__init__.py +5 -3
- ciocore/cli.py +501 -0
- ciocore/common.py +15 -13
- ciocore/conductor_submit.py +77 -60
- ciocore/config.py +127 -13
- ciocore/data.py +162 -77
- ciocore/docsite/404.html +746 -0
- ciocore/docsite/apidoc/api_client/index.html +3605 -0
- ciocore/docsite/apidoc/apidoc/index.html +909 -0
- ciocore/docsite/apidoc/config/index.html +1652 -0
- ciocore/docsite/apidoc/data/index.html +1553 -0
- ciocore/docsite/apidoc/hardware_set/index.html +2460 -0
- ciocore/docsite/apidoc/package_environment/index.html +1507 -0
- ciocore/docsite/apidoc/package_tree/index.html +2386 -0
- ciocore/docsite/assets/_mkdocstrings.css +16 -0
- ciocore/docsite/assets/images/favicon.png +0 -0
- ciocore/docsite/assets/javascripts/bundle.471ce7a9.min.js +29 -0
- ciocore/docsite/assets/javascripts/bundle.471ce7a9.min.js.map +7 -0
- ciocore/docsite/assets/javascripts/lunr/min/lunr.ar.min.js +1 -0
- ciocore/docsite/assets/javascripts/lunr/min/lunr.da.min.js +18 -0
- ciocore/docsite/assets/javascripts/lunr/min/lunr.de.min.js +18 -0
- ciocore/docsite/assets/javascripts/lunr/min/lunr.du.min.js +18 -0
- ciocore/docsite/assets/javascripts/lunr/min/lunr.el.min.js +1 -0
- ciocore/docsite/assets/javascripts/lunr/min/lunr.es.min.js +18 -0
- ciocore/docsite/assets/javascripts/lunr/min/lunr.fi.min.js +18 -0
- ciocore/docsite/assets/javascripts/lunr/min/lunr.fr.min.js +18 -0
- ciocore/docsite/assets/javascripts/lunr/min/lunr.he.min.js +1 -0
- ciocore/docsite/assets/javascripts/lunr/min/lunr.hi.min.js +1 -0
- ciocore/docsite/assets/javascripts/lunr/min/lunr.hu.min.js +18 -0
- ciocore/docsite/assets/javascripts/lunr/min/lunr.hy.min.js +1 -0
- ciocore/docsite/assets/javascripts/lunr/min/lunr.it.min.js +18 -0
- ciocore/docsite/assets/javascripts/lunr/min/lunr.ja.min.js +1 -0
- ciocore/docsite/assets/javascripts/lunr/min/lunr.jp.min.js +1 -0
- ciocore/docsite/assets/javascripts/lunr/min/lunr.kn.min.js +1 -0
- ciocore/docsite/assets/javascripts/lunr/min/lunr.ko.min.js +1 -0
- ciocore/docsite/assets/javascripts/lunr/min/lunr.multi.min.js +1 -0
- ciocore/docsite/assets/javascripts/lunr/min/lunr.nl.min.js +18 -0
- ciocore/docsite/assets/javascripts/lunr/min/lunr.no.min.js +18 -0
- ciocore/docsite/assets/javascripts/lunr/min/lunr.pt.min.js +18 -0
- ciocore/docsite/assets/javascripts/lunr/min/lunr.ro.min.js +18 -0
- ciocore/docsite/assets/javascripts/lunr/min/lunr.ru.min.js +18 -0
- ciocore/docsite/assets/javascripts/lunr/min/lunr.sa.min.js +1 -0
- ciocore/docsite/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +1 -0
- ciocore/docsite/assets/javascripts/lunr/min/lunr.sv.min.js +18 -0
- ciocore/docsite/assets/javascripts/lunr/min/lunr.ta.min.js +1 -0
- ciocore/docsite/assets/javascripts/lunr/min/lunr.te.min.js +1 -0
- ciocore/docsite/assets/javascripts/lunr/min/lunr.th.min.js +1 -0
- ciocore/docsite/assets/javascripts/lunr/min/lunr.tr.min.js +18 -0
- ciocore/docsite/assets/javascripts/lunr/min/lunr.vi.min.js +1 -0
- ciocore/docsite/assets/javascripts/lunr/min/lunr.zh.min.js +1 -0
- ciocore/docsite/assets/javascripts/lunr/tinyseg.js +206 -0
- ciocore/docsite/assets/javascripts/lunr/wordcut.js +6708 -0
- ciocore/docsite/assets/javascripts/workers/search.b8dbb3d2.min.js +42 -0
- ciocore/docsite/assets/javascripts/workers/search.b8dbb3d2.min.js.map +7 -0
- ciocore/docsite/assets/stylesheets/main.3cba04c6.min.css +1 -0
- ciocore/docsite/assets/stylesheets/main.3cba04c6.min.css.map +1 -0
- ciocore/docsite/assets/stylesheets/palette.06af60db.min.css +1 -0
- ciocore/docsite/assets/stylesheets/palette.06af60db.min.css.map +1 -0
- ciocore/docsite/cmdline/docs/index.html +871 -0
- ciocore/docsite/cmdline/downloader/index.html +934 -0
- ciocore/docsite/cmdline/packages/index.html +878 -0
- ciocore/docsite/cmdline/uploader/index.html +995 -0
- ciocore/docsite/how-to-guides/index.html +869 -0
- ciocore/docsite/index.html +895 -0
- ciocore/docsite/logo.png +0 -0
- ciocore/docsite/objects.inv +0 -0
- ciocore/docsite/search/search_index.json +1 -0
- ciocore/docsite/sitemap.xml +3 -0
- ciocore/docsite/sitemap.xml.gz +0 -0
- ciocore/docsite/stylesheets/extra.css +26 -0
- ciocore/docsite/stylesheets/tables.css +167 -0
- ciocore/downloader/base_downloader.py +644 -0
- ciocore/downloader/download_runner_base.py +47 -0
- ciocore/downloader/job_downloader.py +119 -0
- ciocore/{downloader.py → downloader/legacy_downloader.py} +12 -9
- ciocore/downloader/log.py +73 -0
- ciocore/downloader/logging_download_runner.py +87 -0
- ciocore/downloader/perpetual_downloader.py +63 -0
- ciocore/downloader/registry.py +97 -0
- ciocore/downloader/reporter.py +135 -0
- ciocore/exceptions.py +8 -2
- ciocore/file_utils.py +51 -50
- ciocore/hardware_set.py +449 -0
- ciocore/loggeria.py +89 -20
- ciocore/package_environment.py +110 -48
- ciocore/package_query.py +182 -0
- ciocore/package_tree.py +319 -258
- ciocore/retry.py +0 -0
- ciocore/uploader/_uploader.py +547 -364
- ciocore/uploader/thread_queue_job.py +176 -0
- ciocore/uploader/upload_stats/__init__.py +3 -4
- ciocore/uploader/upload_stats/stats_formats.py +10 -4
- ciocore/validator.py +34 -2
- ciocore/worker.py +174 -151
- ciocore-10.0.0b3.dist-info/METADATA +928 -0
- ciocore-10.0.0b3.dist-info/RECORD +128 -0
- {ciocore-5.1.1.dist-info → ciocore-10.0.0b3.dist-info}/WHEEL +1 -1
- ciocore-10.0.0b3.dist-info/entry_points.txt +2 -0
- tests/instance_type_fixtures.py +175 -0
- tests/package_fixtures.py +205 -0
- tests/test_api_client.py +297 -12
- tests/test_base_downloader.py +104 -0
- tests/test_cli.py +149 -0
- tests/test_common.py +1 -7
- tests/test_config.py +40 -18
- tests/test_data.py +162 -173
- tests/test_downloader.py +118 -0
- tests/test_hardware_set.py +139 -0
- tests/test_job_downloader.py +213 -0
- tests/test_package_query.py +38 -0
- tests/test_package_tree.py +91 -291
- tests/test_submit.py +44 -18
- tests/test_uploader.py +1 -4
- ciocore/__about__.py +0 -10
- ciocore/cli/conductor.py +0 -191
- ciocore/compat.py +0 -15
- ciocore-5.1.1.data/scripts/conductor +0 -19
- ciocore-5.1.1.data/scripts/conductor.bat +0 -13
- ciocore-5.1.1.dist-info/METADATA +0 -408
- ciocore-5.1.1.dist-info/RECORD +0 -47
- tests/mocks/api_client_mock.py +0 -51
- /ciocore/{cli → downloader}/__init__.py +0 -0
- {ciocore-5.1.1.dist-info → ciocore-10.0.0b3.dist-info}/top_level.txt +0 -0
ciocore/api_client.py
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The api_client module is used to make requests to the Conductor API.
|
|
3
|
+
"""
|
|
4
|
+
|
|
1
5
|
import base64
|
|
6
|
+
import collections
|
|
7
|
+
import datetime
|
|
8
|
+
import hashlib
|
|
2
9
|
import importlib
|
|
3
10
|
import json
|
|
4
11
|
import jwt
|
|
@@ -9,21 +16,16 @@ import requests
|
|
|
9
16
|
import socket
|
|
10
17
|
import time
|
|
11
18
|
import sys
|
|
19
|
+
import platform
|
|
20
|
+
|
|
21
|
+
from urllib import parse
|
|
12
22
|
|
|
13
|
-
try:
|
|
14
|
-
from urllib import parse
|
|
15
|
-
except ImportError:
|
|
16
|
-
import urlparse as parse
|
|
17
|
-
|
|
18
23
|
import ciocore
|
|
19
24
|
|
|
20
25
|
from ciocore import config
|
|
21
26
|
from ciocore import common, auth
|
|
22
27
|
|
|
23
|
-
|
|
24
|
-
from ciocore.common import CONDUCTOR_LOGGER_NAME
|
|
25
|
-
|
|
26
|
-
logger = logging.getLogger(CONDUCTOR_LOGGER_NAME)
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
27
29
|
|
|
28
30
|
# A convenience tuple of network exceptions that can/should likely be retried by the retry decorator
|
|
29
31
|
try:
|
|
@@ -40,17 +42,43 @@ except AttributeError:
|
|
|
40
42
|
)
|
|
41
43
|
|
|
42
44
|
|
|
45
|
+
def truncate_middle(s, max_length):
|
|
46
|
+
"""
|
|
47
|
+
Truncate the string `s` to `max_length` by removing characters from the middle.
|
|
48
|
+
|
|
49
|
+
:param s: The original string to be truncated.
|
|
50
|
+
:type s: str
|
|
51
|
+
:param max_length: The maximum allowed length of the string after truncation.
|
|
52
|
+
:type max_length: int
|
|
53
|
+
:return: The truncated string.
|
|
54
|
+
:rtype: str
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
if len(s) <= max_length:
|
|
58
|
+
# String is already at or below the maximum length, return it as is
|
|
59
|
+
return s
|
|
60
|
+
|
|
61
|
+
# Calculate the number of characters to keep from the start and end of the string
|
|
62
|
+
num_keep_front = (max_length // 2)
|
|
63
|
+
num_keep_end = max_length - num_keep_front - 1 # -1 for the ellipsis
|
|
64
|
+
|
|
65
|
+
# Construct the truncated string
|
|
66
|
+
return s[:num_keep_front] + '~' + s[-num_keep_end:]
|
|
67
|
+
|
|
43
68
|
|
|
44
69
|
# TODO: appspot_dot_com_cert = os.path.join(common.base_dir(),'auth','appspot_dot_com_cert2') load
|
|
45
70
|
# appspot.com cert into requests lib verify = appspot_dot_com_cert
|
|
46
71
|
|
|
47
|
-
|
|
48
72
|
class ApiClient:
|
|
73
|
+
"""
|
|
74
|
+
The ApiClient class is a wrapper around the requests library that handles authentication and retries.
|
|
75
|
+
"""
|
|
76
|
+
|
|
49
77
|
http_verbs = ["PUT", "POST", "GET", "DELETE", "HEAD", "PATCH"]
|
|
50
|
-
|
|
78
|
+
|
|
51
79
|
USER_AGENT_TEMPLATE = "client {client_name}/{client_version} (ciocore {ciocore_version}; {runtime} {runtime_version}; {platform} {platform_details}; {hostname} {pid}; {python_path})"
|
|
52
80
|
USER_AGENT_MAX_PATH_LENGTH = 1024
|
|
53
|
-
|
|
81
|
+
|
|
54
82
|
user_agent_header = None
|
|
55
83
|
|
|
56
84
|
def __init__(self):
|
|
@@ -61,17 +89,17 @@ class ApiClient:
|
|
|
61
89
|
method=verb, url=conductor_url, headers=headers, params=params, data=data
|
|
62
90
|
)
|
|
63
91
|
|
|
64
|
-
logger.debug("verb:
|
|
65
|
-
logger.debug("conductor_url:
|
|
66
|
-
logger.debug("headers:
|
|
67
|
-
logger.debug("params:
|
|
68
|
-
logger.debug("data:
|
|
92
|
+
logger.debug(f"verb: {verb}")
|
|
93
|
+
logger.debug(f"conductor_url: {conductor_url}")
|
|
94
|
+
logger.debug(f"headers: {headers}")
|
|
95
|
+
logger.debug(f"params: {params}")
|
|
96
|
+
logger.debug(f"data: {data}")
|
|
69
97
|
|
|
70
98
|
# If we get 300s/400s debug out the response. TODO(lws): REMOVE THIS
|
|
71
|
-
if
|
|
99
|
+
if 300 <= response.status_code < 500:
|
|
72
100
|
logger.debug("***** ERROR!! *****")
|
|
73
|
-
logger.debug("Reason:
|
|
74
|
-
logger.debug("Text:
|
|
101
|
+
logger.debug(f"Reason: {response.reason}")
|
|
102
|
+
logger.debug(f"Text: {response.text}")
|
|
75
103
|
|
|
76
104
|
# trigger an exception to be raised for 4XX or 5XX http responses
|
|
77
105
|
if raise_on_error:
|
|
@@ -85,37 +113,38 @@ class ApiClient:
|
|
|
85
113
|
url,
|
|
86
114
|
headers=None,
|
|
87
115
|
params=None,
|
|
88
|
-
|
|
116
|
+
json_payload=None,
|
|
89
117
|
data=None,
|
|
90
118
|
stream=False,
|
|
91
119
|
remove_headers_list=None,
|
|
92
120
|
raise_on_error=True,
|
|
93
121
|
tries=5,
|
|
94
122
|
):
|
|
95
|
-
|
|
96
123
|
"""
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
124
|
+
Make a request to the Conductor API.
|
|
125
|
+
|
|
126
|
+
Deprecated:
|
|
127
|
+
Primarily used to removed enforced headers by requests.Request. Requests 2.x will add
|
|
128
|
+
Transfer-Encoding: chunked with file like object that is 0 bytes, causing s3 failures (501)
|
|
129
|
+
- https://github.com/psf/requests/issues/4215#issuecomment-319521235
|
|
130
|
+
|
|
131
|
+
To get around this bug make_prepared_request has functionality to remove the enforced header
|
|
132
|
+
that would occur when using requests.request(...). Requests 3.x resolves this issue, when
|
|
133
|
+
client is built to use Requests 3.x this function can be removed.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
requests.Response: The response object.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
verb (str): The HTTP verb to use.
|
|
140
|
+
url (str): The URL to make the request to.
|
|
141
|
+
headers (dict): A dictionary of headers to send with the request.
|
|
142
|
+
params (dict): A dictionary of query parameters to send with the request.
|
|
143
|
+
json (dict): A JSON payload to send with the request.
|
|
144
|
+
stream (bool): Whether or not to stream the response.
|
|
145
|
+
remove_headers_list (list): A list of headers to remove from the request.
|
|
146
|
+
raise_on_error (bool): Whether or not to raise an exception if the request fails.
|
|
147
|
+
tries (int): The number of times to retry the request.
|
|
119
148
|
"""
|
|
120
149
|
|
|
121
150
|
req = requests.Request(
|
|
@@ -123,7 +152,7 @@ class ApiClient:
|
|
|
123
152
|
url=url,
|
|
124
153
|
headers=headers,
|
|
125
154
|
params=params,
|
|
126
|
-
json=
|
|
155
|
+
json=json_payload,
|
|
127
156
|
data=data,
|
|
128
157
|
)
|
|
129
158
|
prepped = req.prepare()
|
|
@@ -133,9 +162,11 @@ class ApiClient:
|
|
|
133
162
|
prepped.headers.pop(header, None)
|
|
134
163
|
|
|
135
164
|
# Create a retry wrapper function
|
|
136
|
-
retry_wrapper = common.DecRetry(
|
|
165
|
+
retry_wrapper = common.DecRetry(
|
|
166
|
+
retry_exceptions=CONNECTION_EXCEPTIONS, tries=tries
|
|
167
|
+
)
|
|
137
168
|
|
|
138
|
-
# requests sessions potentially not thread-safe, but need to
|
|
169
|
+
# requests sessions potentially not thread-safe, but need to removed enforced
|
|
139
170
|
# headers by using a prepared request.create which can only be done through an
|
|
140
171
|
# request.Session object. Create Session object per call of make_prepared_request, it will
|
|
141
172
|
# not benefit from connection pooling reuse. https://github.com/psf/requests/issues/1871
|
|
@@ -151,6 +182,7 @@ class ApiClient:
|
|
|
151
182
|
logger.debug("url: %s", prepped.url)
|
|
152
183
|
logger.debug("headers: %s", prepped.headers)
|
|
153
184
|
logger.debug("params: %s", req.params)
|
|
185
|
+
logger.debug("response: %s", response)
|
|
154
186
|
|
|
155
187
|
# trigger an exception to be raised for 4XX or 5XX http responses
|
|
156
188
|
if raise_on_error:
|
|
@@ -168,12 +200,26 @@ class ApiClient:
|
|
|
168
200
|
conductor_url=None,
|
|
169
201
|
raise_on_error=True,
|
|
170
202
|
tries=5,
|
|
171
|
-
use_api_key=False
|
|
203
|
+
use_api_key=False,
|
|
172
204
|
):
|
|
173
205
|
"""
|
|
174
|
-
|
|
206
|
+
Make a request to the Conductor API.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
uri_path (str): The path to the resource to request.
|
|
210
|
+
headers (dict): A dictionary of headers to send with the request.
|
|
211
|
+
params (dict): A dictionary of query parameters to send with the request.
|
|
212
|
+
data (dict): A dictionary of data to send with the request.
|
|
213
|
+
verb (str): The HTTP verb to use.
|
|
214
|
+
conductor_url (str): The Conductor URL.
|
|
215
|
+
raise_on_error (bool): Whether or not to raise an exception if the request fails.
|
|
216
|
+
tries (int): The number of times to retry the request.
|
|
217
|
+
use`_api_key (bool): Whether or not to use the API key for authentication.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
tuple(str, int): The response text and status code.
|
|
175
221
|
"""
|
|
176
|
-
cfg = config.
|
|
222
|
+
cfg = config.get()
|
|
177
223
|
# TODO: set Content Content-Type to json if data arg
|
|
178
224
|
if not headers:
|
|
179
225
|
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
|
@@ -184,10 +230,10 @@ class ApiClient:
|
|
|
184
230
|
raise Exception("Error: Could not get conductor credentials!")
|
|
185
231
|
|
|
186
232
|
headers["Authorization"] = "Bearer %s" % bearer_token
|
|
187
|
-
|
|
233
|
+
|
|
188
234
|
if not ApiClient.user_agent_header:
|
|
189
235
|
self.register_client("ciocore")
|
|
190
|
-
|
|
236
|
+
|
|
191
237
|
headers["User-Agent"] = ApiClient.user_agent_header
|
|
192
238
|
|
|
193
239
|
# Construct URL
|
|
@@ -203,7 +249,9 @@ class ApiClient:
|
|
|
203
249
|
assert verb in self.http_verbs, "Invalid http verb: %s" % verb
|
|
204
250
|
|
|
205
251
|
# Create a retry wrapper function
|
|
206
|
-
retry_wrapper = common.DecRetry(
|
|
252
|
+
retry_wrapper = common.DecRetry(
|
|
253
|
+
retry_exceptions=CONNECTION_EXCEPTIONS, tries=tries
|
|
254
|
+
)
|
|
207
255
|
|
|
208
256
|
# wrap the request function with the retry wrapper
|
|
209
257
|
wrapped_func = retry_wrapper(self._make_request)
|
|
@@ -214,102 +262,56 @@ class ApiClient:
|
|
|
214
262
|
)
|
|
215
263
|
|
|
216
264
|
return response.text, response.status_code
|
|
217
|
-
|
|
218
|
-
@classmethod
|
|
219
|
-
def _get_user_agent_header(cls, client_name, client_version=None):
|
|
220
|
-
'''
|
|
221
|
-
Generates the http User Agent header that includes helpful debug info.
|
|
222
|
-
|
|
223
|
-
The final component is the path to the python executable (MD5 hex).
|
|
224
|
-
|
|
225
|
-
ex: 'ciomaya/0.3.7 (ciocore 4.3.2; python 3.9.5; linux 3.10.0-1160.53.1.el7.x86_64; 0ee7123c2365d7a0d126de5a70f19727)'
|
|
226
|
-
|
|
227
|
-
:param client_name: The name of the client to be used in the header. If it's importable it
|
|
228
|
-
will be queried for its __version__ (unless client_version is supplied)
|
|
229
|
-
:type client_name: str
|
|
230
|
-
|
|
231
|
-
:param client_version: The version to use in the header if client_name can't be queried for
|
|
232
|
-
__version__ (or it needs to be overridden)
|
|
233
|
-
:type client_version: str [default = None]
|
|
234
|
-
|
|
235
|
-
:return: The value for the User Agent header
|
|
236
|
-
:rtype: str
|
|
237
|
-
'''
|
|
238
|
-
|
|
239
|
-
try:
|
|
240
|
-
client_module = importlib.import_module(client_name)
|
|
241
|
-
|
|
242
|
-
except ImportError:
|
|
243
|
-
logger.warning("Unable to import module '{}'. Won't query for version details.")
|
|
244
|
-
client_version = 'unknown'
|
|
245
|
-
|
|
246
|
-
if not client_version:
|
|
247
|
-
try:
|
|
248
|
-
client_version = client_module.__version__
|
|
249
|
-
|
|
250
|
-
except AttributeError:
|
|
251
|
-
logger.warning("Module '{}' has no __version__ attribute. Setting version to 'N/A' in the user agent")
|
|
252
|
-
client_version = 'unknown'
|
|
253
|
-
|
|
254
|
-
ciocore_version = ciocore.__version__
|
|
255
|
-
|
|
256
|
-
python_path = sys.executable
|
|
257
|
-
|
|
258
|
-
# If the length of the path is longer than allowed, truncate the middle
|
|
259
|
-
if len(python_path) > cls.USER_AGENT_MAX_PATH_LENGTH:
|
|
260
|
-
|
|
261
|
-
first_half = int(cls.USER_AGENT_MAX_PATH_LENGTH/2)
|
|
262
|
-
second_half = len(python_path) - int(cls.USER_AGENT_MAX_PATH_LENGTH/2)
|
|
263
|
-
|
|
264
|
-
print(first_half, second_half)
|
|
265
|
-
python_path = "{}...{}".format(python_path[0:first_half], python_path[second_half:-1])
|
|
266
|
-
|
|
267
|
-
python_path_encoded = base64.b64encode(python_path.encode('utf-8')).decode('utf-8')
|
|
268
|
-
|
|
269
|
-
if platform.system() == "Linux":
|
|
270
|
-
platform_details = platform.release()
|
|
271
|
-
|
|
272
|
-
elif platform.system() == "Windows":
|
|
273
|
-
platform_details = platform.version()
|
|
274
|
-
|
|
275
|
-
elif platform.system() == "Darwin":
|
|
276
|
-
platform_details = platform.mac_ver()[0]
|
|
277
|
-
|
|
278
|
-
else:
|
|
279
|
-
raise ValueError("Unrecognized platform '{}'".format(platform.release()))
|
|
280
|
-
|
|
281
|
-
pid = base64.b64encode(str(os.getpid()).encode('utf-8')).decode('utf-8')
|
|
282
|
-
hostname = base64.b64encode(socket.gethostname().encode('utf-8')).decode('utf-8')
|
|
283
|
-
|
|
284
|
-
return cls.USER_AGENT_TEMPLATE.format(client_name=client_name,
|
|
285
|
-
client_version=client_version,
|
|
286
|
-
ciocore_version=ciocore_version,
|
|
287
|
-
runtime='python',
|
|
288
|
-
runtime_version=platform.python_version(),
|
|
289
|
-
platform=sys.platform,
|
|
290
|
-
platform_details=platform_details,
|
|
291
|
-
pid=pid,
|
|
292
|
-
hostname=hostname,
|
|
293
|
-
python_path=python_path_encoded
|
|
294
|
-
)
|
|
295
|
-
|
|
265
|
+
|
|
296
266
|
@classmethod
|
|
297
267
|
def register_client(cls, client_name, client_version=None):
|
|
298
|
-
|
|
268
|
+
"""
|
|
269
|
+
Generates the http User Agent header that includes helpful debug info.
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
# Use the provided client_version.
|
|
273
|
+
if not client_version:
|
|
274
|
+
client_version = 'unknown'
|
|
275
|
+
|
|
276
|
+
python_version = platform.python_version()
|
|
277
|
+
system_info = platform.system()
|
|
278
|
+
release_info = platform.release()
|
|
299
279
|
|
|
280
|
+
|
|
281
|
+
# Get the MD5 hex digest of the path to the python executable
|
|
282
|
+
python_executable_path = truncate_middle(sys.executable.encode('utf-8'), cls.USER_AGENT_MAX_PATH_LENGTH)
|
|
283
|
+
md5_hash = hashlib.md5(python_executable_path).hexdigest()
|
|
284
|
+
|
|
285
|
+
user_agent = (
|
|
286
|
+
f"{client_name}/{client_version} "
|
|
287
|
+
f"(python {python_version}; {system_info} {release_info}; {md5_hash})"
|
|
288
|
+
)
|
|
289
|
+
cls.user_agent_header = user_agent
|
|
300
290
|
|
|
291
|
+
return user_agent
|
|
292
|
+
|
|
301
293
|
|
|
302
294
|
def read_conductor_credentials(use_api_key=False):
|
|
303
295
|
"""
|
|
304
|
-
Read the conductor credentials file
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
296
|
+
Read the conductor credentials file.
|
|
297
|
+
|
|
298
|
+
If the credentials file exists, it will contain a bearer token from either
|
|
299
|
+
the user or the API key.
|
|
300
|
+
|
|
301
|
+
If the credentials file doesn't exist, or is
|
|
302
|
+
expired, or is from a different domain, we try to fetch a new one in the API key scenario or
|
|
303
|
+
prompt the user to log in.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
use_api_key (bool): Whether or not to try to use the API key
|
|
308
307
|
|
|
309
|
-
Returns:
|
|
308
|
+
Returns:
|
|
309
|
+
A Bearer token in the event of a success or None
|
|
310
310
|
|
|
311
311
|
"""
|
|
312
|
-
|
|
312
|
+
|
|
313
|
+
cfg = config.get()
|
|
314
|
+
|
|
313
315
|
logger.debug("Reading conductor credentials...")
|
|
314
316
|
if use_api_key:
|
|
315
317
|
if not cfg.get("api_key"):
|
|
@@ -333,38 +335,40 @@ def read_conductor_credentials(use_api_key=False):
|
|
|
333
335
|
|
|
334
336
|
else:
|
|
335
337
|
auth.run(creds_file, cfg["url"])
|
|
336
|
-
|
|
337
338
|
if not os.path.exists(creds_file):
|
|
338
339
|
return None
|
|
339
340
|
|
|
340
341
|
logger.debug("Reading credentials file...")
|
|
341
|
-
with open(creds_file) as fp:
|
|
342
|
+
with open(creds_file, "r", encoding="utf-8") as fp:
|
|
342
343
|
file_contents = json.loads(fp.read())
|
|
343
|
-
|
|
344
344
|
expiration = file_contents.get("expiration")
|
|
345
|
-
|
|
346
345
|
same_domain = creds_same_domain(file_contents)
|
|
347
|
-
|
|
348
346
|
if same_domain and expiration and expiration >= int(time.time()):
|
|
349
347
|
return file_contents["access_token"]
|
|
350
|
-
|
|
351
348
|
logger.debug("Credentials have expired or are from a different domain")
|
|
352
|
-
|
|
353
349
|
if use_api_key:
|
|
354
350
|
logger.debug("Refreshing API key bearer token!")
|
|
355
351
|
get_api_key_bearer_token(creds_file)
|
|
356
352
|
else:
|
|
357
353
|
logger.debug("Sending to auth page...")
|
|
358
354
|
auth.run(creds_file, cfg["url"])
|
|
359
|
-
|
|
360
355
|
# Re-read the creds file, since it has been re-upped
|
|
361
|
-
with open(creds_file) as fp:
|
|
356
|
+
with open(creds_file, "r", encoding="utf-8") as fp:
|
|
362
357
|
file_contents = json.loads(fp.read())
|
|
363
358
|
return file_contents["access_token"]
|
|
364
359
|
|
|
365
360
|
|
|
366
361
|
def get_api_key_bearer_token(creds_file=None):
|
|
367
|
-
|
|
362
|
+
"""
|
|
363
|
+
Get a bearer token from the API key.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
creds_file (str): The path to the credentials file. If not provided, the bearer token will not be written to disk.
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
A dictionary containing the bearer token and other information.
|
|
370
|
+
"""
|
|
371
|
+
cfg = config.get()
|
|
368
372
|
url = "{}/api/oauth_jwt".format(cfg["url"])
|
|
369
373
|
response = requests.get(
|
|
370
374
|
url,
|
|
@@ -396,7 +400,15 @@ def get_api_key_bearer_token(creds_file=None):
|
|
|
396
400
|
|
|
397
401
|
|
|
398
402
|
def get_creds_path(api_key=False):
|
|
399
|
-
|
|
403
|
+
"""
|
|
404
|
+
Get the path to the credentials file.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
api_key (bool): Whether or not to use the API key.
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
str: The path to the credentials file.
|
|
411
|
+
"""
|
|
400
412
|
creds_dir = os.path.join(os.path.expanduser("~"), ".config", "conductor")
|
|
401
413
|
if api_key:
|
|
402
414
|
creds_file = os.path.join(creds_dir, "api_key_credentials")
|
|
@@ -409,19 +421,31 @@ def get_bearer_token(refresh=False):
|
|
|
409
421
|
"""
|
|
410
422
|
Return the bearer token.
|
|
411
423
|
|
|
424
|
+
Args:
|
|
425
|
+
refresh (bool): Whether or not to refresh the token.
|
|
426
|
+
|
|
412
427
|
TODO: Thread safe multiproc caching, like it used to be pre-python3.7.
|
|
413
428
|
"""
|
|
414
429
|
return read_conductor_credentials(True)
|
|
415
430
|
|
|
416
431
|
|
|
417
432
|
def creds_same_domain(creds):
|
|
418
|
-
|
|
433
|
+
"""
|
|
434
|
+
Check if the creds are for the same domain as the config.
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
creds (dict): The credentials dictionary.
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
bool: Whether or not the creds are for the same domain as the config.
|
|
441
|
+
"""
|
|
442
|
+
cfg = config.get()
|
|
419
443
|
"""Ensure the creds file refers to the domain in config"""
|
|
420
444
|
token = creds.get("access_token")
|
|
421
445
|
if not token:
|
|
422
446
|
return False
|
|
423
447
|
|
|
424
|
-
decoded = jwt.decode(creds["access_token"],
|
|
448
|
+
decoded = jwt.decode(creds["access_token"], algorithms=["HS256"], options={"verify_signature": False})
|
|
425
449
|
audience_domain = decoded.get("aud")
|
|
426
450
|
return (
|
|
427
451
|
audience_domain
|
|
@@ -432,17 +456,30 @@ def creds_same_domain(creds):
|
|
|
432
456
|
def account_id_from_jwt(token):
|
|
433
457
|
"""
|
|
434
458
|
Fetch the accounts id from a jwt token value.
|
|
459
|
+
|
|
460
|
+
Args:
|
|
461
|
+
token (str): The jwt token.
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
str: The account id.
|
|
435
465
|
"""
|
|
436
|
-
payload = jwt.decode(token,
|
|
466
|
+
payload = jwt.decode(token, algorithms=["HS256"], options={"verify_signature": False})
|
|
467
|
+
|
|
437
468
|
return payload.get("account")
|
|
438
469
|
|
|
439
470
|
|
|
440
471
|
def account_name_from_jwt(token):
|
|
441
472
|
"""
|
|
442
473
|
Fetch the accounts name from a jwt token value.
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
token (str): The jwt token.
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
str: The account name.
|
|
443
480
|
"""
|
|
444
|
-
cfg = config.config().config
|
|
445
481
|
account_id = account_id_from_jwt(token)
|
|
482
|
+
cfg = config.get()
|
|
446
483
|
if account_id:
|
|
447
484
|
url = "%s/api/v1/accounts/%s" % (cfg["api_url"], account_id)
|
|
448
485
|
response = requests.get(url, headers={"authorization": "Bearer %s" % token})
|
|
@@ -452,30 +489,47 @@ def account_name_from_jwt(token):
|
|
|
452
489
|
return None
|
|
453
490
|
|
|
454
491
|
|
|
455
|
-
def request_instance_types(as_dict=False):
|
|
492
|
+
def request_instance_types(as_dict=False, filter_param=""):
|
|
456
493
|
"""
|
|
457
494
|
Get the list of available instances types.
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
as_dict (bool): Whether or not to return the instance types as a dictionary.
|
|
498
|
+
filter_param (string): complex RHS string query ex:
|
|
499
|
+
"cpu=gte:8:int,operating_system=ne:windows,gpu.gpu_count=eq:1:int"
|
|
500
|
+
|
|
501
|
+
Returns:
|
|
502
|
+
list: The list of instance types.
|
|
458
503
|
"""
|
|
459
504
|
api = ApiClient()
|
|
460
505
|
response, response_code = api.make_request(
|
|
461
|
-
"api/v1/instance-types", use_api_key=True, raise_on_error=False
|
|
506
|
+
"api/v1/instance-types", use_api_key=True, raise_on_error=False,
|
|
507
|
+
params={"filter":filter_param}
|
|
462
508
|
)
|
|
463
509
|
if response_code not in (200,):
|
|
464
510
|
msg = "Failed to get instance types"
|
|
465
|
-
msg += "\
|
|
511
|
+
msg += "\nAPI responded with status code %s\n" % (response_code)
|
|
466
512
|
raise Exception(msg)
|
|
467
513
|
|
|
468
514
|
instance_types = json.loads(response).get("data", [])
|
|
469
515
|
logger.debug("Found available instance types: %s", instance_types)
|
|
470
516
|
|
|
471
517
|
if as_dict:
|
|
472
|
-
return dict(
|
|
518
|
+
return dict(
|
|
519
|
+
[(instance["description"], instance) for instance in instance_types]
|
|
520
|
+
)
|
|
473
521
|
return instance_types
|
|
474
522
|
|
|
475
523
|
|
|
476
524
|
def request_projects(statuses=("active",)):
|
|
477
525
|
"""
|
|
478
|
-
Query Conductor for all client Projects that are in the given
|
|
526
|
+
Query Conductor for all client Projects that are in the given status(es).
|
|
527
|
+
|
|
528
|
+
Args:
|
|
529
|
+
statuses (tuple): The statuses to filter for.
|
|
530
|
+
|
|
531
|
+
Returns:
|
|
532
|
+
list: The list of project names.
|
|
479
533
|
"""
|
|
480
534
|
api = ApiClient()
|
|
481
535
|
|
|
@@ -504,6 +558,9 @@ def request_projects(statuses=("active",)):
|
|
|
504
558
|
def request_software_packages():
|
|
505
559
|
"""
|
|
506
560
|
Query Conductor for all software packages for the currently available sidecar.
|
|
561
|
+
|
|
562
|
+
Returns:
|
|
563
|
+
list: The list of software packages.
|
|
507
564
|
"""
|
|
508
565
|
api = ApiClient()
|
|
509
566
|
|
|
@@ -516,4 +573,442 @@ def request_software_packages():
|
|
|
516
573
|
msg = "Failed to get software packages for latest sidecar"
|
|
517
574
|
msg += "\nError %s ...\n%s" % (response_code, response)
|
|
518
575
|
raise Exception(msg)
|
|
519
|
-
|
|
576
|
+
|
|
577
|
+
software = json.loads(response).get("data", [])
|
|
578
|
+
software = [sw for sw in software if not ("3dsmax" in sw["product"] and sw["platform"] == "linux")]
|
|
579
|
+
return software
|
|
580
|
+
|
|
581
|
+
def request_extra_environment():
|
|
582
|
+
"""
|
|
583
|
+
Query Conductor for extra environment.
|
|
584
|
+
"""
|
|
585
|
+
api = ApiClient()
|
|
586
|
+
|
|
587
|
+
uri = "api/v1/integrations/env-vars-configs"
|
|
588
|
+
response, response_code = api.make_request(
|
|
589
|
+
uri_path=uri, verb="GET", raise_on_error=False, use_api_key=True
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
if response_code not in [200]:
|
|
593
|
+
msg = "Failed to get extra environment"
|
|
594
|
+
msg += "\nError %s ...\n%s" % (response_code, response)
|
|
595
|
+
raise Exception(msg)
|
|
596
|
+
|
|
597
|
+
all_accounts = json.loads(response).get("data", [])
|
|
598
|
+
|
|
599
|
+
token = read_conductor_credentials(True)
|
|
600
|
+
if not token:
|
|
601
|
+
raise Exception("Error: Could not get conductor credentials!")
|
|
602
|
+
account_id = str(account_id_from_jwt(token))
|
|
603
|
+
|
|
604
|
+
if not account_id:
|
|
605
|
+
raise Exception("Error: Could not get account id from jwt!")
|
|
606
|
+
account_env = next((account for account in all_accounts if account["account_id"] == account_id), None)
|
|
607
|
+
if not account_env:
|
|
608
|
+
raise Exception("Error: Could not get account environment!")
|
|
609
|
+
return account_env.get("env", [])
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def get_jobs(first_jid, last_jid=None):
|
|
615
|
+
"""
|
|
616
|
+
Query Conductor for all jobs between the given job ids.
|
|
617
|
+
|
|
618
|
+
Returns:
|
|
619
|
+
list: The list of jobs.
|
|
620
|
+
|
|
621
|
+
Raises:
|
|
622
|
+
Exception: If the request fails.
|
|
623
|
+
|
|
624
|
+
Examples:
|
|
625
|
+
>>> from ciocore import api_client
|
|
626
|
+
>>> jobs = api_client.get_jobs(1959)
|
|
627
|
+
>>> len(jobs)
|
|
628
|
+
1
|
|
629
|
+
>>> jobs[0]["jid"]
|
|
630
|
+
'01959'
|
|
631
|
+
>>> jobs = api_client.get_jobs(1959, 1961)
|
|
632
|
+
>>> len(jobs)
|
|
633
|
+
3
|
|
634
|
+
"""
|
|
635
|
+
if last_jid is None:
|
|
636
|
+
last_jid = first_jid
|
|
637
|
+
low = str(int(first_jid) - 1).zfill(5)
|
|
638
|
+
high = str(int(last_jid) + 1).zfill(5)
|
|
639
|
+
api = ApiClient()
|
|
640
|
+
uri = "api/v1/jobs"
|
|
641
|
+
|
|
642
|
+
response, response_code = api.make_request(
|
|
643
|
+
uri_path=uri,
|
|
644
|
+
verb="GET",
|
|
645
|
+
raise_on_error=False,
|
|
646
|
+
use_api_key=True,
|
|
647
|
+
params={"filter": f"jid_gt_{low},jid_lt_{high}"},
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
if response_code not in [200]:
|
|
651
|
+
msg = f"Failed to get jobs {first_jid}-{last_jid}"
|
|
652
|
+
msg += "\nError %s ...\n%s" % (response_code, response)
|
|
653
|
+
raise Exception(msg)
|
|
654
|
+
jobs = json.loads(response).get("data")
|
|
655
|
+
return jobs
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def get_log(job_id, task_id):
|
|
659
|
+
"""
|
|
660
|
+
Get the log for the given job and task.
|
|
661
|
+
|
|
662
|
+
Args:
|
|
663
|
+
job_id (str): The job id.
|
|
664
|
+
task_id (str): The task id.
|
|
665
|
+
|
|
666
|
+
Returns:
|
|
667
|
+
list: A list of logs.
|
|
668
|
+
|
|
669
|
+
Raises:
|
|
670
|
+
Exception: If the request fails.
|
|
671
|
+
|
|
672
|
+
Examples:
|
|
673
|
+
>>> from ciocore import api_client
|
|
674
|
+
>>> logs = api_client.get_log(1959, 0)
|
|
675
|
+
{
|
|
676
|
+
"logs": [
|
|
677
|
+
{
|
|
678
|
+
"container_id": "j-5669544198668288-5619559933149184-5095331660038144-stde",
|
|
679
|
+
"instance_name": "renderer-5669544198668288-170062309438-62994",
|
|
680
|
+
"log": [
|
|
681
|
+
"Blender 2.93.0 (hash 84da05a8b806 built 2021-06-02 11:29:24)",
|
|
682
|
+
...
|
|
683
|
+
...
|
|
684
|
+
"Saved: '/var/folders/8r/46lmjdmj50x_0swd9klwptzm0000gq/T/blender_bmw/renders/render_0001.png'",
|
|
685
|
+
" Time: 00:29.22 (Saving: 00:00.32)",
|
|
686
|
+
"",
|
|
687
|
+
"",
|
|
688
|
+
"Blender quit"
|
|
689
|
+
],
|
|
690
|
+
"timestamp": "1.700623521101516E9"
|
|
691
|
+
}
|
|
692
|
+
],
|
|
693
|
+
"new_num_lines": [
|
|
694
|
+
144
|
|
695
|
+
],
|
|
696
|
+
"status_description": "",
|
|
697
|
+
"task_status": "success"
|
|
698
|
+
}
|
|
699
|
+
"""
|
|
700
|
+
job_id = str(job_id).zfill(5)
|
|
701
|
+
task_id = str(task_id).zfill(3)
|
|
702
|
+
|
|
703
|
+
api = ApiClient()
|
|
704
|
+
uri = f"get_log_file?job={job_id}&task={task_id}&num_lines[]=0"
|
|
705
|
+
|
|
706
|
+
response, response_code = api.make_request(
|
|
707
|
+
uri_path=uri, verb="GET", raise_on_error=False, use_api_key=True
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
if response_code not in [200]:
|
|
711
|
+
msg = f"Failed to get log for job {job_id} task {task_id}"
|
|
712
|
+
msg += "\nError %s ...\n%s" % (response_code, response)
|
|
713
|
+
raise Exception(msg)
|
|
714
|
+
|
|
715
|
+
return response
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
def kill_jobs(*job_ids):
|
|
719
|
+
"""
|
|
720
|
+
Kill the given jobs.
|
|
721
|
+
|
|
722
|
+
Args:
|
|
723
|
+
job_ids (list): The list of job ids.
|
|
724
|
+
|
|
725
|
+
Returns:
|
|
726
|
+
dict: The response.
|
|
727
|
+
|
|
728
|
+
Examples:
|
|
729
|
+
>>> from ciocore import api_client
|
|
730
|
+
>>> api_client.kill_jobs("03095","03094")
|
|
731
|
+
{'body': 'success', 'message': "Jobs [u'03095', u'03094'] have been kill."}
|
|
732
|
+
|
|
733
|
+
"""
|
|
734
|
+
job_ids = [str(job_id).zfill(5) for job_id in job_ids]
|
|
735
|
+
api = ApiClient()
|
|
736
|
+
payload = {
|
|
737
|
+
"action": "kill",
|
|
738
|
+
"jobids": job_ids,
|
|
739
|
+
}
|
|
740
|
+
response, response_code = api.make_request(
|
|
741
|
+
uri_path="jobs_multi",
|
|
742
|
+
verb="PUT",
|
|
743
|
+
raise_on_error=False,
|
|
744
|
+
use_api_key=True,
|
|
745
|
+
data=json.dumps(payload)
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
if response_code not in [200]:
|
|
749
|
+
msg = f"Failed to kill jobs {job_ids}"
|
|
750
|
+
msg += "\nError %s ...\n%s" % (response_code, response)
|
|
751
|
+
raise Exception(msg)
|
|
752
|
+
|
|
753
|
+
return json.loads(response)
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def kill_tasks(job_id, *task_ids):
|
|
757
|
+
"""
|
|
758
|
+
Kill the given tasks.
|
|
759
|
+
|
|
760
|
+
Args:
|
|
761
|
+
job_id (str): The job id.
|
|
762
|
+
task_ids (list): The list of task ids.
|
|
763
|
+
|
|
764
|
+
Returns:
|
|
765
|
+
dict: The response.
|
|
766
|
+
|
|
767
|
+
Examples:
|
|
768
|
+
>>> from ciocore import api_client
|
|
769
|
+
>>> api_client.kill_tasks("03096", *range(50,56))
|
|
770
|
+
{'body': 'success', 'message': ' 6 Tasks set to "kill"\n\t050\n\t051\n\t052\n\t053\n\t054\n\t055'}
|
|
771
|
+
"""
|
|
772
|
+
|
|
773
|
+
job_id = str(job_id).zfill(5)
|
|
774
|
+
task_ids = [str(task_id).zfill(3) for task_id in task_ids]
|
|
775
|
+
api = ApiClient()
|
|
776
|
+
payload = {
|
|
777
|
+
"action": "kill",
|
|
778
|
+
"jobid": job_id,
|
|
779
|
+
"taskids": task_ids,
|
|
780
|
+
}
|
|
781
|
+
response, response_code = api.make_request(
|
|
782
|
+
uri_path="tasks_multi",
|
|
783
|
+
verb="PUT",
|
|
784
|
+
raise_on_error=False,
|
|
785
|
+
use_api_key=True,
|
|
786
|
+
data=json.dumps(payload)
|
|
787
|
+
)
|
|
788
|
+
|
|
789
|
+
if response_code not in [200]:
|
|
790
|
+
msg = f"Failed to kill tasks {task_ids} of job {job_id}"
|
|
791
|
+
msg += "\nError %s ...\n%s" % (response_code, response)
|
|
792
|
+
raise Exception(msg)
|
|
793
|
+
|
|
794
|
+
return json.loads(response)
|
|
795
|
+
|
|
796
|
+
def _get_compute_usage(start_time, end_time):
|
|
797
|
+
"""
|
|
798
|
+
Query the account usage to get the raw compute data. Private method.
|
|
799
|
+
|
|
800
|
+
Compute includes licenses, instances and Conductor cost. Everything involved
|
|
801
|
+
with running a job.
|
|
802
|
+
|
|
803
|
+
Please use the public method api_client.get_compute_usage() instead.
|
|
804
|
+
|
|
805
|
+
Args:
|
|
806
|
+
start_time (datetime.datetime): The first day to include in the report. Only the date is considered and it's assumed to be in UTC.
|
|
807
|
+
end_time (datetime.datetime): The last day to include in the report. Only the date is considered and it's assumed to be in UTC.
|
|
808
|
+
|
|
809
|
+
Returns:
|
|
810
|
+
list: A list of billing entries
|
|
811
|
+
|
|
812
|
+
Examples:
|
|
813
|
+
>>> from ciocore import api_client
|
|
814
|
+
>>> api_client._get_compute_usage(start_time, end_time)
|
|
815
|
+
[
|
|
816
|
+
{
|
|
817
|
+
"cores": 0.5,
|
|
818
|
+
"instance_cost": 0.019999999552965164,
|
|
819
|
+
"license_cost": 0.019999999552965164,
|
|
820
|
+
"minutes": 6.9700000286102295,
|
|
821
|
+
"self_link": 0,
|
|
822
|
+
"start_time": "Tue, 09 Jan 2024 18:00:00 GMT"
|
|
823
|
+
},
|
|
824
|
+
{
|
|
825
|
+
"cores": 0.4,
|
|
826
|
+
"instance_cost": 0.019999999552965164,
|
|
827
|
+
"license_cost": 0.019999999552965164,
|
|
828
|
+
"minutes": 6.960000038146973,
|
|
829
|
+
"self_link": 1,
|
|
830
|
+
"start_time": "Tue, 09 Jan 2024 19:00:00 GMT"
|
|
831
|
+
}]
|
|
832
|
+
"""
|
|
833
|
+
|
|
834
|
+
api = ApiClient()
|
|
835
|
+
|
|
836
|
+
payload = {
|
|
837
|
+
"filter": json.dumps(
|
|
838
|
+
[{"field": "start_time", "op": ">=", "value": start_time.date().isoformat()},
|
|
839
|
+
{"field": "start_time", "op": "<", "value": end_time.date().isoformat()}]),
|
|
840
|
+
"group_by": json.dumps(["start_time"]),
|
|
841
|
+
"order_by": "start_time"
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
response, response_code = api.make_request(
|
|
845
|
+
uri_path="billing/get_usage",
|
|
846
|
+
verb="GET",
|
|
847
|
+
raise_on_error=False,
|
|
848
|
+
use_api_key=True,
|
|
849
|
+
params=payload
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
if response_code not in [200]:
|
|
853
|
+
msg = f"Failed to query compute usage"
|
|
854
|
+
msg += "\nError %s ...\n%s" % (response_code, response)
|
|
855
|
+
raise Exception(msg)
|
|
856
|
+
|
|
857
|
+
return json.loads(response)['data']
|
|
858
|
+
|
|
859
|
+
def _get_storage_usage(start_time, end_time):
|
|
860
|
+
"""
|
|
861
|
+
Query the account usage to get the raw storage data. Private method.
|
|
862
|
+
|
|
863
|
+
Please use the public method api_client.get_storage_usage() instead.
|
|
864
|
+
|
|
865
|
+
Args:
|
|
866
|
+
start_time (datetime.datetime): The first day to include in the report. Only the date is considered and it's assumed to be in UTC.
|
|
867
|
+
end_time (datetime.datetime): The last day to include in the report. Only the date is considered and it's assumed to be in UTC.
|
|
868
|
+
|
|
869
|
+
Returns:
|
|
870
|
+
dict: A dict of billing details related to storage
|
|
871
|
+
|
|
872
|
+
Examples:
|
|
873
|
+
>>> from ciocore import api_client
|
|
874
|
+
>>> api_client._get_storage_usage(start_time, end_time)
|
|
875
|
+
{
|
|
876
|
+
"cost": "28.96",
|
|
877
|
+
"cost_per_day": [
|
|
878
|
+
4.022,
|
|
879
|
+
4.502,
|
|
880
|
+
4.502,
|
|
881
|
+
5.102,
|
|
882
|
+
5.102,
|
|
883
|
+
5.732
|
|
884
|
+
],
|
|
885
|
+
"currency": "USD",
|
|
886
|
+
"daily_price": "0.006",
|
|
887
|
+
"end_date": "2024-01-07",
|
|
888
|
+
"gibs_per_day": [
|
|
889
|
+
679.714,
|
|
890
|
+
750.34,
|
|
891
|
+
750.34,
|
|
892
|
+
850.36,
|
|
893
|
+
850.35,
|
|
894
|
+
955.32
|
|
895
|
+
],
|
|
896
|
+
"gibs_used": "806.07",
|
|
897
|
+
"monthly_price": "0.18",
|
|
898
|
+
"start_date": "2024-01-01",
|
|
899
|
+
"storage_unit": "GiB"
|
|
900
|
+
}
|
|
901
|
+
]
|
|
902
|
+
}
|
|
903
|
+
"""
|
|
904
|
+
|
|
905
|
+
api = ApiClient()
|
|
906
|
+
|
|
907
|
+
# Add one day to the end time as the query is exclusive of the last day but
|
|
908
|
+
# we want consistency with _get_compute_usage()
|
|
909
|
+
payload = {
|
|
910
|
+
"start": start_time.date().isoformat(),
|
|
911
|
+
"end": (end_time.date() + datetime.timedelta(days=1)).isoformat()
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
response, response_code = api.make_request(
|
|
915
|
+
uri_path="billing/get_storage_usage",
|
|
916
|
+
verb="GET",
|
|
917
|
+
raise_on_error=False,
|
|
918
|
+
use_api_key=True,
|
|
919
|
+
params=payload
|
|
920
|
+
)
|
|
921
|
+
|
|
922
|
+
if response_code not in [200]:
|
|
923
|
+
msg = f"Failed to query storage usage"
|
|
924
|
+
msg += "\nError %s ...\n%s" % (response_code, response)
|
|
925
|
+
raise Exception(msg)
|
|
926
|
+
|
|
927
|
+
return json.loads(response)['data'][0]
|
|
928
|
+
|
|
929
|
+
def get_compute_usage(start_time, end_time):
|
|
930
|
+
'''
|
|
931
|
+
Query the compute usage for an account.
|
|
932
|
+
|
|
933
|
+
Compute includes licenses, instances and Conductor cost. Everything involved
|
|
934
|
+
with running a job.
|
|
935
|
+
|
|
936
|
+
Args:
|
|
937
|
+
start_time (datetime.datetime): The first day to include in the report. Only the date is considered and it's assumed to be in UTC.
|
|
938
|
+
end_time (datetime.datetime): The last day to include in the report. Only the date is considered and it's assumed to be in UTC.
|
|
939
|
+
|
|
940
|
+
Returns:
|
|
941
|
+
dict: Each key is a date (UTC). The value is a dict with values for:
|
|
942
|
+
- cost: The total accumulated compute cost for the day
|
|
943
|
+
- corehours: The total accumulated core hours for the day
|
|
944
|
+
- walltime: The number of minutes that instances (regardless of type) were running
|
|
945
|
+
|
|
946
|
+
Examples:
|
|
947
|
+
>>> from ciocore import api_client
|
|
948
|
+
>>> api_client.get_compute_usage(start_time, end_time)
|
|
949
|
+
{ '2024-01-09': { 'cost': 0.08,
|
|
950
|
+
'corehours': 0.9,
|
|
951
|
+
'walltime': 13.93},
|
|
952
|
+
'2024-01-16': { 'cost': 0.12,
|
|
953
|
+
'corehours': 0.9613,
|
|
954
|
+
'walltime': 7.21}}
|
|
955
|
+
'''
|
|
956
|
+
date_format = "%a, %d %b %Y %H:%M:%S %Z"
|
|
957
|
+
data = _get_compute_usage(start_time, end_time)
|
|
958
|
+
|
|
959
|
+
# Create a nested default dictionary with initial float values of 0.0
|
|
960
|
+
results = collections.defaultdict(lambda: collections.defaultdict(float))
|
|
961
|
+
|
|
962
|
+
for entry in data:
|
|
963
|
+
entry_start_date = datetime.datetime.strptime(entry['start_time'], date_format).date().isoformat()
|
|
964
|
+
|
|
965
|
+
results[entry_start_date]['walltime'] += entry['minutes']
|
|
966
|
+
results[entry_start_date]['corehours'] += entry['cores']
|
|
967
|
+
results[entry_start_date]['cost'] += entry['license_cost'] + entry['instance_cost']
|
|
968
|
+
|
|
969
|
+
# Round the data to avoid FP errors
|
|
970
|
+
results[entry_start_date]['walltime'] = round(results[entry_start_date]['walltime'], 4)
|
|
971
|
+
results[entry_start_date]['corehours'] = round(results[entry_start_date]['corehours'], 4)
|
|
972
|
+
results[entry_start_date]['cost'] = round(results[entry_start_date]['cost'], 4)
|
|
973
|
+
|
|
974
|
+
return results
|
|
975
|
+
|
|
976
|
+
def get_storage_usage(start_time, end_time):
|
|
977
|
+
'''
|
|
978
|
+
Query the storage usage for an account.
|
|
979
|
+
|
|
980
|
+
Storage is calculated twice a day (UTC) and the average is used.
|
|
981
|
+
|
|
982
|
+
Args:
|
|
983
|
+
start_time (datetime.datetime): The first day to include in the report. Only the date is considered and it's assumed to be in UTC.
|
|
984
|
+
end_time (datetime.datetime): The last day to include in the report. Only the date is considered and it's assumed to be in UTC.
|
|
985
|
+
|
|
986
|
+
Returns:
|
|
987
|
+
dict: Each key is a date (UTC). The value is a dict with values for:
|
|
988
|
+
- cost: The cost of accumulated storage for that one day
|
|
989
|
+
- GiB: The total amount of storage used on that day
|
|
990
|
+
|
|
991
|
+
Examples:
|
|
992
|
+
>>> from ciocore import api_client
|
|
993
|
+
>>> api_client.get_storage_usage(start_time, end_time)
|
|
994
|
+
{ '2024-01-01': {'cost': 4.022, 'GiB': 679.714},
|
|
995
|
+
'2024-01-02': {'cost': 4.502, 'GiB': 750.34},
|
|
996
|
+
'2024-01-03': {'cost': 4.502, 'GiB': 750.34}}
|
|
997
|
+
'''
|
|
998
|
+
one_day = datetime.timedelta(days=1)
|
|
999
|
+
|
|
1000
|
+
data = _get_storage_usage(start_time, end_time)
|
|
1001
|
+
|
|
1002
|
+
results = {}
|
|
1003
|
+
|
|
1004
|
+
entry_date = datetime.date.fromisoformat(data['start_date'])
|
|
1005
|
+
|
|
1006
|
+
for cnt, entry in enumerate(data["cost_per_day"]):
|
|
1007
|
+
|
|
1008
|
+
entry_start_date = entry_date.isoformat()
|
|
1009
|
+
results[entry_start_date] = {'cost': float(entry), 'GiB': float(data['gibs_per_day'][cnt])}
|
|
1010
|
+
entry_date += one_day
|
|
1011
|
+
|
|
1012
|
+
return results
|
|
1013
|
+
|
|
1014
|
+
|