qarnot 2.17.0__py3-none-any.whl → 2.19.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
qarnot/_version.py CHANGED
@@ -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-09-04T17:06:17+0200",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "559a43baceed056873c5b9dab07d987025306be8",
15
- "version": "v2.17.0"
14
+ "full-revisionid": "2436d80006a87be548b2235b12f0a6f041ef66c1",
15
+ "version": "v2.19.0"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -0,0 +1,340 @@
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, reservation_name: str, 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
+ self.reservation_name = reservation_name
90
+ """:type: :class:`str`
91
+
92
+ Name of the reservation.
93
+ """
94
+
95
+ @classmethod
96
+ def from_json(cls, json: Dict[str, Any]):
97
+ """Create a new UserReservedSchedulingQuota object from json describing a reserved scheduling quota for a user.
98
+
99
+ :param dict json: Dictionary representing the user reserved scheduling quota
100
+ :returns: The created :class:`~qarnot.computing_quotas.UserReservedSchedulingQuota`.
101
+ """
102
+ if json is None:
103
+ return None
104
+ return cls(
105
+ json.get('reservationName'),
106
+ json.get('machineKey'),
107
+ json.get('maxCores'),
108
+ json.get('runningCoresCount'),
109
+ json.get('maxInstances'),
110
+ json.get('runningInstancesCount'),
111
+ )
112
+
113
+
114
+ class UserComputingQuotas(object):
115
+ """Describes the user's computing quotas.
116
+ """
117
+
118
+ def __init__(self, flex: UserSchedulingQuota, on_demand: UserSchedulingQuota, reserved: List[UserReservedSchedulingQuota]):
119
+ """Create a new UserComputingQuotas object describing the user's computing quotas.
120
+
121
+ :param `~qarnot.computing_quotas.UserSchedulingQuota` flex: Quotas for Flex scheduling plan.
122
+ :param `~qarnot.computing_quotas.UserSchedulingQuota` on_demand: Quotas for OnDemand scheduling plan.
123
+ :param List of `~qarnot.computing_quotas.UserReservedSchedulingQuota` reserved: Quotas for Reserved scheduling plan.
124
+ :returns: The created :class:`~qarnot.computing_quotas.UserComputingQuotas`.
125
+ """
126
+ self.flex = flex
127
+ """:type: :class:`~qarnot.computing_quotas.UserSchedulingQuota`
128
+
129
+ Quotas for Flex scheduling plan."""
130
+ self.on_demand = on_demand
131
+ """:type: :class:`~qarnot.computing_quotas.UserSchedulingQuota`
132
+
133
+ Quotas for OnDemand scheduling plan."""
134
+ self.reserved = reserved
135
+ """:type: list(:class:`~qarnot.computing_quotas.UserReservedSchedulingQuota`)
136
+
137
+ Quotas for Reserved scheduling plan."""
138
+
139
+ @classmethod
140
+ def from_json(cls, json: Dict[str, Any]):
141
+ """Create a new UserComputingQuotas object from json describing the user's computing quotas.
142
+
143
+ :param dict json: Dictionary representing the user computing quota
144
+ :returns: The created :class:`~qarnot.computing_quotas.UserComputingQuotas`.
145
+ """
146
+ if json is None:
147
+ return None
148
+ return cls(
149
+ UserSchedulingQuota.from_json(json.get('flex')),
150
+ UserSchedulingQuota.from_json(json.get('onDemand')),
151
+ [UserReservedSchedulingQuota.from_json(v) for v in (json.get('reserved') if json.get('reserved') is not None else []) if v is not None]
152
+ )
153
+
154
+
155
+ class OrganizationSchedulingQuota(object):
156
+ """Describes a scheduling quota for the organization.
157
+ """
158
+
159
+ def __init__(self, max_cores: int, running_cores_count: int, max_instances: int, running_instances_count: int):
160
+ """Create a new OrganizationSchedulingQuota object describing a scheduling quota for the organization.
161
+
162
+ :param int max_cores: Maximum number of cores that can be simultaneously used with this scheduling plan.
163
+ :param int running_cores_count: Number of cores that are currently running with this scheduling plan.
164
+ :param int max_instances: Maximum number of instances that can be simultaneously used with this scheduling plan.
165
+ :param int running_instances_count: Number of instances that are currently running with this scheduling plan.
166
+ :returns: The created :class:`~qarnot.computing_quotas.OrganizationSchedulingQuota`.
167
+ """
168
+ self.max_cores = max_cores
169
+ """:type: :class:`int`
170
+
171
+ Maximum number of cores that can be simultaneously used with this scheduling plan.
172
+ """
173
+ self.running_cores_count = running_cores_count
174
+ """:type: :class:`int`
175
+
176
+ Number of cores that are currently running with this scheduling plan.
177
+ """
178
+ self.max_instances = max_instances
179
+ """:type: :class:`int`
180
+
181
+ Maximum number of instances that can be simultaneously used with this scheduling plan.
182
+ """
183
+ self.running_instances_count = running_instances_count
184
+ """:type: :class:`int`
185
+
186
+ Number of instances that are currently running with this scheduling plan.
187
+ """
188
+
189
+ @classmethod
190
+ def from_json(cls, json: Dict[str, Any]):
191
+ """Create a new OrganizationSchedulingQuota object from json describing a scheduling quota for the organization.
192
+
193
+ :param dict json: Dictionary representing the organization scheduling plan
194
+ :returns: The created :class:`~qarnot.computing_quotas.OrganizationSchedulingQuota`.
195
+ """
196
+ if json is None:
197
+ return None
198
+ return cls(
199
+ json.get('maxCores'),
200
+ json.get('runningCoresCount'),
201
+ json.get('maxInstances'),
202
+ json.get('runningInstancesCount'),
203
+ )
204
+
205
+
206
+ class OrganizationReservedSchedulingQuota(OrganizationSchedulingQuota):
207
+ """Describes a reserved scheduling quota for the organization.
208
+ """
209
+
210
+ def __init__(self, reservation_name: str, machine_key: str, max_cores: int, running_cores_count: int, max_instances: int, running_instances_count: int):
211
+ """Create a new OrganizationReservedSchedulingQuota object describing a reserved scheduling quota for the organization.
212
+
213
+ :param str machine_key: Machine key of the reservation.
214
+ :param int max_cores: Maximum number of cores that can be simultaneously used with this reserved machine specification.
215
+ :param int running_cores_count: Number of cores that are currently running with this reserved machine specification.
216
+ :param int max_instances: Maximum number of instances that can be simultaneously used with this reserved machine specification.
217
+ :param int running_instances_count: Number of instances that are currently running with this reserved machine specification.
218
+ :returns: The created :class:`~qarnot.computing_quotas.OrganizationReservedSchedulingQuota`.
219
+ """
220
+ super().__init__(max_cores, running_cores_count, max_instances, running_instances_count)
221
+ self.machine_key = machine_key
222
+ """:type: :class:`str`
223
+
224
+ Machine key of the reservation.
225
+ """
226
+ self.reservation_name = reservation_name
227
+ """:type: :class:`str`
228
+
229
+ Name of the reservation.
230
+ """
231
+
232
+ @classmethod
233
+ def from_json(cls, json: Dict[str, Any]):
234
+ """Create a new OrganizationReservedSchedulingQuota object from json describing a reserved scheduling quota for a organization.
235
+
236
+ :param dict json: Dictionary representing the organization reserved scheduling quota
237
+ :returns: The created :class:`~qarnot.computing_quotas.OrganizationReservedSchedulingQuota`.
238
+ """
239
+ if json is None:
240
+ return None
241
+ return cls(
242
+ json.get('reservationName'),
243
+ json.get('machineKey'),
244
+ json.get('maxCores'),
245
+ json.get('runningCoresCount'),
246
+ json.get('maxInstances'),
247
+ json.get('runningInstancesCount'),
248
+ )
249
+
250
+
251
+ class OrganizationComputingQuotas(object):
252
+ """Describes the organization's computing quotas.
253
+ """
254
+
255
+ def __init__(self, name: str, flex: OrganizationSchedulingQuota, on_demand: OrganizationSchedulingQuota, reserved: List[OrganizationReservedSchedulingQuota]):
256
+ """Create a new OrganizationComputingQuotas object describing the organization's computing quotas.
257
+
258
+ :param `str` name: Name of the organization.
259
+ :param `~qarnot.computing_quotas.OrganizationSchedulingQuota` flex: Quotas for Flex scheduling plan.
260
+ :param `~qarnot.computing_quotas.OrganizationSchedulingQuota` on_demand: Quotas for OnDemand scheduling plan.
261
+ :param List of `~qarnot.computing_quotas.OrganizationReservedSchedulingQuota` reserved: Quotas for Reserved scheduling plan.
262
+ :returns: The created :class:`~qarnot.computing_quotas.OrganizationComputingQuotas`.
263
+ """
264
+ self.name = name
265
+ """:type: :class:`str`
266
+
267
+ Name of the organization."""
268
+ self.flex = flex
269
+ """:type: :class:`~qarnot.computing_quotas.OrganizationSchedulingQuota`
270
+
271
+ Quotas for Flex scheduling plan."""
272
+ self.on_demand = on_demand
273
+ """:type: :class:`~qarnot.computing_quotas.OrganizationSchedulingQuota`
274
+
275
+ Quotas for OnDemand scheduling plan."""
276
+ self.reserved = reserved
277
+ """:type: list(:class:`~qarnot.computing_quotas.OrganizationReservedSchedulingQuota`)
278
+
279
+ Quotas for Reserved scheduling plan."""
280
+
281
+ @classmethod
282
+ def from_json(cls, json: Dict[str, Any]):
283
+ """Create a new OrganizationComputingQuotas object from json describing the organization's computing quotas.
284
+
285
+ :param dict json: Dictionary representing the organization computing quota
286
+ :returns: The created :class:`~qarnot.computing_quotas.OrganizationComputingQuotas`.
287
+ """
288
+ if json is None:
289
+ return None
290
+ return cls(
291
+ json.get('name'),
292
+ OrganizationSchedulingQuota.from_json(json.get('flex')),
293
+ OrganizationSchedulingQuota.from_json(json.get('onDemand')),
294
+ [OrganizationReservedSchedulingQuota.from_json(v) for v in (json.get('reserved') if json.get('reserved') is not None else []) if v is not None]
295
+ )
296
+
297
+
298
+ class ComputingQuotas(object):
299
+ """Describes user and organization computing quotas.
300
+ """
301
+
302
+ def __init__(self, user_computing_quotas: Optional[UserComputingQuotas], organization_computing_quotas: Optional[OrganizationComputingQuotas] = None):
303
+ """Create a new ComputingQuotas object describing user and organization computing quotas.
304
+
305
+ :param user_computing_quotas: the user related computing quotas
306
+ :type user_computing_quotas: `~qarnot.computing_quotas.UserComputingQuotas`, optional
307
+ :param organization_computing_quotas: the organization related computing quotas
308
+ :type organization_computing_quotas: `~qarnot.computing_quotas.OrganizationComputingQuotas`, optional
309
+ :returns: The created :class:`~qarnot.computing_quotas.ComputingQuotas`.
310
+ """
311
+ self.user = user_computing_quotas
312
+ """:type: :class:`~qarnot.computing_quotas.UserComputingQuotas`
313
+
314
+ The user related computing quotas."""
315
+ self.organization = organization_computing_quotas
316
+ """:type: :class:`~qarnot.computing_quotas.OrganizationComputingQuotas`
317
+
318
+ The organization related computing quotas."""
319
+
320
+ @classmethod
321
+ def from_json(cls, json: Dict[str, Any]):
322
+ """Create a new ComputingQuotas object from json describing user and organization computing quotas.
323
+
324
+ :param dict json: Dictionary representing the computing quotas
325
+ :returns: The created :class:`~qarnot.computing_quotas.ComputingQuotas`
326
+ """
327
+ if json is None:
328
+ return None
329
+ user_computing_quotas = UserComputingQuotas.from_json(json.get('user'))
330
+ organization_computing_quotas = OrganizationComputingQuotas.from_json(json.get('organization'))
331
+ return cls(user_computing_quotas, organization_computing_quotas)
332
+
333
+ @classmethod
334
+ def from_json_legacy(cls, json: Dict[str, Any]):
335
+ if json is None:
336
+ return None
337
+ flex = UserSchedulingQuota(json.get('maxFlexCores'), json.get('runningFlexCoreCount'), json.get('maxFlexInstances'), json.get('runningFlexInstanceCount'))
338
+ onDemand = UserSchedulingQuota(json.get('maxOnDemandCores'), json.get('runningOnDemandCoreCount'), json.get('maxOnDemandInstances'), json.get('runningOnDemandInstanceCount'))
339
+ user = UserComputingQuotas(flex, onDemand, [])
340
+ return cls(user, None)
qarnot/connection.py CHANGED
@@ -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
 
