qarnot 2.16.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.
Files changed (84) hide show
  1. {qarnot-2.16.0/qarnot.egg-info → qarnot-2.17.0}/PKG-INFO +1 -1
  2. qarnot-2.17.0/doc/source/api/compute/carbon_facts.rst +8 -0
  3. {qarnot-2.16.0 → qarnot-2.17.0}/doc/source/api/compute/computeindex.rst +1 -0
  4. {qarnot-2.16.0 → qarnot-2.17.0}/qarnot/__init__.py +11 -1
  5. {qarnot-2.16.0 → qarnot-2.17.0}/qarnot/_version.py +3 -3
  6. {qarnot-2.16.0 → qarnot-2.17.0}/qarnot/advanced_bucket.py +1 -1
  7. {qarnot-2.16.0 → qarnot-2.17.0}/qarnot/bucket.py +13 -13
  8. qarnot-2.17.0/qarnot/carbon_facts.py +208 -0
  9. {qarnot-2.16.0 → qarnot-2.17.0}/qarnot/connection.py +28 -28
  10. {qarnot-2.16.0 → qarnot-2.17.0}/qarnot/error.py +3 -3
  11. {qarnot-2.16.0 → qarnot-2.17.0}/qarnot/forced_network_rule.py +11 -11
  12. {qarnot-2.16.0 → qarnot-2.17.0}/qarnot/hardware_constraint.py +9 -9
  13. {qarnot-2.16.0 → qarnot-2.17.0}/qarnot/job.py +18 -18
  14. {qarnot-2.16.0 → qarnot-2.17.0}/qarnot/pool.py +54 -34
  15. {qarnot-2.16.0 → qarnot-2.17.0}/qarnot/privileges.py +1 -1
  16. {qarnot-2.16.0 → qarnot-2.17.0}/qarnot/retry_settings.py +2 -2
  17. {qarnot-2.16.0 → qarnot-2.17.0}/qarnot/scheduling_type.py +3 -3
  18. {qarnot-2.16.0 → qarnot-2.17.0}/qarnot/secrets.py +4 -4
  19. {qarnot-2.16.0 → qarnot-2.17.0}/qarnot/status.py +66 -66
  20. {qarnot-2.16.0 → qarnot-2.17.0}/qarnot/task.py +67 -47
  21. {qarnot-2.16.0 → qarnot-2.17.0/qarnot.egg-info}/PKG-INFO +1 -1
  22. {qarnot-2.16.0 → qarnot-2.17.0}/qarnot.egg-info/SOURCES.txt +3 -0
  23. qarnot-2.17.0/test/test_carbon_facts.py +80 -0
  24. {qarnot-2.16.0 → qarnot-2.17.0}/test/test_connection.py +30 -0
  25. {qarnot-2.16.0 → qarnot-2.17.0}/LICENSE +0 -0
  26. {qarnot-2.16.0 → qarnot-2.17.0}/MANIFEST.in +0 -0
  27. {qarnot-2.16.0 → qarnot-2.17.0}/README.rst +0 -0
  28. {qarnot-2.16.0 → qarnot-2.17.0}/doc/Makefile +0 -0
  29. {qarnot-2.16.0 → qarnot-2.17.0}/doc/make.bat +0 -0
  30. {qarnot-2.16.0 → qarnot-2.17.0}/doc/source/_static/qarnot.png +0 -0
  31. {qarnot-2.16.0 → qarnot-2.17.0}/doc/source/api/compute/forced_network_rule.rst +0 -0
  32. {qarnot-2.16.0 → qarnot-2.17.0}/doc/source/api/compute/hardware_constraint.rst +0 -0
  33. {qarnot-2.16.0 → qarnot-2.17.0}/doc/source/api/compute/job.rst +0 -0
  34. {qarnot-2.16.0 → qarnot-2.17.0}/doc/source/api/compute/paginate.rst +0 -0
  35. {qarnot-2.16.0 → qarnot-2.17.0}/doc/source/api/compute/pool.rst +0 -0
  36. {qarnot-2.16.0 → qarnot-2.17.0}/doc/source/api/compute/privileges.rst +0 -0
  37. {qarnot-2.16.0 → qarnot-2.17.0}/doc/source/api/compute/retry_settings.rst +0 -0
  38. {qarnot-2.16.0 → qarnot-2.17.0}/doc/source/api/compute/scheduling_type.rst +0 -0
  39. {qarnot-2.16.0 → qarnot-2.17.0}/doc/source/api/compute/secrets.rst +0 -0
  40. {qarnot-2.16.0 → qarnot-2.17.0}/doc/source/api/compute/status.rst +0 -0
  41. {qarnot-2.16.0 → qarnot-2.17.0}/doc/source/api/compute/task.rst +0 -0
  42. {qarnot-2.16.0 → qarnot-2.17.0}/doc/source/api/connection.rst +0 -0
  43. {qarnot-2.16.0 → qarnot-2.17.0}/doc/source/api/exceptions.rst +0 -0
  44. {qarnot-2.16.0 → qarnot-2.17.0}/doc/source/api/storage/advanced_bucket.rst +0 -0
  45. {qarnot-2.16.0 → qarnot-2.17.0}/doc/source/api/storage/bucket.rst +0 -0
  46. {qarnot-2.16.0 → qarnot-2.17.0}/doc/source/api/storage/storage.rst +0 -0
  47. {qarnot-2.16.0 → qarnot-2.17.0}/doc/source/api/storage/storageindex.rst +0 -0
  48. {qarnot-2.16.0 → qarnot-2.17.0}/doc/source/basic.rst +0 -0
  49. {qarnot-2.16.0 → qarnot-2.17.0}/doc/source/conf.py +0 -0
  50. {qarnot-2.16.0 → qarnot-2.17.0}/doc/source/index.rst +0 -0
  51. {qarnot-2.16.0 → qarnot-2.17.0}/doc/source/installation.rst +0 -0
  52. {qarnot-2.16.0 → qarnot-2.17.0}/doc/source/qarnot.rst +0 -0
  53. {qarnot-2.16.0 → qarnot-2.17.0}/pyproject.toml +0 -0
  54. {qarnot-2.16.0 → qarnot-2.17.0}/qarnot/_filter.py +0 -0
  55. {qarnot-2.16.0 → qarnot-2.17.0}/qarnot/_retry.py +0 -0
  56. {qarnot-2.16.0 → qarnot-2.17.0}/qarnot/_util.py +0 -0
  57. {qarnot-2.16.0 → qarnot-2.17.0}/qarnot/exceptions.py +0 -0
  58. {qarnot-2.16.0 → qarnot-2.17.0}/qarnot/forced_constant.py +0 -0
  59. {qarnot-2.16.0 → qarnot-2.17.0}/qarnot/helper.py +0 -0
  60. {qarnot-2.16.0 → qarnot-2.17.0}/qarnot/paginate.py +0 -0
  61. {qarnot-2.16.0 → qarnot-2.17.0}/qarnot/storage.py +0 -0
  62. {qarnot-2.16.0 → qarnot-2.17.0}/qarnot.egg-info/dependency_links.txt +0 -0
  63. {qarnot-2.16.0 → qarnot-2.17.0}/qarnot.egg-info/requires.txt +0 -0
  64. {qarnot-2.16.0 → qarnot-2.17.0}/qarnot.egg-info/top_level.txt +0 -0
  65. {qarnot-2.16.0 → qarnot-2.17.0}/requirements-doc.txt +0 -0
  66. {qarnot-2.16.0 → qarnot-2.17.0}/requirements-lint.txt +0 -0
  67. {qarnot-2.16.0 → qarnot-2.17.0}/requirements-optional.txt +0 -0
  68. {qarnot-2.16.0 → qarnot-2.17.0}/requirements-test.txt +0 -0
  69. {qarnot-2.16.0 → qarnot-2.17.0}/requirements.txt +0 -0
  70. {qarnot-2.16.0 → qarnot-2.17.0}/setup.cfg +0 -0
  71. {qarnot-2.16.0 → qarnot-2.17.0}/setup.py +0 -0
  72. {qarnot-2.16.0 → qarnot-2.17.0}/test/test_advanced_bucket.py +0 -0
  73. {qarnot-2.16.0 → qarnot-2.17.0}/test/test_bucket.py +0 -0
  74. {qarnot-2.16.0 → qarnot-2.17.0}/test/test_hardware_constraints.py +0 -0
  75. {qarnot-2.16.0 → qarnot-2.17.0}/test/test_import.py +0 -0
  76. {qarnot-2.16.0 → qarnot-2.17.0}/test/test_job.py +0 -0
  77. {qarnot-2.16.0 → qarnot-2.17.0}/test/test_paginate.py +0 -0
  78. {qarnot-2.16.0 → qarnot-2.17.0}/test/test_pool.py +0 -0
  79. {qarnot-2.16.0 → qarnot-2.17.0}/test/test_retry.py +0 -0
  80. {qarnot-2.16.0 → qarnot-2.17.0}/test/test_secrets.py +0 -0
  81. {qarnot-2.16.0 → qarnot-2.17.0}/test/test_status.py +0 -0
  82. {qarnot-2.16.0 → qarnot-2.17.0}/test/test_task.py +0 -0
  83. {qarnot-2.16.0 → qarnot-2.17.0}/test/test_util.py +0 -0
  84. {qarnot-2.16.0 → qarnot-2.17.0}/versioneer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: qarnot
