qarnot 2.17.0__tar.gz → 2.18.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 (87) hide show
  1. {qarnot-2.17.0/qarnot.egg-info → qarnot-2.18.0}/PKG-INFO +3 -2
  2. {qarnot-2.17.0 → qarnot-2.18.0}/doc/source/api/compute/computeindex.rst +1 -0
  3. qarnot-2.18.0/doc/source/api/compute/computing_quotas.rst +8 -0
  4. {qarnot-2.17.0 → qarnot-2.18.0}/qarnot/_version.py +3 -3
  5. qarnot-2.18.0/qarnot/computing_quotas.py +328 -0
  6. {qarnot-2.17.0 → qarnot-2.18.0}/qarnot/connection.py +23 -6
  7. {qarnot-2.17.0 → qarnot-2.18.0}/qarnot/helper.py +4 -3
  8. {qarnot-2.17.0 → qarnot-2.18.0}/qarnot/pool.py +22 -0
  9. {qarnot-2.17.0 → qarnot-2.18.0}/qarnot/task.py +24 -2
  10. {qarnot-2.17.0 → qarnot-2.18.0/qarnot.egg-info}/PKG-INFO +3 -2
  11. {qarnot-2.17.0 → qarnot-2.18.0}/qarnot.egg-info/SOURCES.txt +2 -0
  12. qarnot-2.18.0/requirements.txt +6 -0
  13. {qarnot-2.17.0 → qarnot-2.18.0}/test/test_connection.py +118 -3
  14. {qarnot-2.17.0 → qarnot-2.18.0}/test/test_pool.py +29 -2
  15. {qarnot-2.17.0 → qarnot-2.18.0}/test/test_task.py +33 -3
  16. qarnot-2.17.0/requirements.txt +0 -6
  17. {qarnot-2.17.0 → qarnot-2.18.0}/LICENSE +0 -0
  18. {qarnot-2.17.0 → qarnot-2.18.0}/MANIFEST.in +0 -0
  19. {qarnot-2.17.0 → qarnot-2.18.0}/README.rst +0 -0
  20. {qarnot-2.17.0 → qarnot-2.18.0}/doc/Makefile +0 -0
  21. {qarnot-2.17.0 → qarnot-2.18.0}/doc/make.bat +0 -0
  22. {qarnot-2.17.0 → qarnot-2.18.0}/doc/source/_static/qarnot.png +0 -0
  23. {qarnot-2.17.0 → qarnot-2.18.0}/doc/source/api/compute/carbon_facts.rst +0 -0
  24. {qarnot-2.17.0 → qarnot-2.18.0}/doc/source/api/compute/forced_network_rule.rst +0 -0
  25. {qarnot-2.17.0 → qarnot-2.18.0}/doc/source/api/compute/hardware_constraint.rst +0 -0
  26. {qarnot-2.17.0 → qarnot-2.18.0}/doc/source/api/compute/job.rst +0 -0
  27. {qarnot-2.17.0 → qarnot-2.18.0}/doc/source/api/compute/paginate.rst +0 -0
  28. {qarnot-2.17.0 → qarnot-2.18.0}/doc/source/api/compute/pool.rst +0 -0
  29. {qarnot-2.17.0 → qarnot-2.18.0}/doc/source/api/compute/privileges.rst +0 -0
  30. {qarnot-2.17.0 → qarnot-2.18.0}/doc/source/api/compute/retry_settings.rst +0 -0
  31. {qarnot-2.17.0 → qarnot-2.18.0}/doc/source/api/compute/scheduling_type.rst +0 -0
  32. {qarnot-2.17.0 → qarnot-2.18.0}/doc/source/api/compute/secrets.rst +0 -0
  33. {qarnot-2.17.0 → qarnot-2.18.0}/doc/source/api/compute/status.rst +0 -0
  34. {qarnot-2.17.0 → qarnot-2.18.0}/doc/source/api/compute/task.rst +0 -0
  35. {qarnot-2.17.0 → qarnot-2.18.0}/doc/source/api/connection.rst +0 -0
  36. {qarnot-2.17.0 → qarnot-2.18.0}/doc/source/api/exceptions.rst +0 -0
  37. {qarnot-2.17.0 → qarnot-2.18.0}/doc/source/api/storage/advanced_bucket.rst +0 -0
  38. {qarnot-2.17.0 → qarnot-2.18.0}/doc/source/api/storage/bucket.rst +0 -0
  39. {qarnot-2.17.0 → qarnot-2.18.0}/doc/source/api/storage/storage.rst +0 -0
  40. {qarnot-2.17.0 → qarnot-2.18.0}/doc/source/api/storage/storageindex.rst +0 -0
  41. {qarnot-2.17.0 → qarnot-2.18.0}/doc/source/basic.rst +0 -0
  42. {qarnot-2.17.0 → qarnot-2.18.0}/doc/source/conf.py +0 -0
  43. {qarnot-2.17.0 → qarnot-2.18.0}/doc/source/index.rst +0 -0
  44. {qarnot-2.17.0 → qarnot-2.18.0}/doc/source/installation.rst +0 -0
  45. {qarnot-2.17.0 → qarnot-2.18.0}/doc/source/qarnot.rst +0 -0
  46. {qarnot-2.17.0 → qarnot-2.18.0}/pyproject.toml +0 -0
  47. {qarnot-2.17.0 → qarnot-2.18.0}/qarnot/__init__.py +0 -0
  48. {qarnot-2.17.0 → qarnot-2.18.0}/qarnot/_filter.py +0 -0
  49. {qarnot-2.17.0 → qarnot-2.18.0}/qarnot/_retry.py +0 -0
  50. {qarnot-2.17.0 → qarnot-2.18.0}/qarnot/_util.py +0 -0
  51. {qarnot-2.17.0 → qarnot-2.18.0}/qarnot/advanced_bucket.py +0 -0
  52. {qarnot-2.17.0 → qarnot-2.18.0}/qarnot/bucket.py +0 -0
  53. {qarnot-2.17.0 → qarnot-2.18.0}/qarnot/carbon_facts.py +0 -0
  54. {qarnot-2.17.0 → qarnot-2.18.0}/qarnot/error.py +0 -0
  55. {qarnot-2.17.0 → qarnot-2.18.0}/qarnot/exceptions.py +0 -0
  56. {qarnot-2.17.0 → qarnot-2.18.0}/qarnot/forced_constant.py +0 -0
  57. {qarnot-2.17.0 → qarnot-2.18.0}/qarnot/forced_network_rule.py +0 -0
  58. {qarnot-2.17.0 → qarnot-2.18.0}/qarnot/hardware_constraint.py +0 -0
  59. {qarnot-2.17.0 → qarnot-2.18.0}/qarnot/job.py +0 -0
  60. {qarnot-2.17.0 → qarnot-2.18.0}/qarnot/paginate.py +0 -0
  61. {qarnot-2.17.0 → qarnot-2.18.0}/qarnot/privileges.py +0 -0
  62. {qarnot-2.17.0 → qarnot-2.18.0}/qarnot/retry_settings.py +0 -0
  63. {qarnot-2.17.0 → qarnot-2.18.0}/qarnot/scheduling_type.py +0 -0
  64. {qarnot-2.17.0 → qarnot-2.18.0}/qarnot/secrets.py +0 -0
  65. {qarnot-2.17.0 → qarnot-2.18.0}/qarnot/status.py +0 -0
  66. {qarnot-2.17.0 → qarnot-2.18.0}/qarnot/storage.py +0 -0
  67. {qarnot-2.17.0 → qarnot-2.18.0}/qarnot.egg-info/dependency_links.txt +0 -0
  68. {qarnot-2.17.0 → qarnot-2.18.0}/qarnot.egg-info/requires.txt +0 -0
  69. {qarnot-2.17.0 → qarnot-2.18.0}/qarnot.egg-info/top_level.txt +0 -0
  70. {qarnot-2.17.0 → qarnot-2.18.0}/requirements-doc.txt +0 -0
  71. {qarnot-2.17.0 → qarnot-2.18.0}/requirements-lint.txt +0 -0
  72. {qarnot-2.17.0 → qarnot-2.18.0}/requirements-optional.txt +0 -0
  73. {qarnot-2.17.0 → qarnot-2.18.0}/requirements-test.txt +0 -0
  74. {qarnot-2.17.0 → qarnot-2.18.0}/setup.cfg +0 -0
  75. {qarnot-2.17.0 → qarnot-2.18.0}/setup.py +0 -0
  76. {qarnot-2.17.0 → qarnot-2.18.0}/test/test_advanced_bucket.py +0 -0
  77. {qarnot-2.17.0 → qarnot-2.18.0}/test/test_bucket.py +0 -0
  78. {qarnot-2.17.0 → qarnot-2.18.0}/test/test_carbon_facts.py +0 -0
  79. {qarnot-2.17.0 → qarnot-2.18.0}/test/test_hardware_constraints.py +0 -0
  80. {qarnot-2.17.0 → qarnot-2.18.0}/test/test_import.py +0 -0
  81. {qarnot-2.17.0 → qarnot-2.18.0}/test/test_job.py +0 -0
  82. {qarnot-2.17.0 → qarnot-2.18.0}/test/test_paginate.py +0 -0
  83. {qarnot-2.17.0 → qarnot-2.18.0}/test/test_retry.py +0 -0
  84. {qarnot-2.17.0 → qarnot-2.18.0}/test/test_secrets.py +0 -0
  85. {qarnot-2.17.0 → qarnot-2.18.0}/test/test_status.py +0 -0
  86. {qarnot-2.17.0 → qarnot-2.18.0}/test/test_util.py +0 -0
  87. {qarnot-2.17.0 → qarnot-2.18.0}/versioneer.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: qarnot