@@ -20,7 +20,18 @@ class ForcedNetworkRule(object):
20
20
  priority: str = None,
21
21
  description: str = None,
22
22
  to_qbox: Optional[bool] = None,
23
- to_payload: Optional[bool] = None):
23
+ to_payload: Optional[bool] = None,
24
+ name: str = None,
25
+ application_type: str = None):
26
+
27
+ self.name = name
28
+ """:type: :class:`str`
29
+
30
+ Name of the associated rule."""
31
+ self.application_type = application_type
32
+ """:type: :class:`str`
33
+
34
+ Application layer protocol used / hint about it (e.g. ssh, http, https...)."""
24
35
  self.inbound = inbound
25
36
  """:type: :class:`bool`
26
37
 
@@ -84,6 +95,14 @@ class ForcedNetworkRule(object):
84
95
  :returns: The created :class:`~qarnot.forced_network_rule.ForcedNetworkRule`
85
96
  """
86
97
 
98
+ name: str = None
99
+ if 'name' in json:
100
+ name = str(json.get("name"))
101
+
102
+ application_type: str = None
103
+ if 'applicationType' in json:
104
+ application_type = str(json.get("applicationType"))
105
+
87
106
  inbound: bool = bool(json.get("inbound"))
88
107
  proto: str = str(json.get("proto"))
