ansys-grantami-jobqueue 1.0.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.
- ansys/grantami/jobqueue/__init__.py +53 -0
- ansys/grantami/jobqueue/_connection.py +552 -0
- ansys/grantami/jobqueue/_logger.py +26 -0
- ansys/grantami/jobqueue/_models.py +1269 -0
- ansys/grantami/jobqueue/py.typed +0 -0
- ansys_grantami_jobqueue-1.0.0.dist-info/AUTHORS +12 -0
- ansys_grantami_jobqueue-1.0.0.dist-info/LICENSE +21 -0
- ansys_grantami_jobqueue-1.0.0.dist-info/METADATA +119 -0
- ansys_grantami_jobqueue-1.0.0.dist-info/RECORD +10 -0
- ansys_grantami_jobqueue-1.0.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Copyright (C) 2024 ANSYS, Inc. and/or its affiliates.
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
#
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
# copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
# SOFTWARE.
|
|
22
|
+
|
|
23
|
+
"""Python client for Granta MI Server API Job Queue."""
|
|
24
|
+
|
|
25
|
+
import importlib.metadata as importlib_metadata
|
|
26
|
+
|
|
27
|
+
from ._connection import Connection, JobQueueApiClient
|
|
28
|
+
from ._models import (
|
|
29
|
+
AsyncJob,
|
|
30
|
+
ExcelExportJobRequest,
|
|
31
|
+
ExcelImportJobRequest,
|
|
32
|
+
ExportRecord,
|
|
33
|
+
JobQueueProcessingConfiguration,
|
|
34
|
+
JobRequest,
|
|
35
|
+
JobStatus,
|
|
36
|
+
JobType,
|
|
37
|
+
TextImportJobRequest,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"AsyncJob",
|
|
42
|
+
"Connection",
|
|
43
|
+
"ExcelExportJobRequest",
|
|
44
|
+
"ExcelImportJobRequest",
|
|
45
|
+
"ExportRecord",
|
|
46
|
+
"JobQueueApiClient",
|
|
47
|
+
"JobQueueProcessingConfiguration",
|
|
48
|
+
"JobRequest",
|
|
49
|
+
"JobStatus",
|
|
50
|
+
"JobType",
|
|
51
|
+
"TextImportJobRequest",
|
|
52
|
+
]
|
|
53
|
+
__version__ = importlib_metadata.version(__name__.replace(".", "-"))
|
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
# Copyright (C) 2024 ANSYS, Inc. and/or its affiliates.
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
#
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
# copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
# SOFTWARE.
|
|
22
|
+
"""Module for connections."""
|
|
23
|
+
import time
|
|
24
|
+
from typing import Dict, List, Optional, Tuple, cast
|
|
25
|
+
import warnings
|
|
26
|
+
|
|
27
|
+
from ansys.grantami.serverapi_openapi import api, models
|
|
28
|
+
from ansys.openapi.common import (
|
|
29
|
+
ApiClient,
|
|
30
|
+
ApiClientFactory,
|
|
31
|
+
ApiException,
|
|
32
|
+
SessionConfiguration,
|
|
33
|
+
UndefinedObjectWarning,
|
|
34
|
+
generate_user_agent,
|
|
35
|
+
)
|
|
36
|
+
import requests # type: ignore[import-untyped]
|
|
37
|
+
|
|
38
|
+
from ._logger import logger
|
|
39
|
+
from ._models import AsyncJob, JobQueueProcessingConfiguration, JobRequest, JobStatus, JobType
|
|
40
|
+
|
|
41
|
+
PROXY_PATH = "/proxy/v1.svc/mi"
|
|
42
|
+
AUTH_PATH = "/Health/v2.svc"
|
|
43
|
+
API_DEFINITION_PATH = "/swagger/v1/swagger.json"
|
|
44
|
+
GRANTA_APPLICATION_NAME_HEADER = "PyGranta JobQueue"
|
|
45
|
+
|
|
46
|
+
MINIMUM_GRANTA_MI_VERSION = (24, 2)
|
|
47
|
+
|
|
48
|
+
_ArgNotProvided = "_ArgNotProvided"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _get_mi_server_version(client: ApiClient) -> Tuple[int, ...]:
|
|
52
|
+
"""
|
|
53
|
+
Get the Granta MI version as a tuple.
|
|
54
|
+
|
|
55
|
+
This method makes direct use of the underlying ``serverapi-openapi`` package.
|
|
56
|
+
The API methods in this package may change over time, and so this method is expected
|
|
57
|
+
to grow to support multiple versions of the ``serverapi-openapi`` package.
|
|
58
|
+
|
|
59
|
+
Parameters
|
|
60
|
+
----------
|
|
61
|
+
client : :class:`~.RecordListApiClient`
|
|
62
|
+
Client object.
|
|
63
|
+
|
|
64
|
+
Returns
|
|
65
|
+
-------
|
|
66
|
+
tuple of int
|
|
67
|
+
Granta MI version number.
|
|
68
|
+
"""
|
|
69
|
+
schema_api = api.SchemaApi(client)
|
|
70
|
+
server_version_response = schema_api.get_version()
|
|
71
|
+
assert server_version_response.version
|
|
72
|
+
server_version_elements = server_version_response.version.split(".")
|
|
73
|
+
server_version = tuple([int(e) for e in server_version_elements])
|
|
74
|
+
return server_version
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class JobQueueApiClient(ApiClient):
|
|
78
|
+
"""
|
|
79
|
+
Communicates with Granta MI.
|
|
80
|
+
|
|
81
|
+
This class is instantiated by the :class:`Connection` class
|
|
82
|
+
and should not be instantiated directly.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
session: requests.Session,
|
|
88
|
+
service_layer_url: str,
|
|
89
|
+
configuration: SessionConfiguration,
|
|
90
|
+
) -> None:
|
|
91
|
+
self._service_layer_url = service_layer_url
|
|
92
|
+
api_url = service_layer_url + PROXY_PATH
|
|
93
|
+
|
|
94
|
+
logger.debug("Creating JobQueueApiClient")
|
|
95
|
+
logger.debug(f"Base Service Layer URL: {self._service_layer_url}")
|
|
96
|
+
logger.debug(f"Service URL: {api_url}")
|
|
97
|
+
|
|
98
|
+
super().__init__(session, api_url, configuration)
|
|
99
|
+
self.job_queue_api = api.JobQueueApi(self)
|
|
100
|
+
|
|
101
|
+
self._user: Optional[models.GrantaServerApiAsyncJobsCurrentUser] = None
|
|
102
|
+
self._processing_configuration: Optional[JobQueueProcessingConfiguration] = None
|
|
103
|
+
|
|
104
|
+
self._jobs: Dict[str, AsyncJob] = {}
|
|
105
|
+
|
|
106
|
+
self._wait_retries = 5
|
|
107
|
+
|
|
108
|
+
def __repr__(self) -> str:
|
|
109
|
+
"""Printable representation of the object."""
|
|
110
|
+
return f"<{self.__class__.__name__} url: {self._service_layer_url}>"
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def processing_configuration(self) -> JobQueueProcessingConfiguration:
|
|
114
|
+
"""
|
|
115
|
+
Current job queue configuration information from the server.
|
|
116
|
+
|
|
117
|
+
Performs an HTTP request against the Granta MI Server API.
|
|
118
|
+
|
|
119
|
+
Returns
|
|
120
|
+
-------
|
|
121
|
+
JobQueueProcessingConfiguration
|
|
122
|
+
Current job queue processing configuration on the server.
|
|
123
|
+
"""
|
|
124
|
+
if self._processing_configuration is None:
|
|
125
|
+
processing_config = self.job_queue_api.get_processing_config()
|
|
126
|
+
self._processing_configuration = JobQueueProcessingConfiguration(
|
|
127
|
+
purge_job_age_in_milliseconds=cast(
|
|
128
|
+
int, processing_config.purge_job_age_in_milliseconds
|
|
129
|
+
),
|
|
130
|
+
purge_interval_in_milliseconds=cast(
|
|
131
|
+
int, processing_config.purge_interval_in_milliseconds
|
|
132
|
+
),
|
|
133
|
+
polling_interval_in_milliseconds=cast(
|
|
134
|
+
int, processing_config.polling_interval_in_milliseconds
|
|
135
|
+
),
|
|
136
|
+
concurrency=cast(int, processing_config.concurrency),
|
|
137
|
+
)
|
|
138
|
+
return self._processing_configuration
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def is_admin_user(self) -> bool:
|
|
142
|
+
"""
|
|
143
|
+
Flag indicating if the current user is an administrator of the job queue.
|
|
144
|
+
|
|
145
|
+
Administrators can promote jobs to the top of the queue and interact with other users' jobs.
|
|
146
|
+
|
|
147
|
+
Performs an HTTP request against the Granta MI Server API.
|
|
148
|
+
|
|
149
|
+
Returns
|
|
150
|
+
-------
|
|
151
|
+
bool
|
|
152
|
+
``True`` if the user is an administrator, ``False`` otherwise.
|
|
153
|
+
"""
|
|
154
|
+
if self._user is None:
|
|
155
|
+
self._refetch_user()
|
|
156
|
+
assert self._user
|
|
157
|
+
return cast(bool, self._user.is_admin)
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def can_write_job(self) -> bool:
|
|
161
|
+
"""
|
|
162
|
+
Flag indicating if the current user can create jobs.
|
|
163
|
+
|
|
164
|
+
Performs an HTTP request against the Granta MI Server API.
|
|
165
|
+
|
|
166
|
+
Returns
|
|
167
|
+
-------
|
|
168
|
+
bool
|
|
169
|
+
``True`` if the user can create jobs, ``False`` otherwise.
|
|
170
|
+
"""
|
|
171
|
+
if self._user is None:
|
|
172
|
+
self._refetch_user()
|
|
173
|
+
assert self._user
|
|
174
|
+
return cast(bool, self._user.has_write_access)
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def num_jobs(self) -> int:
|
|
178
|
+
"""
|
|
179
|
+
Number of jobs in the job queue, including completed and failed jobs.
|
|
180
|
+
|
|
181
|
+
Performs an HTTP request against the Granta MI Server API.
|
|
182
|
+
|
|
183
|
+
Returns
|
|
184
|
+
-------
|
|
185
|
+
int
|
|
186
|
+
Number of jobs in the job queue.
|
|
187
|
+
"""
|
|
188
|
+
with warnings.catch_warnings():
|
|
189
|
+
warnings.simplefilter("ignore", UndefinedObjectWarning)
|
|
190
|
+
jobs = self.job_queue_api.get_jobs()
|
|
191
|
+
return len(cast(List[models.GrantaServerApiAsyncJobsJob], jobs.results))
|
|
192
|
+
|
|
193
|
+
def _refetch_user(self) -> None:
|
|
194
|
+
"""Refetch the current user information from the server."""
|
|
195
|
+
self._user = self.job_queue_api.get_current_user()
|
|
196
|
+
assert self._user
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def jobs(self) -> "List[AsyncJob]":
|
|
200
|
+
"""
|
|
201
|
+
List of all jobs on the server visible to the current user.
|
|
202
|
+
|
|
203
|
+
Running or pending jobs are sorted according to their positions in the queue.
|
|
204
|
+
Completed or failed jobs are returned last.
|
|
205
|
+
|
|
206
|
+
Performs an HTTP request against the Granta MI Server API.
|
|
207
|
+
|
|
208
|
+
Returns
|
|
209
|
+
-------
|
|
210
|
+
list[AsyncJob]
|
|
211
|
+
List of all jobs on the server visible to the current user.
|
|
212
|
+
"""
|
|
213
|
+
self._refetch_jobs()
|
|
214
|
+
return sorted(self._jobs.values(), key=lambda x: (x.position is None, x.position))
|
|
215
|
+
|
|
216
|
+
def jobs_where(
|
|
217
|
+
self,
|
|
218
|
+
name: Optional[str] = None,
|
|
219
|
+
job_type: Optional[JobType] = None,
|
|
220
|
+
description: Optional[str] = None,
|
|
221
|
+
submitter_name: Optional[str] = None,
|
|
222
|
+
status: Optional[JobStatus] = None,
|
|
223
|
+
) -> "List[AsyncJob]":
|
|
224
|
+
"""
|
|
225
|
+
Get a list of jobs on the server matching a query.
|
|
226
|
+
|
|
227
|
+
Running or queued jobs are sorted according to their positions in the queue.
|
|
228
|
+
Completed or failed jobs are returned last.
|
|
229
|
+
|
|
230
|
+
Performs an HTTP request against the Granta MI Server API.
|
|
231
|
+
|
|
232
|
+
Parameters
|
|
233
|
+
----------
|
|
234
|
+
name : str, default: None
|
|
235
|
+
Text that must appear in the job name.
|
|
236
|
+
job_type : JobType, default: None
|
|
237
|
+
Type of job to search for.
|
|
238
|
+
description : str, default: None
|
|
239
|
+
Text that must appear in the job description.
|
|
240
|
+
submitter_name : str, default: None
|
|
241
|
+
Text that must equal the name of the user who submitted the job.
|
|
242
|
+
status : JobStatus, default: None
|
|
243
|
+
Status of the job.
|
|
244
|
+
|
|
245
|
+
Returns
|
|
246
|
+
-------
|
|
247
|
+
list of AsyncJob
|
|
248
|
+
List of jobs on the server matching the query.
|
|
249
|
+
"""
|
|
250
|
+
kwargs = {
|
|
251
|
+
"name_filter": name,
|
|
252
|
+
"job_type": job_type.value if job_type else None,
|
|
253
|
+
"status": status.value if status else None,
|
|
254
|
+
"description_filter": description,
|
|
255
|
+
"submitter_name_filter": submitter_name,
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
with warnings.catch_warnings():
|
|
259
|
+
warnings.simplefilter("ignore", UndefinedObjectWarning)
|
|
260
|
+
filtered_job_resp = self.job_queue_api.get_jobs(
|
|
261
|
+
**{k: v for k, v in kwargs.items() if v is not None}
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
job_list = filtered_job_resp.results
|
|
265
|
+
assert isinstance(job_list, list)
|
|
266
|
+
self._update_job_list_from_resp(job_resp=job_list)
|
|
267
|
+
if not filtered_job_resp.results:
|
|
268
|
+
return []
|
|
269
|
+
filtered_ids = [job.id for job in filtered_job_resp.results]
|
|
270
|
+
return [job for id_, job in self._jobs.items() if id_ in filtered_ids]
|
|
271
|
+
|
|
272
|
+
def get_job_by_id(self, job_id: str) -> "AsyncJob":
|
|
273
|
+
"""
|
|
274
|
+
Get the job with a given job ID.
|
|
275
|
+
|
|
276
|
+
Parameters
|
|
277
|
+
----------
|
|
278
|
+
job_id : str
|
|
279
|
+
Job ID.
|
|
280
|
+
|
|
281
|
+
Returns
|
|
282
|
+
-------
|
|
283
|
+
AsyncJob
|
|
284
|
+
Job with the given ID.
|
|
285
|
+
"""
|
|
286
|
+
return next(job for id_, job in self._jobs.items() if id_ == job_id)
|
|
287
|
+
|
|
288
|
+
def delete_jobs(self, jobs: "List[AsyncJob]") -> None:
|
|
289
|
+
"""
|
|
290
|
+
Delete one or more jobs from the server.
|
|
291
|
+
|
|
292
|
+
Parameters
|
|
293
|
+
----------
|
|
294
|
+
jobs : list of AsyncJob
|
|
295
|
+
List of jobs to delete from the server.
|
|
296
|
+
"""
|
|
297
|
+
for job in jobs:
|
|
298
|
+
self.job_queue_api.delete_job(id=job.id)
|
|
299
|
+
self._jobs.pop(job.id, None)
|
|
300
|
+
job._is_deleted = True
|
|
301
|
+
self._refetch_jobs()
|
|
302
|
+
|
|
303
|
+
def _refetch_jobs(self) -> None:
|
|
304
|
+
"""Refetch the list of jobs from the server."""
|
|
305
|
+
with warnings.catch_warnings():
|
|
306
|
+
warnings.simplefilter("ignore", UndefinedObjectWarning)
|
|
307
|
+
job_resp = self.job_queue_api.get_jobs()
|
|
308
|
+
job_list = job_resp.results
|
|
309
|
+
assert isinstance(job_list, list)
|
|
310
|
+
self._update_job_list_from_resp(job_resp=job_list, flush_jobs=True)
|
|
311
|
+
|
|
312
|
+
def _update_job_list_from_resp(
|
|
313
|
+
self, job_resp: List[models.GrantaServerApiAsyncJobsJob], flush_jobs: bool = False
|
|
314
|
+
) -> None:
|
|
315
|
+
"""
|
|
316
|
+
Update the internal job list with a list of job objects from the server.
|
|
317
|
+
|
|
318
|
+
Parameters
|
|
319
|
+
----------
|
|
320
|
+
job_resp : List[models.GrantaServerApiAsyncJobsJob]
|
|
321
|
+
List of job objects from the server.
|
|
322
|
+
flush_jobs : bool, default: False
|
|
323
|
+
Whether to remove jobs from the internal list that are not in
|
|
324
|
+
the ``job_resp`` list.
|
|
325
|
+
"""
|
|
326
|
+
remote_ids = [remote_job.id for remote_job in job_resp]
|
|
327
|
+
if flush_jobs:
|
|
328
|
+
for job_id in self._jobs:
|
|
329
|
+
if job_id not in remote_ids:
|
|
330
|
+
self._jobs.pop(job_id)
|
|
331
|
+
for job_obj in job_resp:
|
|
332
|
+
job_id = cast(str, job_obj.id)
|
|
333
|
+
if job_id not in self._jobs:
|
|
334
|
+
self._jobs[job_id] = AsyncJob(job_obj, self.job_queue_api)
|
|
335
|
+
elif job_obj is not self._jobs[job_id]:
|
|
336
|
+
self._jobs[job_id]._update_job(job_obj)
|
|
337
|
+
|
|
338
|
+
def create_job_and_wait(self, job_request: "JobRequest") -> "AsyncJob": # noqa: D205, D400
|
|
339
|
+
"""
|
|
340
|
+
Create a job from an Excel import or export request or from a text import request.
|
|
341
|
+
|
|
342
|
+
This method create a job from an :class:`~.ExcelImportJobRequest`, :class:`~.ExcelExportJobRequest`, or
|
|
343
|
+
:class:`~.TextImportJobRequest` object and waits until the job is complete.
|
|
344
|
+
|
|
345
|
+
This method also uploads the files included in the job request as a part of the job
|
|
346
|
+
submission process.
|
|
347
|
+
|
|
348
|
+
Parameters
|
|
349
|
+
----------
|
|
350
|
+
job_request : JobRequest
|
|
351
|
+
Job request to submit to the job queue.
|
|
352
|
+
|
|
353
|
+
Returns
|
|
354
|
+
-------
|
|
355
|
+
AsyncJob
|
|
356
|
+
Object representing the completed job.
|
|
357
|
+
"""
|
|
358
|
+
job = self.create_job(job_request=job_request)
|
|
359
|
+
request_count = 0
|
|
360
|
+
last_exception: Optional[Exception] = None
|
|
361
|
+
time.sleep(1)
|
|
362
|
+
while request_count < self._wait_retries:
|
|
363
|
+
try:
|
|
364
|
+
job.update()
|
|
365
|
+
status = job.status
|
|
366
|
+
if status not in [JobStatus.Pending, JobStatus.Running]:
|
|
367
|
+
return job
|
|
368
|
+
except ApiException as exception_info:
|
|
369
|
+
request_count += 1
|
|
370
|
+
last_exception = exception_info
|
|
371
|
+
except Exception as exception_info:
|
|
372
|
+
last_exception = exception_info
|
|
373
|
+
break
|
|
374
|
+
time.sleep(1)
|
|
375
|
+
if last_exception:
|
|
376
|
+
raise last_exception
|
|
377
|
+
else:
|
|
378
|
+
return job
|
|
379
|
+
|
|
380
|
+
def create_job(self, job_request: "JobRequest") -> "AsyncJob": # noqa: D205, D400
|
|
381
|
+
"""
|
|
382
|
+
Create a job from an Excel import or export request or from a text import request.
|
|
383
|
+
|
|
384
|
+
This method creates a job from an :class:`~.ExcelImportJobRequest`, :class:`~.ExcelExportJobRequest`, or
|
|
385
|
+
:class:`~.TextImportJobRequest` object.
|
|
386
|
+
|
|
387
|
+
This method also uploads the files included in the job request as a part of the job
|
|
388
|
+
submission process.
|
|
389
|
+
|
|
390
|
+
Parameters
|
|
391
|
+
----------
|
|
392
|
+
job_request : JobRequest
|
|
393
|
+
Job request to submit to the server.
|
|
394
|
+
|
|
395
|
+
Returns
|
|
396
|
+
-------
|
|
397
|
+
AsyncJob
|
|
398
|
+
Object representing the in-progress job.
|
|
399
|
+
"""
|
|
400
|
+
job_request._post_files(api_client=self.job_queue_api)
|
|
401
|
+
|
|
402
|
+
job_response = self.job_queue_api.create_job(body=job_request._get_job_for_submission())
|
|
403
|
+
self._update_job_list_from_resp([job_response])
|
|
404
|
+
return self._jobs[cast(str, job_response.id)]
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
class Connection(ApiClientFactory):
|
|
408
|
+
"""
|
|
409
|
+
Connects to a Granta MI Server API instance.
|
|
410
|
+
|
|
411
|
+
This is a subclass of the :class:`ansys.openapi.common.ApiClientFactory` class. All methods in
|
|
412
|
+
this class are documented as returning :class:`~ansys.openapi.common.ApiClientFactory` class
|
|
413
|
+
instances of the :class:`ansys.grantami.jobqueue.Connection` class.
|
|
414
|
+
|
|
415
|
+
Parameters
|
|
416
|
+
----------
|
|
417
|
+
servicelayer_url : str
|
|
418
|
+
Base URL of the Granta MI Service Layer application.
|
|
419
|
+
session_configuration : :class:`~ansys.openapi.common.SessionConfiguration`, default: None
|
|
420
|
+
Additional configuration settings for the requests session. If ``None``, the
|
|
421
|
+
:class:`~ansys.openapi.common.SessionConfiguration` class with default parameters
|
|
422
|
+
is used.
|
|
423
|
+
|
|
424
|
+
Notes
|
|
425
|
+
-----
|
|
426
|
+
For advanced usage, including configuring session-specific properties and timeouts, see the
|
|
427
|
+
:external+openapi-common:doc:`OpenAPI-Common API reference documentation <api/index>`.
|
|
428
|
+
Specifically, see the documentation for the :class:`~ansys.openapi.common.ApiClientFactory`
|
|
429
|
+
base class and the :class:`~ansys.openapi.common.SessionConfiguration` class.
|
|
430
|
+
|
|
431
|
+
1. Create the connection builder object and specify the server to connect to.
|
|
432
|
+
2. Specify the authentication method to use for the connection and provide credentials if
|
|
433
|
+
required.
|
|
434
|
+
3. Connect to the server, which returns the client object.
|
|
435
|
+
|
|
436
|
+
The examples show this process for different authentication methods.
|
|
437
|
+
|
|
438
|
+
Examples
|
|
439
|
+
--------
|
|
440
|
+
>>> client = Connection("http://my_mi_server/mi_servicelayer").with_autologon().connect()
|
|
441
|
+
>>> client
|
|
442
|
+
<JobQueueApiClient: url=http://my_mi_server/mi_servicelayer>
|
|
443
|
+
|
|
444
|
+
>>> client = (
|
|
445
|
+
... Connection("http://my_mi_server/mi_servicelayer")
|
|
446
|
+
... .with_credentials(username="my_username", password="my_password")
|
|
447
|
+
... .connect()
|
|
448
|
+
... )
|
|
449
|
+
>>> client
|
|
450
|
+
<JobQueueApiClient: url: http://my_mi_server/mi_servicelayer>
|
|
451
|
+
"""
|
|
452
|
+
|
|
453
|
+
def __init__(
|
|
454
|
+
self, servicelayer_url: str, session_configuration: Optional[SessionConfiguration] = None
|
|
455
|
+
):
|
|
456
|
+
from . import __version__
|
|
457
|
+
|
|
458
|
+
auth_url = servicelayer_url.strip("/") + AUTH_PATH
|
|
459
|
+
super().__init__(auth_url, session_configuration)
|
|
460
|
+
self._base_service_layer_url = servicelayer_url
|
|
461
|
+
self._session_configuration.headers["X-Granta-ApplicationName"] = (
|
|
462
|
+
GRANTA_APPLICATION_NAME_HEADER
|
|
463
|
+
)
|
|
464
|
+
self._session_configuration.headers["User-Agent"] = generate_user_agent(
|
|
465
|
+
"ansys-grantami-jobqueue", __version__
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
def connect(self) -> JobQueueApiClient:
|
|
469
|
+
"""
|
|
470
|
+
Finalize the :class:`.JobQueueApiClient` client and return it for use.
|
|
471
|
+
|
|
472
|
+
Authentication must be configured for this method to succeed.
|
|
473
|
+
|
|
474
|
+
Returns
|
|
475
|
+
-------
|
|
476
|
+
:class:`.JobQueueApiClient`
|
|
477
|
+
Client object that can be used to connect to Granta MI and interact with the job queue
|
|
478
|
+
API.
|
|
479
|
+
"""
|
|
480
|
+
self._validate_builder()
|
|
481
|
+
client = JobQueueApiClient(
|
|
482
|
+
self._session,
|
|
483
|
+
self._base_service_layer_url,
|
|
484
|
+
self._session_configuration,
|
|
485
|
+
)
|
|
486
|
+
client.setup_client(models)
|
|
487
|
+
self._test_connection(client)
|
|
488
|
+
return client
|
|
489
|
+
|
|
490
|
+
@staticmethod
|
|
491
|
+
def _test_connection(client: JobQueueApiClient) -> None:
|
|
492
|
+
"""
|
|
493
|
+
Check if the created client can be used to perform a request.
|
|
494
|
+
|
|
495
|
+
This method tests both that the API definition can be accessed and that the Granta MI
|
|
496
|
+
version is compatible with this package.
|
|
497
|
+
|
|
498
|
+
The first checks ensures that the Server API exists and is functional. The second check
|
|
499
|
+
ensures that the Granta MI server version is compatible with this version of the package.
|
|
500
|
+
|
|
501
|
+
A failure at any point raises a ``ConnectionError``.
|
|
502
|
+
|
|
503
|
+
Parameters
|
|
504
|
+
----------
|
|
505
|
+
client : :class:`~.JobQueueApiClient`
|
|
506
|
+
Client object to test.
|
|
507
|
+
|
|
508
|
+
Raises
|
|
509
|
+
------
|
|
510
|
+
ConnectionError
|
|
511
|
+
Error raised if the connection test fails.
|
|
512
|
+
"""
|
|
513
|
+
try:
|
|
514
|
+
client.call_api(resource_path=API_DEFINITION_PATH, method="GET")
|
|
515
|
+
except ApiException as e:
|
|
516
|
+
if e.status_code == 404:
|
|
517
|
+
raise ConnectionError(
|
|
518
|
+
"Cannot find the Server API definition in the Granta MI Service Layer. Ensure "
|
|
519
|
+
"that a compatible version of Granta MI is available and try again."
|
|
520
|
+
) from e
|
|
521
|
+
else:
|
|
522
|
+
raise ConnectionError(
|
|
523
|
+
"An unexpected error occurred when trying to connect to the Server API in the Granta "
|
|
524
|
+
" MI Service Layer. Check the Service Layer logs for more information and try "
|
|
525
|
+
"again."
|
|
526
|
+
) from e
|
|
527
|
+
except requests.exceptions.RetryError as e:
|
|
528
|
+
raise ConnectionError(
|
|
529
|
+
"An unexpected error occurred when trying to connect to the Granta MI Server API. Check "
|
|
530
|
+
"that SSL certificates have been configured for communications between the Granta MI "
|
|
531
|
+
"Server and client Granta MI applications."
|
|
532
|
+
) from e
|
|
533
|
+
|
|
534
|
+
try:
|
|
535
|
+
server_version = _get_mi_server_version(client)
|
|
536
|
+
except ApiException as e:
|
|
537
|
+
raise ConnectionError(
|
|
538
|
+
"Cannot check the Granta MI server version. Ensure that the Granta MI server version "
|
|
539
|
+
f"is at least {'.'.join([str(e) for e in MINIMUM_GRANTA_MI_VERSION])}."
|
|
540
|
+
) from e
|
|
541
|
+
|
|
542
|
+
# Once there are multiple versions of this package targeting different Granta MI server
|
|
543
|
+
# versions, the error message should direct users towards the PyGranta metapackage for
|
|
544
|
+
# earlier versions. This is not necessary now though, because there is no support for
|
|
545
|
+
# versions earlier than 2023 R2.
|
|
546
|
+
|
|
547
|
+
if server_version < MINIMUM_GRANTA_MI_VERSION:
|
|
548
|
+
raise ConnectionError(
|
|
549
|
+
f"This package requires a more recent Granta MI version. Detected Granta MI server "
|
|
550
|
+
f"version is {'.'.join([str(e) for e in server_version])}, but this package "
|
|
551
|
+
f"requires at least {'.'.join([str(e) for e in MINIMUM_GRANTA_MI_VERSION])}."
|
|
552
|
+
)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Copyright (C) 2024 ANSYS, Inc. and/or its affiliates.
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
#
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
# copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
# SOFTWARE.
|
|
22
|
+
"""Logger module."""
|
|
23
|
+
import logging
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger("ansys.grantami.jobqueue")
|
|
26
|
+
logger.addHandler(logging.NullHandler())
|