3
- Version: 2.17.0
3
+ Version: 2.18.0
4
4
  Summary: Qarnot Computing SDK
5
5
  Home-page: https://computing.qarnot.com
6
6
  Author: Qarnot computing
@@ -27,6 +27,7 @@ Dynamic: classifier
27
27
  Dynamic: description
28
28
  Dynamic: home-page
29
29
  Dynamic: license
30
+ Dynamic: license-file
30
31
  Dynamic: requires-dist
31
32
  Dynamic: requires-python
32
33
  Dynamic: summary
@@ -15,4 +15,5 @@ Compute
15
15
  scheduling_type
16
16
  secrets
17
17
  carbon_facts
18
+ computing_quotas
18
19
 
@@ -0,0 +1,8 @@
1
+ Computing Quotas
2
+ ----------------
3
+
4
+ .. automodule:: qarnot.computing_quotas
5
+ :members:
6
+ :show-inheritance:
7
+ :special-members:
8
+ :exclude-members: __dict__,__weakref__
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2025-03-05T10:05:53+0100",
11
+ "date": "2025-05-22T14:41:22+0200",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "559a43baceed056873c5b9dab07d987025306be8",
15
- "version": "v2.17.0"
14
+ "full-revisionid": "9361585630d3dbcc8fa50ecbb9d20d592234a808",
15
+ "version": "v2.18.0"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -0,0 +1,328 @@
1
+ # Copyright 2025 Qarnot computing
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from typing import Optional, List, Dict, Any
16
+
17
+
18
+ class UserSchedulingQuota(object):
19
+ """Describes a scheduling quota for the user.
20
+ """
21
+
22
+ def __init__(self, max_cores: int, running_cores_count: int, max_instances: int, running_instances_count: int):
23
+ """Create a new UserSchedulingQuota object describing a scheduling quota for the user.
24
+
25
+ :param int max_cores: Maximum number of cores that can be simultaneously used with this scheduling plan.
26
+ :param int running_cores_count: Number of cores that are currently running with this scheduling plan.
27
+ :param int max_instances: Maximum number of instances that can be simultaneously used with this scheduling plan.
28
+ :param int running_instances_count: Number of instances that are currently running with this scheduling plan.
29
+ :returns: The created :class:`~qarnot.computing_quotas.UserSchedulingQuota`.
30
+ """
31
+ self.max_cores = max_cores
32
+ """:type: :class:`int`
33
+
34
+ Maximum number of cores that can be simultaneously used with this scheduling plan.
35
+ """
36
+ self.running_cores_count = running_cores_count
37
+ """:type: :class:`int`
38
+
39
+ Number of cores that are currently running with this scheduling plan.
40
+ """
41
+ self.max_instances = max_instances
42
+ """:type: :class:`int`
43
+
44
+ Maximum number of instances that can be simultaneously used with this scheduling plan.
45
+ """
46
+ self.running_instances_count = running_instances_count
47
+ """:type: :class:`int`
48
+
49
+ Number of instances that are currently running with this scheduling plan.
50
+ """
51
+
52
+ @classmethod
53
+ def from_json(cls, json: Dict[str, Any]):
54
+ """Create a new UserSchedulingQuota object from json describing a scheduling quota for a user.
55
+
56
+ :param dict json: Dictionary representing the user scheduling plan
57
+ :returns: The created :class:`~qarnot.computing_quotas.UserSchedulingQuota`.
58
+ """
59
+ if json is None:
60
+ return None
61
+ return cls(
62
+ json.get('maxCores'),
63
+ json.get('runningCoresCount'),
64
+ json.get('maxInstances'),
65
+ json.get('runningInstancesCount'),
66
+ )
67
+
68
+
69
+ class UserReservedSchedulingQuota(UserSchedulingQuota):
70
+ """Describes a reserved scheduling quota for the user.
71
+ """
72
+
73
+ def __init__(self, machine_key: str, max_cores: int, running_cores_count: int, max_instances: int, running_instances_count: int):
74
+ """Create a new UserReservedSchedulingQuota object describing a reserved scheduling quota for the user.
75
+
76
+ :param str machine_key: Machine key of the reservation.
77
+ :param int max_cores: Maximum number of cores that can be simultaneously used with this reserved machine specification.
78
+ :param int running_cores_count: Number of cores that are currently running with this reserved machine specification.
79
+ :param int max_instances: Maximum number of instances that can be simultaneously used with this reserved machine specification.
80
+ :param int running_instances_count: Number of instances that are currently running with this reserved machine specification.
81
+ :returns: The created :class:`~qarnot.computing_quotas.UserReservedSchedulingQuota`.
82
+ """
83
+ super().__init__(max_cores, running_cores_count, max_instances, running_instances_count)
84
+ self.machine_key = machine_key
85
+ """:type: :class:`str`
86
+
87
+ Machine key of the reservation.
88
+ """
89
+
90
+ @classmethod
91
+ def from_json(cls, json: Dict[str, Any]):
92
+ """Create a new UserReservedSchedulingQuota object from json describing a reserved scheduling quota for a user.
93
+
94
+ :param dict json: Dictionary representing the user reserved scheduling quota
95
+ :returns: The created :class:`~qarnot.computing_quotas.UserReservedSchedulingQuota`.
96
+ """
97
+ if json is None:
98
+ return None
99
+ return cls(
100
+ json.get('machineKey'),
101
+ json.get('maxCores'),
102
+ json.get('runningCoresCount'),
103
+ json.get('maxInstances'),
104
+ json.get('runningInstancesCount'),
105
+ )
106
+
107
+
108
+ class UserComputingQuotas(object):
109
+ """Describes the user's computing quotas.
110
+ """
111
+
112
+ def __init__(self, flex: UserSchedulingQuota, on_demand: UserSchedulingQuota, reserved: List[UserReservedSchedulingQuota]):
113
+ """Create a new UserComputingQuotas object describing the user's computing quotas.
114
+
115
+ :param `~qarnot.computing_quotas.UserSchedulingQuota` flex: Quotas for Flex scheduling plan.
116
+ :param `~qarnot.computing_quotas.UserSchedulingQuota` on_demand: Quotas for OnDemand scheduling plan.
117
+ :param List of `~qarnot.computing_quotas.UserReservedSchedulingQuota` reserved: Quotas for Reserved scheduling plan.
118
+ :returns: The created :class:`~qarnot.computing_quotas.UserComputingQuotas`.
119
+ """
120
+ self.flex = flex
121
+ """:type: :class:`~qarnot.computing_quotas.UserSchedulingQuota`
122
+
123
+ Quotas for Flex scheduling plan."""
124
+ self.on_demand = on_demand
125
+ """:type: :class:`~qarnot.computing_quotas.UserSchedulingQuota`
126
+
127
+ Quotas for OnDemand scheduling plan."""
128
+ self.reserved = reserved
129
+ """:type: list(:class:`~qarnot.computing_quotas.UserReservedSchedulingQuota`)
130
+
131
+ Quotas for Reserved scheduling plan."""
132
+
133
+ @classmethod
134
+ def from_json(cls, json: Dict[str, Any]):
135
+ """Create a new UserComputingQuotas object from json describing the user's computing quotas.
136
+
137
+ :param dict json: Dictionary representing the user computing quota
138
+ :returns: The created :class:`~qarnot.computing_quotas.UserComputingQuotas`.
139
+ """
140
+ if json is None:
141
+ return None
142
+ return cls(
143
+ UserSchedulingQuota.from_json(json.get('flex')),
144
+ UserSchedulingQuota.from_json(json.get('onDemand')),
145
+ [UserReservedSchedulingQuota.from_json(v) for v in json.get('reserved', []) if v is not None]
146
+ )
147
+
148
+
149
+ class OrganizationSchedulingQuota(object):
150
+ """Describes a scheduling quota for the organization.
151
+ """
152
+
153
+ def __init__(self, max_cores: int, running_cores_count: int, max_instances: int, running_instances_count: int):
154
+ """Create a new OrganizationSchedulingQuota object describing a scheduling quota for the organization.
155
+
156
+ :param int max_cores: Maximum number of cores that can be simultaneously used with this scheduling plan.
157
+ :param int running_cores_count: Number of cores that are currently running with this scheduling plan.
158
+ :param int max_instances: Maximum number of instances that can be simultaneously used with this scheduling plan.
159
+ :param int running_instances_count: Number of instances that are currently running with this scheduling plan.
160
+ :returns: The created :class:`~qarnot.computing_quotas.OrganizationSchedulingQuota`.
161
+ """
162
+ self.max_cores = max_cores
163
+ """:type: :class:`int`
164
+
165
+ Maximum number of cores that can be simultaneously used with this scheduling plan.
166
+ """
167
+ self.running_cores_count = running_cores_count
168
+ """:type: :class:`int`
169
+
170
+ Number of cores that are currently running with this scheduling plan.
171
+ """
172
+ self.max_instances = max_instances
173
+ """:type: :class:`int`
174
+
175
+ Maximum number of instances that can be simultaneously used with this scheduling plan.
176
+ """
177
+ self.running_instances_count = running_instances_count
178
+ """:type: :class:`int`
179
+
180
+ Number of instances that are currently running with this scheduling plan.
181
+ """
182
+
183
+ @classmethod
184
+ def from_json(cls, json: Dict[str, Any]):
185
+ """Create a new OrganizationSchedulingQuota object from json describing a scheduling quota for the organization.
186
+
187
+ :param dict json: Dictionary representing the organization scheduling plan
188
+ :returns: The created :class:`~qarnot.computing_quotas.OrganizationSchedulingQuota`.
189
+ """
190
+ if json is None:
191
+ return None
192
+ return cls(
193
+ json.get('maxCores'),
194
+ json.get('runningCoresCount'),
195
+ json.get('maxInstances'),
196
+ json.get('runningInstancesCount'),
197
+ )
198
+
199
+
200
+ class OrganizationReservedSchedulingQuota(OrganizationSchedulingQuota):
201
+ """Describes a reserved scheduling quota for the organization.
202
+ """
203
+
204
+ def __init__(self, machine_key: str, max_cores: int, running_cores_count: int, max_instances: int, running_instances_count: int):
205
+ """Create a new OrganizationReservedSchedulingQuota object describing a reserved scheduling quota for the organization.
206
+
207
+ :param str machine_key: Machine key of the reservation.
208
+ :param int max_cores: Maximum number of cores that can be simultaneously used with this reserved machine specification.
209
+ :param int running_cores_count: Number of cores that are currently running with this reserved machine specification.
210
+ :param int max_instances: Maximum number of instances that can be simultaneously used with this reserved machine specification.
211
+ :param int running_instances_count: Number of instances that are currently running with this reserved machine specification.
212
+ :returns: The created :class:`~qarnot.computing_quotas.OrganizationReservedSchedulingQuota`.
213
+ """
214
+ super().__init__(max_cores, running_cores_count, max_instances, running_instances_count)
215
+ self.machine_key = machine_key
216
+ """:type: :class:`str`
217
+
218
+ Machine key of the reservation.
219
+ """
220
+
221
+ @classmethod
222
+ def from_json(cls, json: Dict[str, Any]):
223
+ """Create a new OrganizationReservedSchedulingQuota object from json describing a reserved scheduling quota for a organization.
224
+
225
+ :param dict json: Dictionary representing the organization reserved scheduling quota
226
+ :returns: The created :class:`~qarnot.computing_quotas.OrganizationReservedSchedulingQuota`.
227
+ """
228
+ if json is None:
229
+ return None
230
+ return cls(
231
+ json.get('machineKey'),
232
+ json.get('maxCores'),
233
+ json.get('runningCoresCount'),
234
+ json.get('maxInstances'),
235
+ json.get('runningInstancesCount'),
236
+ )
237
+
238
+
239
+ class OrganizationComputingQuotas(object):
240
+ """Describes the organization's computing quotas.
241
+ """
242
+
243
+ def __init__(self, name: str, flex: OrganizationSchedulingQuota, on_demand: OrganizationSchedulingQuota, reserved: List[OrganizationReservedSchedulingQuota]):
244
+ """Create a new OrganizationComputingQuotas object describing the organization's computing quotas.
245
+
246
+ :param `str` name: Name of the organization.
247
+ :param `~qarnot.computing_quotas.OrganizationSchedulingQuota` flex: Quotas for Flex scheduling plan.
248
+ :param `~qarnot.computing_quotas.OrganizationSchedulingQuota` on_demand: Quotas for OnDemand scheduling plan.
249
+ :param List of `~qarnot.computing_quotas.OrganizationReservedSchedulingQuota` reserved: Quotas for Reserved scheduling plan.
250
+ :returns: The created :class:`~qarnot.computing_quotas.OrganizationComputingQuotas`.
251
+ """
252
+ self.name = name
253
+ """:type: :class:`str`
254
+
255
+ Name of the organization."""
256
+ self.flex = flex
257
+ """:type: :class:`~qarnot.computing_quotas.OrganizationSchedulingQuota`
258
+
259
+ Quotas for Flex scheduling plan."""
260
+ self.on_demand = on_demand
261
+ """:type: :class:`~qarnot.computing_quotas.OrganizationSchedulingQuota`
262
+
263
+ Quotas for OnDemand scheduling plan."""
264
+ self.reserved = reserved
265
+ """:type: list(:class:`~qarnot.computing_quotas.OrganizationReservedSchedulingQuota`)
266
+
267
+ Quotas for Reserved scheduling plan."""
268
+
269
+ @classmethod
270
+ def from_json(cls, json: Dict[str, Any]):
271
+ """Create a new OrganizationComputingQuotas object from json describing the organization's computing quotas.
272
+
273
+ :param dict json: Dictionary representing the organization computing quota
274
+ :returns: The created :class:`~qarnot.computing_quotas.OrganizationComputingQuotas`.
275
+ """
276
+ if json is None:
277
+ return None
278
+ return cls(
279
+ json.get('name'),
280
+ OrganizationSchedulingQuota.from_json(json.get('flex')),
281
+ OrganizationSchedulingQuota.from_json(json.get('onDemand')),
282
+ [OrganizationReservedSchedulingQuota.from_json(v) for v in json.get('reserved', []) if v is not None]
283
+ )
284
+
285
+
286
+ class ComputingQuotas(object):
287
+ """Describes user and organization computing quotas.
288
+ """
289
+
290
+ def __init__(self, user_computing_quotas: Optional[UserComputingQuotas], organization_computing_quotas: Optional[OrganizationComputingQuotas] = None):
291
+ """Create a new ComputingQuotas object describing user and organization computing quotas.
292
+
293
+ :param user_computing_quotas: the user related computing quotas
294
+ :type user_computing_quotas: `~qarnot.computing_quotas.UserComputingQuotas`, optional
295
+ :param organization_computing_quotas: the organization related computing quotas
296
+ :type organization_computing_quotas: `~qarnot.computing_quotas.OrganizationComputingQuotas`, optional
297
+ :returns: The created :class:`~qarnot.computing_quotas.ComputingQuotas`.
298
+ """
299
+ self.user = user_computing_quotas
300
+ """:type: :class:`~qarnot.computing_quotas.UserComputingQuotas`
301
+
302
+ The user related computing quotas."""
303
+ self.organization = organization_computing_quotas
304
+ """:type: :class:`~qarnot.computing_quotas.OrganizationComputingQuotas`
305
+
306
+ The organization related computing quotas."""
307
+
308
+ @classmethod
309
+ def from_json(cls, json: Dict[str, Any]):
310
+ """Create a new ComputingQuotas object from json describing user and organization computing quotas.
311
+
312
+ :param dict json: Dictionary representing the computing quotas
313
+ :returns: The created :class:`~qarnot.computing_quotas.ComputingQuotas`
314
+ """
315
+ if json is None:
316
+ return None
317
+ user_computing_quotas = UserComputingQuotas.from_json(json.get('user'))
318
+ organization_computing_quotas = OrganizationComputingQuotas.from_json(json.get('organization'))
319
+ return cls(user_computing_quotas, organization_computing_quotas)
320
+
321
+ @classmethod
322
+ def from_json_legacy(cls, json: Dict[str, Any]):
323
+ if json is None:
324
+ return None
325
+ flex = UserSchedulingQuota(json.get('maxFlexCores'), json.get('runningFlexCoreCount'), json.get('maxFlexInstances'), json.get('runningFlexInstanceCount'))
326
+ onDemand = UserSchedulingQuota(json.get('maxOnDemandCores'), json.get('runningOnDemandCoreCount'), json.get('maxOnDemandInstances'), json.get('runningOnDemandInstanceCount'))
327
+ user = UserComputingQuotas(flex, onDemand, [])
328
+ return cls(user, None)
@@ -28,6 +28,7 @@ from .task import Task, BulkTaskResponse
28
28
  from .pool import Pool
