qarnot 2.15.0__tar.gz → 2.17.0__tar.gz
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.
- {qarnot-2.15.0/qarnot.egg-info → qarnot-2.17.0}/PKG-INFO +12 -3
- qarnot-2.17.0/doc/source/api/compute/carbon_facts.rst +8 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/doc/source/api/compute/computeindex.rst +3 -0
- qarnot-2.17.0/doc/source/api/compute/forced_network_rule.rst +8 -0
- qarnot-2.17.0/doc/source/api/compute/secrets.rst +8 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/doc/source/conf.py +4 -4
- {qarnot-2.15.0 → qarnot-2.17.0}/qarnot/__init__.py +11 -1
- {qarnot-2.15.0 → qarnot-2.17.0}/qarnot/_version.py +3 -3
- {qarnot-2.15.0 → qarnot-2.17.0}/qarnot/advanced_bucket.py +1 -1
- {qarnot-2.15.0 → qarnot-2.17.0}/qarnot/bucket.py +13 -13
- qarnot-2.17.0/qarnot/carbon_facts.py +208 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/qarnot/connection.py +29 -29
- {qarnot-2.15.0 → qarnot-2.17.0}/qarnot/error.py +3 -3
- qarnot-2.17.0/qarnot/forced_constant.py +44 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/qarnot/forced_network_rule.py +12 -12
- {qarnot-2.15.0 → qarnot-2.17.0}/qarnot/hardware_constraint.py +9 -9
- {qarnot-2.15.0 → qarnot-2.17.0}/qarnot/job.py +18 -18
- {qarnot-2.15.0 → qarnot-2.17.0}/qarnot/pool.py +87 -35
- {qarnot-2.15.0 → qarnot-2.17.0}/qarnot/privileges.py +1 -1
- {qarnot-2.15.0 → qarnot-2.17.0}/qarnot/retry_settings.py +2 -2
- {qarnot-2.15.0 → qarnot-2.17.0}/qarnot/scheduling_type.py +3 -3
- {qarnot-2.15.0 → qarnot-2.17.0}/qarnot/secrets.py +5 -5
- {qarnot-2.15.0 → qarnot-2.17.0}/qarnot/status.py +66 -66
- {qarnot-2.15.0 → qarnot-2.17.0}/qarnot/task.py +74 -91
- {qarnot-2.15.0 → qarnot-2.17.0/qarnot.egg-info}/PKG-INFO +12 -3
- {qarnot-2.15.0 → qarnot-2.17.0}/qarnot.egg-info/SOURCES.txt +6 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/qarnot.egg-info/requires.txt +1 -1
- {qarnot-2.15.0 → qarnot-2.17.0}/requirements-lint.txt +2 -2
- {qarnot-2.15.0 → qarnot-2.17.0}/requirements-optional.txt +1 -1
- qarnot-2.17.0/requirements.txt +6 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/setup.py +1 -1
- qarnot-2.17.0/test/test_carbon_facts.py +80 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/test/test_connection.py +30 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/test/test_task.py +34 -0
- qarnot-2.15.0/requirements.txt +0 -6
- {qarnot-2.15.0 → qarnot-2.17.0}/LICENSE +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/MANIFEST.in +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/README.rst +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/doc/Makefile +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/doc/make.bat +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/doc/source/_static/qarnot.png +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/doc/source/api/compute/hardware_constraint.rst +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/doc/source/api/compute/job.rst +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/doc/source/api/compute/paginate.rst +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/doc/source/api/compute/pool.rst +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/doc/source/api/compute/privileges.rst +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/doc/source/api/compute/retry_settings.rst +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/doc/source/api/compute/scheduling_type.rst +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/doc/source/api/compute/status.rst +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/doc/source/api/compute/task.rst +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/doc/source/api/connection.rst +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/doc/source/api/exceptions.rst +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/doc/source/api/storage/advanced_bucket.rst +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/doc/source/api/storage/bucket.rst +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/doc/source/api/storage/storage.rst +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/doc/source/api/storage/storageindex.rst +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/doc/source/basic.rst +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/doc/source/index.rst +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/doc/source/installation.rst +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/doc/source/qarnot.rst +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/pyproject.toml +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/qarnot/_filter.py +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/qarnot/_retry.py +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/qarnot/_util.py +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/qarnot/exceptions.py +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/qarnot/helper.py +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/qarnot/paginate.py +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/qarnot/storage.py +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/qarnot.egg-info/dependency_links.txt +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/qarnot.egg-info/top_level.txt +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/requirements-doc.txt +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/requirements-test.txt +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/setup.cfg +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/test/test_advanced_bucket.py +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/test/test_bucket.py +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/test/test_hardware_constraints.py +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/test/test_import.py +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/test/test_job.py +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/test/test_paginate.py +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/test/test_pool.py +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/test/test_retry.py +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/test/test_secrets.py +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/test/test_status.py +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/test/test_util.py +0 -0
- {qarnot-2.15.0 → qarnot-2.17.0}/versioneer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: qarnot
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.17.0
|
|
4
4
|
Summary: Qarnot Computing SDK
|
|
5
5
|
Home-page: https://computing.qarnot.com
|
|
6
6
|
Author: Qarnot computing
|
|
@@ -17,10 +17,19 @@ Classifier: License :: OSI Approved :: Apache Software License
|
|
|
17
17
|
Requires-Python: >=3.6
|
|
18
18
|
License-File: LICENSE
|
|
19
19
|
Requires-Dist: requests
|
|
20
|
-
Requires-Dist: boto3
|
|
20
|
+
Requires-Dist: boto3>=1.36
|
|
21
21
|
Requires-Dist: wheel
|
|
22
22
|
Requires-Dist: deprecation
|
|
23
23
|
Requires-Dist: simplejson
|
|
24
|
+
Dynamic: author
|
|
25
|
+
Dynamic: author-email
|
|
26
|
+
Dynamic: classifier
|
|
27
|
+
Dynamic: description
|
|
28
|
+
Dynamic: home-page
|
|
29
|
+
Dynamic: license
|
|
30
|
+
Dynamic: requires-dist
|
|
31
|
+
Dynamic: requires-python
|
|
32
|
+
Dynamic: summary
|
|
24
33
|
|
|
25
34
|
Qarnot computing Python SDK
|
|
26
35
|
===========================
|
|
@@ -78,7 +78,7 @@ release = v
|
|
|
78
78
|
#
|
|
79
79
|
# This is also used if you do content translation via gettext catalogs.
|
|
80
80
|
# Usually you set "language" from the command line for these cases.
|
|
81
|
-
language =
|
|
81
|
+
language = 'en'
|
|
82
82
|
|
|
83
83
|
# List of patterns, relative to source directory, that match files and
|
|
84
84
|
# directories to ignore when looking for source files.
|
|
@@ -106,7 +106,7 @@ html_theme = 'sphinx_rtd_theme'
|
|
|
106
106
|
#html_theme_options = {}
|
|
107
107
|
|
|
108
108
|
# Add any paths that contain custom themes here, relative to this directory.
|
|
109
|
-
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
|
109
|
+
#html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # WARNING: Calling get_html_theme_path is deprecated. If you are calling it to define html_theme_path, you are safe to remove that code.
|
|
110
110
|
|
|
111
111
|
# The name for this set of Sphinx documents. If None, it defaults to
|
|
112
112
|
# "<project> v<release> documentation".
|
|
@@ -187,6 +187,6 @@ texinfo_documents = [
|
|
|
187
187
|
]
|
|
188
188
|
|
|
189
189
|
# Example configuration for intersphinx: refer to the Python standard library.
|
|
190
|
-
intersphinx_mapping = {'https://docs.python.org/3'
|
|
191
|
-
'http://boto3.readthedocs.io/en/latest/'
|
|
190
|
+
intersphinx_mapping = {'python': ('https://docs.python.org/3', None),
|
|
191
|
+
'boto': ('http://boto3.readthedocs.io/en/latest/', None)}
|
|
192
192
|
autodoc_member_order = 'bysource'
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
# limitations under the License.
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
from typing import Dict
|
|
19
20
|
from .exceptions import QarnotGenericException, SecretConflictException, SecretNotFoundException, UnauthorizedException
|
|
20
21
|
from ._util import get_error_message_from_http_response
|
|
21
22
|
|
|
@@ -65,7 +66,8 @@ def get_url(key, **kwargs):
|
|
|
65
66
|
'task stderr': '/tasks/{uuid}/stderr', # GET -> task stderr
|
|
66
67
|
'task instance stdout': '/tasks/{uuid}/stdout/{instanceId}', # GET -> task instance stdout
|
|
67
68
|
'task instance stderr': '/tasks/{uuid}/stderr/{instanceId}', # GET -> task instance stderr
|
|
68
|
-
'task abort': '/tasks/{uuid}/abort', #
|
|
69
|
+
'task abort': '/tasks/{uuid}/abort', # POST -> abort task
|
|
70
|
+
'task carbon facts': '/carbon/v1/tasks/{uuid}/carbon-facts', # GET -> task carbon facts
|
|
69
71
|
'pools': '/pools', # POST -> submit pool
|
|
70
72
|
'paginate pools': '/pools/paginate', # GET -> paginate pools
|
|
71
73
|
'paginate pools summaries': '/pools/summaries/paginate', # GET -> paginate pools summaries
|
|
@@ -76,6 +78,7 @@ def get_url(key, **kwargs):
|
|
|
76
78
|
'pool stderr': '/pools/{uuid}/stderr', # GET -> pool stderr
|
|
77
79
|
'pool instance stdout': '/pools/{uuid}/stdout/{instanceId}', # GET -> pool instance stdout
|
|
78
80
|
'pool instance stderr': '/pools/{uuid}/stderr/{instanceId}', # GET -> pool instance stderr
|
|
81
|
+
'pool carbon facts': '/carbon/v1/pools/{uuid}/carbon-facts', # GET -> pool carbon facts
|
|
79
82
|
'secrets data': '/secrets-manager/data/{secret_key}', # GET -> get secret , PUT -> create secret, PATCH -> update secret, DELETE -> delete secret
|
|
80
83
|
'secrets search': '/secrets-manager/search/{secret_prefix}', # GET -> lists secrets starting with prefix
|
|
81
84
|
'user': '/info', # GET -> user info
|
|
@@ -88,6 +91,13 @@ def get_url(key, **kwargs):
|
|
|
88
91
|
return urls[key].format(**kwargs)
|
|
89
92
|
|
|
90
93
|
|
|
94
|
+
def get_url_with_param(key, params: Dict[str, str], **kwargs):
|
|
95
|
+
if (params is None or len(params) == 0):
|
|
96
|
+
return get_url(key, **kwargs)
|
|
97
|
+
param_string = "&".join(f'{key}={value}' for key, value in params.items())
|
|
98
|
+
return "{url}?{param}".format(url=get_url(key, **kwargs), param=param_string)
|
|
99
|
+
|
|
100
|
+
|
|
91
101
|
from ._version import get_versions # noqa
|
|
92
102
|
__version__ = get_versions()['version'] # type: ignore
|
|
93
103
|
del get_versions
|
|
@@ -8,11 +8,11 @@ import json
|
|
|
8
8
|
|
|
9
9
|
version_json = '''
|
|
10
10
|
{
|
|
11
|
-
"date": "
|
|
11
|
+
"date": "2025-03-05T10:05:53+0100",
|
|
12
12
|
"dirty": false,
|
|
13
13
|
"error": null,
|
|
14
|
-
"full-revisionid": "
|
|
15
|
-
"version": "v2.
|
|
14
|
+
"full-revisionid": "559a43baceed056873c5b9dab07d987025306be8",
|
|
15
|
+
"version": "v2.17.0"
|
|
16
16
|
}
|
|
17
17
|
''' # END VERSION_JSON
|
|
18
18
|
|
|
@@ -180,7 +180,7 @@ class PrefixResourcesTransformation(AbstractResourcesTransformation):
|
|
|
180
180
|
:return: The PrefixResourcesTransformation new object
|
|
181
181
|
:rtype: :class:`PrefixResourcesTransformation`
|
|
182
182
|
"""
|
|
183
|
-
return PrefixResourcesTransformation(json
|
|
183
|
+
return PrefixResourcesTransformation(json.get("prefix"))
|
|
184
184
|
|
|
185
185
|
def to_json(self) -> object:
|
|
186
186
|
"""Get a dict ready to be json packed.
|
|
@@ -107,14 +107,14 @@ class Bucket(Storage): # pylint: disable=W0223
|
|
|
107
107
|
:returns: The created :class:`~qarnot.bucket.Bucket`.
|
|
108
108
|
"""
|
|
109
109
|
filtering = None
|
|
110
|
-
if "filtering" in json_bucket and json_bucket
|
|
111
|
-
filtering = Filtering.from_json(json_bucket
|
|
110
|
+
if "filtering" in json_bucket and json_bucket.get('filtering'):
|
|
111
|
+
filtering = Filtering.from_json(json_bucket.get('filtering'))
|
|
112
112
|
|
|
113
113
|
resource_transformation = None
|
|
114
|
-
if "resourcesTransformation" in json_bucket and json_bucket
|
|
115
|
-
resource_transformation = ResourcesTransformation.from_json(json_bucket
|
|
114
|
+
if "resourcesTransformation" in json_bucket and json_bucket.get('resourcesTransformation'):
|
|
115
|
+
resource_transformation = ResourcesTransformation.from_json(json_bucket.get('resourcesTransformation'))
|
|
116
116
|
|
|
117
|
-
bucket = Bucket(connection, json_bucket
|
|
117
|
+
bucket = Bucket(connection, json_bucket.get('bucketName'), create=False, filtering=filtering, resources_transformation=resource_transformation, cacheTTLSec=json_bucket.get('cacheTTLSec'))
|
|
118
118
|
return bucket
|
|
119
119
|
|
|
120
120
|
def with_filtering(self, filtering):
|
|
@@ -218,7 +218,7 @@ class Bucket(Storage): # pylint: disable=W0223
|
|
|
218
218
|
)
|
|
219
219
|
self._connection.s3client.delete_bucket(Bucket=self._uuid)
|
|
220
220
|
except self._connection.s3resource.meta.client.exceptions.NoSuchBucket as err:
|
|
221
|
-
raise MissingBucketException("Cannot delete {}. Bucket not found.".format(err.response
|
|
221
|
+
raise MissingBucketException("Cannot delete {}. Bucket not found.".format(err.response.get('Error').get('BucketName'))) from err
|
|
222
222
|
|
|
223
223
|
def list_files(self):
|
|
224
224
|
"""List files in the bucket
|
|
@@ -231,7 +231,7 @@ class Bucket(Storage): # pylint: disable=W0223
|
|
|
231
231
|
bucket = self._connection.s3resource.Bucket(self._uuid)
|
|
232
232
|
return [b for b in bucket.objects.all() if b.key is not None]
|
|
233
233
|
except self._connection.s3resource.meta.client.exceptions.NoSuchBucket as err:
|
|
234
|
-
raise MissingBucketException("Cannot list files. Bucket {} not found.".format(err.response
|
|
234
|
+
raise MissingBucketException("Cannot list files. Bucket {} not found.".format(err.response.get('Error').get('BucketName'))) from err
|
|
235
235
|
|
|
236
236
|
def directory(self, directory=''):
|
|
237
237
|
"""List files in a directory of the bucket according to prefix.
|
|
@@ -281,7 +281,7 @@ class Bucket(Storage): # pylint: disable=W0223
|
|
|
281
281
|
list_files_only = [x for x in entries if not x.key.endswith('/')]
|
|
282
282
|
list_directories_only = [x for x in entries if x.key.endswith('/')]
|
|
283
283
|
except self._connection.s3resource.meta.client.exceptions.NoSuchBucket as err:
|
|
284
|
-
raise MissingBucketException("Cannot synchronize. Bucket {} not found.".format(err.response
|
|
284
|
+
raise MissingBucketException("Cannot synchronize. Bucket {} not found.".format(err.response.get('Error').get('BucketName'))) from err
|
|
285
285
|
|
|
286
286
|
for directory in list_directories_only:
|
|
287
287
|
if not os.path.isdir(os.path.join(local_directoy, get_key_for_local(directory.key))):
|
|
@@ -459,7 +459,7 @@ class Bucket(Storage): # pylint: disable=W0223
|
|
|
459
459
|
self._connection.logger.info("Copy %s to %s" % (entry[0].name, link.name))
|
|
460
460
|
self.copy_file(entry[0].name, link.name)
|
|
461
461
|
except self._connection.s3resource.meta.client.exceptions.NoSuchBucket as err:
|
|
462
|
-
raise MissingBucketException("Cannot sync files. Bucket {} not found.".format(err.response
|
|
462
|
+
raise MissingBucketException("Cannot sync files. Bucket {} not found.".format(err.response.get('Error').get('BucketName'))) from err
|
|
463
463
|
|
|
464
464
|
def add_string(self, string, remote):
|
|
465
465
|
"""Add a string on the storage.
|
|
@@ -484,7 +484,7 @@ class Bucket(Storage): # pylint: disable=W0223
|
|
|
484
484
|
try:
|
|
485
485
|
self._connection.s3client.upload_fileobj(file_, self._uuid, dest, Config=s3_multipart_config)
|
|
486
486
|
except self._connection.s3resource.meta.client.exceptions.NoSuchBucket as err:
|
|
487
|
-
raise MissingBucketException("Cannot add string. Bucket {} not found.".format(err.response
|
|
487
|
+
raise MissingBucketException("Cannot add string. Bucket {} not found.".format(err.response.get('Error').get('BucketName'))) from err
|
|
488
488
|
finally:
|
|
489
489
|
if tobeclosed:
|
|
490
490
|
file_.close()
|
|
@@ -523,7 +523,7 @@ class Bucket(Storage): # pylint: disable=W0223
|
|
|
523
523
|
}
|
|
524
524
|
return self._connection.s3client.copy_object(CopySource=copy_source, Bucket=self._uuid, Key=dest)
|
|
525
525
|
except self._connection.s3resource.meta.client.exceptions.NoSuchBucket as err:
|
|
526
|
-
raise MissingBucketException("Cannot copy file {} to {} from bucket {}. Bucket not found.".format(source, dest, err.response
|
|
526
|
+
raise MissingBucketException("Cannot copy file {} to {} from bucket {}. Bucket not found.".format(source, dest, err.response.get('Error').get('BucketName'))) from err
|
|
527
527
|
|
|
528
528
|
@deprecation.deprecated(deprecated_in="2.6.0", removed_in="3.0",
|
|
529
529
|
current_version=__version__, # type: ignore
|
|
@@ -547,7 +547,7 @@ class Bucket(Storage): # pylint: disable=W0223
|
|
|
547
547
|
try:
|
|
548
548
|
self._connection.s3client.download_fileobj(self._uuid, remote, data)
|
|
549
549
|
except self._connection.s3resource.meta.client.exceptions.NoSuchBucket as err:
|
|
550
|
-
raise MissingBucketException("Cannot download file {} from bucket {}. Bucket not found.".format(remote, err.response
|
|
550
|
+
raise MissingBucketException("Cannot download file {} from bucket {}. Bucket not found.".format(remote, err.response.get('Error').get('BucketName'))) from err
|
|
551
551
|
return local
|
|
552
552
|
|
|
553
553
|
@_util.copy_docs(Storage.delete_file)
|
|
@@ -557,7 +557,7 @@ class Bucket(Storage): # pylint: disable=W0223
|
|
|
557
557
|
remote = _util.get_sanitized_bucket_path(remote, self._connection._show_bucket_warnings)
|
|
558
558
|
self._connection.s3client.delete_object(Bucket=self._uuid, Key=remote)
|
|
559
559
|
except self._connection.s3resource.meta.client.exceptions.NoSuchBucket as err:
|
|
560
|
-
raise MissingBucketException("Cannot delete file {} from bucket {}. Bucket not found.".format(remote, err.response
|
|
560
|
+
raise MissingBucketException("Cannot delete file {} from bucket {}. Bucket not found.".format(remote, err.response.get('Error').get('BucketName'))) from err
|
|
561
561
|
|
|
562
562
|
@property
|
|
563
563
|
def uuid(self):
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""Carbon facts prototype"""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict
|
|
4
|
+
from requests import Response
|
|
5
|
+
from .exceptions import MissingPoolException, MissingTaskException
|
|
6
|
+
from . import get_url, get_url_with_param, raise_on_error, _util
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CarbonFacts(object):
|
|
10
|
+
"""
|
|
11
|
+
Carbon facts details of a computed element
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
energy_consumption: float = 0,
|
|
17
|
+
energy_it: float = 0,
|
|
18
|
+
energy_reuse: float = 0,
|
|
19
|
+
carbon_footprint: float = 0,
|
|
20
|
+
equivalent_dc_name: str = "",
|
|
21
|
+
equivalent_dc_carbon_footprint: float = 0,
|
|
22
|
+
saved_carbon_footprint_compute: float = 0,
|
|
23
|
+
saved_carbon_footprint_heat: float = 0,
|
|
24
|
+
saved_carbon_footprint_compute_heat: float = 0,
|
|
25
|
+
saved_carbon_footprint_percent: float = 0,
|
|
26
|
+
PUE: float = 0,
|
|
27
|
+
ERE: float = 0,
|
|
28
|
+
ERF: float = 0,
|
|
29
|
+
WUE: float = 0):
|
|
30
|
+
"""The CarbonFacts constructor
|
|
31
|
+
|
|
32
|
+
:param energy_consumption: the total energy consumed, in W.h
|
|
33
|
+
:type energy_consumption: `float`
|
|
34
|
+
:param energy_it: the energy consumed by IT, in W.h
|
|
35
|
+
:type energy_it: `float`
|
|
36
|
+
:param energy_reuse: the reuse heat, in W.h
|
|
37
|
+
:type energy_reuse: `float`
|
|
38
|
+
:param carbon_footprint: the actual carbon footprint of the computation, in gCO2eq
|
|
39
|
+
:type carbon_footprint: `float`
|
|
40
|
+
:param equivalent_dc_name: the name of the equivalent datacenter used for comparison
|
|
41
|
+
:type equivalent_dc_name: `str`
|
|
42
|
+
:param equivalent_dc_carbon_footprint: the carbon footprint the computation would generate in an equivalent DC, in gCO2eq
|
|
43
|
+
:type equivalent_dc_carbon_footprint: `float`
|
|
44
|
+
:param saved_carbon_footprint_compute: the carbon footprint saved by the computation part by using Qarnot instead of the equivalent DC, in gCO2eq
|
|
45
|
+
:type saved_carbon_footprint_compute: `float`
|
|
46
|
+
:param saved_carbon_footprint_heat: the carbon footprint saved by the heating part by using Qarnot instead of the equivalent DC, in gCO2eq
|
|
47
|
+
:type saved_carbon_footprint_heat: `float`
|
|
48
|
+
:param saved_carbon_footprint_compute_heat: the total carbon footprint saved by using Qarnot instead of the equivalent DC, gCO2eq
|
|
49
|
+
:type saved_carbon_footprint_compute_heat: `float`
|
|
50
|
+
:param saved_carbon_footprint_percent: the percentage of carbon footprint saved by using Qarnot instead of the equivalent DC, in %
|
|
51
|
+
:type saved_carbon_footprint_percent: `float`
|
|
52
|
+
:param PUE: the energy efficiency of the computation site
|
|
53
|
+
:type PUE: `float`
|
|
54
|
+
:param ERE: the energy reuse ratio of the computation site
|
|
55
|
+
:type ERE: `float`
|
|
56
|
+
:param ERF: the heat reuse ratio of the computation site
|
|
57
|
+
:type ERF: `float`
|
|
58
|
+
:param WUE: the water consumption of the computation site, in L/kWh
|
|
59
|
+
:type WUE: `float`
|
|
60
|
+
"""
|
|
61
|
+
self.energy_consumption_Wh: float = energy_consumption
|
|
62
|
+
self.energy_it_Wh: float = energy_it
|
|
63
|
+
self.energy_reuse_Wh: float = energy_reuse
|
|
64
|
+
self.carbon_footprint_gC02eq: float = carbon_footprint
|
|
65
|
+
self.equivalent_datacenter_name: str = equivalent_dc_name
|
|
66
|
+
self.equivalent_dc_carbon_footprint_gC02eq: float = equivalent_dc_carbon_footprint
|
|
67
|
+
self.saved_carbon_footprint_compute_gC02eq: float = saved_carbon_footprint_compute
|
|
68
|
+
self.saved_carbon_footprint_heat_gC02eq: float = saved_carbon_footprint_heat
|
|
69
|
+
self.saved_carbon_footprint_compute_heat_gC02eq: float = saved_carbon_footprint_compute_heat
|
|
70
|
+
self.saved_carbon_footprint_percent: float = saved_carbon_footprint_percent
|
|
71
|
+
self.PUE: float = PUE
|
|
72
|
+
self.ERE: float = ERE
|
|
73
|
+
self.ERF: float = ERF
|
|
74
|
+
self.WUE: float = WUE
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def from_json(cls, json: Dict[str, Any]):
|
|
78
|
+
"""Create a CarbonFacts from a json representation
|
|
79
|
+
|
|
80
|
+
:param json: the json to use to create the SecretsAccessRights object.
|
|
81
|
+
:type json: `Dict[str, float]`
|
|
82
|
+
:returns: The created :class:`~qarnot.carbon_facts.CarbonFacts`.
|
|
83
|
+
"""
|
|
84
|
+
if json is None:
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
energy_consumption = json.get("total_consumed_energy_Wh")
|
|
88
|
+
energy_it = json.get("total_energy_it_Wh")
|
|
89
|
+
energy_reuse = json.get("total_reused_energy_Wh")
|
|
90
|
+
carbon_footprint = json.get("qarnot_carbon_footprint")
|
|
91
|
+
equivalent_datacenter_name = json.get("equivalent_datacenter_name")
|
|
92
|
+
equivalent_dc_carbon_footprint = json.get("equivalent_DC_carbon_footprint")
|
|
93
|
+
saved_carbon_footprint_compute = json.get("saved_carbon_footprint_compute")
|
|
94
|
+
saved_carbon_footprint_heat = json.get("saved_carbon_footprint_heat")
|
|
95
|
+
saved_carbon_footprint_compute_heat = json.get("saved_carbon_footprint_compute_heat")
|
|
96
|
+
saved_carbon_footprint_percent = json.get("saved_carbon_footprint_percent")
|
|
97
|
+
PUE = json.get("PUE")
|
|
98
|
+
ERE = json.get("ERE")
|
|
99
|
+
ERF = json.get("ERF")
|
|
100
|
+
WUE = json.get("WUE")
|
|
101
|
+
|
|
102
|
+
return CarbonFacts(energy_consumption, energy_it, energy_reuse, carbon_footprint, equivalent_datacenter_name,
|
|
103
|
+
equivalent_dc_carbon_footprint, saved_carbon_footprint_compute, saved_carbon_footprint_heat,
|
|
104
|
+
saved_carbon_footprint_compute_heat, saved_carbon_footprint_percent, PUE, ERE, ERF, WUE)
|
|
105
|
+
|
|
106
|
+
def to_json(self) -> object:
|
|
107
|
+
"""Get a dict ready to be json packed.
|
|
108
|
+
|
|
109
|
+
:return: the json elements of the class.
|
|
110
|
+
:rtype: `dict`
|
|
111
|
+
|
|
112
|
+
"""
|
|
113
|
+
return {
|
|
114
|
+
"total_consumed_energy_Wh": self.energy_consumption_Wh,
|
|
115
|
+
"total_energy_it_Wh": self.energy_it_Wh,
|
|
116
|
+
"total_reused_energy_Wh": self.energy_reuse_Wh,
|
|
117
|
+
"qarnot_carbon_footprint": self.carbon_footprint_gC02eq,
|
|
118
|
+
"equivalent_datacenter_name": self.equivalent_datacenter_name,
|
|
119
|
+
"equivalent_DC_carbon_footprint": self.equivalent_dc_carbon_footprint_gC02eq,
|
|
120
|
+
"saved_carbon_footprint_compute": self.saved_carbon_footprint_compute_gC02eq,
|
|
121
|
+
"saved_carbon_footprint_heat": self.saved_carbon_footprint_heat_gC02eq,
|
|
122
|
+
"saved_carbon_footprint_compute_heat": self.saved_carbon_footprint_compute_heat_gC02eq,
|
|
123
|
+
"saved_carbon_footprint_percent": self.saved_carbon_footprint_percent,
|
|
124
|
+
"PUE": self.PUE,
|
|
125
|
+
"ERE": self.ERE,
|
|
126
|
+
"ERF": self.ERF,
|
|
127
|
+
"WUE": self.WUE
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class CarbonClient(object):
|
|
132
|
+
"""
|
|
133
|
+
Client used to interact with the Qarnot carbon API.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
def __init__(self, connection, datacenter_name: str = None):
|
|
137
|
+
"""The CarbonClient constructor.
|
|
138
|
+
|
|
139
|
+
:param connection: the cluster from where carbon facts are retrieved.
|
|
140
|
+
:type connection: `qarnot.connection.Connection`
|
|
141
|
+
:param datacenter_name: the name of the datacenter used as reference to compare carbon facts.
|
|
142
|
+
:type datacenter_name: `str`
|
|
143
|
+
"""
|
|
144
|
+
self._connection = connection
|
|
145
|
+
self.reference_datacenter_name = datacenter_name
|
|
146
|
+
|
|
147
|
+
def _get_carbon_facts_url(self, resource_type: str, uuid: str):
|
|
148
|
+
if self.reference_datacenter_name is None or self.reference_datacenter_name == "":
|
|
149
|
+
return get_url('%s carbon facts' % resource_type, uuid=uuid)
|
|
150
|
+
return get_url_with_param('%s carbon facts' % resource_type, {'comparisonDatacenter': self.reference_datacenter_name}, uuid=uuid)
|
|
151
|
+
|
|
152
|
+
def _get_pool_carbon_facts_raw(self, pool_id: str) -> Response:
|
|
153
|
+
"""Requests the carbon facts for the pool `pool_id`.
|
|
154
|
+
|
|
155
|
+
:param pool_id: the uuid of the pool
|
|
156
|
+
:type pool_id: `str`
|
|
157
|
+
:rtype: `requests.Response`
|
|
158
|
+
:raises ~qarnot.exceptions.MissingPoolException: Pool was not found.
|
|
159
|
+
:raises ~qarnot.exceptions.UnauthorizedException: Unauthorized.
|
|
160
|
+
:raises ~qarnot.exceptions.QarnotGenericException: API general error, see message for details
|
|
161
|
+
"""
|
|
162
|
+
response = self._connection._get(self._get_carbon_facts_url('pool', pool_id))
|
|
163
|
+
if response.status_code == 404:
|
|
164
|
+
raise MissingPoolException(_util.get_error_message_from_http_response(response))
|
|
165
|
+
raise_on_error(response)
|
|
166
|
+
return response
|
|
167
|
+
|
|
168
|
+
def get_pool_carbon_facts(self, pool_id: str) -> CarbonFacts:
|
|
169
|
+
"""Retrieves the carbon facts of the pool `pool_id` and parses it to a CarbonFacts object.
|
|
170
|
+
|
|
171
|
+
:param pool_id: the uuid of the pool
|
|
172
|
+
:type pool_id: `str`
|
|
173
|
+
:rtype: CarbonFacts
|
|
174
|
+
:raises ~qarnot.exceptions.MissingPoolException: Pool was not found.
|
|
175
|
+
:raises ~qarnot.exceptions.UnauthorizedException: Unauthorized.
|
|
176
|
+
:raises ~qarnot.exceptions.QarnotGenericException: API general error, see message for details
|
|
177
|
+
"""
|
|
178
|
+
raw_carbon_facts = self._get_pool_carbon_facts_raw(pool_id)
|
|
179
|
+
return CarbonFacts.from_json(raw_carbon_facts.json())
|
|
180
|
+
|
|
181
|
+
def _get_task_carbon_facts_raw(self, task_id: str) -> Response:
|
|
182
|
+
"""Requests the carbon facts for the task `task_id`.
|
|
183
|
+
|
|
184
|
+
:param task_id: the uuid of the task
|
|
185
|
+
:type task_id: `str`
|
|
186
|
+
:rtype: `requests.Response`
|
|
187
|
+
:raises ~qarnot.exceptions.MissingTaskException: Task was not found.
|
|
188
|
+
:raises ~qarnot.exceptions.UnauthorizedException: Unauthorized.
|
|
189
|
+
:raises ~qarnot.exceptions.QarnotGenericException: API general error, see message for details
|
|
190
|
+
"""
|
|
191
|
+
response = self._connection._get(self._get_carbon_facts_url('task', task_id))
|
|
192
|
+
if response.status_code == 404:
|
|
193
|
+
raise MissingTaskException(_util.get_error_message_from_http_response(response))
|
|
194
|
+
raise_on_error(response)
|
|
195
|
+
return response
|
|
196
|
+
|
|
197
|
+
def get_task_carbon_facts(self, task_id: str) -> CarbonFacts:
|
|
198
|
+
"""Retrieves the carbon facts of the task `task_id` and parses it to a CarbonFacts object.
|
|
199
|
+
|
|
200
|
+
:param task_id: the uuid of the task
|
|
201
|
+
:type task_id: `str`
|
|
202
|
+
:rtype: CarbonFacts
|
|
203
|
+
:raises ~qarnot.exceptions.MissingTaskException: Task was not found.
|
|
204
|
+
:raises ~qarnot.exceptions.UnauthorizedException: Unauthorized.
|
|
205
|
+
:raises ~qarnot.exceptions.QarnotGenericException: API general error, see message for details
|
|
206
|
+
"""
|
|
207
|
+
raw_carbon_facts = self._get_task_carbon_facts_raw(task_id)
|
|
208
|
+
return CarbonFacts.from_json(raw_carbon_facts.json())
|
|
@@ -196,7 +196,7 @@ class Connection(object):
|
|
|
196
196
|
|
|
197
197
|
user = self.user_info
|
|
198
198
|
session = boto3.session.Session()
|
|
199
|
-
conf = botocore.config.Config(user_agent=self._version)
|
|
199
|
+
conf = botocore.config.Config(user_agent=self._version, request_checksum_calculation="when_required")
|
|
200
200
|
|
|
201
201
|
should_verify_or_certificate_path = True
|
|
202
202
|
if storage_unsafe:
|
|
@@ -589,8 +589,8 @@ class Connection(object):
|
|
|
589
589
|
filters = create_pool_filter(tags=tags, tags_intersect=tags_intersect)
|
|
590
590
|
url = get_url('paginate pools summaries') if summary and filters is None else get_url('paginate pools')
|
|
591
591
|
result = self._page_call(url, self._paginate_request(filters, token, maximum))
|
|
592
|
-
data = [Pool.from_json(self, pool, summary) for pool in result
|
|
593
|
-
return PaginateResponse(token=result.get("token", token), next_token=result
|
|
592
|
+
data = [Pool.from_json(self, pool, summary) for pool in result.get("data")]
|
|
593
|
+
return PaginateResponse(token=result.get("token", token), next_token=result.get("nextToken"), is_truncated=result.get("isTruncated"), page_data=data)
|
|
594
594
|
|
|
595
595
|
def tasks_page(self, token: Optional[str] = None, maximum: Optional[int] = None, summary: bool = True, tags: List = None, tags_intersect: List = None) -> PaginateResponse:
|
|
596
596
|
"""Return a paginate task object.
|
|
@@ -610,8 +610,8 @@ class Connection(object):
|
|
|
610
610
|
filters = create_task_filter(tags=tags, tags_intersect=tags_intersect)
|
|
611
611
|
url = get_url('paginate tasks summaries') if summary and filters is None else get_url('paginate tasks')
|
|
612
612
|
result = self._page_call(url, self._paginate_request(filters, token, maximum))
|
|
613
|
-
data = [Task.from_json(self, task, summary) for task in result
|
|
614
|
-
return PaginateResponse(token=result.get("token", token), next_token=result
|
|
613
|
+
data = [Task.from_json(self, task, summary) for task in result.get("data")]
|
|
614
|
+
return PaginateResponse(token=result.get("token", token), next_token=result.get("nextToken"), is_truncated=result.get("isTruncated"), page_data=data)
|
|
615
615
|
|
|
616
616
|
def jobs_page(self, token: Optional[str] = None, maximum: Optional[int] = None, tags: List = None, tags_intersect: List = None) -> PaginateResponse:
|
|
617
617
|
"""Return a paginate job object.
|
|
@@ -628,8 +628,8 @@ class Connection(object):
|
|
|
628
628
|
|
|
629
629
|
filters = create_job_filter(tags=tags, tags_intersect=tags_intersect)
|
|
630
630
|
result = self._page_call(get_url('paginate jobs'), self._paginate_request(filters, token, maximum))
|
|
631
|
-
data = [Job.from_json(self, job) for job in result
|
|
632
|
-
return PaginateResponse(token=result.get("token", token), next_token=result
|
|
631
|
+
data = [Job.from_json(self, job) for job in result.get("data")]
|
|
632
|
+
return PaginateResponse(token=result.get("token", token), next_token=result.get("nextToken"), is_truncated=result.get("isTruncated"), page_data=data)
|
|
633
633
|
|
|
634
634
|
def hardware_constraints_page(self, limit: Optional[int] = 50, offset: Optional[int] = 0) -> OffsetResponse:
|
|
635
635
|
"""Return a list of hardware constraints limited with offset.
|
|
@@ -643,8 +643,8 @@ class Connection(object):
|
|
|
643
643
|
"""
|
|
644
644
|
|
|
645
645
|
result = self._offset_call(get_url('hardware constraints'), self._offset_request(limit, offset))
|
|
646
|
-
data = [HardwareConstraint.from_json(hw_constraint) for hw_constraint in result
|
|
647
|
-
return OffsetResponse(total=result
|
|
646
|
+
data = [HardwareConstraint.from_json(hw_constraint) for hw_constraint in result.get("data")]
|
|
647
|
+
return OffsetResponse(total=result.get("total"), limit=result.get("limit"), offset=result.get("offset"), page_data=data)
|
|
648
648
|
|
|
649
649
|
def search_cpu_model_constraints(self, cpu_model: str) -> List[CpuModelHardware]:
|
|
650
650
|
"""Return a list of CPU model hardware constraints matching the search term.
|
|
@@ -933,7 +933,7 @@ class UserInfo(object):
|
|
|
933
933
|
""":type: :class:`str`
|
|
934
934
|
|
|
935
935
|
User email address."""
|
|
936
|
-
self.max_bucket = info
|
|
936
|
+
self.max_bucket = info.get('maxBucket')
|
|
937
937
|
""":type: :class:`int`
|
|
938
938
|
|
|
939
939
|
Maximum number of buckets allowed (resource and result buckets)."""
|
|
@@ -941,51 +941,51 @@ class UserInfo(object):
|
|
|
941
941
|
""":type: :class:`int`
|
|
942
942
|
|
|
943
943
|
Number of buckets owned by the user."""
|
|
944
|
-
self.quota_bytes_bucket = info
|
|
944
|
+
self.quota_bytes_bucket = info.get('quotaBytesBucket')
|
|
945
945
|
""":type: :class:`int`
|
|
946
946
|
|
|
947
947
|
Total storage space allowed for the user's buckets (in Bytes)."""
|
|
948
|
-
self.used_quota_bytes_bucket = info
|
|
948
|
+
self.used_quota_bytes_bucket = info.get('usedQuotaBytesBucket')
|
|
949
949
|
""":type: :class:`int`
|
|
950
950
|
|
|
951
951
|
Total storage space used by the user's buckets (in Bytes)."""
|
|
952
|
-
self.task_count = info
|
|
952
|
+
self.task_count = info.get('taskCount')
|
|
953
953
|
""":type: :class:`int`
|
|
954
954
|
|
|
955
955
|
Total number of tasks belonging to the user."""
|
|
956
|
-
self.max_task = info
|
|
956
|
+
self.max_task = info.get('maxTask')
|
|
957
957
|
""":type: :class:`int`
|
|
958
958
|
|
|
959
959
|
Maximum number of tasks the user is allowed to create."""
|
|
960
|
-
self.running_task_count = info
|
|
960
|
+
self.running_task_count = info.get('runningTaskCount')
|
|
961
961
|
""":type: :class:`int`
|
|
962
962
|
|
|
963
963
|
Number of tasks currently in 'Submitted' state."""
|
|
964
|
-
self.max_running_task = info
|
|
964
|
+
self.max_running_task = info.get('maxRunningTask')
|
|
965
965
|
""":type: :class:`int`
|
|
966
966
|
|
|
967
967
|
Maximum number of running tasks."""
|
|
968
|
-
self.max_instances = info
|
|
968
|
+
self.max_instances = info.get('maxInstances')
|
|
969
969
|
""":type: :class:`int`
|
|
970
970
|
|
|
971
971
|
Maximum number of instances."""
|
|
972
|
-
self.max_cores = info
|
|
972
|
+
self.max_cores = info.get('maxFlexCores')
|
|
973
973
|
""":type: :class:`int`
|
|
974
974
|
|
|
975
975
|
Maximum number of cores."""
|
|
976
|
-
self.max_pool = info
|
|
976
|
+
self.max_pool = info.get('maxPool')
|
|
977
977
|
""":type: :class:`int`
|
|
978
978
|
|
|
979
979
|
Maximum number of pool the user is allowed to create."""
|
|
980
|
-
self.pool_count = info
|
|
980
|
+
self.pool_count = info.get('poolCount')
|
|
981
981
|
""":type: :class:`int`
|
|
982
982
|
|
|
983
983
|
Total number of pools belonging to the user."""
|
|
984
|
-
self.max_running_pool = info
|
|
984
|
+
self.max_running_pool = info.get('maxRunningPool')
|
|
985
985
|
""":type: :class:`int`
|
|
986
986
|
|
|
987
987
|
Maximum number of running pools the user is allowed to create."""
|
|
988
|
-
self.running_pool_count = info
|
|
988
|
+
self.running_pool_count = info.get('runningPoolCount')
|
|
989
989
|
""":type: :class:`int`
|
|
990
990
|
|
|
991
991
|
Number of pools currently submitted or running."""
|
|
@@ -997,19 +997,19 @@ class UserInfo(object):
|
|
|
997
997
|
""":type: :class:`int`
|
|
998
998
|
|
|
999
999
|
Number of cores currently submitted or running."""
|
|
1000
|
-
self.max_flex_instances = info.get('maxFlexInstances'
|
|
1000
|
+
self.max_flex_instances = info.get('maxFlexInstances')
|
|
1001
1001
|
""":type: :class:`int`
|
|
1002
1002
|
|
|
1003
1003
|
Maximum number of instances simultaneously used with Flex scheduling plan."""
|
|
1004
|
-
self.max_flex_cores = info.get('maxFlexCores'
|
|
1004
|
+
self.max_flex_cores = info.get('maxFlexCores')
|
|
1005
1005
|
""":type: :class:`int`
|
|
1006
1006
|
|
|
1007
1007
|
Maximum number of cores simultaneously used with Flex scheduling plan."""
|
|
1008
|
-
self.max_on_demand_instances = info.get('maxOnDemandInstances'
|
|
1008
|
+
self.max_on_demand_instances = info.get('maxOnDemandInstances')
|
|
1009
1009
|
""":type: :class:`int`
|
|
1010
1010
|
|
|
1011
1011
|
Maximum number of instances simultaneously used with OnDemand scheduling plan."""
|
|
1012
|
-
self.max_on_demand_cores = info.get('maxOnDemandCores'
|
|
1012
|
+
self.max_on_demand_cores = info.get('maxOnDemandCores')
|
|
1013
1013
|
""":type: :class:`int`
|
|
1014
1014
|
|
|
1015
1015
|
Maximum number of cores simultaneously used with OnDemand scheduling plan."""
|
|
@@ -1018,12 +1018,12 @@ class UserInfo(object):
|
|
|
1018
1018
|
class Profile(object):
|
|
1019
1019
|
"""Information about a profile."""
|
|
1020
1020
|
def __init__(self, info):
|
|
1021
|
-
self.name = info
|
|
1021
|
+
self.name = info.get('name', '')
|
|
1022
1022
|
""":type: :class:`str`
|
|
1023
1023
|
|
|
1024
1024
|
Name of the profile."""
|
|
1025
|
-
self.constants = tuple((cst
|
|
1026
|
-
for cst in info
|
|
1025
|
+
self.constants = tuple((cst.get('name'), cst.get('value'))
|
|
1026
|
+
for cst in info.get('constants', []))
|
|
1027
1027
|
""":type: List of (:class:`str`, :class:`str`)
|
|
1028
1028
|
|
|
1029
1029
|
List of couples (name, value) representing constants for this profile
|
|
@@ -7,17 +7,17 @@ class Error(object):
|
|
|
7
7
|
.. note:: Read-only class
|
|
8
8
|
"""
|
|
9
9
|
def __init__(self, json):
|
|
10
|
-
self.code = json
|
|
10
|
+
self.code = json.get('code')
|
|
11
11
|
""":type: :class:`str`
|
|
12
12
|
|
|
13
13
|
Error code."""
|
|
14
14
|
|
|
15
|
-
self.message = json
|
|
15
|
+
self.message = json.get('message')
|
|
16
16
|
""":type: :class:`str`
|
|
17
17
|
|
|
18
18
|
Error message."""
|
|
19
19
|
|
|
20
|
-
self.debug = json
|
|
20
|
+
self.debug = json.get('debug')
|
|
21
21
|
""":type: :class:`str`
|
|
22
22
|
|
|
23
23
|
Optional extra debug information"""
|