3
- Version: 2.16.0
3
+ Version: 2.17.0
4
4
  Summary: Qarnot Computing SDK
5
5
  Home-page: https://computing.qarnot.com
6
6
  Author: Qarnot computing
@@ -0,0 +1,8 @@
1
+ Carbon Facts
2
+ ------------
3
+
4
+ .. automodule:: qarnot.carbon_facts
5
+ :members:
6
+ :show-inheritance:
7
+ :special-members:
8
+ :exclude-members: __dict__,__weakref__,__eq__,__str__,__repr__
@@ -14,4 +14,5 @@ Compute
14
14
  retry_settings
15
15
  scheduling_type
16
16
  secrets
17
+ carbon_facts
17
18
 
@@ -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', # GET -> task
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": "2025-02-07T16:27:29+0100",
11
+ "date": "2025-03-05T10:05:53+0100",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "23daecdc71207c207e56afae3e55190f0734bd0d",
15
- "version": "v2.16.0"
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["prefix"])
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['filtering']:
111
- filtering = Filtering.from_json(json_bucket['filtering'])
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['resourcesTransformation']:
115
- resource_transformation = ResourcesTransformation.from_json(json_bucket['resourcesTransformation'])
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['bucketName'], create=False, filtering=filtering, resources_transformation=resource_transformation, cacheTTLSec=json_bucket['cacheTTLSec'])
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['Error']['BucketName'])) from err
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['Error']['BucketName'])) from err
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['Error']['BucketName'])) from err
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['Error']['BucketName'])) from err
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['Error']['BucketName'])) from err
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['Error']['BucketName'])) from err
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['Error']['BucketName'])) from err
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['Error']['BucketName'])) from err
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())
@@ -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["data"]]
593
- return PaginateResponse(token=result.get("token", token), next_token=result["nextToken"], is_truncated=result["isTruncated"], page_data=data)
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["data"]]
614
- return PaginateResponse(token=result.get("token", token), next_token=result["nextToken"], is_truncated=result["isTruncated"], page_data=data)
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["data"]]
632
- return PaginateResponse(token=result.get("token", token), next_token=result["nextToken"], is_truncated=result["isTruncated"], page_data=data)
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["data"]]
647
- return OffsetResponse(total=result["total"], limit=result["limit"], offset=result["offset"], page_data=data)
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['maxBucket']
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['quotaBytesBucket']
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['usedQuotaBytesBucket']
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['taskCount']
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['maxTask']
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['runningTaskCount']
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['maxRunningTask']
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['maxInstances']
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['maxFlexCores']
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['maxPool']
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['poolCount']
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['maxRunningPool']
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['runningPoolCount']
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', -1)
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', -1)
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', -1)
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', -1)
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['name']
1021
+ self.name = info.get('name', '')
1022
1022
  """:type: :class:`str`
1023
1023
 
1024
1024
  Name of the profile."""
