Skip to content

Commit 53f1188

Browse files
Madhan-Reddy-niPriyadarshini Piramanayagam
and
Priyadarshini Piramanayagam
authored
feat: Add client for SystemLink results API (#82)
Co-authored-by: Priyadarshini Piramanayagam <priydarshini.piramanayagam@emerson.com>
1 parent 5e91510 commit 53f1188

15 files changed

+1177
-2
lines changed

docs/api_reference/testmonitor.rst

+7
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ nisystemlink.clients.testmonitor
77
:exclude-members: __init__
88

99
.. automethod:: __init__
10+
.. automethod:: create_results
11+
.. automethod:: get_results
12+
.. automethod:: query_results
13+
.. automethod:: query_result_values
14+
.. automethod:: update_results
15+
.. automethod:: delete_result
16+
.. automethod:: delete_results
1017

1118
.. automodule:: nisystemlink.clients.testmonitor.models
1219
:members:

docs/getting_started.rst

+27
Original file line numberDiff line numberDiff line change
@@ -253,3 +253,30 @@ Delete a feed.
253253
.. literalinclude:: ../examples/feeds/delete_feed.py
254254
:language: python
255255
:linenos:
256+
257+
TestMonitor API (Results)
258+
-------
259+
260+
Overview
261+
~~~~~~~~
262+
263+
The :class:`.TestMonitorClient` class is the primary entry point of the Test Monitor API
264+
used to interact with test results (Results).
265+
266+
When constructing a :class:`.TestMonitorClient`, you can pass an
267+
:class:`.HttpConfiguration` (like one retrieved from the
268+
:class:`.HttpConfigurationManager`), or let :class:`.TestMonitorClient` use the
269+
default connection. The default connection depends on your environment.
270+
271+
With a :class:`.TestMonitorClient` object, you can:
272+
273+
* Create, update, query, and delete results
274+
275+
Examples
276+
~~~~~~~~
277+
278+
Create, query, update, and delete some results
279+
280+
.. literalinclude:: ../examples/result/results.py
281+
:language: python
282+
:linenos:

examples/testmonitor/results.py

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from nisystemlink.clients.testmonitor import TestMonitorClient
2+
from nisystemlink.clients.testmonitor.models import (
3+
CreateResultRequest,
4+
QueryResultsRequest,
5+
QueryResultValuesRequest,
6+
ResultField,
7+
Status,
8+
StatusType,
9+
)
10+
11+
program_name = "Example Name"
12+
host_name = "Example Host"
13+
status_type = StatusType.PASSED
14+
15+
16+
def create_some_results():
17+
"""Create two example results on your server."""
18+
new_results = [
19+
CreateResultRequest(
20+
part_number="Example 123 AA",
21+
program_name=program_name,
22+
host_name=host_name,
23+
status=Status.PASSED(),
24+
keywords=["original keyword"],
25+
properties={"original property key": "yes"},
26+
),
27+
CreateResultRequest(
28+
part_number="Example 123 AA1",
29+
program_name=program_name,
30+
host_name=host_name,
31+
status=Status(status_type=StatusType.CUSTOM, status_name="Custom"),
32+
keywords=["original keyword"],
33+
properties={"original property key": "original"},
34+
),
35+
]
36+
create_response = client.create_results(new_results)
37+
return create_response
38+
39+
40+
# Server configuration is not required when used with Systemlink Client or run throught Jupyter on SLE
41+
server_configuration = None
42+
43+
# # Example of setting up the server configuration to point to your instance of SystemLink Enterprise
44+
# server_configuration = HttpConfiguration(
45+
# server_uri="https://yourserver.yourcompany.com",
46+
# api_key="YourAPIKeyGeneratedFromSystemLink",
47+
# )
48+
49+
client = TestMonitorClient(configuration=server_configuration)
50+
51+
create_response = create_some_results()
52+
53+
# Get all the results using the continuation token in batches of 100 at a time.
54+
response = client.get_results(take=100, return_count=True)
55+
all_results = response.results
56+
while response.continuation_token:
57+
response = client.get_results(
58+
take=100, continuation_token=response.continuation_token, return_count=True
59+
)
60+
all_results.extend(response.results)
61+
62+
# use get for first result created
63+
created_result = client.get_result(create_response.results[0].id)
64+
65+
# Query results without continuation
66+
query_request = QueryResultsRequest(
67+
filter=f'status.statusType="{status_type.value}"', return_count=True
68+
)
69+
response = client.query_results(query_request)
70+
71+
# Update the first result that you just created and replace the keywords
72+
updated_result = create_response.results[0]
73+
updated_result.keywords = ["new keyword"]
74+
updated_result.properties = {"new property key": "new value"}
75+
update_response = client.update_results([create_response.results[0]], replace=True)
76+
77+
# Query for just the ids of results that match the family
78+
values_query = QueryResultValuesRequest(
79+
filter=f'programName="{program_name}"', field=ResultField.ID
80+
)
81+
values_response = client.query_result_values(query=values_query)
82+
83+
# delete each created result individually by id
84+
for result in create_response.results:
85+
client.delete_result(result.id)
86+
87+
# Create some more and delete them with a single call to delete.
88+
create_response = create_some_results()
89+
client.delete_results([result.id for result in create_response.results])

nisystemlink/clients/testmonitor/_test_monitor_client.py

+159-2
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
"""Implementation of TestMonitor Client"""
22

3-
from typing import Optional
3+
from typing import List, Optional
44

55
from nisystemlink.clients import core
66
from nisystemlink.clients.core._uplink._base_client import BaseClient
7-
from nisystemlink.clients.core._uplink._methods import get
7+
from nisystemlink.clients.core._uplink._methods import delete, get, post
8+
from nisystemlink.clients.testmonitor.models import (
9+
CreateResultRequest,
10+
UpdateResultRequest,
11+
)
12+
from uplink import Field, Query, retry, returns
813

914
from . import models
1015

1116

17+
@retry(
18+
when=retry.when.status([408, 429, 502, 503, 504]), stop=retry.stop.after_attempt(5)
19+
)
1220
class TestMonitorClient(BaseClient):
1321
# prevent pytest from thinking this is a test class
1422
__test__ = False
@@ -40,3 +48,152 @@ def api_info(self) -> models.ApiInfo:
4048
ApiException: if unable to communicate with the `ni``/nitestmonitor``` service.
4149
"""
4250
...
51+
52+
@post("results", args=[Field("results")])
53+
def create_results(
54+
self, results: List[CreateResultRequest]
55+
) -> models.CreateResultsPartialSuccess:
56+
"""Creates one or more results and returns errors for failed creations.
57+
58+
Args:
59+
results: A list of results to attempt to create.
60+
61+
Returns: A list of created results, results that failed to create, and errors for
62+
failures.
63+
64+
Raises:
65+
ApiException: if unable to communicate with the ``/nitestmonitor`` service of provided invalid
66+
arguments.
67+
"""
68+
...
69+
70+
@get(
71+
"results",
72+
args=[Query("continuationToken"), Query("take"), Query("returnCount")],
73+
)
74+
def get_results(
75+
self,
76+
continuation_token: Optional[str] = None,
77+
take: Optional[int] = None,
78+
return_count: Optional[bool] = None,
79+
) -> models.PagedResults:
80+
"""Reads a list of results.
81+
82+
Args:
83+
continuation_token: The token used to paginate results.
84+
take: The number of results to get in this request.
85+
return_count: Whether or not to return the total number of results available.
86+
87+
Returns:
88+
A list of results.
89+
90+
Raises:
91+
ApiException: if unable to communicate with the ``/nitestmonitor`` Service
92+
or provided an invalid argument.
93+
"""
94+
...
95+
96+
@get("results/{id}")
97+
def get_result(self, id: str) -> models.Result:
98+
"""Retrieves a single result by id.
99+
100+
Args:
101+
id (str): Unique ID of a result.
102+
103+
Returns:
104+
The single result matching `id`
105+
106+
Raises:
107+
ApiException: if unable to communicate with the ``/nitestmonitor`` Service
108+
or provided an invalid argument.
109+
"""
110+
...
111+
112+
@post("query-results")
113+
def query_results(self, query: models.QueryResultsRequest) -> models.PagedResults:
114+
"""Queries for results that match the filter.
115+
116+
Args:
117+
query : The query contains a DynamicLINQ query string in addition to other details
118+
about how to filter and return the list of results.
119+
120+
Returns:
121+
A paged list of results with a continuation token to get the next page.
122+
123+
Raises:
124+
ApiException: if unable to communicate with the ``/nitestmonitor`` Service or provided invalid
125+
arguments.
126+
"""
127+
...
128+
129+
@returns.json # type: ignore
130+
@post("query-result-values")
131+
def query_result_values(self, query: models.QueryResultValuesRequest) -> List[str]:
132+
"""Queries for results that match the query and returns a list of the requested field.
133+
134+
Args:
135+
query : The query for the fields.
136+
137+
Returns:
138+
A list of the values of the queried field.
139+
140+
Raises:
141+
ApiException: if unable to communicate with the ``/nitestmonitor`` Service or provided
142+
invalid arguments.
143+
"""
144+
...
145+
146+
@post("update-results", args=[Field("results"), Field("replace")])
147+
def update_results(
148+
self, results: List[UpdateResultRequest], replace: bool = False
149+
) -> models.UpdateResultsPartialSuccess:
150+
"""Updates a list of results with optional field replacement.
151+
152+
Args:
153+
`results`: A list of results to update. Results are matched for update by id.
154+
`replace`: Replace the existing fields instead of merging them. Defaults to `False`.
155+
If this is `True`, then `keywords` and `properties` for the result will be
156+
replaced by what is in the `results` provided in this request.
157+
If this is `False`, then the `keywords` and `properties` in this request will
158+
merge with what is already present in the server resource.
159+
160+
Returns: A list of updates results, results that failed to update, and errors for
161+
failures.
162+
163+
Raises:
164+
ApiException: if unable to communicate with the ``/nitestmonitor`` Service
165+
or provided an invalid argument.
166+
"""
167+
...
168+
169+
@delete("results/{id}")
170+
def delete_result(self, id: str) -> None:
171+
"""Deletes a single result by id.
172+
173+
Args:
174+
id (str): Unique ID of a result.
175+
176+
Raises:
177+
ApiException: if unable to communicate with the ``/nitestmonitor`` Service
178+
or provided an invalid argument.
179+
"""
180+
...
181+
182+
@post("delete-results", args=[Field("ids")])
183+
def delete_results(
184+
self, ids: List[str]
185+
) -> Optional[models.DeleteResultsPartialSuccess]:
186+
"""Deletes multiple results.
187+
188+
Args:
189+
ids (List[str]): List of unique IDs of results.
190+
191+
Returns:
192+
A partial success if any results failed to delete, or None if all
193+
results were deleted successfully.
194+
195+
Raises:
196+
ApiException: if unable to communicate with the ``/nitestmonitor`` Service
197+
or provided an invalid argument.
198+
"""
199+
...
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
11
from ._api_info import Operation, V2Operations, ApiInfo
2+
from ._result import Result
3+
from ._status import StatusType, Status
4+
from ._create_results_partial_success import CreateResultsPartialSuccess
5+
from ._update_results_partial_success import UpdateResultsPartialSuccess
6+
from ._delete_results_partial_success import DeleteResultsPartialSuccess
7+
from ._paged_results import PagedResults
8+
from ._create_result_request import CreateResultRequest
9+
from ._update_result_request import UpdateResultRequest
10+
from ._query_results_request import (
11+
QueryResultsRequest,
12+
QueryResultValuesRequest,
13+
ResultField,
14+
ResultProjection,
15+
)
216

317
# flake8: noqa
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from datetime import datetime
2+
from typing import Dict, List, Optional
3+
4+
from nisystemlink.clients.core._uplink._json_model import JsonModel
5+
from nisystemlink.clients.testmonitor.models._status import Status
6+
7+
8+
class CreateResultRequest(JsonModel):
9+
"""Contains information about a result."""
10+
11+
status: Status
12+
"""The status of the result."""
13+
14+
started_at: Optional[datetime]
15+
"""The time that the result started."""
16+
17+
program_name: str
18+
"""The name of the program that generated this result."""
19+
20+
system_id: Optional[str]
21+
"""The id of the system that generated this result."""
22+
23+
host_name: Optional[str]
24+
"""The name of the host that generated this result."""
25+
26+
part_number: Optional[str]
27+
"""The part number is the unique identifier of a product within a single org."""
28+
29+
serial_number: Optional[str]
30+
"""The serial number of the system that generated this result."""
31+
32+
total_time_in_seconds: Optional[float]
33+
"""The total time that the result took to run in seconds."""
34+
35+
keywords: Optional[List[str]]
36+
"""A list of keywords that categorize this result."""
37+
38+
properties: Optional[Dict[str, str]]
39+
"""A list of custom properties for this result."""
40+
41+
operator: Optional[str]
42+
"""The operator that ran the result."""
43+
44+
file_ids: Optional[List[str]]
45+
"""A list of file ids that are attached to this result."""
46+
47+
data_table_ids: Optional[List[str]]
48+
"""A list of data table ids that are attached to this result."""
49+
50+
workspace: Optional[str]
51+
"""The id of the workspace that this product belongs to."""

0 commit comments

Comments
 (0)