29
29
  from .paginate import PaginateResponse, OffsetResponse
30
30
  from .bucket import Bucket
31
+ from .computing_quotas import ComputingQuotas
31
32
  from .job import Job
32
33
  from ._filter import create_pool_filter, create_task_filter, create_job_filter
33
34
  from ._retry import with_retry
@@ -95,8 +96,8 @@ class Connection(object):
95
96
  unsafe=False
96
97
 
97
98
  """
98
- self.logger = logger if logger is not None else Log.get_logger_for_stream(sys.stdout)
99
- self.logger_stderr = logger if logger is not None else Log.get_logger_for_stream(sys.stderr) # to avoid breaking change of task stderr logs
99
+ self.logger = logger if logger is not None else Log.get_logger_for_stream(sys.stdout, "stdout")
100
+ self.logger_stderr = logger if logger is not None else Log.get_logger_for_stream(sys.stderr, "stderr") # to avoid breaking change of task stderr logs
100
101
  self._version = "qarnot-sdk-python/" + __version__
101
102
  self._http = requests.session()
102
103
  self._retry_count = retry_count
@@ -997,21 +998,37 @@ class UserInfo(object):
997
998
  """:type: :class:`int`
998
999
 
999
1000
  Number of cores currently submitted or running."""
1000
- self.max_flex_instances = info.get('maxFlexInstances')
1001
+ self.computing_quotas = ComputingQuotas.from_json(info.get('computingQuotas')) or ComputingQuotas.from_json_legacy(info)
1002
+ """:type: :class:`~qarnot.computing_quotas.ComputingQuotas`
1003
+
1004
+ Computing quotas information of the user and his organization."""
1005
+ self.max_flex_instances = self.computing_quotas.user.flex.max_instances if self.computing_quotas is not None else info.get('maxFlexInstances')
1001
1006
  """:type: :class:`int`
