Skip to content

Commit fa61f4e

Browse files
priyadarshini-niPriyadarshini PiramanayagamShriramS-Emerson
authored
feat: Execute work item API (#199)
Co-authored-by: Priyadarshini Piramanayagam <priydarshini.piramanayagam@emerson.com> Co-authored-by: ShriramS-Emerson <shriram.sakthivel@emerson.com>
1 parent 0578b48 commit fa61f4e

File tree

10 files changed

+419
-7
lines changed

10 files changed

+419
-7
lines changed

docs/api_reference/work_item.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ nisystemlink.clients.work_item
1313
.. automethod:: update_work_items
1414
.. automethod:: schedule_work_items
1515
.. automethod:: delete_work_items
16+
.. automethod:: execute_work_item
1617
.. automethod:: create_work_item_templates
1718
.. automethod:: query_work_item_templates
1819
.. automethod:: update_work_item_templates

docs/getting_started.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -456,13 +456,13 @@ default connection. The default connection depends on your environment.
456456

457457
With a :class:`.WorkItemClient` object, you can:
458458

459-
* Create, query, get, update, schedule and delete work items
459+
* Create, query, get, update, schedule, delete and execute work items
460460
* Create, query, update and delete work item templates
461461

462462
Examples
463463
~~~~~~~~
464464

465-
Create, query, get, update, schedule and delete work items
465+
Create, query, get, update, schedule, delete and execute work items
466466

467467
.. literalinclude:: ../examples/work_item/work_items.py
468468
:language: python

examples/work_item/work_items.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from datetime import datetime
22

33
from nisystemlink.clients.core import HttpConfiguration
4-
from nisystemlink.clients.work_item import WorkItemClient
4+
from nisystemlink.clients.work_item import WorkItemClient, WorkItemExecuteApiException
55
from nisystemlink.clients.work_item.models import (
66
CreateWorkItemRequest,
77
Dashboard,
@@ -105,6 +105,7 @@
105105
dashboard=Dashboard(
106106
id="DashboardId", variables={"product": "PXIe-4080", "location": "Lab1"}
107107
),
108+
workflow_id="example-workflow-id",
108109
execution_actions=[
109110
ManualExecution(action="boot", type="MANUAL"),
110111
JobExecution(
@@ -202,6 +203,28 @@
202203
f"Scheduled work item with ID: {schedule_work_items_response.scheduled_work_items[0].id}"
203204
)
204205

206+
# Execute work item action
207+
if created_work_item_id is not None:
208+
try:
209+
execute_response = client.execute_work_item(
210+
work_item_id=created_work_item_id, action="START"
211+
)
212+
if execute_response.result is not None:
213+
print(f"Executed action successfully. Type: {execute_response.result.type}")
214+
215+
# Use type narrowing to access type-specific fields
216+
if execute_response.result.type == "NOTEBOOK":
217+
print(f"Notebook execution ID: {execute_response.result.execution_id}")
218+
elif execute_response.result.type == "JOB":
219+
if execute_response.result.job_ids:
220+
print(f"Job IDs: {', '.join(execute_response.result.job_ids)}")
221+
except WorkItemExecuteApiException as e:
222+
print(f"Execution failed (HTTP {e.http_status_code}): {e.error}")
223+
if e.result is not None:
224+
print(f"Partial result: {e.result.type}")
225+
except Exception as e:
226+
print(f"Could not execute action: {e}")
227+
205228
# Delete work item
206229
if created_work_item_id is not None:
207230
client.delete_work_items(ids=[created_work_item_id])

nisystemlink/clients/core/_api_exception.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""Implementation of ApiException."""
44

55
import typing
6+
from typing import Any, Dict
67

78
from nisystemlink.clients import core
89

@@ -16,6 +17,7 @@ def __init__(
1617
error: core.ApiError | None = None,
1718
http_status_code: int | None = None,
1819
inner: Exception | None = None,
20+
response_data: Dict[str, Any] | None = None,
1921
) -> None:
2022
"""Initialize an exception.
2123
@@ -25,11 +27,14 @@ def __init__(
2527
http_status_code: The HTTP status code, if this exception was the result of
2628
an HTTP error.
2729
inner: The inner exception that caused the error.
30+
response_data: The full parsed JSON response body, if the server returned
31+
one. Useful when an error response still contains actionable data.
2832
"""
2933
self._message = message
3034
self._error = error
3135
self._http_status_code = http_status_code
3236
self._inner = inner
37+
self._response_data = response_data
3338

3439
@property
3540
def message(self) -> str | None: # noqa:D401
@@ -56,6 +61,16 @@ def inner_exception(self) -> Exception | None: # noqa: D401
5661
"""The exception that caused this failure, if any."""
5762
return self._inner
5863

64+
@property
65+
def response_data(self) -> Dict[str, Any] | None: # noqa: D401
66+
"""The full parsed JSON response body returned by the server, or None.
67+
68+
This is populated for HTTP error responses that include a JSON body.
69+
It allows callers to recover structured data from error responses — for
70+
example, a partial-failure response may contain result data alongside the error details.
71+
"""
72+
return dict(self._response_data) if self._response_data is not None else None
73+
5974
def __str__(self) -> str:
6075
txt = self._message or "API exception occurred"
6176
if self._error:
@@ -71,6 +86,7 @@ def __eq__(self, other: object) -> bool:
7186
self._error == other_._error,
7287
self._http_status_code == other_._http_status_code,
7388
self._inner == other_._inner,
89+
self._response_data == other_._response_data,
7490
)
7591
)
7692

nisystemlink/clients/core/_uplink/_base_client.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ def _handle_http_status(response: Response) -> Response | None:
3434
err_obj = None
3535

3636
raise core.ApiException(
37-
msg, error=err_obj, http_status_code=response.status_code
37+
msg,
38+
error=err_obj,
39+
http_status_code=response.status_code,
40+
response_data=content,
3841
)
3942
except JSONDecodeError:
4043
if response.text:
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
from ._work_item_client import WorkItemClient
1+
from ._work_item_client import WorkItemClient, WorkItemExecuteApiException
22

33
# flake8: noqa

nisystemlink/clients/work_item/_work_item_client.py

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,31 @@
55
from nisystemlink.clients.core._uplink._base_client import BaseClient
66
from nisystemlink.clients.core._uplink._methods import get, post
77
from nisystemlink.clients.work_item import models
8-
from uplink import Field, retry
8+
from uplink import Field, Path, retry
9+
10+
11+
class WorkItemExecuteApiException(core.ApiException):
12+
"""Raised when the execute work item API returns an error with a structured response body.
13+
14+
The API may return partial results (e.g. cancelled job IDs) alongside an error.
15+
This exception exposes those results via the :attr:`result` property.
16+
"""
17+
18+
def __init__(
19+
self,
20+
msg: str,
21+
*,
22+
http_status_code: int,
23+
error: core.ApiError | None,
24+
result: models.ExecutionResult | None,
25+
) -> None:
26+
super().__init__(msg, error=error, http_status_code=http_status_code)
27+
self._result = result
28+
29+
@property
30+
def result(self) -> models.ExecutionResult | None:
31+
"""The partial execution result returned alongside the error, if any."""
32+
return self._result
933

1034

1135
@retry(
@@ -132,6 +156,54 @@ def delete_work_items(
132156
"""
133157
...
134158

159+
def execute_work_item(
160+
self, work_item_id: str, action: str
161+
) -> models.ExecuteWorkItemResponse:
162+
"""Executes the specified action for the work item.
163+
164+
Args:
165+
work_item_id: The ID of the work item the action will be performed on.
166+
action: The action to execute on the work item.
167+
168+
Returns:
169+
The response containing the execution result.
170+
171+
Raises:
172+
WorkItemExecuteApiException: if the API returns an error response with
173+
an execute-specific body. The :attr:`~WorkItemExecuteApiException.result`
174+
property may contain partial results (e.g. cancelled job IDs).
175+
ApiException: if unable to communicate with the `/niworkitem` service
176+
or provided invalid arguments.
177+
"""
178+
try:
179+
return self._execute_work_item(work_item_id=work_item_id, action=action)
180+
except core.ApiException as e:
181+
data = e.response_data
182+
if not data or "result" not in data:
183+
raise
184+
185+
try:
186+
response = models.ExecuteWorkItemResponse.model_validate(data)
187+
except Exception:
188+
raise e
189+
190+
raise WorkItemExecuteApiException(
191+
str(e),
192+
http_status_code=e.http_status_code or 0,
193+
error=response.error,
194+
result=response.result,
195+
) from e
196+
197+
@post(
198+
"workitems/{workItemId}/execute",
199+
args=[Path(name="workItemId"), Field("action")],
200+
)
201+
def _execute_work_item(
202+
self, work_item_id: str, action: str
203+
) -> models.ExecuteWorkItemResponse:
204+
"""Internal implementation of execute_work_item."""
205+
...
206+
135207
@post("workitem-templates", args=[Field("workItemTemplates")])
136208
def create_work_item_templates(
137209
self, work_item_templates: List[models.CreateWorkItemTemplateRequest]

nisystemlink/clients/work_item/models/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,18 @@
5858
)
5959
from ._update_work_items_request import UpdateWorkItemsRequest
6060

61+
from ._execute_work_item_response import (
62+
ExecuteWorkItemResponse,
63+
ExecutionResult,
64+
ExecutionResultBase,
65+
JobExecutionResult,
66+
ManualExecutionResult,
67+
NoneExecutionResult,
68+
NotebookExecutionResult,
69+
ScheduleExecutionResult,
70+
UnscheduleExecutionResult,
71+
)
72+
6173
from ._create_work_item_template_request import CreateWorkItemTemplateRequest
6274
from ._create_work_item_templates_partial_success_response import (
6375
CreateWorkItemTemplatesPartialSuccessResponse,
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
from typing import Annotated, List, Literal
2+
3+
from nisystemlink.clients.core._api_error import ApiError
4+
from nisystemlink.clients.core._uplink._json_model import JsonModel
5+
from pydantic import Field
6+
7+
8+
class ExecutionResultBase(JsonModel):
9+
"""Base class for execution results containing common attributes."""
10+
11+
error: ApiError | None = None
12+
"""Error information if the execution encountered an error."""
13+
14+
15+
class NoneExecutionResult(ExecutionResultBase):
16+
"""Result of executing a work item action with no execution implementation."""
17+
18+
type: Literal["NONE"] = Field(default="NONE")
19+
"""Type of execution."""
20+
21+
22+
class ManualExecutionResult(ExecutionResultBase):
23+
"""Result of executing a manual work item action."""
24+
25+
type: Literal["MANUAL"] = Field(default="MANUAL")
26+
"""Type of execution."""
27+
28+
29+
class NotebookExecutionResult(ExecutionResultBase):
30+
"""Result of executing a notebook work item action."""
31+
32+
type: Literal["NOTEBOOK"] = Field(default="NOTEBOOK")
33+
"""Type of execution."""
34+
35+
execution_id: str | None = None
36+
"""The notebook execution ID."""
37+
38+
39+
class JobExecutionResult(ExecutionResultBase):
40+
"""Result of executing a job work item action."""
41+
42+
type: Literal["JOB"] = Field(default="JOB")
43+
"""Type of execution."""
44+
45+
job_ids: List[str] | None = None
46+
"""The list of job IDs."""
47+
48+
49+
class ScheduleExecutionResult(ExecutionResultBase):
50+
"""Result of executing a schedule work item action."""
51+
52+
type: Literal["SCHEDULE"] = Field(default="SCHEDULE")
53+
"""Type of execution."""
54+
55+
56+
class UnscheduleExecutionResult(ExecutionResultBase):
57+
"""Result of executing an unschedule work item action."""
58+
59+
type: Literal["UNSCHEDULE"] = Field(default="UNSCHEDULE")
60+
"""Type of execution."""
61+
62+
63+
ExecutionResult = Annotated[
64+
NoneExecutionResult
65+
| ManualExecutionResult
66+
| NotebookExecutionResult
67+
| JobExecutionResult
68+
| ScheduleExecutionResult
69+
| UnscheduleExecutionResult,
70+
Field(discriminator="type"),
71+
]
72+
"""Result of executing a work item action."""
73+
74+
75+
class ExecuteWorkItemResponse(JsonModel):
76+
"""Response for executing a work item action."""
77+
78+
error: ApiError | None = None
79+
"""Error information if the action failed."""
80+
81+
result: ExecutionResult | None = None
82+
"""Result of the action execution."""

0 commit comments

Comments
 (0)