qctrl-workflow-client 2.4.1__tar.gz → 3.1.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.1
2
2
  Name: qctrl-workflow-client
3
- Version: 2.4.1
3
+ Version: 3.1.0
4
4
  Summary: Q-CTRL Client
5
5
  Home-page: https://q-ctrl.com
6
6
  License: https://q-ctrl.com/terms
@@ -33,7 +33,7 @@ Requires-Dist: importlib-metadata (>=6.8.0,<7.0.0)
33
33
  Requires-Dist: packaging (>=23.1.0,<24.0.0)
34
34
  Requires-Dist: qctrl-client (>=9.1.0,<10.0.0)
35
35
  Requires-Dist: qctrl-commons (>=22.0.0,<23.0.0)
36
- Requires-Dist: requests (>=2.31.0,<3.0.0)
36
+ Requires-Dist: requests (>=2.32.0,<3.0.0)
37
37
  Requires-Dist: tenacity (>=8.1.0,<9.0.0)
38
38
  Requires-Dist: tomli (>=2.0.1,<3.0.0)
39
39
  Project-URL: Documentation, https://docs.q-ctrl.com
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "qctrl-workflow-client"
3
- version = "2.4.1"
3
+ version = "3.1.0"
4
4
  description = "Q-CTRL Client"
5
5
  license = "https://q-ctrl.com/terms"
6
6
  authors = ["Q-CTRL <support@q-ctrl.com>"]
@@ -79,13 +79,13 @@ tomli = "^2.0.1"
79
79
  importlib-metadata = "^6.8.0"
80
80
  gql = "^3.4.0"
81
81
  tenacity = "^8.1.0"
82
- requests = "^2.31.0"
82
+ requests = "^2.32.0"
83
83
  packaging = "^23.1.0"
84
84
  qctrl-commons = { version = "^22.0.0", source = "PyPI" }
85
85
  qctrl-client = { version = "^9.1.0", source = "PyPI" }
86
86
 
87
87
  [tool.poetry.group.dev.dependencies]
88
- black = "^23.7.0"
88
+ black = "^24.3.0"
89
89
  isort = "^5.12.0"
90
90
  pre-commit = "^3.3.3"
91
91
  pylint = "^2.14.4"
@@ -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__ = "2.4.1"
14
+ __version__ = "3.1.0"
15
15
 