1002
1007
 
1008
+ .. deprecated:: v2.18.0
1009
+ Use `self.computing_quotas` instead.
1010
+
1003
1011
  Maximum number of instances simultaneously used with Flex scheduling plan."""
1004
- self.max_flex_cores = info.get('maxFlexCores')
1012
+ self.max_flex_cores = self.computing_quotas.user.flex.max_cores if self.computing_quotas is not None else info.get('maxFlexCores')
1005
1013
  """:type: :class:`int`
1006
1014
 
1015
+ .. deprecated:: v2.18.0
1016
+ Use `self.computing_quotas` instead.
1017
+
1007
1018
  Maximum number of cores simultaneously used with Flex scheduling plan."""
1008
- self.max_on_demand_instances = info.get('maxOnDemandInstances')
1019
+ self.max_on_demand_instances = self.computing_quotas.user.on_demand.max_instances if self.computing_quotas is not None else info.get('maxOnDemandInstances')
1009
1020
  """:type: :class:`int`
1010
1021
 
1022
+ .. deprecated:: v2.18.0
1023
+ Use `self.computing_quotas` instead.
1024
+
1011
1025
  Maximum number of instances simultaneously used with OnDemand scheduling plan."""
1012
- self.max_on_demand_cores = info.get('maxOnDemandCores')
1026
+ self.max_on_demand_cores = self.computing_quotas.user.on_demand.max_cores if self.computing_quotas is not None else info.get('maxOnDemandCores')
1013
1027
  """:type: :class:`int`
