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.
@@ -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())