qctrl-workflow-client 7.2.0__tar.gz → 8.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: qctrl-workflow-client
3
- Version: 7.2.0
3
+ Version: 8.0.0
4
4
  Summary: Q-CTRL Workflow Client
5
5
  License: https://q-ctrl.com/terms
6
6
  Keywords: black opal,boulder opal,fire opal,ironstone opal,nisq,open controls,q control,q ctrl,q-control,q-ctrl,qcontrol,qctrl,quantum,quantum algorithms,quantum circuits,quantum coding,quantum coding software,quantum computing,quantum control,quantum control software,quantum control theory,quantum engineering,quantum error correction,quantum firmware,quantum fundamentals,quantum navigation,quantum sensing,qubit,qudit
@@ -8,7 +8,7 @@ Author: Q-CTRL
8
8
  Author-email: support@q-ctrl.com
9
9
  Maintainer: Q-CTRL
10
10
  Maintainer-email: support@q-ctrl.com
11
- Requires-Python: >=3.9, <3.14
11
+ Requires-Python: >=3.10, <3.14
12
12
  Classifier: Development Status :: 5 - Production/Stable
13
13
  Classifier: Environment :: Console
14
14
  Classifier: Intended Audience :: Developers
@@ -18,7 +18,6 @@ Classifier: License :: Other/Proprietary License
18
18
  Classifier: Natural Language :: English
19
19
  Classifier: Operating System :: OS Independent
20
20
  Classifier: Programming Language :: Python :: 3
21
- Classifier: Programming Language :: Python :: 3.9
22
21
  Classifier: Programming Language :: Python :: 3.10
23
22
  Classifier: Programming Language :: Python :: 3.11
24
23
  Classifier: Programming Language :: Python :: 3.12
@@ -1,8 +1,8 @@
1
1
  [project]
2
2
  name = "qctrl-workflow-client"
3
3
  description = "Q-CTRL Workflow Client"
4
- requires-python = ">=3.9, <3.14"
5
- version = "7.2.0"
4
+ requires-python = ">=3.10, <3.14"
5
+ version = "8.0.0"
6
6
  license = "https://q-ctrl.com/terms"
7
7
  authors = [
8
8
  { name = "Q-CTRL", email = "support@q-ctrl.com" },
@@ -70,6 +70,10 @@ classifiers = [
70
70
  "Intended Audience :: Science/Research",
71
71
  "Natural Language :: English",
72
72
  "Operating System :: OS Independent",
73
+ "Programming Language :: Python :: 3.10",
74
+ "Programming Language :: Python :: 3.11",
75
+ "Programming Language :: Python :: 3.12",
76
+ "Programming Language :: Python :: 3.13",
73
77
  "Topic :: Internet :: WWW/HTTP",
74
78
  "Topic :: Scientific/Engineering :: Physics",
75
79
  "Topic :: Scientific/Engineering :: Visualization",
@@ -100,19 +104,32 @@ qctrl-commons = { version = "^25.1.2", source = "PyPI" }
100
104
  qctrl-client = { version = "^14.0.0", source = "PyPI" }
101
105
 
102
106
  [tool.poetry.group.dev.dependencies]
103
- black = "^25.1.0"
104
- isort = "^6.0.0"
105
107
  pre-commit = "^4.1.0"
106
- pylint = "^3.3.4"
107
108
  pytest = "^8.3.4"
108
109
  pytest-cov = "^7.0.0"
109
110
  pytest-mock = "^3.10.0"
110
111
  pytest-xdist = "^3.2.1"
111
112
  qctrl-core-workflow-manager = "^5.1.1"
113
+ ruff = "^0.6.0"
112
114
 
113
- [tool.isort]
114
- profile = "black"
115
- force_grid_wrap = "2"
115
+ [tool.ruff]
116
+ line-length = 100
117
+
118
+ [tool.ruff.lint]
119
+ select = ['ALL']
120
+ ignore = [
121
+ "ANN", # Type annotations
122
+ "D", # docstring
123
+ ]
124
+
125
+ [tool.ruff.lint.per-file-ignores]
126
+ "tests/**/*.py" = [
127
+ "S101", # Assertions in code
128
+ "SLF001", # Accessing private members
129
+ ]
130
+
131
+ [tool.ruff.lint.pydocstyle]
132
+ convention = "numpy" # https://numpydoc.readthedocs.io/en/latest/format.html#sections
116
133
 
117
134
  [tool.pytest.ini_options]
118
135
  addopts = "--cov=qctrlworkflowclient --cov-fail-under=80 --cov-report=term-missing:skip-covered"
@@ -11,7 +11,7 @@
11
11
  # distributed under the License is distributed on an "AS IS" BASIS. See the
12
12
  # License for the specific language.
13
13
 
14
- __version__ = "7.2.0"
14
+ __version__ = "8.0.0"
15
15
 
16
16
  from .defaults import get_authenticated_client_for_product
17
17
  from .functions import (
@@ -28,3 +28,15 @@ from .router import (
28
28
  LocalRouter,
29
29
  )
30
30
  from .settings import CoreClientSettings
31
+
32
+ __all__ = [
33
+ "get_authenticated_client_for_product",
34
+ "core_workflow",
35
+ "print_warnings",
36
+ "Product",
37
+ "ProductInfo",
38
+ "ApiRouter",
39
+ "BaseRouter",
40
+ "LocalRouter",
41
+ "CoreClientSettings",
42
+ ]
@@ -13,13 +13,17 @@
13
13
 
14
14
  from __future__ import annotations
15
15
 
16
- from typing import Optional
16
+ from typing import TYPE_CHECKING
17
+
18
+ if TYPE_CHECKING:
19
+ from qctrlclient.auth import BaseAuth
20
+ else:
21
+ from qctrlclient.auth import BaseAuth # noqa: TCH002
17
22
 
18
23
  from qctrlclient import (
19
24
  CliAuth,
20
25
  GraphQLClient,
21
26
  )
22
- from qctrlclient.auth import BaseAuth
23
27
  from qctrlclient.defaults import (
24
28
  get_default_api_url,
25
29
  get_default_cli_auth,
@@ -31,8 +35,8 @@ from .utils import get_installed_version
31
35
 
32
36
  def get_authenticated_client_for_product(
33
37
  package_name: str,
34
- api_url: Optional[str] = None,
35
- auth: Optional[BaseAuth | str] = None,
38
+ api_url: str | None = None,
39
+ auth: BaseAuth | str | None = None,
36
40
  ) -> GraphQLClient:
37
41
  """
38
42
  Return a `GraphQLClient` using default URL and Auth (if not provided)
@@ -48,19 +52,17 @@ def get_authenticated_client_for_product(
48
52
  The authentication object (or a URL as str to create one) to use.
49
53
  If not provided, the default authentication object will be used.
50
54
  """
51
-
52
55
  if isinstance(auth, str):
53
56
  auth = CliAuth(auth)
54
57
 
55
58
  headers = {
56
59
  "User-Agent": generate_user_agent(
57
- package_name, get_installed_version(package_name)
60
+ package_name,
61
+ get_installed_version(package_name),
58
62
  ),
59
63
  }
60
- client = GraphQLClient(
64
+ return GraphQLClient(
61
65
  url=api_url or get_default_api_url(),
62
66
  headers=headers,
63
67
  auth=auth or get_default_cli_auth(),
64
68
  )
65
-
66
- return client
@@ -11,25 +11,28 @@
11
11
  # distributed under the License is distributed on an "AS IS" BASIS. See the
12
12
  # License for the specific language.
13
13
 
14
+ from __future__ import annotations
15
+
14
16
  from functools import wraps
15
17
  from typing import (
18
+ TYPE_CHECKING,
16
19
  Any,
17
- Callable,
18
- Dict,
19
- Optional,
20
20
  )
21
21
  from warnings import warn
22
22
 
23
23
  from qctrlcommons.exceptions import QctrlArgumentsValueError
24
24
 
25
+ if TYPE_CHECKING:
26
+ from collections.abc import Callable
27
+
25
28
  from qctrlworkflowclient.router.api import ApiRouter
26
29
 
27
30
 
28
31
  def core_workflow(
29
32
  get_config: Callable,
30
33
  workflow: str,
31
- formatter: Optional[Callable] = None,
32
- registry_selector: Optional[Callable] = None,
34
+ formatter: Callable | None = None,
35
+ registry_selector: Callable | None = None,
33
36
  ):
34
37
  """
35
38
  Decorator for a function which will execute a workflow.
@@ -103,8 +106,8 @@ def core_workflow(
103
106
  def async_core_workflow(
104
107
  get_config: Callable,
105
108
  workflow: str,
106
- formatter: Optional[Callable] = None,
107
- registry_selector: Optional[Callable] = None,
109
+ formatter: Callable | None = None,
110
+ registry_selector: Callable | None = None,
108
111
  ):
109
112
  """
110
113
  Decorator for a function which will execute asynchronously workflow.
@@ -180,7 +183,7 @@ def async_core_workflow(
180
183
  return decorator
181
184
 
182
185
 
183
- def print_warnings(result: Dict[str, Any]):
186
+ def print_warnings(result: dict[str, Any]):
184
187
  """
185
188
  Result formatter which prints all `warnings` in
186
189
  the result and removes them from the result.
@@ -188,6 +191,6 @@ def print_warnings(result: Dict[str, Any]):
188
191
  warnings = result.pop("warnings", [])
189
192
 
190
193
  for warning in warnings:
191
- warn(warning, RuntimeWarning)
194
+ warn(warning, RuntimeWarning, stacklevel=2)
192
195
 
193
196
  return result
@@ -14,3 +14,5 @@
14
14
  from .api import ApiRouter
15
15
  from .base import BaseRouter
16
16
  from .local import LocalRouter
17
+
18
+ __all__ = ["ApiRouter", "BaseRouter", "LocalRouter"]
@@ -17,16 +17,25 @@ from dataclasses import dataclass
17
17
  from enum import Enum
18
18
  from functools import cached_property
19
19
  from typing import (
20
+ TYPE_CHECKING,
20
21
  Any,
21
- Callable,
22
- Dict,
23
- List,
24
- Optional,
22
+ ClassVar,
25
23
  )
26
24
  from warnings import warn
27
25
 
28
26
  import gql
29
- from qctrlclient import GraphQLClient
27
+
28
+ if TYPE_CHECKING:
29
+ from collections.abc import Callable
30
+
31
+ from qctrlclient import GraphQLClient
32
+
33
+ from qctrlworkflowclient.settings import CoreClientSettings
34
+ else:
35
+ from qctrlclient import GraphQLClient # noqa: TCH002
36
+
37
+ from qctrlworkflowclient.settings import CoreClientSettings # noqa: TCH001
38
+
30
39
  from qctrlclient.exceptions import GraphQLClientError
31
40
  from qctrlcommons.preconditions import check_argument
32
41
  from qctrlcommons.serializers import (
@@ -42,7 +51,6 @@ from tenacity import (
42
51
  from typing_extensions import TypedDict
43
52
 
44
53
  from qctrlworkflowclient.products import Product
45
- from qctrlworkflowclient.settings import CoreClientSettings
46
54
  from qctrlworkflowclient.utils import get_installed_version
47
55
 
48
56
  from .base import BaseRouter
@@ -88,7 +96,7 @@ class Organization:
88
96
  slug: str
89
97
  name: str
90
98
 
91
- def to_dict(self) -> Dict[str, str]:
99
+ def to_dict(self) -> dict[str, str]:
92
100
  """
93
101
  The dictionary representation of the organization.
94
102
  """
@@ -135,9 +143,9 @@ class Action:
135
143
  """
136
144
 
137
145
  action_id: str
138
- status: Optional[str] = None
139
- raw_result: Optional[Any] = None
140
- errors: Optional[List[Dict[str, Any]]] = None
146
+ status: str | None = None
147
+ raw_result: Any | None = None
148
+ errors: list[dict[str, Any]] | None = None
141
149
 
142
150
  @property
143
151
  def result(self) -> DecodedResult:
@@ -205,7 +213,7 @@ class ApiRouter(BaseRouter):
205
213
  registered in.
206
214
  """
207
215
 
208
- _TRACKED_PACKAGES = [
216
+ _TRACKED_PACKAGES: ClassVar[list[str]] = [
209
217
  "boulder-opal",
210
218
  "fire-opal",
211
219
  "qctrl-client",
@@ -220,7 +228,7 @@ class ApiRouter(BaseRouter):
220
228
  self._parallel_task_collector = None
221
229
  self._async = False
222
230
 
223
- def set_async_state(self, is_async: bool):
231
+ def set_async_state(self, *, is_async: bool):
224
232
  """
225
233
  Toggle asynchronous state of workflow execution.
226
234
  """
@@ -230,9 +238,9 @@ class ApiRouter(BaseRouter):
230
238
  """
231
239
  Perform validation checks on the settings.
232
240
  """
233
-
234
241
  if not self._settings.product:
235
- raise GraphQLClientError("`product` must be configured in settings")
242
+ error_message = "`product` must be configured in settings"
243
+ raise GraphQLClientError(error_message)
236
244
 
237
245
  self._check_organization_config()
238
246
 
@@ -248,7 +256,6 @@ class ApiRouter(BaseRouter):
248
256
  If the above rules cannot be guaranteed, an error message
249
257
  will be displayed.
250
258
  """
251
-
252
259
  # organization configured by user
253
260
  if self._settings.organization:
254
261
  found = False
@@ -259,22 +266,24 @@ class ApiRouter(BaseRouter):
259
266
  break
260
267
 
261
268
  if not found:
262
- raise RuntimeError(
269
+ error_message = (
263
270
  f"Configured organization not found, is not set up, or does not have valid "
264
271
  f"product access: `{self._settings.organization}`"
265
272
  )
273
+ raise RuntimeError(error_message)
266
274
 
267
275
  # organization not configured by user
268
276
  else:
269
277
  # no valid organizations found for the user
270
278
  if not self._organizations:
271
- raise RuntimeError(
279
+ error_message = (
272
280
  "No organizations are set up or have a valid subscription to the product. "
273
281
  "Please ensure that your organization is set up and has an active subscription "
274
282
  "to the product you are trying to access. If you believe this is an error, "
275
283
  "contact your Q-CTRL representative or visit the Q-CTRL support portal at "
276
284
  "https://support.q-ctrl.com."
277
285
  )
286
+ raise RuntimeError(error_message)
278
287
 
279
288
  # user is a member of multiple organizations
280
289
  if len(self._organizations) > 1:
@@ -290,12 +299,11 @@ class ApiRouter(BaseRouter):
290
299
  self._settings.organization = self._organizations[0].slug
291
300
 
292
301
  @cached_property
293
- def _organizations(self) -> List[Organization]:
302
+ def _organizations(self) -> list[Organization]:
294
303
  """
295
304
  Return the list of organizations that the user is
296
305
  assigned to which provide access to the configured product.
297
306
  """
298
-
299
307
  query = gql.gql(
300
308
  """
301
309
  query {
@@ -317,36 +325,32 @@ class ApiRouter(BaseRouter):
317
325
  }
318
326
  }
319
327
  }
320
- """
328
+ """,
321
329
  )
322
330
 
323
331
  response = self._client.execute(query)
324
332
  data = response["profile"]["profile"]["organizations"]
325
- organizations = []
326
-
327
- for organization_data in data:
333
+ return [
334
+ Organization(
335
+ organization_id=organization_data["id"],
336
+ slug=organization_data["slug"],
337
+ name=organization_data["name"],
338
+ )
339
+ for organization_data in data
328
340
  if self._has_product_access(
329
- organization_data, self._settings.product.value.name
330
- ):
331
- organizations.append(
332
- Organization(
333
- organization_id=organization_data["id"],
334
- slug=organization_data["slug"],
335
- name=organization_data["name"],
336
- )
337
- )
338
-
339
- return organizations
341
+ organization_data,
342
+ self._settings.product.value.name,
343
+ )
344
+ ]
340
345
 
341
346
  @staticmethod
342
- def _has_product_access(organization_data: Dict, product_name: str) -> bool:
347
+ def _has_product_access(organization_data: dict, product_name: str) -> bool:
343
348
  """
344
349
  Convenience function to check if the organization
345
350
  has access to the given product. The format of
346
351
  `organization_data` is based on the output of the query
347
352
  in `_get_organizations`.
348
353
  """
349
-
350
354
  for product_data in organization_data["products"]:
351
355
  if product_data["name"] == product_name:
352
356
  return product_data["active"]
@@ -354,7 +358,8 @@ class ApiRouter(BaseRouter):
354
358
  return False
355
359
 
356
360
  def enable_parallel(
357
- self, callback: Optional[Callable[[DecodedResult], dict]] = None
361
+ self,
362
+ callback: Callable[[DecodedResult], dict] | None = None,
358
363
  ):
359
364
  """
360
365
  Return a context manager to collect parallel tasks.
@@ -404,7 +409,7 @@ class ApiRouter(BaseRouter):
404
409
  }
405
410
  }
406
411
  }
407
- """
412
+ """,
408
413
  )
409
414
 
410
415
  client_metadata = self._get_client_metadata()
@@ -415,9 +420,10 @@ class ApiRouter(BaseRouter):
415
420
  _data = json.dumps(data, cls=DataTypeEncoder)
416
421
  payload_size = len(_data.encode("utf-8"))
417
422
  if payload_size > _MAX_PAYLOAD_SIZE_MB * 1e6:
418
- raise ValueError(
423
+ error_message = (
419
424
  f"Payload size {payload_size}, exceeds max allowed {_MAX_PAYLOAD_SIZE_MB} MB."
420
425
  )
426
+ raise ValueError(error_message)
421
427
  input_ = {
422
428
  "registry": registry,
423
429
  "workflow": workflow,
@@ -427,8 +433,6 @@ class ApiRouter(BaseRouter):
427
433
 
428
434
  response = self._client.execute(query, {"input": input_})
429
435
 
430
- # pylint:disable=unsubscriptable-object
431
-
432
436
  self._handle_warnings(response["startCoreWorkflow"]["warnings"])
433
437
  action_data = response["startCoreWorkflow"]["action"]
434
438
 
@@ -449,12 +453,11 @@ class ApiRouter(BaseRouter):
449
453
 
450
454
  return self.get_result(action)
451
455
 
452
- def _get_client_metadata(self) -> Dict[str, Any]:
456
+ def _get_client_metadata(self) -> dict[str, Any]:
453
457
  """
454
458
  Return the client metadata to be included on the
455
459
  request to start the workflow.
456
460
  """
457
-
458
461
  package_versions = {}
459
462
 
460
463
  for package in self._TRACKED_PACKAGES:
@@ -474,17 +477,17 @@ class ApiRouter(BaseRouter):
474
477
  if organization.slug == self._settings.organization:
475
478
  return organization
476
479
 
477
- raise RuntimeError(f"Organization not found: {self._settings.organization}")
480
+ error_message = f"Organization not found: {self._settings.organization}"
481
+ raise RuntimeError(error_message)
478
482
 
479
483
  @staticmethod
480
- def _handle_warnings(warnings_data: List[Dict[str, Any]]):
484
+ def _handle_warnings(warnings_data: list[dict[str, Any]]):
481
485
  """
482
486
  Handle warnings returned when starting a workflow.
483
487
  """
484
-
485
488
  for warning_data in warnings_data:
486
489
  message = warning_data["message"]
487
- warn(Warning(message))
490
+ warn(Warning(message), stacklevel=2)
488
491
 
489
492
  def update_action_status(self, action: Action) -> Action:
490
493
  """
@@ -508,7 +511,7 @@ class ApiRouter(BaseRouter):
508
511
  }
509
512
  }
510
513
  }
511
- """
514
+ """,
512
515
  )
513
516
 
514
517
  response = self._client.execute(_query, {"modelId": action.action_id})
@@ -532,21 +535,23 @@ class ApiRouter(BaseRouter):
532
535
  return self.update_action_status(action)
533
536
 
534
537
  def get_result(
535
- self, action: Action, revoke_on_interrupt: bool = True
538
+ self,
539
+ action: Action,
540
+ *,
541
+ revoke_on_interrupt: bool = True,
536
542
  ) -> DecodedResult:
537
543
  """
538
544
  Return the result of the action.
539
545
  """
540
- return self._fetch_action(action, revoke_on_interrupt).result
546
+ return self._fetch_action(action, revoke_on_interrupt=revoke_on_interrupt).result
541
547
 
542
- def _fetch_action(self, action: Action, revoke_on_interrupt: bool = True) -> Any:
548
+ def _fetch_action(self, action: Action, *, revoke_on_interrupt: bool = True) -> Any:
543
549
  """
544
550
  Fetch the action from the server. If the action
545
551
  has not finished, the API will be polled until it has.
546
552
  If the action has failed, a `RuntimeError` will be
547
553
  raised.
548
554
  """
549
-
550
555
  if not action.is_finished():
551
556
  try:
552
557
  action = self._poll_for_completion(action)
@@ -570,9 +575,8 @@ class ApiRouter(BaseRouter):
570
575
 
571
576
  if action.is_revoked():
572
577
  self._settings.event_dispatch("action.revoked", action=action)
573
- raise RuntimeError(
574
- f'Your task (action_id="{action.action_id}") has been cancelled.'
575
- )
578
+ error_message = f'Your task (action_id="{action.action_id}") has been cancelled.'
579
+ raise RuntimeError(error_message)
576
580
 
577
581
  self._settings.event_dispatch("action.success", action=action)
578
582
  return action
@@ -581,7 +585,6 @@ class ApiRouter(BaseRouter):
581
585
  """
582
586
  Update the status of the Action to REVOKED.
583
587
  """
584
-
585
588
  _query = gql.gql(
586
589
  """
587
590
  mutation updateActionMutation($modelId: String!, $status: ActionStatusEnum ) {
@@ -598,11 +601,12 @@ class ApiRouter(BaseRouter):
598
601
  }
599
602
  }
600
603
  }
601
- """
604
+ """,
602
605
  )
603
606
 
604
607
  self._client.execute(
605
- _query, {"modelId": action.action_id, "status": ActionStatus.REVOKED.value}
608
+ _query,
609
+ {"modelId": action.action_id, "status": ActionStatus.REVOKED.value},
606
610
  )
607
611
 
608
612
  def request_machines(self, machine_count: int):
@@ -622,21 +626,19 @@ class ApiRouter(BaseRouter):
622
626
  The minimum number of machines requested to be online.
623
627
  """
624
628
  if not isinstance(machine_count, int) or machine_count < 1:
625
- raise GraphQLClientError(
626
- "The number of machines requested must be an integer greater than 0."
627
- )
629
+ error_message = "The number of machines requested must be an integer greater than 0."
630
+ raise GraphQLClientError(error_message)
628
631
 
629
632
  if self._request_minimum_number_of_machines(machine_count) > 0:
630
633
  _s = "" if machine_count == 1 else "s"
631
- print(f"Waiting for {machine_count} machine{_s} to be online...")
634
+ print("Waiting for %d machine%s to be online...", machine_count, _s) # noqa: T201
632
635
  self.wait_for_machine_instantiation(machine_count)
633
- print(f"Requested machines ({machine_count}) are online.")
636
+ print("Requested machines (%d) are online.", machine_count) # noqa: T201
634
637
 
635
638
  def _request_minimum_number_of_machines(self, machine_count: int) -> int:
636
639
  """
637
640
  Request the minimum number of machines that are to be provisioned.
638
641
  """
639
-
640
642
  _query = gql.gql(
641
643
  """
642
644
  mutation requestMachines($minimum: Int!, $organizationId: ID!) {
@@ -648,7 +650,7 @@ class ApiRouter(BaseRouter):
648
650
  }
649
651
  }
650
652
  }
651
- """
653
+ """,
652
654
  )
653
655
  response = self._client.execute(
654
656
  _query,
@@ -666,14 +668,13 @@ class ApiRouter(BaseRouter):
666
668
  @retry(
667
669
  wait=wait_fixed(10),
668
670
  retry=retry_if_result(
669
- lambda response: response["online"] < response["requested"]
671
+ lambda response: response["online"] < response["requested"],
670
672
  ),
671
673
  )
672
674
  def wait_for_machine_instantiation(self, number_of_machines_requested: int):
673
675
  """
674
676
  Wait until the requested number of machines are online.
675
677
  """
676
-
677
678
  number_of_machines_online = self.get_machine_status()["online"]
678
679
 
679
680
  def machines(count: int) -> str:
@@ -681,9 +682,10 @@ class ApiRouter(BaseRouter):
681
682
  return "1 machine"
682
683
  return f"{count} machines"
683
684
 
684
- print(
685
- f"Current environment: {machines(number_of_machines_online)} online, "
686
- f"{machines(number_of_machines_requested - number_of_machines_online)} pending."
685
+ print( # noqa: T201
686
+ "Current environment: %s online, %s pending.",
687
+ machines(number_of_machines_online),
688
+ machines(number_of_machines_requested - number_of_machines_online),
687
689
  )
688
690
  return {
689
691
  "online": number_of_machines_online,
@@ -712,7 +714,7 @@ class ApiRouter(BaseRouter):
712
714
  }
713
715
  }
714
716
  }
715
- """
717
+ """,
716
718
  )
717
719
 
718
720
  response = self._client.execute(
@@ -741,7 +743,7 @@ class ApiRouter(BaseRouter):
741
743
  }
742
744
  }
743
745
  }
744
- """
746
+ """,
745
747
  )
746
748
 
747
749
  response = self._client.execute(
@@ -773,7 +775,7 @@ class ApiRouter(BaseRouter):
773
775
  }
774
776
  }
775
777
  }
776
- """
778
+ """,
777
779
  )
778
780
  return self._client.execute(
779
781
  _query,
@@ -787,7 +789,8 @@ class ApiRouter(BaseRouter):
787
789
  self,
788
790
  limit: int,
789
791
  offset: int = 0,
790
- status: Optional[str] = None,
792
+ status: str | None = None,
793
+ *,
791
794
  only_user_results: bool = True,
792
795
  ) -> list[dict[str, Any]]:
793
796
  """
@@ -830,7 +833,7 @@ class ApiRouter(BaseRouter):
830
833
  }
831
834
  }
832
835
  }
833
- """
836
+ """,
834
837
  )
835
838
 
836
839
  check_argument(limit >= 1, "Limit must be at least 1.", {"limit": limit})
@@ -851,11 +854,16 @@ class ApiRouter(BaseRouter):
851
854
  filter_by["status"] = {"exact": status}
852
855
 
853
856
  response = self._client.execute(
854
- _query, {"limit": limit, "offset": offset, "filterBy": filter_by}
857
+ _query,
858
+ {"limit": limit, "offset": offset, "filterBy": filter_by},
855
859
  )
856
860
  return response["actions"]["actions"]
857
861
 
858
- def create_presigned_url_upload(self, checksum: str) -> PresignedUrlData:
862
+ def create_presigned_url_upload(
863
+ self,
864
+ checksum: str,
865
+ registry: str,
866
+ ) -> PresignedUrlData:
859
867
  """
860
868
  Create a presigned URL for uploading data using the provided checksum.
861
869
 
@@ -863,6 +871,8 @@ class ApiRouter(BaseRouter):
863
871
  ----------
864
872
  checksum : str
865
873
  The Base64-encoded SHA-256 checksum of the data to be uploaded.
874
+ registry : str
875
+ The client registry name.
866
876
 
867
877
  Returns
868
878
  -------
@@ -888,16 +898,16 @@ class ApiRouter(BaseRouter):
888
898
  }
889
899
  }
890
900
  }
891
- """
901
+ """,
892
902
  )
893
903
 
894
904
  response = self._client.execute(
895
905
  query,
896
906
  variable_values={
897
907
  "input": {
898
- "clientApp": self._settings.product.value.registry,
908
+ "clientApp": registry,
899
909
  "checksum": checksum,
900
- }
910
+ },
901
911
  },
902
912
  )
903
913
  return response["createPresignedUrlUpload"]["presignedUrlData"]
@@ -908,24 +918,26 @@ class ParallelCollector:
908
918
  Collect tasks to run them in parallel.
909
919
  """
910
920
 
911
- def __init__(self, router: ApiRouter, callback: Optional[Callable[dict, dict]]):
921
+ def __init__(self, router: ApiRouter, callback: Callable[dict, dict] | None):
912
922
  self._router = router
913
923
  self._callback = (lambda x: x.decoded) if callback is None else callback
914
924
  self._async_results = []
915
925
 
916
- def add(self, async_result: Dict[str, Action]) -> None:
926
+ def add(self, async_result: dict[str, Action]) -> None:
917
927
  """
918
928
  The async result as a dictionary is added for fetching the response later
919
929
  when existing the context manager.
920
930
  """
921
- assert len(async_result) == 1
931
+ if len(async_result) != 1:
932
+ error_message = "Expected exactly one async result"
933
+ raise ValueError(error_message)
922
934
  self._async_results.append(async_result)
923
935
 
924
936
  def __enter__(self):
925
- self._router._parallel_task_collector = self
937
+ self._router._parallel_task_collector = self # noqa: SLF001
926
938
 
927
939
  def __exit__(self, exc_type, exc_value, traceback):
928
- self._router._parallel_task_collector = None
940
+ self._router._parallel_task_collector = None # noqa: SLF001
929
941
 
930
942
  # Do not try to call any functions upon an exception.
931
943
  if isinstance(exc_value, Exception):
@@ -933,13 +945,16 @@ class ParallelCollector:
933
945
 
934
946
  for result in self._async_results:
935
947
  action = result.pop("async_result", None)
936
- assert action is not None
937
- response = self._router._fetch_action(action)
948
+ if action is None:
949
+ error_message = "Action cannot be None"
950
+ raise ValueError(error_message)
951
+ response = self._router._fetch_action(action) # noqa: SLF001
938
952
  if action.action_id != response.action_id:
939
- raise RuntimeError(
953
+ error_message = (
940
954
  f"Got the wrong result of action {response.action_id}, "
941
955
  f"expected to update the result of action {action.action_id}."
942
956
  )
957
+ raise RuntimeError(error_message)
943
958
  result.update(self._callback(response.result))
944
959
 
945
960
  return True
@@ -10,6 +10,14 @@
10
10
  # Unless required by applicable law or agreed to in writing, software
11
11
  # distributed under the License is distributed on an "AS IS" BASIS. See the
12
12
  # License for the specific language.
13
+ from __future__ import annotations
14
+
15
+ from typing import TYPE_CHECKING
16
+
17
+ if TYPE_CHECKING:
18
+ from qctrlcoreworkflowmanager import CallableResolver
19
+ else:
20
+ from qctrlcoreworkflowmanager import CallableResolver # noqa: TCH002
13
21
 
14
22
  from .base import BaseRouter
15
23
 
@@ -21,12 +29,12 @@ class LocalRouter(BaseRouter):
21
29
 
22
30
  Parameters
23
31
  ----------
24
- resolver : BaseResolver
32
+ resolver : CallableResolver
25
33
  A resolver object for the registry which contains all
26
34
  required workflows.
27
35
  """
28
36
 
29
- def __init__(self, resolver: "BaseResolver"):
37
+ def __init__(self, resolver: CallableResolver):
30
38
  self._resolver = resolver
31
39
 
32
40
  def __call__(self, workflow, data=None):
@@ -43,5 +51,4 @@ class LocalRouter(BaseRouter):
43
51
  data = data or {}
44
52
  task = self._resolver.get_workflow_task_from_signature(workflow, **data)
45
53
  func = self._resolver(task)
46
- result = func()
47
- return result
54
+ return func()
@@ -13,20 +13,21 @@
13
13
 
14
14
  from __future__ import annotations
15
15
 
16
+ import contextlib
17
+ from collections.abc import Callable
16
18
  from dataclasses import dataclass
17
19
  from functools import cached_property
18
20
  from typing import (
21
+ TYPE_CHECKING,
19
22
  Any,
20
- Callable,
21
- Dict,
22
- Optional,
23
- Union,
24
23
  )
25
24
 
26
- from .products import Product
27
25
  from .router.base import BaseRouter
28
26
 
29
- EventListener = Callable[[str, Dict[str, Any]], None]
27
+ if TYPE_CHECKING:
28
+ from .products import Product
29
+
30
+ EventListener = Callable[[str, dict[str, Any]], None]
30
31
 
31
32
 
32
33
  @dataclass
@@ -53,19 +54,19 @@ class CoreClientSettings:
53
54
  will be dispatched to.
54
55
  """
55
56
 
56
- router: Union[BaseRouter, Callable]
57
- product: Optional[Product] = None
58
- organization: Optional[str] = None
59
- event_listeners: Optional[list[EventListener]] = None
57
+ router: BaseRouter | Callable
58
+ product: Product | None = None
59
+ organization: str | None = None
60
+ event_listeners: list[EventListener] | None = None
60
61
 
61
62
  def update(self, **kwargs):
62
63
  """
63
64
  Update settings fields.
64
65
  """
65
-
66
66
  for attr, value in kwargs.items():
67
67
  if not hasattr(self, attr):
68
- raise AttributeError(f"Invalid field: {attr}")
68
+ error_message = f"Invalid field: {attr}"
69
+ raise AttributeError(error_message)
69
70
 
70
71
  setattr(self, attr, value)
71
72
 
@@ -76,17 +77,14 @@ class CoreClientSettings:
76
77
  """
77
78
  Clear the cached router.
78
79
  """
79
- try:
80
+ with contextlib.suppress(AttributeError):
80
81
  delattr(self, "_router")
81
- except AttributeError:
82
- pass
83
82
 
84
83
  @cached_property
85
84
  def _router(self) -> BaseRouter:
86
85
  """
87
86
  Prepare the router to be used by the core client.
88
87
  """
89
-
90
88
  router = self.router
91
89
 
92
90
  if isinstance(router, BaseRouter):
@@ -96,7 +94,8 @@ class CoreClientSettings:
96
94
  router = router()
97
95
 
98
96
  if not isinstance(router, BaseRouter):
99
- raise ValueError(f"Invalid router: {router}")
97
+ error_message = f"Invalid router: {router}"
98
+ raise TypeError(error_message)
100
99
 
101
100
  return router
102
101
 
@@ -19,8 +19,10 @@ from importlib.metadata import (
19
19
  version,
20
20
  )
21
21
  from pathlib import Path
22
- from types import ModuleType
23
- from typing import Optional
22
+ from typing import TYPE_CHECKING
23
+
24
+ if TYPE_CHECKING:
25
+ from types import ModuleType
24
26
 
25
27
  import requests
26
28
  import tomli
@@ -28,7 +30,7 @@ from importlib_metadata import packages_distributions
28
30
  from packaging.version import parse
29
31
 
30
32
 
31
- def get_installed_version(package: str) -> Optional[str]:
33
+ def get_installed_version(package: str) -> str | None:
32
34
  """
33
35
  Get the installed version of the package. If
34
36
  the version cannot be found (e.g. package not installed)
@@ -52,7 +54,7 @@ class PackageInfo:
52
54
  name: str
53
55
  install_name: str
54
56
  import_name: str
55
- changelog_url: Optional[str] = None
57
+ changelog_url: str | None = None
56
58
 
57
59
 
58
60
  def get_latest_pypi_version(package: str) -> str:
@@ -70,15 +72,17 @@ def check_package_version(package: PackageInfo) -> None:
70
72
  show an upgrade message if the current version is outdated.
71
73
  """
72
74
  local_package = sys.modules.get(package.import_name)
73
- assert local_package is not None, f"{package.import_name} is not found."
74
- local_version = getattr(local_package, "__version__")
75
+ if local_package is None:
76
+ error_message = f"{package.import_name} is not found."
77
+ raise ImportError(error_message)
78
+ local_version = local_package.__version__
75
79
  latest_version = get_latest_pypi_version(package.install_name)
76
80
 
77
81
  if parse(local_version) < parse(latest_version):
78
- print(f"{package.name} update available.")
79
- print(f"Latest version is {latest_version}, you have {local_version}.")
82
+ print(f"{package.name} update available.") # noqa: T201
83
+ print(f"Latest version is {latest_version}, you have {local_version}.") # noqa: T201
80
84
  if package.changelog_url is not None:
81
- print(f"Visit {package.changelog_url} for the latest product updates.")
85
+ print(f"Visit {package.changelog_url} for the latest product updates.") # noqa: T201
82
86
 
83
87
 
84
88
  def package_versions_table(package_names: list[str]) -> str:
@@ -96,26 +100,28 @@ def package_versions_table(package_names: list[str]) -> str:
96
100
  str
97
101
  A string containing the Markdown-formatted table.
98
102
  """
99
-
100
103
  # List containing the items in the different rows.
101
104
  table_items = get_package_versions(package_names=package_names)
102
105
 
103
106
  # Widths of the table columns.
104
- package_width = max(max(len(item[0]) for item in table_items), 7)
105
- version_width = max(max(len(item[1]) for item in table_items), 7)
107
+ all_items = [*table_items, ("Package", "Version")]
108
+ min_column_count = 2
109
+ package_width = max(len(item[0]) for item in all_items if len(item) >= min_column_count)
110
+ version_width = max(len(item[1]) for item in all_items if len(item) >= min_column_count)
106
111
 
107
112
  # Add headers and Python version at top of table.
108
113
  table_items = [
109
114
  ("Package", "Version"),
110
115
  ("-" * package_width, "-" * version_width),
111
- ] + table_items
116
+ *table_items,
117
+ ]
112
118
 
113
119
  # Build table.
114
120
  return "\n".join(
115
121
  [
116
122
  f"| {name:{package_width}s} | {version_:{version_width}s} |"
117
123
  for name, version_ in table_items
118
- ]
124
+ ],
119
125
  )
120
126
 
121
127
 
@@ -144,9 +150,8 @@ def _get_package_name(module: ModuleType) -> str:
144
150
  return _package_names_mapping[module.__name__][0]
145
151
 
146
152
  # The package is in editable mode: look in pyproject.toml to get the package name.
147
- with open(
148
- Path.joinpath(Path(module.__path__[0]).parent, "pyproject.toml"), "rb"
149
- ) as file:
153
+ toml_path = Path(module.__path__[0]).parent / "pyproject.toml"
154
+ with toml_path.open("rb") as file:
150
155
  config = tomli.load(file)
151
156
  return config["project"]["name"]
152
157
 
@@ -165,7 +170,6 @@ def get_package_versions(package_names: list[str]) -> list[tuple[str, str]]:
165
170
  list[tuple[str, str]]
166
171
  The package name and package version.
167
172
  """
168
-
169
173
  package_versions = [
170
174
  (
171
175
  _get_package_name(sys.modules[module_name]),
@@ -174,10 +178,10 @@ def get_package_versions(package_names: list[str]) -> list[tuple[str, str]]:
174
178
  for module_name in package_names
175
179
  if module_name in sys.modules
176
180
  ]
177
- package_versions = [
181
+ return [
178
182
  (
179
183
  "Python",
180
184
  f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
181
- )
182
- ] + package_versions
183
- return package_versions
185
+ ),
186
+ *package_versions,
187
+ ]