1014
1028
 
1029
+ .. deprecated:: v2.18.0
1030
+ Use `self.computing_quotas` instead.
1031
+
1015
1032
  Maximum number of cores simultaneously used with OnDemand scheduling plan."""
1016
1033
 
1017
1034
 
@@ -14,7 +14,7 @@ class Log():
14
14
  """
15
15
 
16
16
  @staticmethod
17
- def get_logger_for_stream(stream: TextIO = None, log_format: str = DEFAULT_LOG_FORMAT):
17
+ def get_logger_for_stream(stream: TextIO = None, name: str = None, log_format: str = DEFAULT_LOG_FORMAT):
18
18
  """Create a logger whose output is a stream.
19
19
 
20
20
  :param TextIO stream:
@@ -31,12 +31,13 @@ class Log():
31
31
  :rtype: logging.Logger
32
32
  :returns: The created logger.
33
33
  """
34
-
34
+ if name is None:
35
+ name = __name__
35
36
  formatter = logging.Formatter(log_format)
36
37
  handler = logging.StreamHandler(stream if stream is not None else sys.stdout)
37
38
  handler.setFormatter(formatter)
38
39
 
39
- logger = logging.getLogger(__name__)
40
+ logger = logging.getLogger(name)
40
41
  logger.addHandler(handler)
41
42
  logger.setLevel(logging.INFO)
42
43
  return logger
@@ -114,6 +114,7 @@ class Pool(object):
114
114
  self._queued_or_running_task_instances_count = 0.0
115
115
 
116
116
  self._completion_time_to_live = "00:00:00"
117
+ self._max_time_queue_seconds: int = None
117
118
  self._auto_delete = False
118
119
  self._tasks_wait_for_synchronization = False
119
120
 
@@ -215,6 +216,8 @@ class Pool(object):
215
216
  if 'completionTimeToLive' in json_pool:
216
217
  self._completion_time_to_live = json_pool.get("completionTimeToLive")
217
218
 
219
+ self._max_time_queue_seconds = json_pool.get("maxTimeQueueSeconds", None)
220
+
218
221
  if 'elasticProperty' in json_pool:
219
222
  elasticProperty = json_pool.get("elasticProperty")
220
223
  self._is_elastic = elasticProperty.get("isElastic")
@@ -297,6 +300,9 @@ class Pool(object):
297
300
  json_pool['privileges'] = self._privileges.to_json()
298
301
  json_pool['defaultRetrySettings'] = self._default_retry_settings.to_json()
299
302
 
303
+ if self._max_time_queue_seconds is not None:
304
+ json_pool['maxTimeQueueSeconds'] = self._max_time_queue_seconds
305
+
300
306
  if self._scheduling_type is not None:
301
307
  json_pool['schedulingType'] = self._scheduling_type.schedulingType
302
308
 
@@ -1431,6 +1437,22 @@ class Pool(object):
1431
1437
 
1432
1438
  self._privileges._exportApiAndStorageCredentialsInEnvironment = True
1433
1439
 
1440
+ @property
1441
+ def max_time_queue_seconds(self):
1442
+ """
1443
+ :type: :class:`uint`
1444
+ :getter: Max time to wait before time out when there is not any place to execute the pool.
1445
+
1446
+ pool's max time queue seconds
1447
+ """
1448
+ self._update_if_summary()
1449
+ return self._max_time_queue_seconds
1450
+
1451
+ @max_time_queue_seconds.setter
1452
+ def max_time_queue_seconds(self, value: int):
1453
+ """Setter for max_time_queue_seconds."""
1454
+ self._max_time_queue_seconds = value
1455
+
1434
1456
  @property
1435
1457
  def default_retry_settings(self) -> RetrySettings:
1436
1458
  """:type: :class:`~qarnot.retry_settings.RetrySettings`
@@ -19,7 +19,7 @@ from os import makedirs, path
19
19
  import time
20
20
  import warnings
21
21
  import sys
22
- from typing import Dict, Optional, Union, List, Any, Callable
22
+ from typing import Dict, Optional, Union, List, Any, Callable, Sequence
23
23
 
24
24
  from qarnot.carbon_facts import CarbonClient, CarbonFacts
25
25
  from qarnot.retry_settings import RetrySettings
@@ -40,6 +40,7 @@ from .exceptions import MissingTaskException, MaxTaskException, NotEnoughCredits
40
40
 
41
41
  try:
42
42
  from progressbar import AnimatedMarker, Bar, Percentage, AdaptiveETA, ProgressBar
43
+ from progressbar.widgets import WidgetBase
43
44
  except ImportError:
44
45
  pass
45
46
 
@@ -172,6 +173,7 @@ class Task(object):
172
173
  self._progress = None
173
174
  self._execution_time = None
174
175
  self._wall_time = None
176
+ self._max_time_queue_seconds: int = None
175
177
  self._end_date = None
176
178
  self._upload_results_on_cancellation: Optional[bool] = None
177
179
  self._hardware_constraints: List[HardwareConstraint] = []
@@ -513,6 +515,7 @@ class Task(object):
513
515
  self._progress = json_task.get("progress", None)
514
516
  self._execution_time = json_task.get("executionTime", None)
515
517
  self._wall_time = json_task.get("wallTime", None)
518
+ self._max_time_queue_seconds = json_task.get("maxTimeQueueSeconds", None)
516
519
  self._end_date = json_task.get("endDate", None)
517
520
  self._labels = json_task.get("labels", {})
518
521
  self._hardware_constraints = [HardwareConstraint.from_json(hw_constraint_dict) for hw_constraint_dict in json_task.get("hardwareConstraints", [])]
@@ -589,7 +592,7 @@ class Task(object):
589
592
 
590
593
  if live_progress:
591
594
  try:
592
- widgets = [
595
+ widgets: Sequence[WidgetBase | str] = [
593
596
  Percentage(),
594
597
  ' ', AnimatedMarker(),
595
598
  ' ', Bar(),
@@ -1778,6 +1781,22 @@ class Task(object):
1778
1781
  self._update_if_summary()
1779
1782
  return self._wall_time
1780
1783
 
1784
+ @property
1785
+ def max_time_queue_seconds(self):
1786
+ """
1787
+ :type: :class:`uint`
1788
+ :getter: Max time to wait before time out when there is not any place to execute the task.
1789
+
1790
+ task's max time queue seconds
1791
+ """
1792
+ self._update_if_summary()
1793
+ return self._max_time_queue_seconds
1794
+
1795
+ @max_time_queue_seconds.setter
1796
+ def max_time_queue_seconds(self, value: int):
1797
+ """Setter for max_time_queue_seconds."""
1798
+ self._max_time_queue_seconds = value
1799
+
1781
1800
  @property
1782
1801
  def snapshot_interval(self):
1783
1802
  """
@@ -1836,6 +1855,9 @@ class Task(object):
1836
1855
  self._resource_object_advanced = [x.to_json() for x in self._resource_objects]
1837
1856
  json_task['advancedResourceBuckets'] = self._resource_object_advanced
1838
1857
 
1858
+ if self._max_time_queue_seconds is not None:
1859
+ json_task['maxTimeQueueSeconds'] = self._max_time_queue_seconds
1860
+
1839
1861
  if self._result_object is not None:
1840
1862
  json_task['resultBucket'] = self._result_object.uuid
1841
1863
  if self._result_object._cache_ttl_sec is not None:
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: qarnot
3
- Version: 2.17.0
3
+ Version: 2.18.0
4
4
  Summary: Qarnot Computing SDK
5
5
  Home-page: https://computing.qarnot.com
6
6
  Author: Qarnot computing
@@ -27,6 +27,7 @@ Dynamic: classifier
27
27
  Dynamic: description
28
28
  Dynamic: home-page
29
29
  Dynamic: license
30
+ Dynamic: license-file
30
31
  Dynamic: requires-dist
31
32
  Dynamic: requires-python
32
33
  Dynamic: summary
@@ -22,6 +22,7 @@ doc/source/api/connection.rst
22
22
  doc/source/api/exceptions.rst
23
23
  doc/source/api/compute/carbon_facts.rst
24
24
  doc/source/api/compute/computeindex.rst
25
+ doc/source/api/compute/computing_quotas.rst
25
26
  doc/source/api/compute/forced_network_rule.rst
26
27
  doc/source/api/compute/hardware_constraint.rst
27
28
  doc/source/api/compute/job.rst
@@ -45,6 +46,7 @@ qarnot/_version.py
45
46
  qarnot/advanced_bucket.py
46
47
  qarnot/bucket.py
47
48
  qarnot/carbon_facts.py
49
+ qarnot/computing_quotas.py
48
50
  qarnot/connection.py
49
51
  qarnot/error.py
50
52
  qarnot/exceptions.py
@@ -0,0 +1,6 @@
1
+ requests==2.32.3
2
+ boto3==1.38.18
3
+ wheel==0.45.1
4
+ deprecation==2.1.0
5
+ simplejson==3.20.1
6
+ setuptools==80.7.1
@@ -442,7 +442,7 @@ class TestConnectionPaginateMethods():
442
442
  next(iterator)
443
443
  assert mock_page_call.call_count == 2
444
444
 
445
- def test_user_information(self):
445
+ def test_user_information_legacy_computing_quotas(self):
446
446
  connec = self.get_connection()
447
447
  with patch("qarnot.connection.Connection._get") as get_user:
448
448
  user_json = {
@@ -464,8 +464,12 @@ class TestConnectionPaginateMethods():
464
464
  "runningCoreCount":19,
465
465
  "maxFlexInstances":20,
466
466
  "maxFlexCores":21,
467
- "maxOnDemandInstances":22,
468
- "maxOnDemandCores":23,
467
+ "runningFlexCoreCount":22,
468
+ "runningFlexInstanceCount":23,
469
+ "maxOnDemandInstances":24,
470
+ "maxOnDemandCores":25,
471
+ "runningOnDemandCoreCount":26,
472
+ "runningOnDemandInstanceCount":27,
469
473
  }
470
474
  get_user.return_value.status_code = 200
471
475
  get_user.return_value.json.return_value = user_json
@@ -492,6 +496,117 @@ class TestConnectionPaginateMethods():
492
496
  assert user.max_on_demand_instances == user_json['maxOnDemandInstances']
493
497
  assert user.max_on_demand_cores == user_json['maxOnDemandCores']
494
498
 