1025
- self.constants = tuple((cst['name'], cst['value'])
1026
- for cst in info['constants'])
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['code']
10
+ self.code = json.get('code')
11
11
  """:type: :class:`str`
12
12
 
13
13
  Error code."""
14
14
 
15
- self.message = json['message']
15
+ self.message = json.get('message')
16
16
  """:type: :class:`str`
17
17
 
18
18
  Error message."""
19
19
 
20
- self.debug = json['debug']
20
+ self.debug = json.get('debug')
21
21
  """:type: :class:`str`
22
22
 
23
23
  Optional extra debug information"""
@@ -84,44 +84,44 @@ class ForcedNetworkRule(object):
84
84
  :returns: The created :class:`~qarnot.forced_network_rule.ForcedNetworkRule`
85
85
  """
86
86
 
87
- inbound: bool = bool(json["inbound"])
88
- proto: str = str(json["proto"])
87
+ inbound: bool = bool(json.get("inbound"))
88
+ proto: str = str(json.get("proto"))
89
89
 
90
90
  port: str = None
91
91
  if 'port' in json:
92
- port = str(json["port"])
92
+ port = str(json.get("port"))
93
93
 
94
94
  to: str = None
95
95
  if 'to' in json:
96
- to = str(json["to"])
96
+ to = str(json.get("to"))
97
97
 
98
98
  public_host: str = None
99
99
  if 'public_host' in json:
100
- public_host = str(json["public_host"])
100
+ public_host = str(json.get("public_host"))
101
101
 
102
102
  public_port: str = None
103
103
  if 'public_port' in json:
104
- public_port = str(json["public_port"])
104
+ public_port = str(json.get("public_port"))
105
105
 
106
106
  forwarder: str = None
107
107
  if 'forwarder' in json:
108
- forwarder = str(json["forwarder"])
108
+ forwarder = str(json.get("forwarder"))
109
109
 
110
110
  priority: str = None
111
111
  if 'priority' in json:
112
- priority = str(json["priority"])
112
+ priority = str(json.get("priority"))
113
113
 
114
114
  description: str = None
115
115
  if 'description' in json:
116
- description = str(json["description"])
116
+ description = str(json.get("description"))
117
117
 
118
118
  to_qbox: Optional[bool] = None
119
119
  if 'to_qbox' in json:
120
- to_qbox = bool(json["to_qbox"])
120
+ to_qbox = bool(json.get("to_qbox"))
121
121
 
122
122
  to_payload: Optional[bool] = None
123
123
  if 'to_payload' in json:
124
- to_payload = bool(json["to_payload"])
124
+ to_payload = bool(json.get("to_payload"))
125
125
 
126
126
  return ForcedNetworkRule(
127
127
  inbound,