89
108
 
@@ -96,12 +115,12 @@ class ForcedNetworkRule(object):
96
115
  to = str(json.get("to"))
97
116
 
98
117
  public_host: str = None
99
- if 'public_host' in json:
100
- public_host = str(json.get("public_host"))
118
+ if 'publicHost' in json:
119
+ public_host = str(json.get("publicHost"))
101
120
 
102
121
  public_port: str = None
103
- if 'public_port' in json:
104
- public_port = str(json.get("public_port"))
122
+ if 'publicPort' in json:
123
+ public_port = str(json.get("publicPort"))
105
124
 
106
125
  forwarder: str = None
107
126
  if 'forwarder' in json:
@@ -116,12 +135,12 @@ class ForcedNetworkRule(object):
116
135
  description = str(json.get("description"))
117
136
 
118
137
  to_qbox: Optional[bool] = None
119
- if 'to_qbox' in json:
120
- to_qbox = bool(json.get("to_qbox"))
138
+ if 'toQBox' in json:
139
+ to_qbox = bool(json.get("toQBox"))
121
140
 
122
141
  to_payload: Optional[bool] = None
123
- if 'to_payload' in json:
124
- to_payload = bool(json.get("to_payload"))
142
+ if 'toPayload' in json:
143
+ to_payload = bool(json.get("toPayload"))
125
144
 
126
145
  return ForcedNetworkRule(
127
146
  inbound,
@@ -134,7 +153,9 @@ class ForcedNetworkRule(object):
134
153
  priority,
135
154
  description,
136
155
  to_qbox,
137
- to_payload)
156
+ to_payload,
157
+ name,
158
+ application_type)
138
159
 
139
160
  def to_json(self):
140
161
  result: Dict[str, Union[str, bool]] = {
@@ -142,6 +163,12 @@ class ForcedNetworkRule(object):
142
163
  "proto": self.proto,
143
164
  }
144
165
 
166
+ if self.name is not None:
167
+ result["name"] = self.name
168
+
169
+ if self.application_type is not None:
170
+ result["applicationType"] = self.application_type
171
+
145
172
  if self.port is not None:
146
173
  result["port"] = self.port
147
174
 
@@ -149,10 +176,10 @@ class ForcedNetworkRule(object):
149
176
  result["to"] = self.to
150
177
 
151
178
  if self.public_host is not None:
152
- result["public_host"] = self.public_host
179
+ result["publicHost"] = self.public_host
153
180
 
154
181
  if self.public_port is not None:
155
- result["public_port"] = self.public_port
182
+ result["publicPort"] = self.public_port
156
183
 
157
184
  if self.forwarder is not None:
158
185
  result["forwarder"] = self.forwarder
@@ -164,9 +191,9 @@ class ForcedNetworkRule(object):
164
191
  result["description"] = self.description
165
192
 
166
193
  if self.to_qbox is not None:
167
- result["to_qbox"] = self.to_qbox
194
+ result["toQBox"] = self.to_qbox
168
195
 
169
196
  if self.to_payload is not None:
170
- result["to_payload"] = self.to_payload
197
+ result["toPayload"] = self.to_payload
171
198
 
172
199
  return result
qarnot/helper.py CHANGED
@@ -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
@@ -0,0 +1,48 @@
1
+ """Multi Slots Settings that can be used when creating a pool"""
2
+
3
+ from typing import Dict
4
+
5
+
6
+ class MultiSlotsSettings(object):
7
+ """Represents task multi slots settings."""
8
+
9
+ _slotsPerNode: int = None
10
+
11
+ def __init__(self, slotsPerNode: int = None):
12
+ """Create a new :class:`~qarnot.multi_slots_settings.MultiSlotsSettings`.
13
+
14
+ :param slotsPerNode: slots per node
15
+ :type slotsPerNode: int
16
+ """
17
+ self._slotsPerNode = slotsPerNode
18
+
19
+ @classmethod
20
+ def from_json(cls, json: Dict[str, int]):
21
+ """Create the multi slots settings from json.
22
+
23
+ :param dict json: Dictionary representing the multi slots settings
24
+ :returns: The created :class:`~qarnot.multi_slots_settings.MultiSlotsSettings`
25
+ """
26
+ slotsPerNode: int = json.get("slotsPerNode")
27
+ return MultiSlotsSettings(slotsPerNode)
28
+
29
+ def to_json(self) -> Dict[str, int]:
30
+ """Get a dict ready to be json packed.
31
+
32
+ :return: the json elements of the class.
33
+ :rtype: `dict`
34
+ """
35
+ return {
36
+ "slotsPerNode": self._slotsPerNode
37
+ }
38
+
39
+ def __eq__(self, other):
40
+ if other is None or not isinstance(other, MultiSlotsSettings):
41
+ return False
42
+ return self._slotsPerNode == other._slotsPerNode
43
+
44
+ def __str__(self) -> str:
45
+ return "multi slots settings: slotsPerNode {}.".format(self._slotsPerNode)
46
+
47
+ def __repr__(self) -> str:
48
+ return "multi_slots_settings.MultiSlotsSettings(slotsPerNode: {})".format(self._slotsPerNode)
qarnot/pool.py CHANGED
@@ -19,6 +19,7 @@ from typing import Dict, List, Optional
19
19
 
20
20
  from qarnot.carbon_facts import CarbonClient, CarbonFacts
21
21
  from qarnot.retry_settings import RetrySettings
22
+ from qarnot.multi_slots_settings import MultiSlotsSettings
22
23
  from qarnot.forced_network_rule import ForcedNetworkRule
23
24
  from qarnot.secrets import SecretsAccessRights
24
25
 
@@ -86,6 +87,7 @@ class Pool(object):
86
87
  self._update_cache_time = 5
87
88
  self._scheduling_type = scheduling_type
88
89
  self._targeted_reserved_machine_key: str = None
90
+ self._targeted_reservation_name: str = None
89
91
 
90
92
  self._last_cache = time.time()
91
93
  self._instancecount = instancecount
@@ -114,6 +116,7 @@ class Pool(object):
114
116
  self._queued_or_running_task_instances_count = 0.0
115
117
 
116
118
  self._completion_time_to_live = "00:00:00"
119
+ self._max_time_queue_seconds: int = None
117
120
  self._auto_delete = False
118
121
  self._tasks_wait_for_synchronization = False
119
122
 
@@ -127,6 +130,7 @@ class Pool(object):
127
130
  self._default_resources_cache_ttl_sec: Optional[int] = None
128
131
  self._privileges: Privileges = Privileges()
129
132
  self._default_retry_settings: RetrySettings = RetrySettings()
133
+ self._multi_slots_settings: Optional[MultiSlotsSettings] = None
130
134
  self._forced_network_rules: List[ForcedNetworkRule] = []
131
135
  self._secrets_access_rights: SecretsAccessRights = SecretsAccessRights()
132
136
 
@@ -215,6 +219,8 @@ class Pool(object):
215
219
  if 'completionTimeToLive' in json_pool:
216
220
  self._completion_time_to_live = json_pool.get("completionTimeToLive")
217
221
 
222
+ self._max_time_queue_seconds = json_pool.get("maxTimeQueueSeconds", None)
223
+
218
224
  if 'elasticProperty' in json_pool:
219
225
  elasticProperty = json_pool.get("elasticProperty")
220
226
  self._is_elastic = elasticProperty.get("isElastic")
@@ -235,10 +241,13 @@ class Pool(object):
235
241
  self._hardware_constraints = [HardwareConstraint.from_json(hw_constraint_dict) for hw_constraint_dict in json_pool.get("hardwareConstraints", [])]
236
242
  self._default_resources_cache_ttl_sec = json_pool.get("defaultResourcesCacheTTLSec", None)
237
243
  self._targeted_reserved_machine_key = json_pool.get("targetedReservedMachineKey", None)
244
+ self._targeted_reservation_name = json_pool.get("targetedReservationName", None)
238
245
  if 'privileges' in json_pool:
239
246
  self._privileges = Privileges.from_json(json_pool.get("privileges"))
240
247
  if 'defaultRetrySettings' in json_pool:
241
248
  self._default_retry_settings = RetrySettings.from_json(json_pool.get("defaultRetrySettings"))
249
+ if 'multiSlotsSettings' in json_pool:
250
+ self._multi_slots_settings = MultiSlotsSettings.from_json(json_pool.get("multiSlotsSettings"))
242
251
  if 'schedulingType' in json_pool:
243
252
  self._scheduling_type = SchedulingType.from_string(json_pool.get("schedulingType"))
244
253
  self._forced_network_rules = [ForcedNetworkRule.from_json(forced_network_dict) for forced_network_dict in json_pool.get("forcedNetworkRules", [])]
@@ -297,18 +306,27 @@ class Pool(object):
297
306
  json_pool['privileges'] = self._privileges.to_json()
298
307
  json_pool['defaultRetrySettings'] = self._default_retry_settings.to_json()
299
308
 
309
+ if self._max_time_queue_seconds is not None:
310
+ json_pool['maxTimeQueueSeconds'] = self._max_time_queue_seconds
311
+
300
312
  if self._scheduling_type is not None:
301
313
  json_pool['schedulingType'] = self._scheduling_type.schedulingType
302
314
 
303
315
  if self._targeted_reserved_machine_key is not None:
304
316
  json_pool['targetedReservedMachineKey'] = self._targeted_reserved_machine_key
305
317
 
318
+ if self._targeted_reservation_name is not None:
319
+ json_pool['targetedReservationName'] = self._targeted_reservation_name
320
+
306
321
  if self._forced_network_rules is not None:
307
322
  json_pool['forcedNetworkRules'] = [x.to_json() for x in self._forced_network_rules]
308
323
 
309
324
  if self._secrets_access_rights:
310
325
  json_pool['secretsAccessRights'] = self._secrets_access_rights.to_json()
311
326
 
327
+ if self._multi_slots_settings:
328
+ json_pool['multiSlotsSettings'] = self._multi_slots_settings.to_json()
329
+
312
330
  return json_pool
313
331
 
314
332
  def submit(self):
@@ -1273,6 +1291,24 @@ class Pool(object):
1273
1291
  raise AttributeError("can't set attribute on a submitted job")
1274
1292
  self._completion_time_to_live = _util.parse_to_timespan_string(value)
1275
1293
 
1294
+ @property
1295
+ def multi_slots_settings(self):
1296
+ """
1297
+ :getter: Returns this pool's multi slots settings.
1298
+ :type: :class:`~qarnot.multi_slots_settings.MultiSlotsSettings`
1299
+ :default_value: None
1300
+ """
1301
+ self._update_if_summary()
1302
+ return self._multi_slots_settings
1303
+
1304
+ @multi_slots_settings.setter
1305
+ def multi_slots_settings(self, value):
1306
+ """Setter for multi_slots_settings, this can only be set before pool's submission"""
1307
+ self._update_if_summary()
1308
+ if self._multi_slots_settings is not None:
1309
+ raise AttributeError("can't set attribute on a submitted pool")
1310
+ self._multi_slots_settings = value
1311
+
1276
1312
  @property
1277
1313
  def previous_state(self):
1278
1314
  """
@@ -1431,6 +1467,22 @@ class Pool(object):
1431
1467
 
1432
1468
  self._privileges._exportApiAndStorageCredentialsInEnvironment = True
1433
1469
 
1470
+ @property
1471
+ def max_time_queue_seconds(self):
1472
+ """
1473
+ :type: :class:`uint`
1474
+ :getter: Max time to wait before time out when there is not any place to execute the pool.
1475
+
1476
+ pool's max time queue seconds
1477
+ """
1478
+ self._update_if_summary()
1479
+ return self._max_time_queue_seconds
1480
+
1481
+ @max_time_queue_seconds.setter
1482
+ def max_time_queue_seconds(self, value: int):
1483
+ """Setter for max_time_queue_seconds."""
1484
+ self._max_time_queue_seconds = value
1485
+
1434
1486
  @property
1435
1487
  def default_retry_settings(self) -> RetrySettings:
1436
1488
  """:type: :class:`~qarnot.retry_settings.RetrySettings`
@@ -1475,6 +1527,9 @@ class Pool(object):
1475
1527
 
1476
1528
  :getter: The reserved machine key when using the "reserved" scheduling type
1477
1529
 
1530
+ .. deprecated:: v2.19.0
1531
+ Use `self.targeted_reservation_name` instead.
1532
+
1478
1533
  :raises AttributeError: trying to set this after the pool is submitted
1479
1534
  """
1480
1535
  return self._targeted_reserved_machine_key
@@ -1488,6 +1543,25 @@ class Pool(object):
1488
1543
 
1489
1544
  self._targeted_reserved_machine_key = value
1490
1545
 
1546
+ @property
1547
+ def targeted_reservation_name(self) -> str:
1548
+ """:type: :class:`str`
1549
+
1550
+ :getter: The name of the reservation that describes the targeted machines when using the "reserved" scheduling type
1551
+
1552
+ :raises AttributeError: trying to set this after the task is submitted
1553
+ """
1554
+ return self._targeted_reservation_name
1555
+
1556
+ @targeted_reservation_name.setter
1557
+ def targeted_reservation_name(self, value: str):
1558
+ """Setted for targeted_reservation_name
1559
+ """
1560
+ if self.uuid is not None:
1561
+ raise AttributeError("can't set attribute on a launched task")
1562
+
1563
+ self._targeted_reservation_name = value
1564
+
1491
1565
  def __repr__(self):
1492
1566
  return '{0} - {1} - {2} - {3} - {5} - InstanceCount : {4} - Resources : {6} '\
1493
1567
  'Tag {7} - IsElastic {8} - ElasticMin {9} - ElasticMax {10} - ElasticMinIdle {11} -'\
qarnot/task.py CHANGED
@@ -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
 
@@ -131,6 +132,7 @@ class Task(object):
131
132
  self._secrets_access_rights: SecretsAccessRights = SecretsAccessRights()
132
133
  self._scheduling_type = scheduling_type
133
134
  self._targeted_reserved_machine_key: str = None
135
+ self._targeted_reservation_name: str = None
134
136
  self._dependentOn: List[Uuid] = []
135
137
 
136
138
  self._auto_update = True
@@ -172,6 +174,7 @@ class Task(object):
172
174
  self._progress = None
173
175
  self._execution_time = None
174
176
  self._wall_time = None
177
+ self._max_time_queue_seconds: int = None
175
178
  self._end_date = None
176
179
  self._upload_results_on_cancellation: Optional[bool] = None
177
180
  self._hardware_constraints: List[HardwareConstraint] = []
@@ -513,11 +516,13 @@ class Task(object):
513
516
  self._progress = json_task.get("progress", None)
514
517
  self._execution_time = json_task.get("executionTime", None)
515
518
  self._wall_time = json_task.get("wallTime", None)
519
+ self._max_time_queue_seconds = json_task.get("maxTimeQueueSeconds", None)
516
520
  self._end_date = json_task.get("endDate", None)
517
521
  self._labels = json_task.get("labels", {})
518
522
  self._hardware_constraints = [HardwareConstraint.from_json(hw_constraint_dict) for hw_constraint_dict in json_task.get("hardwareConstraints", [])]
519
523
  self._default_resources_cache_ttl_sec = json_task.get("defaultResourcesCacheTTLSec", None)
520
524
  self._targeted_reserved_machine_key = json_task.get("targetedReservedMachineKey", None)
525
+ self._targeted_reservation_name = json_task.get("targetedReservationName", None)
521
526
  if 'privileges' in json_task:
522
527
  self._privileges = Privileges.from_json(json_task.get("privileges"))
523
528
  if 'retrySettings' in json_task:
@@ -589,7 +594,7 @@ class Task(object):
589
594
 
590
595
  if live_progress:
591
596
  try:
592
- widgets = [
597
+ widgets: Sequence[WidgetBase | str] = [
593
598
  Percentage(),
594
599
  ' ', AnimatedMarker(),
595
600
  ' ', Bar(),
@@ -1638,6 +1643,9 @@ class Task(object):
1638
1643
 
1639
1644
  :getter: The reserved machine key when using the "reserved" scheduling type
1640
1645
 
1646
+ .. deprecated:: v2.19.0
1647
+ Use `self.targeted_reservation_name` instead.
1648
+
1641
1649
  :raises AttributeError: trying to set this after the task is submitted
1642
1650
  """
1643
1651
  return self._targeted_reserved_machine_key
@@ -1651,6 +1659,25 @@ class Task(object):
1651
1659
 
1652
1660
  self._targeted_reserved_machine_key = value
1653
1661
 
1662
+ @property
1663
+ def targeted_reservation_name(self) -> str:
1664
+ """:type: :class:`str`
1665
+
1666
+ :getter: The name of the reservation that describes the targeted machines when using the "reserved" scheduling type
1667
+
1668
+ :raises AttributeError: trying to set this after the task is submitted
1669
+ """
1670
+ return self._targeted_reservation_name
1671
+
1672
+ @targeted_reservation_name.setter
1673
+ def targeted_reservation_name(self, value: str):
1674
+ """Setted for targeted_reservation_name
1675
+ """
1676
+ if self.uuid is not None:
1677
+ raise AttributeError("can't set attribute on a launched task")
1678
+
1679
+ self._targeted_reservation_name = value
1680
+
1654
1681
  @property
1655
1682
  def auto_delete(self):
1656
1683
  """Autodelete this Task if it is finished and your max number of task is reach
@@ -1778,6 +1805,22 @@ class Task(object):
1778
1805
  self._update_if_summary()
1779
1806
  return self._wall_time
1780
1807
 
1808
+ @property
1809
+ def max_time_queue_seconds(self):
1810
+ """
1811
+ :type: :class:`uint`
1812
+ :getter: Max time to wait before time out when there is not any place to execute the task.
1813
+
1814
+ task's max time queue seconds
1815
+ """
1816
+ self._update_if_summary()
1817
+ return self._max_time_queue_seconds
1818
+
1819
+ @max_time_queue_seconds.setter
1820
+ def max_time_queue_seconds(self, value: int):
1821
+ """Setter for max_time_queue_seconds."""
1822
+ self._max_time_queue_seconds = value
1823
+
1781
1824
  @property
1782
1825
  def snapshot_interval(self):
1783
1826
  """
@@ -1836,6 +1879,9 @@ class Task(object):
1836
1879
  self._resource_object_advanced = [x.to_json() for x in self._resource_objects]
1837
1880
  json_task['advancedResourceBuckets'] = self._resource_object_advanced
1838
1881
 
1882
+ if self._max_time_queue_seconds is not None:
1883
+ json_task['maxTimeQueueSeconds'] = self._max_time_queue_seconds
1884
+
1839
1885
  if self._result_object is not None:
1840
1886
  json_task['resultBucket'] = self._result_object.uuid
1841
1887
  if self._result_object._cache_ttl_sec is not None:
@@ -1870,6 +1916,9 @@ class Task(object):
1870
1916
  if self._targeted_reserved_machine_key is not None:
1871
1917
  json_task["targetedReservedMachineKey"] = self._targeted_reserved_machine_key
1872
1918
 
1919
+ if self._targeted_reservation_name is not None:
1920
+ json_task["targetedReservationName"] = self._targeted_reservation_name
1921
+
1873
1922
  if self._forced_network_rules is not None:
1874
1923
  json_task['forcedNetworkRules'] = [x.to_json() for x in self._forced_network_rules]
1875
1924
 
@@ -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.19.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
@@ -2,29 +2,31 @@ qarnot/__init__.py,sha256=BCIFvWIe1EbzdZAMeKnyK9HYIqNHlSsbaaDMIL7VghA,5626
2
2
  qarnot/_filter.py,sha256=J--0lOY2rverPEE3zrvuipYkOd9T_4HrW4eVNJqheac,10109
3
3
  qarnot/_retry.py,sha256=4QdtI5Y_Jshja8zDxhx_g17dzABCIUGXQsjcPl65u7g,890
4
4
  qarnot/_util.py,sha256=fmAq55CmfDTTB7yaOLspayC3Mvre7SCzp0YwWsnwpDA,5673
5
- qarnot/_version.py,sha256=chMgPEmbIXhGcBM27CW6fzvB4p7Bey3pm6cl8OJmKEI,499
5
+ qarnot/_version.py,sha256=cs1IDGNzxS3VmGc68RuKml3o2BY_Hw0Eo2b6p2eCVwI,499
6
6
  qarnot/advanced_bucket.py,sha256=dfhHOz0foJfEQoaCdGiS_-28w4Y6rq36ZloPgoEQiLE,7927
7
7
  qarnot/bucket.py,sha256=aNuJqfYUm2YCQ0m8SBl384xspYkc9SJy6_G-FEKs8nk,24449
8
8
  qarnot/carbon_facts.py,sha256=E_tBMsa2byEMgKAEilIFRsCE6_T978Zl7-ZXk4--PPI,10292
9
- qarnot/connection.py,sha256=qdwKRE66qeag9DKmLwK6VM0ppcK8h0Y2kyMjt3Y9F9o,45006
9
+ qarnot/computing_quotas.py,sha256=gXKAZ1QqnXGzR17yTYNmjDF1TBEXALtCHzEe51wk4to,15882
10
+ qarnot/connection.py,sha256=x71AuXeO33iyxbdoWEQecjsyI_8VSvX7PXztzsNpzLE,46022
10
11
  qarnot/error.py,sha256=WCkkILJzOi06Q5QRBfacU41D0MQeFCPsQc9Ub1Y6SXw,734
11
12
  qarnot/exceptions.py,sha256=yt_iwCw_9pFdoKeOTxsr05kqW5Gu-th3gSfos5zI26g,2729
12
13
  qarnot/forced_constant.py,sha256=-i4b_JO10YiWuJ7Q0bmWE_TEwtf8qSeLlkMTAbH55EM,1309
13
- qarnot/forced_network_rule.py,sha256=g83LOCRy3c6e5WayQMlk-eYUXQFZICROLh6FDQ8G73k,4515
14
+ qarnot/forced_network_rule.py,sha256=gAalmj5p3MDXEG_jRfszhUTpOSKDMH_BFiXDuSnnyOc,5306
14
15
  qarnot/hardware_constraint.py,sha256=YZi4FgaJQ84mxVDT4u2WxqJ-i_bikwssidTRHRc3JHA,13694
15
- qarnot/helper.py,sha256=SAMcXt2GEcPuhV_ZzTOYs1Ucy4OdYGHVur472k36uoI,1498
16
+ qarnot/helper.py,sha256=HWy0ollEMoGvCXbAY_FfgkhJjpHRRmkNtavvrJba2PY,1564
16
17
  qarnot/job.py,sha256=bag9NbugWCSf18J2c6nFKmWFFusXP58dfYYxd8CMsy8,19508
18
+ qarnot/multi_slots_settings.py,sha256=RsSC-3YGu0wBsQlOgvJ8oMKFvApN6XEGdC5ozgQyXXs,1542
17
19
  qarnot/paginate.py,sha256=DaUYDPAS0M8hf0hph8GuMSzTASymSUOpp-WqbfevX-s,1387
18
- qarnot/pool.py,sha256=xBpRXQ0HB9u6fkdbDT18kbwUw9gErycKbUaL9TZ1-AQ,56269
20
+ qarnot/pool.py,sha256=IaRYjCamrLHm9BWpjDmbxRhNIjb8f7m88wDIi6gGRa8,59165
19
21
  qarnot/privileges.py,sha256=6j3n8q_RNdZ8bBPD9misHEpS0CbcQByqzxlOu8CS-rU,1896
20
22
  qarnot/retry_settings.py,sha256=Illobh-1U8hPdzkffJu5TFKM_NdzCNYS2j1teBljX94,2512
21
23
  qarnot/scheduling_type.py,sha256=j90APc1ji7xe2Z9OH9l81tjO12k0Wf9bSaG-_UCyeNI,2223
22
24
  qarnot/secrets.py,sha256=v6-1UNhCnnW71eJYMeqhcUYLHnsnONGi4Qa3aoiG9qc,12354
23
25
  qarnot/status.py,sha256=dCVsh9_ewIASZcreATbUd2qJ-cUMk0cRkyVLdrOot3Q,12348
24
26
  qarnot/storage.py,sha256=jAist_J_6yzmRrkF5jqYNG3mhEP_y7KqCdNv4dJcHuM,6929
25
- qarnot/task.py,sha256=RCjJBKziSsmaeM4X8mwETw8zWmuvBinNPrYM8SNDdNU,75285
26
- qarnot-2.17.0.dist-info/LICENSE,sha256=tAkwu8-AdEyGxGoSvJ2gVmQdcicWw3j1ZZueVV74M-E,11357
27
- qarnot-2.17.0.dist-info/METADATA,sha256=Lu6BjY0AJyHk86KwBLu2_CC9vupq-Td07c2Yh2xFeKY,2521
28
- qarnot-2.17.0.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
29
- qarnot-2.17.0.dist-info/top_level.txt,sha256=acRyoLZNyf_kuGTwHQgfZv2MfdTcZstyNjBhOxFtHzU,7
30
- qarnot-2.17.0.dist-info/RECORD,,
27
+ qarnot/task.py,sha256=uUfnF2BDq6X8jcGaVRDgK2emFWydwbH_b2QdwXnhtzc,77172
28
+ qarnot-2.19.0.dist-info/licenses/LICENSE,sha256=tAkwu8-AdEyGxGoSvJ2gVmQdcicWw3j1ZZueVV74M-E,11357
29
+ qarnot-2.19.0.dist-info/METADATA,sha256=rR-mDeDXMVjIRA3PuZbTs2bLt9TAAxPO_EDv9mSRAb0,2543
30
+ qarnot-2.19.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
31
+ qarnot-2.19.0.dist-info/top_level.txt,sha256=acRyoLZNyf_kuGTwHQgfZv2MfdTcZstyNjBhOxFtHzU,7
32
+ qarnot-2.19.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.2)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5