499
+ assert user.computing_quotas.user.flex.max_instances == user_json['maxFlexInstances']
500
+ assert user.computing_quotas.user.flex.max_cores == user_json['maxFlexCores']
501
+ assert user.computing_quotas.user.flex.running_cores_count == user_json['runningFlexCoreCount']
502
+ assert user.computing_quotas.user.flex.running_instances_count == user_json['runningFlexInstanceCount']
503
+ assert user.computing_quotas.user.on_demand.max_instances == user_json['maxOnDemandInstances']
504
+ assert user.computing_quotas.user.on_demand.max_cores == user_json['maxOnDemandCores']
505
+ assert user.computing_quotas.user.on_demand.running_cores_count == user_json['runningOnDemandCoreCount']
506
+ assert user.computing_quotas.user.on_demand.running_instances_count == user_json['runningOnDemandInstanceCount']
507
+ assert user.computing_quotas.user.reserved == []
508
+
509
+ def test_user_information_new_format_computing_quotas(self):
510
+ connec = self.get_connection()
511
+ with patch("qarnot.connection.Connection._get") as get_user:
512
+ user_json = {
513
+ "email":"",
514
+ "maxBucket":5,
515
+ "bucketCount":6,
516
+ "quotaBytesBucket":7,
517
+ "usedQuotaBytesBucket":8,
518
+ "taskCount":9,
519
+ "maxTask":10,
520
+ "runningTaskCount":11,
521
+ "maxRunningTask":12,
522
+ "maxInstances":13,
523
+ "maxPool":14,
524
+ "poolCount":15,
525
+ "maxRunningPool":16,
526
+ "runningPoolCount":17,
527
+ "runningInstanceCount":18,
528
+ "runningCoreCount":19,
529
+ "computingQuotas": {
530
+ "user": {
531
+ "flex": {
532
+ "maxInstances": 64,
533
+ "maxCores": 512,
534
+ "runningInstancesCount": 1,
535
+ "runningCoresCount": 2
536
+ },
537
+ "onDemand": {
538
+ "maxInstances": 642,
539
+ "maxCores": 5122,
540
+ "runningInstancesCount": 3,
541
+ "runningCoresCount": 4
542
+ },
543
+ "reserved": [
544
+ {
545
+ "machineKey": "string",
546
+ "maxInstances": 643,
547
+ "maxCores": 5123,
548
+ "runningInstancesCount": 5,
549
+ "runningCoresCount": 6
550
+ }
551
+ ]
552
+ },
553
+ "organization": {
554
+ "name": "string",
555
+ "flex": {
556
+ "maxInstances": 644,
557
+ "maxCores": 5124,
558
+ "runningInstancesCount": 7,
559
+ "runningCoresCount": 8
560
+ },
561
+ "onDemand": {
562
+ "maxInstances": 645,
563
+ "maxCores": 5125,
564
+ "runningInstancesCount": 9,
565
+ "runningCoresCount": 10
566
+ },
567
+ "reserved": [
568
+ {
569
+ "machineKey": "string",
570
+ "maxInstances": 646,
571
+ "maxCores": 5126,
572
+ "runningInstancesCount": 11,
573
+ "runningCoresCount": 12
574
+ }
575
+ ]
576
+ }
577
+ },
578
+ }
579
+ get_user.return_value.status_code = 200
580
+ get_user.return_value.json.return_value = user_json
581
+ user = connec.user_info
582
+ assert user.email == user_json.get('email', '')
583
+ assert user.max_bucket == user_json['maxBucket']
584
+ assert user.bucket_count == user_json.get('bucketCount', -1)
585
+ assert user.quota_bytes_bucket == user_json['quotaBytesBucket']
586
+ assert user.used_quota_bytes_bucket == user_json['usedQuotaBytesBucket']
587
+ assert user.task_count == user_json['taskCount']
588
+ assert user.max_task == user_json['maxTask']
589
+ assert user.running_task_count == user_json['runningTaskCount']
590
+ assert user.max_running_task == user_json['maxRunningTask']
591
+ assert user.max_instances == user_json['maxInstances']
592
+ assert user.max_pool == user_json['maxPool']
593
+ assert user.pool_count == user_json['poolCount']
594
+ assert user.max_running_pool == user_json['maxRunningPool']
595
+ assert user.running_pool_count == user_json['runningPoolCount']
596
+ assert user.running_instance_count == user_json['runningInstanceCount']
597
+ assert user.running_core_count == user_json['runningCoreCount']
598
+
599
+
600
+ assert user.computing_quotas.user.flex.max_instances == user_json['computingQuotas']['user']['flex']['maxInstances']
601
+ assert user.computing_quotas.user.flex.max_cores == user_json['computingQuotas']['user']['flex']['maxCores']
602
+ assert user.computing_quotas.user.flex.running_cores_count == user_json['computingQuotas']['user']['flex']['runningCoresCount']
603
+ assert user.computing_quotas.user.flex.running_instances_count == user_json['computingQuotas']['user']['flex']['runningInstancesCount']
604
+
605
+ assert user.computing_quotas.organization.on_demand.max_instances == user_json['computingQuotas']['organization']['onDemand']['maxInstances']
606
+ assert user.computing_quotas.organization.on_demand.max_cores == user_json['computingQuotas']['organization']['onDemand']['maxCores']
607
+ assert user.computing_quotas.organization.on_demand.running_cores_count == user_json['computingQuotas']['organization']['onDemand']['runningCoresCount']
608
+ assert user.computing_quotas.organization.on_demand.running_instances_count == user_json['computingQuotas']['organization']['onDemand']['runningInstancesCount']
609
+
495
610
  def test_user_with_missing_information(self): # To ensure it doesn't throw when modifying a field in rest-computing side
496
611
  connec = self.get_connection()
497
612
  with patch("qarnot.connection.Connection._get") as get_user:
@@ -44,14 +44,31 @@ class TestPoolProperties:
44
44
  pool.completion_ttl = "4.11:08:06"
45
45
  assert "4.11:08:06" == pool.completion_ttl
46
46
 
47
- def test_pool_are_in_pool_to_json(self):
47
+ def test_fields_are_in_json_from_task(self):
48
48
  pool = Pool(self.conn, "pool-name", "profile")
49
49
  pool.completion_ttl = "4.11:08:06"
50
50
  pool.auto_delete = True
51
+ pool.max_time_queue_seconds = 10
51
52
  json_pool = pool._to_json()
52
53
 
53
54
  assert json_pool['completionTimeToLive'] == '4.11:08:06'
54
55
  assert json_pool['autoDeleteOnCompletion'] == True
