qctrl-workflow-client 7.3.0__tar.gz → 8.0.1__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.
- {qctrl_workflow_client-7.3.0 → qctrl_workflow_client-8.0.1}/PKG-INFO +2 -3
- {qctrl_workflow_client-7.3.0 → qctrl_workflow_client-8.0.1}/pyproject.toml +26 -9
- {qctrl_workflow_client-7.3.0 → qctrl_workflow_client-8.0.1}/qctrlworkflowclient/__init__.py +13 -1
- {qctrl_workflow_client-7.3.0 → qctrl_workflow_client-8.0.1}/qctrlworkflowclient/defaults.py +11 -9
- {qctrl_workflow_client-7.3.0 → qctrl_workflow_client-8.0.1}/qctrlworkflowclient/functions.py +14 -11
- {qctrl_workflow_client-7.3.0 → qctrl_workflow_client-8.0.1}/qctrlworkflowclient/router/__init__.py +2 -0
- {qctrl_workflow_client-7.3.0 → qctrl_workflow_client-8.0.1}/qctrlworkflowclient/router/api.py +98 -87
- {qctrl_workflow_client-7.3.0 → qctrl_workflow_client-8.0.1}/qctrlworkflowclient/router/local.py +11 -4
- {qctrl_workflow_client-7.3.0 → qctrl_workflow_client-8.0.1}/qctrlworkflowclient/settings.py +16 -17
- {qctrl_workflow_client-7.3.0 → qctrl_workflow_client-8.0.1}/qctrlworkflowclient/utils.py +26 -22
- {qctrl_workflow_client-7.3.0 → qctrl_workflow_client-8.0.1}/LICENSE +0 -0
- {qctrl_workflow_client-7.3.0 → qctrl_workflow_client-8.0.1}/README.md +0 -0
- {qctrl_workflow_client-7.3.0 → qctrl_workflow_client-8.0.1}/qctrlworkflowclient/products.py +0 -0
- {qctrl_workflow_client-7.3.0 → qctrl_workflow_client-8.0.1}/qctrlworkflowclient/router/base.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: qctrl-workflow-client
|
|
3
|
-
Version:
|
|
3
|
+
Version: 8.0.1
|
|
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.
|
|
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.
|
|
5
|
-
version = "
|
|
4
|
+
requires-python = ">=3.10, <3.14"
|
|
5
|
+
version = "8.0.1"
|
|
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
|
-
qctrl-core-workflow-manager = "^
|
|
112
|
+
qctrl-core-workflow-manager = "^6.0.0"
|
|
113
|
+
ruff = "^0.6.0"
|
|
112
114
|
|
|
113
|
-
[tool.
|
|
114
|
-
|
|
115
|
-
|
|
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__ = "
|
|
14
|
+
__version__ = "8.0.1"
|
|
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
|
|
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:
|
|
35
|
-
auth:
|
|
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,
|
|
60
|
+
package_name,
|
|
61
|
+
get_installed_version(package_name),
|
|
58
62
|
),
|
|
59
63
|
}
|
|
60
|
-
|
|
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
|
{qctrl_workflow_client-7.3.0 → qctrl_workflow_client-8.0.1}/qctrlworkflowclient/functions.py
RENAMED
|
@@ -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:
|
|
32
|
-
registry_selector:
|
|
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.
|
|
@@ -76,7 +79,7 @@ def core_workflow(
|
|
|
76
79
|
router = config.get_router()
|
|
77
80
|
|
|
78
81
|
if isinstance(router, ApiRouter):
|
|
79
|
-
router.set_async_state(kwargs.pop("is_async", False))
|
|
82
|
+
router.set_async_state(is_async=kwargs.pop("is_async", False))
|
|
80
83
|
|
|
81
84
|
data = func(*args, **kwargs)
|
|
82
85
|
|
|
@@ -103,8 +106,8 @@ def core_workflow(
|
|
|
103
106
|
def async_core_workflow(
|
|
104
107
|
get_config: Callable,
|
|
105
108
|
workflow: str,
|
|
106
|
-
formatter:
|
|
107
|
-
registry_selector:
|
|
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.
|
|
@@ -151,7 +154,7 @@ def async_core_workflow(
|
|
|
151
154
|
router = config.get_router()
|
|
152
155
|
|
|
153
156
|
# Set the async state to True
|
|
154
|
-
router.set_async_state(True)
|
|
157
|
+
router.set_async_state(is_async=True)
|
|
155
158
|
if "is_async" in kwargs:
|
|
156
159
|
raise QctrlArgumentsValueError(
|
|
157
160
|
description="The asynchronous state of this function cannot be altered.",
|
|
@@ -180,7 +183,7 @@ def async_core_workflow(
|
|
|
180
183
|
return decorator
|
|
181
184
|
|
|
182
185
|
|
|
183
|
-
def print_warnings(result:
|
|
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
|
{qctrl_workflow_client-7.3.0 → qctrl_workflow_client-8.0.1}/qctrlworkflowclient/router/api.py
RENAMED
|
@@ -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
|
-
|
|
22
|
-
Dict,
|
|
23
|
-
List,
|
|
24
|
-
Optional,
|
|
22
|
+
ClassVar,
|
|
25
23
|
)
|
|
26
24
|
from warnings import warn
|
|
27
25
|
|
|
28
26
|
import gql
|
|
29
|
-
|
|
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) ->
|
|
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:
|
|
139
|
-
raw_result:
|
|
140
|
-
errors:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) ->
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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,
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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:
|
|
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,
|
|
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
|
-
|
|
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) ->
|
|
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
|
-
|
|
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:
|
|
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,
|
|
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
|
-
|
|
574
|
-
|
|
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,
|
|
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
|
-
|
|
626
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
686
|
-
|
|
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:
|
|
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,12 +854,15 @@ class ApiRouter(BaseRouter):
|
|
|
851
854
|
filter_by["status"] = {"exact": status}
|
|
852
855
|
|
|
853
856
|
response = self._client.execute(
|
|
854
|
-
_query,
|
|
857
|
+
_query,
|
|
858
|
+
{"limit": limit, "offset": offset, "filterBy": filter_by},
|
|
855
859
|
)
|
|
856
860
|
return response["actions"]["actions"]
|
|
857
861
|
|
|
858
862
|
def create_presigned_url_upload(
|
|
859
|
-
self,
|
|
863
|
+
self,
|
|
864
|
+
checksum: str,
|
|
865
|
+
registry: str,
|
|
860
866
|
) -> PresignedUrlData:
|
|
861
867
|
"""
|
|
862
868
|
Create a presigned URL for uploading data using the provided checksum.
|
|
@@ -892,7 +898,7 @@ class ApiRouter(BaseRouter):
|
|
|
892
898
|
}
|
|
893
899
|
}
|
|
894
900
|
}
|
|
895
|
-
"""
|
|
901
|
+
""",
|
|
896
902
|
)
|
|
897
903
|
|
|
898
904
|
response = self._client.execute(
|
|
@@ -901,7 +907,7 @@ class ApiRouter(BaseRouter):
|
|
|
901
907
|
"input": {
|
|
902
908
|
"clientApp": registry,
|
|
903
909
|
"checksum": checksum,
|
|
904
|
-
}
|
|
910
|
+
},
|
|
905
911
|
},
|
|
906
912
|
)
|
|
907
913
|
return response["createPresignedUrlUpload"]["presignedUrlData"]
|
|
@@ -912,24 +918,26 @@ class ParallelCollector:
|
|
|
912
918
|
Collect tasks to run them in parallel.
|
|
913
919
|
"""
|
|
914
920
|
|
|
915
|
-
def __init__(self, router: ApiRouter, callback:
|
|
921
|
+
def __init__(self, router: ApiRouter, callback: Callable[dict, dict] | None):
|
|
916
922
|
self._router = router
|
|
917
923
|
self._callback = (lambda x: x.decoded) if callback is None else callback
|
|
918
924
|
self._async_results = []
|
|
919
925
|
|
|
920
|
-
def add(self, async_result:
|
|
926
|
+
def add(self, async_result: dict[str, Action]) -> None:
|
|
921
927
|
"""
|
|
922
928
|
The async result as a dictionary is added for fetching the response later
|
|
923
929
|
when existing the context manager.
|
|
924
930
|
"""
|
|
925
|
-
|
|
931
|
+
if len(async_result) != 1:
|
|
932
|
+
error_message = "Expected exactly one async result"
|
|
933
|
+
raise ValueError(error_message)
|
|
926
934
|
self._async_results.append(async_result)
|
|
927
935
|
|
|
928
936
|
def __enter__(self):
|
|
929
|
-
self._router._parallel_task_collector = self
|
|
937
|
+
self._router._parallel_task_collector = self # noqa: SLF001
|
|
930
938
|
|
|
931
939
|
def __exit__(self, exc_type, exc_value, traceback):
|
|
932
|
-
self._router._parallel_task_collector = None
|
|
940
|
+
self._router._parallel_task_collector = None # noqa: SLF001
|
|
933
941
|
|
|
934
942
|
# Do not try to call any functions upon an exception.
|
|
935
943
|
if isinstance(exc_value, Exception):
|
|
@@ -937,13 +945,16 @@ class ParallelCollector:
|
|
|
937
945
|
|
|
938
946
|
for result in self._async_results:
|
|
939
947
|
action = result.pop("async_result", None)
|
|
940
|
-
|
|
941
|
-
|
|
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
|
|
942
952
|
if action.action_id != response.action_id:
|
|
943
|
-
|
|
953
|
+
error_message = (
|
|
944
954
|
f"Got the wrong result of action {response.action_id}, "
|
|
945
955
|
f"expected to update the result of action {action.action_id}."
|
|
946
956
|
)
|
|
957
|
+
raise RuntimeError(error_message)
|
|
947
958
|
result.update(self._callback(response.result))
|
|
948
959
|
|
|
949
960
|
return True
|
{qctrl_workflow_client-7.3.0 → qctrl_workflow_client-8.0.1}/qctrlworkflowclient/router/local.py
RENAMED
|
@@ -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 :
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
57
|
-
product:
|
|
58
|
-
organization:
|
|
59
|
-
event_listeners:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
23
|
-
|
|
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) ->
|
|
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:
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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
|
-
|
|
148
|
-
|
|
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
|
-
|
|
181
|
+
return [
|
|
178
182
|
(
|
|
179
183
|
"Python",
|
|
180
184
|
f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
|
|
185
|
+
),
|
|
186
|
+
*package_versions,
|
|
187
|
+
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{qctrl_workflow_client-7.3.0 → qctrl_workflow_client-8.0.1}/qctrlworkflowclient/router/base.py
RENAMED
|
File without changes
|