16
16
  from .defaults import (
17
17
  get_authenticated_client_for_product,
@@ -24,8 +24,10 @@ from qctrlclient.client import (
24
24
  GraphQLClient,
25
25
  GraphQLClientError,
26
26
  )
27
+ from qctrlcommons.utils import generate_user_agent
27
28
 
28
29
  from .globals import global_value
30
+ from .utils import get_installed_version
29
31
 
30
32
  _DEFAULT_API_URL = "https://federation-service.q-ctrl.com"
31
33
  _DEFAULT_OIDC_URL = "https://id.q-ctrl.com"
@@ -58,6 +60,7 @@ def get_default_cli_auth() -> CliAuth:
58
60
 
59
61
  def get_authenticated_client_for_product(
60
62
  product_access_required: str,
63
+ package_name: str,
61
64
  api_url: Optional[str] = None,
62
65
  auth: Optional[BaseAuth | str] = None,
63
66
  invalid_access_error_message: Optional[str] = None,
@@ -70,6 +73,8 @@ def get_authenticated_client_for_product(
70
73
  ----------
71
74
  product_access_required : str
72
75
  The product access required for the user to use the CLI.
76
+ package_name : str
77
+ The package name to include in the User-Agent header.
73
78
  api_url : str, optional
74
79
  The API URL to use. If not provided, the default API URL will be used.
75
80
  auth : BaseAuth or str, optional
@@ -82,8 +87,15 @@ def get_authenticated_client_for_product(
82
87
  if isinstance(auth, str):
83
88
  auth = CliAuth(auth)
84
89
 
90
+ headers = {
91
+ "User-Agent": generate_user_agent(
92
+ package_name, get_installed_version(package_name)
93
+ ),
94
+ }
85
95
  client = GraphQLClient(
86
- api_url or get_default_oidc_url(), auth=auth or get_default_cli_auth()
96
+ url=api_url or get_default_oidc_url(),
97
+ headers=headers,
98
+ auth=auth or get_default_cli_auth(),
87
99
  )
88
100
 
89
101
  try:
@@ -20,6 +20,8 @@ from typing import (
20
20
  )
21
21
  from warnings import warn
22
22
 
23
+ from qctrlcommons.exceptions import QctrlArgumentsValueError
24
+
23
25
 
24
26
  def core_workflow(
25
27
  get_config: Callable, workflow: str, formatter: Optional[Callable] = None
@@ -61,6 +63,71 @@ def core_workflow(
61
63
  # the function
62
64
  config = get_config()
63
65
  router = config.get_router()
66
+
67
+ router.set_async_state(kwargs.pop("is_async", False))
68
+
69
+ data = func(*args, **kwargs)
70
+ result = router(workflow, data)
71
+
72
+ if formatter:
73
+ result = formatter(result)
74
+
75
+ return result
76
+
77
+ return customized_decorator
78
+
79
+ return decorator
80
+
81
+
82
+ def async_core_workflow(
83
+ get_config: Callable, workflow: str, formatter: Optional[Callable] = None
84
+ ):
85
+ """
86
+ Decorator for a function which will execute asynchronously workflow.
87
+ The decorated function should return the data to be used
88
+ during async workflow execution. When being used in a client
89
+ package, it is recommended to use a partial to provide
90
+ a default value for `get_config` e.g.
91
+
92
+ async_fire_opal_workflow = partial(
93
+ async_core_workflow,
94
+ get_fire_opal_config
95
+ )
96
+
97
+ @async_fire_opal_workflow("execute")
98
+ def execute(...):
99
+
100
+ Parameters
101
+ ----------
102
+ get_config : Callable
103
+ Returns a `CoreClientSettings` instance. The configured
104
+ router will be used to execute the workflow.
105
+ workflow : str
106
+ The registered name of the workflow to be executed.
107
+ formatter : Callable, optional
108
+ Optional callable which can be used to format the workflow
109
+ result. The callable should accept exactly one argument
110
+ which is the raw result fo the workflow. If used, the
111
+ decorated function will return the result of this callable.
112
+ """
113
+
114
+ def decorator(func: Callable):
115
+ @wraps(func)
116
+ def customized_decorator(*args, **kwargs):
117
+ # router is instantiated before function is called
118
+ # so any alteration to settings is visible within
119
+ # the function
120
+ config = get_config()
121
+ router = config.get_router()
122
+
123
+ # Set the async state to True
124
+ router.set_async_state(True)
125
+ if "is_async" in kwargs:
126
+ raise QctrlArgumentsValueError(
127
+ description="The asynchronous state of this function cannot be altered.",
128
+ arguments={"is_async": kwargs.get("is_async")},
129
+ )
130
+
64
131
  data = func(*args, **kwargs)
65
132
  result = router(workflow, data)
66
133
 
@@ -199,6 +199,13 @@ class ApiRouter(BaseRouter):
199
199
  self._settings = settings
200
200
  self._validate()
201
201
  self._parallel_task_collector = None
202
+ self._async = False
203
+
204
+ def set_async_state(self, is_async: bool):
205
+ """
206
+ Toggle asynchronous state of workflow execution.
207
+ """
208
+ self._async = is_async
202
209
 
203
210
  def _validate(self):
204
211
  """
@@ -384,6 +391,9 @@ class ApiRouter(BaseRouter):
384
391
  errors=action_data["errors"],
385
392
  )
386
393
 
394
+ if self._async:
395
+ return {"async_result": action}
396
+
387
397
  if self._parallel_task_collector is not None:
388
398
  async_result = {"async_result": action}
389
399
  self._parallel_task_collector.add(async_result)
@@ -404,7 +414,6 @@ class ApiRouter(BaseRouter):
404
414
 
405
415
  return {
406
416
  "package_versions": package_versions,
407
- "organization_slug": self._settings.organization,
408
417
  "organization": self._get_configured_organization().to_dict(),
409
418
  }
410
419
 
@@ -429,16 +438,11 @@ class ApiRouter(BaseRouter):
429
438
  message = warning_data["message"]
430
439
  warn(Warning(message))
431
440
 
432
- @retry(
433
- wait=_POLL_WAIT_CHAIN,
434
- retry=retry_if_result(lambda action: not action.is_finished()),
435
- )
436
- def _poll_for_completion(self, action: Action) -> Action:
441
+ def update_action_status(self, action: Action) -> Action:
437
442
  """
438
- Poll the API waiting for the action to be finished.
439
- When finished, an updated `Action` object is returned.
443
+ Update the action status. When finished, an updated
444
+ `Action` object is returned.
440
445
  """
441
-
442
446
  _query = gql.gql(
443
447
  """
444
448
  query($modelId: String!) {
@@ -468,6 +472,17 @@ class ApiRouter(BaseRouter):
468
472
 
469
473
  return action
470
474
 
475
+ @retry(
476
+ wait=_POLL_WAIT_CHAIN,
477
+ retry=retry_if_result(lambda action: not action.is_finished()),
478
+ )
479
+ def _poll_for_completion(self, action: Action) -> Action:
480
+ """
481
+ Poll the API waiting for the action to be finished.
482
+ When finished, an updated `Action` object is returned.
483
+ """
484
+ return self.update_action_status(action)
485
+
471
486
  def get_result(self, action: Action) -> DecodedResult:
472
487
  """
473
488
  Return the result of the action.