56
+ assert json_pool['maxTimeQueueSeconds'] == 10
57
+
58
+ def test_fields_are_in_task_from_json(self):
59
+ json_pool = {}
60
+ json_pool['maxTimeQueueSeconds'] = 10
61
+
62
+ # fields that need to be non null for the deserialization to not fail
63
+ json_pool['creationDate'] = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
64
+ json_pool['uuid'] = str(uuid.uuid4())
65
+ json_pool['state'] = 'Submitted'
66
+ json_pool['runningCoreCount'] = 0
67
+ json_pool['runningInstanceCount'] = 0
68
+
69
+ pool_from_json = Pool(self.conn, "pool-name", "profile")
70
+ pool_from_json._update(json_pool)
71
+ assert pool_from_json.max_time_queue_seconds == 10
55
72
 
56
73
  def test_update_resources_send_the_good_url(self):
57
74
  update_connection = MockConnection()
@@ -70,6 +87,7 @@ class TestPoolProperties:
70
87
  ("execution_time", None),
71
88
  ("end_date", None),
72
89
  ("tasks_default_wait_for_pool_resources_synchronization", False),
90
+ ("max_time_queue_seconds", None),
73
91
  ("privileges", Privileges()),
74
92
  ])
75
93
  def test_pool_property_default_value(self, property_name, expected_value):
@@ -96,12 +114,21 @@ class TestPoolProperties:
96
114
  ("privileges", default_json_pool["privileges"]),
97
115
  ("defaultRetrySettings", default_json_pool["defaultRetrySettings"]),
98
116
  ])
99
- def test_pool_property_send_to_json_representation(self, property_name, expected_value):
117
+ def test_pool_property_with_default_send_to_json_representation(self, property_name, expected_value):
100
118
  pool = Pool(self.conn, "pool-name", "profile")
101
119
  pool._update(default_json_pool)
102
120
  pool_json = pool._to_json()
103
121
  assert pool_json[property_name] == expected_value
104
122
 
123
+ @pytest.mark.parametrize("property_name", [
124
+ ("max_time_queue_seconds"),
125
+ ])
126
+ def test_task_property_default_not_send_in_json(self, property_name):
127
+ pool = Pool(self.conn, "pool-name", "profile")
128
+ pool._update(default_json_pool)
129
+ pool_json = pool._to_json()
130
+ assert property_name not in pool_json
131
+
105
132
  @pytest.mark.parametrize("property_name, set_value, expected_value", [
106
133
  ("name", "name", "name")
107
134
  ])
@@ -50,14 +50,31 @@ class TestTaskProperties:
50
50
  task.completion_ttl = "4.11:08:06"
51
51
  assert "4.11:08:06" == task.completion_ttl
52
52
 
53
- def test_task_are_in_task_to_json(self, mock_conn):
53
+ def test_fields_are_in_json_from_task(self, mock_conn):
54
54
  task = Task(mock_conn, "task-name")
55
55
  task.completion_ttl = "4.11:08:06"
56
56
  task.auto_delete = True
57
+ task.max_time_queue_seconds = 10
57
58
  json_task = task._to_json() # pylint: disable=protected-access
58
59
 
59
60
  assert json_task['completionTimeToLive'] == '4.11:08:06'
60
61
  assert json_task['autoDeleteOnCompletion'] is True
62
+ assert json_task['maxTimeQueueSeconds'] == 10
63
+
64
+ def test_fields_are_in_task_from_json(self, mock_conn):
65
+ json_task = {}
66
+ json_task['maxTimeQueueSeconds'] = 10
67
+
68
+ # fields that need to be non null for the deserialization to not fail
69
+ json_task['creationDate'] = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
70
+ json_task['uuid'] = str(uuid.uuid4())
71
+ json_task['state'] = 'Submitted'
72
+ json_task['runningCoreCount'] = 0
73
+ json_task['runningInstanceCount'] = 0
74
+
75
+ task = Task.from_json(mock_conn, json_task)
76
+ assert task.max_time_queue_seconds == 10
77
+
61
78
 
62
79
  @pytest.mark.parametrize("property_name, expected_value", [
63
80
  ("previous_state", None),
@@ -69,6 +86,7 @@ class TestTaskProperties:
69
86
  ("execution_time", None),
70
87
  ("wall_time", None),
71
88
  ("end_date", None),
89
+ ("max_time_queue_seconds", None),
72
90
  ("privileges", Privileges()),
73
91
  ])
74
92
  def test_task_property_default_value(self, mock_conn, property_name, expected_value):
@@ -103,6 +121,14 @@ class TestTaskProperties:
103
121
  task_json = task._to_json()
104
122
  assert task_json[property_name] == expected_value
105
123
 
124
+ @pytest.mark.parametrize("property_name", [
125
+ ("max_time_queue_seconds"),
126
+ ])
127
+ def test_task_property_default_not_send_in_json(self, mock_conn, property_name):
128
+ task = Task(mock_conn, "task-name")
129
+ task_json = task._to_json()
130
+ assert property_name not in task_json
131
+
106
132
  @pytest.mark.parametrize("property_name, set_value, expected_value", [
107
133
  ("name", "name", "name")
108
134
  ])
@@ -499,8 +525,8 @@ class TestTaskProperties:
499
525
  capturedStderr = StringIO()
500
526
  sys.stderr = capturedStderr
501
527
 
502
- mock_conn.logger = Log.get_logger_for_stream(sys.stdout)
503
- mock_conn.logger_stderr = Log.get_logger_for_stream(sys.stderr)
528
+ mock_conn.logger = Log.get_logger_for_stream(sys.stdout, "stdout")
529
+ mock_conn.logger_stderr = Log.get_logger_for_stream(sys.stderr, "stderr")
504
530
  # Mock the responses for task update
505
531
  task = Task(mock_conn, "task-name")
506
532
  task_json = copy.deepcopy(default_json_task)
@@ -563,6 +589,10 @@ class TestTaskProperties:
563
589
  assert warn_logs is not None, "the stderr should contain task update stderr"
564
590
  for state in states:
565
591
  assert state in info_logs, "All state updates should be printed on stdout"
592
+ assert info_logs.count(state) == 1, "All state logs should be printed only once"
566
593
  for i in range(0,len(states)):
567
594
  assert "stdout %s" % i in info_logs, "All task stdout should be printed to user logs stream with info level"
595
+ assert info_logs.count("stdout %s" % i) == 2, "All stdout logs should be printed only once" # each stdout logs are here sent twice in the test (before and after state change)
568
596
  assert "stderr %s" % i in warn_logs, "All task stderr should be printed to user logs stream with warning level"
597
+ assert warn_logs.count("stderr %s" % i) == 2, "All stderr logs should be printed only once" # each stderr logs are here sent twice in the test (before and after state change)
598
+ assert "stderr %s" % i not in info_logs, "All task stderr should not be printed to user logs stream with info level"
@@ -1,6 +0,0 @@
1
- requests==2.32.3
2
- boto3==1.36.6
3
- wheel==0.45.1
4
- deprecation==2.1.0
5
- simplejson==3.19.3
6
- setuptools==75.7.0
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes