From 36cb9f4eac6072919c5c36453329b6ac8e0acbff Mon Sep 17 00:00:00 2001 From: Madhan Mohan Reddy Peram <madhan.mohan.reddy.peram@ni.com> Date: Thu, 7 Nov 2024 08:38:56 +0530 Subject: [PATCH 1/5] First Pass --- nisystemlink/clients/result/__init__.py | 3 + nisystemlink/clients/result/_result_client.py | 180 +++++++++ .../clients/result/models/__init__.py | 11 + .../models/_create_results_partial_success.py | 20 + .../models/_delete_results_partial_success.py | 17 + .../clients/result/models/_paged_results.py | 14 + .../result/models/_query_results_request.py | 141 +++++++ nisystemlink/clients/result/models/_result.py | 89 +++++ .../integration/result/test_result_client.py | 368 ++++++++++++++++++ 9 files changed, 843 insertions(+) create mode 100644 nisystemlink/clients/result/__init__.py create mode 100644 nisystemlink/clients/result/_result_client.py create mode 100644 nisystemlink/clients/result/models/__init__.py create mode 100644 nisystemlink/clients/result/models/_create_results_partial_success.py create mode 100644 nisystemlink/clients/result/models/_delete_results_partial_success.py create mode 100644 nisystemlink/clients/result/models/_paged_results.py create mode 100644 nisystemlink/clients/result/models/_query_results_request.py create mode 100644 nisystemlink/clients/result/models/_result.py create mode 100644 tests/integration/result/test_result_client.py diff --git a/nisystemlink/clients/result/__init__.py b/nisystemlink/clients/result/__init__.py new file mode 100644 index 00000000..45fc19fb --- /dev/null +++ b/nisystemlink/clients/result/__init__.py @@ -0,0 +1,3 @@ +from ._result_client import ResultClient + +# flake8: noqa diff --git a/nisystemlink/clients/result/_result_client.py b/nisystemlink/clients/result/_result_client.py new file mode 100644 index 00000000..5b6ee2c8 --- /dev/null +++ b/nisystemlink/clients/result/_result_client.py @@ -0,0 +1,180 @@ +"""Implementations of the Result Client.""" + +from typing import List, Optional + +from nisystemlink.clients import core +from nisystemlink.clients.core._uplink._base_client import BaseClient +from nisystemlink.clients.core._uplink._methods import delete, get, post +from nisystemlink.clients.result.models import Result +from uplink import Field, Query, retry, returns + +from . import models + + +@retry(when=retry.when.status(429), stop=retry.stop.after_attempt(5)) +class ResultClient(BaseClient): + + def __init__(self, configuration: Optional[core.HttpConfiguration] = None): + """Initialize an instance. + + Args: + configuration: Defines the web server to connect to and information about + how to connect. If not provided, the + :class:`HttpConfigurationManager <nisystemlink.clients.core.HttpConfigurationManager>` + is used to obtain the configuration. + + Raises: + ApiException: if unable to communicate with the Result Service. + """ + if configuration is None: + configuration = core.HttpConfigurationManager.get_configuration() + super().__init__(configuration, base_path="/nitestmonitor/v2/") + + @post("results", args=[Field("results")]) + def create_results( + self, results: List[Result] + ) -> models.CreateResultsPartialSuccess: + """Creates one or more results and returns errors for failed creations. + + Args: + results: A list of results to attempt to create. + + Returns: A list of created results, results that failed to create, and errors for + failures. + + Raises: + ApiException: if unable to communicate with the ``/nitestmonitor`` service of provided invalid + arguments. + """ + ... + + @get( + "results", + args=[Query("continuationToken"), Query("take"), Query("returnCount")], + ) + def get_results( + self, + continuation_token: Optional[str] = None, + take: Optional[int] = None, + return_count: Optional[bool] = None, + ) -> models.PagedResults: + """Reads a list of results. + + Args: + continuation_token: The token used to paginate results. + take: The number of results to get in this request. + return_count: Whether or not to return the total number of results available. + + Returns: + A list of results. + + Raises: + ApiException: if unable to communicate with the ``/nitestmonitor`` Service + or provided an invalid argument. + """ + ... + + @get("results/{id}") + def get_result(self, id: str) -> models.Result: + """Retrieves a single result by id. + + Args: + id (str): Unique ID of a result. + + Returns: + The single result matching `id` + + Raises: + ApiException: if unable to communicate with the ``/nitestmonitor`` Service + or provided an invalid argument. + """ + ... + + @post("query-results") + def query_results(self, query: models.QueryResultsRequest) -> models.PagedResults: + """Queries for results that match the filter. + + Args: + query : The query contains a DynamicLINQ query string in addition to other details + about how to filter and return the list of results. + + Returns: + A paged list of results with a continuation token to get the next page. + + Raises: + ApiException: if unable to communicate with the ``/nitestmonitor`` Service or provided invalid + arguments. + """ + ... + + @returns.json # type: ignore + @post("query-result-values") + def query_result_values(self, query: models.QueryResultValuesRequest) -> List[str]: + """Queries for results that match the query and returns a list of the requested field. + + Args: + query : The query for the fields you want. + + Returns: + A list of the values of the field you requested. + + Raises: + ApiException: if unable to communicate with the ``/nitestmonitor`` Service or provided + invalid arguments. + """ + ... + + @post("update-results", args=[Field("results"), Field("replace")]) + def update_results( + self, results: List[Result], replace: bool = False + ) -> models.CreateResultsPartialSuccess: + """Updates a list of results with optional field replacement. + + Args: + `results`: A list of results to update. Results are matched for update by id. + `replace`: Replace the existing fields instead of merging them. Defaults to `False`. + If this is `True`, then `keywords` and `properties` for the result will be + replaced by what is in the `results` provided in this request. + If this is `False`, then the `keywords` and `properties` in this request will + merge with what is already present in the server resource. + + Returns: A list of updates results, results that failed to update, and errors for + failures. + + Raises: + ApiException: if unable to communicate with the ``/nitestmonitor`` Service + or provided an invalid argument. + """ + ... + + @delete("results/{id}") + def delete_result(self, id: str) -> None: + """Deletes a single result by id. + + Args: + id (str): Unique ID of a result. + + Raises: + ApiException: if unable to communicate with the ``/nitestmonitor`` Service + or provided an invalid argument. + """ + ... + + @post("delete-results", args=[Field("ids")]) + def delete_results( + self, ids: List[str] + ) -> Optional[models.DeleteResultsPartialSuccess]: + """Deletes multiple results. + + Args: + ids (List[str]): List of unique IDs of results. + + Returns: + A partial success if any results failed to delete, or None if all + results were deleted successfully. + + Raises: + ApiException: if unable to communicate with the ``/nitestmonitor`` Service + or provided an invalid argument. + """ + ... diff --git a/nisystemlink/clients/result/models/__init__.py b/nisystemlink/clients/result/models/__init__.py new file mode 100644 index 00000000..14328bbf --- /dev/null +++ b/nisystemlink/clients/result/models/__init__.py @@ -0,0 +1,11 @@ +from ._result import Result, StatusObject, StatusType +from ._create_results_partial_success import CreateResultsPartialSuccess +from ._delete_results_partial_success import DeleteResultsPartialSuccess +from ._paged_results import PagedResults +from ._query_results_request import ( + QueryResultsRequest, + ResultField, + QueryResultValuesRequest, +) + +# flake8: noqa diff --git a/nisystemlink/clients/result/models/_create_results_partial_success.py b/nisystemlink/clients/result/models/_create_results_partial_success.py new file mode 100644 index 00000000..b9511dc6 --- /dev/null +++ b/nisystemlink/clients/result/models/_create_results_partial_success.py @@ -0,0 +1,20 @@ +from typing import List, Optional + +from nisystemlink.clients.core import ApiError +from nisystemlink.clients.core._uplink._json_model import JsonModel +from nisystemlink.clients.result.models import Result + + +class CreateResultsPartialSuccess(JsonModel): + results: List[Result] + """The list of results that were successfully created.""" + + failed: Optional[List[Result]] = None + """The list of results that were not created. + If this is `None`, then all results were successfully created. + """ + + error: Optional[ApiError] = None + """Error messages for results that were not created. + If this is `None`, then all results were successfully created. + """ diff --git a/nisystemlink/clients/result/models/_delete_results_partial_success.py b/nisystemlink/clients/result/models/_delete_results_partial_success.py new file mode 100644 index 00000000..2583c08a --- /dev/null +++ b/nisystemlink/clients/result/models/_delete_results_partial_success.py @@ -0,0 +1,17 @@ +from typing import List, Optional + +from nisystemlink.clients.core import ApiError +from nisystemlink.clients.core._uplink._json_model import JsonModel + + +class DeleteResultsPartialSuccess(JsonModel): + """The result of deleting multiple results when one or more results could not be deleted.""" + + ids: List[str] + """The IDs of the results that were successfully deleted.""" + + failed: Optional[List[str]] + """The IDs of the results that could not be deleted.""" + + error: Optional[ApiError] + """The error that occurred when deleting the results.""" diff --git a/nisystemlink/clients/result/models/_paged_results.py b/nisystemlink/clients/result/models/_paged_results.py new file mode 100644 index 00000000..d5e09a40 --- /dev/null +++ b/nisystemlink/clients/result/models/_paged_results.py @@ -0,0 +1,14 @@ +from typing import List, Optional + +from nisystemlink.clients.core._uplink._with_paging import WithPaging +from nisystemlink.clients.result.models import Result + + +class PagedResults(WithPaging): + """The response for a Results query containing matched results.""" + + results: List[Result] + """A list of all the results in this page.""" + + total_count: Optional[int] + """The total number of results that match the query.""" diff --git a/nisystemlink/clients/result/models/_query_results_request.py b/nisystemlink/clients/result/models/_query_results_request.py new file mode 100644 index 00000000..0303c575 --- /dev/null +++ b/nisystemlink/clients/result/models/_query_results_request.py @@ -0,0 +1,141 @@ +from enum import Enum +from typing import List, Optional + +from nisystemlink.clients.core._uplink._json_model import JsonModel +from pydantic import Field + + +class ResultField(str, Enum): + """The valid ways to order a result query.""" + + ID = "ID" + STARTED_AT = "STARTED_AT" + UPDATED_AT = "UPDATED_AT" + PROGRAM_NAME = "PROGRAM_NAME" + SYSTEM_ID = "SYSTEM_ID" + HOST_NAME = "HOST_NAME" + OPERATOR = "OPERATOR" + SERIAL_NUMBER = "SERIAL_NUMBER" + PART_NUMBER = "PART_NUMBER" + PROPERTIES = "PROPERTIES" + TOTAL_TIME_IN_SECONDS = "TOTAL_TIME_IN_SECONDS" + + +class ComparisonType(str, Enum): + """The valid ways to order a result query.""" + + DEFAULT = "DEFAULT" + NUMERIC = "NUMERIC" + LEXICOGRAPHIC = "LEXICOGRAPHIC" + + +class QueryResultsBase(JsonModel): + filter: Optional[str] = None + """ + The result query filter in Dynamic Linq format. + Allowed properties in the filter are: + - `id`: String for the global identifier of the result + - `status`: String for the status of the result + - `systemId`: String for the system identifier of the result + - `hostName`: String for the host name of the result + - `operator`: String for the operator of the result + - `serialNumber`: String for the serial number of the result + - `totalTimeInSeconds`: Float for the total time in seconds of the result + - `partNumber`: String representing the part number of the result + - `programName`: String of the program name + - `startedAt`: ISO-8601 formatted UTC timestamp indicating when the result was started. + - `updatedAt`: ISO-8601 formatted UTC timestamp indicating when the result was last updated. + - `keywords`: A list of keyword strings + - `properties`: A dictionary of additional string to string properties + - `fileIds`: A list of string ids for files stored in the file service (`/nifile`) + - `dataTableIds`: A list of string ids for data tables stored in the data frame service (`/nidataframe`) + - `workspaceId`: String for the workspace identifier of the result + See [Dynamic Linq](https://github.com/ni/systemlink-OpenAPI-documents/wiki/Dynamic-Linq-Query-Language) + documentation for more details. + `"@0"`, `"@1"` etc. can be used in conjunction with the `substitutions` parameter to keep this + query string more simple and reusable. + """ + + substitutions: Optional[List[str]] = None + """String substitutions into the `filter`. + Makes substitutions in the query filter expression. Substitutions for the query expression are + indicated by non-negative integers that are prefixed with the "at" symbol. Each substitution in + the given expression will be replaced by the element at the corresponding index (zero-based) in + this list. For example, "@0" in the filter expression will be replaced with the element at the + zeroth index of the substitutions list. + """ + + +class QueryProductsBase(JsonModel): + product_filter: Optional[str] = None + """ + The product query filter in Dynamic Linq format. + Allowed properties in the filter are: + - `id`: String for the global identifier of the product + - `partNumber`: String representing the part number of the product + - `name`: String of the product name + - `family`: String for the product family + - `updatedAt`: ISO-8601 formatted UTC timestamp indicating when the product was last updated. + - `keywords`: A list of keyword strings + - `properties`: A dictionary of additional string to string properties + - `fileIds`: A list of string ids for files stored in the file service (`/nifile`) + See [Dynamic Linq](https://github.com/ni/systemlink-OpenAPI-documents/wiki/Dynamic-Linq-Query-Language) + documentation for more details. + `"@0"`, `"@1"` etc. can be used in conjunction with the `substitutions` parameter to keep this + query string more simple and reusable. + """ + + product_substitutions: Optional[List[str]] = None + """String substitutions into the `filter`. + Makes substitutions in the query filter expression. Substitutions for the query expression are + indicated by non-negative integers that are prefixed with the "at" symbol. Each substitution in + the given expression will be replaced by the element at the corresponding index (zero-based) in + this list. For example, "@0" in the filter expression will be replaced with the element at the + zeroth index of the substitutions list. + """ + + +class QueryResultsRequest(QueryResultsBase, QueryProductsBase): + + order_by: Optional[ResultField] = Field(None, alias="orderBy") + """Specifies the fields to use to sort the results. + By default, results are sorted by `id` + """ + order_by_key: Optional[str] = Field(None, alias="orderByKey") + """Specifies the property to use to sort the results when ordering by PROPERTIES. + Results that do not contain the orderByKey will be considered the smallest value. + """ + order_by_comparison_type: Optional[ComparisonType] = Field( + None, alias="orderByComparisonType" + ) + """An enumeration of comparison types that can be used for ordered queries. + For non-DEFAULT comparisons, values that cannot be converted will be considered the smallest value. + """ + descending: Optional[bool] = None + """Specifies whether to return the results in descending order. + By default, this value is `false` and results are sorted in ascending order. + """ + take: Optional[int] = None + """Maximum number of results to return in the current API response. + Uses the default if the specified value is negative. The default value is `1000` results. + """ + continuation_token: Optional[str] = None + """Allows users to continue the query at the next result that matches the given criteria. + To retrieve the next page of results, pass the continuation token from the previous + page in the next request. The service responds with the next page of data and provides a new + continuation token. To paginate results, continue sending requests with the newest continuation + token provided in each response. + """ + return_count: Optional[bool] = None + """If true, the response will include a count of all results matching the filter. + By default, this value is `False` and count is not returned. Note that returning the count may + incur performance penalties as the service may have to do a complete walk of the database to + compute count. """ + + +class QueryResultValuesRequest(QueryResultsBase): + field: Optional[ResultField] = None + """The result field to return for this query.""" + + starts_with: Optional[str] = None + """Only return string parameters prefixed by this value (case sensitive).""" diff --git a/nisystemlink/clients/result/models/_result.py b/nisystemlink/clients/result/models/_result.py new file mode 100644 index 00000000..3835c904 --- /dev/null +++ b/nisystemlink/clients/result/models/_result.py @@ -0,0 +1,89 @@ +from datetime import datetime +from enum import Enum +from typing import Dict, List, Optional + +from nisystemlink.clients.core._uplink._json_model import JsonModel + + +class StatusType(str, Enum): + """The types of statuses that a result can have.""" + + LOOPING = "LOOPING" + SKIPPED = "SKIPPED" + CUSTOM = "CUSTOM" + DONE = "DONE" + PASSED = "PASSED" + FAILED = "FAILED" + RUNNING = "RUNNING" + WAITING = "WAITING" + TERMINATED = "TERMINATED" + ERRORED = "ERRORED" + TIMED_OUT = "TIMED_OUT" + + +class StatusObject(JsonModel): + """Contains information about a status object.""" + + status_type: StatusType + """The type of status.""" + + status_name: Optional[str] + """The name of the status.""" + + +class Result(JsonModel): + """Contains information about a result.""" + + status: StatusObject + """The status of the result.""" + + started_at: Optional[datetime] + """The time that the result started.""" + + updated_at: Optional[datetime] + """The last time that this result was updated.""" + + program_name: str + """The name of the program that generated this result.""" + + id: Optional[str] + """The globally unique id of the result.""" + + system_id: Optional[str] + """The id of the system that generated this result.""" + + host_name: Optional[str] + """The name of the host that generated this result.""" + + part_number: Optional[str] + """The part number is the unique identifier of a product within a single org.""" + + serial_number: Optional[str] + """The serial number of the system that generated this result.""" + + total_time_in_seconds: Optional[float] + """The total time that the result took to run in seconds.""" + + keywords: Optional[List[str]] + """A list of keywords that categorize this result.""" + + properties: Optional[Dict[str, str]] + """A list of custom properties for this result.""" + + operator: Optional[str] + """The operator that ran the result.""" + + file_ids: Optional[List[str]] + """A list of file ids that are attached to this result.""" + + data_table_ids: Optional[List[str]] + """A list of data table ids that are attached to this result.""" + + status_type_summary: Optional[Dict[str, int]] + """A summary of the status types in the result.""" + + workspace: Optional[str] + """The id of the workspace that this product belongs to.""" + + is_finalized: Optional[bool] + """Whether the result is finalized.""" diff --git a/tests/integration/result/test_result_client.py b/tests/integration/result/test_result_client.py new file mode 100644 index 00000000..1db5c667 --- /dev/null +++ b/tests/integration/result/test_result_client.py @@ -0,0 +1,368 @@ +import uuid +from typing import List + +import pytest +from nisystemlink.clients.core._http_configuration import HttpConfiguration +from nisystemlink.clients.result._result_client import ResultClient +from nisystemlink.clients.result.models import ( + CreateResultsPartialSuccess, + Result, + StatusObject, + StatusType, +) +from nisystemlink.clients.result.models._paged_results import PagedResults +from nisystemlink.clients.result.models._query_results_request import ( + QueryResultsRequest, + QueryResultValuesRequest, + ResultField, +) + + +@pytest.fixture(scope="class") +def client(enterprise_config: HttpConfiguration) -> ResultClient: + """Fixture to create a ResultClient instance.""" + return ResultClient(enterprise_config) + + +@pytest.fixture +def unique_identifier() -> str: + """Unique result id for this test.""" + result_id = uuid.uuid1().hex + return result_id + + +@pytest.fixture +def create_results(client: ResultClient): + """Fixture to return a factory that creates results.""" + responses: List[CreateResultsPartialSuccess] = [] + + def _create_results(results: List[Result]) -> CreateResultsPartialSuccess: + response = client.create_results(results) + responses.append(response) + return response + + yield _create_results + + created_results: List[Result] = [] + for response in responses: + if response.results: + created_results = created_results + response.results + client.delete_results(ids=[str(result.id) for result in created_results]) + + +@pytest.mark.integration +@pytest.mark.enterprise +class TestResultClient: + + def test__create_single_result__one_result_created_with_right_field_values( + self, client: ResultClient, create_results, unique_identifier + ): + part_number = unique_identifier + keywords = ["testing"] + properties = {"test_property": "yes"} + program_name = "Test Program" + status = StatusObject(status_type=StatusType.PASSED, status_name="Passed") + host_name = "Test Host" + system_id = "Test System" + serial_number = "Test Serial Number" + result = Result( + part_number=part_number, + keywords=keywords, + properties=properties, + program_name=program_name, + status=status, + host_name=host_name, + system_id=system_id, + serial_number=serial_number, + ) + + response: CreateResultsPartialSuccess = create_results([result]) + + assert response is not None + assert len(response.results) == 1 + created_result = response.results[0] + assert created_result.part_number == part_number + assert created_result.keywords == keywords + assert created_result.properties == properties + assert created_result.program_name == program_name + assert created_result.status == status + assert created_result.host_name == host_name + assert created_result.system_id == system_id + assert created_result.serial_number == serial_number + + def test__create_multiple_results__multiple_creates_succeed( + self, client: ResultClient, create_results + ): + program_name = "Test Program" + status = StatusObject(status_type=StatusType.PASSED, status_name="Passed") + results = [ + Result( + part_number=uuid.uuid1().hex, program_name=program_name, status=status + ), + Result( + part_number=uuid.uuid1().hex, program_name=program_name, status=status + ), + ] + + response: CreateResultsPartialSuccess = create_results(results) + + assert response is not None + assert len(response.results) == 2 + + def test__create_single_result_and_get_results__at_least_one_result_exists( + self, client: ResultClient, create_results, unique_identifier + ): + program_name = "Test Program" + status = StatusObject(status_type=StatusType.PASSED, status_name="Passed") + results = [ + Result( + part_number=unique_identifier, program_name=program_name, status=status + ) + ] + create_results(results) + + get_response = client.get_results() + + assert get_response is not None + assert len(get_response.results) >= 1 + + def test__create_multiple_results_and_get_results_with_take__only_take_returned( + self, client: ResultClient, create_results, unique_identifier + ): + program_name = "Test Program" + status = StatusObject(status_type=StatusType.PASSED, status_name="Passed") + results = [ + Result( + part_number=unique_identifier, program_name=program_name, status=status + ), + Result( + part_number=unique_identifier, program_name=program_name, status=status + ), + ] + create_results(results) + + get_response = client.get_results(take=1) + + assert get_response is not None + assert len(get_response.results) == 1 + + def test__create_multiple_results_and_get_results_with_count_at_least_one_count( + self, client: ResultClient, create_results, unique_identifier + ): + program_name = "Test Program" + status = StatusObject(status_type=StatusType.PASSED, status_name="Passed") + results = [ + Result( + part_number=unique_identifier, program_name=program_name, status=status + ), + Result( + part_number=unique_identifier, program_name=program_name, status=status + ), + ] + create_results(results) + + get_response: PagedResults = client.get_results(return_count=True) + + assert get_response is not None + assert get_response.total_count is not None and get_response.total_count >= 2 + + def test__get_result_by_id__result_matches_expected( + self, client: ResultClient, create_results, unique_identifier + ): + part_number = unique_identifier + program_name = "Test Program" + status = StatusObject(status_type=StatusType.PASSED, status_name="Passed") + results = [ + Result(part_number=part_number, program_name=program_name, status=status) + ] + + create_response: CreateResultsPartialSuccess = create_results(results) + + assert create_response is not None + id = str(create_response.results[0].id) + result = client.get_result(id) + assert result is not None + assert result.part_number == part_number + assert result.program_name == program_name + assert result.status == status + + def test__query_result_by_part_number__matches_expected( + self, client: ResultClient, create_results, unique_identifier + ): + part_number = unique_identifier + program_name = "Test Program" + status = StatusObject(status_type=StatusType.PASSED, status_name="Passed") + results = [ + Result(part_number=part_number, program_name=program_name, status=status) + ] + + create_response: CreateResultsPartialSuccess = create_results(results) + + assert create_response is not None + query_request = QueryResultsRequest( + filter=f'partNumber="{part_number}"', return_count=True + ) + query_response: PagedResults = client.query_results(query_request) + assert query_response.total_count == 1 + assert query_response.results[0].part_number == part_number + + def test__query_result_values_for_name__name_matches( + self, client: ResultClient, create_results, unique_identifier + ): + part_number = unique_identifier + program_name = "Test Program" + status = StatusObject(status_type=StatusType.PASSED, status_name="Passed") + + create_response: CreateResultsPartialSuccess = create_results( + [Result(part_number=part_number, program_name=program_name, status=status)] + ) + assert create_response is not None + query_request = QueryResultValuesRequest( + filter=f'partNumber="{part_number}"', field=ResultField.PROGRAM_NAME + ) + query_response: List[str] = client.query_result_values(query_request) + + assert query_response is not None + assert len(query_response) == 1 + assert query_response[0] == program_name + + def test__update_keywords_with_replace__keywords_replaced( + self, client: ResultClient, create_results, unique_identifier + ): + original_keyword = "originalKeyword" + updated_keyword = "updatedKeyword" + program_name = "Test Program" + status = StatusObject(status_type=StatusType.PASSED, status_name="Passed") + create_response: CreateResultsPartialSuccess = create_results( + [ + Result( + part_number=unique_identifier, + keywords=[original_keyword], + program_name=program_name, + status=status, + ) + ] + ) + assert create_response is not None + assert len(create_response.results) == 1 + + updated_result = create_response.results[0] + updated_result.keywords = [updated_keyword] + update_response = client.update_results([updated_result], replace=True) + + assert update_response is not None + assert len(update_response.results) == 1 + assert ( + update_response.results[0].keywords is not None + and updated_keyword in update_response.results[0].keywords + ) + assert original_keyword not in update_response.results[0].keywords + + def test__update_keywords_no_replace__keywords_appended( + self, client: ResultClient, create_results, unique_identifier + ): + original_keyword = "originalKeyword" + additional_keyword = "additionalKeyword" + program_name = "Test Program" + status = StatusObject(status_type=StatusType.PASSED, status_name="Passed") + create_response: CreateResultsPartialSuccess = create_results( + [ + Result( + part_number=unique_identifier, + keywords=[original_keyword], + program_name=program_name, + status=status, + ) + ] + ) + assert create_response is not None + assert len(create_response.results) == 1 + + updated_result = create_response.results[0] + updated_result.keywords = [additional_keyword] + update_response = client.update_results([updated_result], replace=False) + + assert update_response is not None + assert len(update_response.results) == 1 + assert ( + update_response.results[0].keywords is not None + and original_keyword in update_response.results[0].keywords + ) + assert ( + update_response.results[0].keywords is not None + and additional_keyword in update_response.results[0].keywords + ) + + def test__update_properties_with_replace__properties_replaced( + self, client: ResultClient, create_results, unique_identifier + ): + new_key = "newKey" + original_properties = {"originalKey": "originalValue"} + program_name = "Test Program" + status = StatusObject(status_type=StatusType.PASSED, status_name="Passed") + new_properties = {new_key: "newValue"} + create_response: CreateResultsPartialSuccess = create_results( + [ + Result( + part_number=unique_identifier, + properties=original_properties, + program_name=program_name, + status=status, + ) + ] + ) + assert create_response is not None + assert len(create_response.results) == 1 + + updated_result = create_response.results[0] + updated_result.properties = new_properties + update_response = client.update_results([updated_result], replace=True) + + assert update_response is not None + assert len(update_response.results) == 1 + assert ( + update_response.results[0].properties is not None + and len(update_response.results[0].properties) == 1 + ) + assert new_key in update_response.results[0].properties.keys() + assert update_response.results[0].properties[new_key] == new_properties[new_key] + + def test__update_properties_append__properties_appended( + self, client: ResultClient, create_results, unique_identifier + ): + original_key = "originalKey" + new_key = "newKey" + original_properties = {original_key: "originalValue"} + program_name = "Test Program" + status = StatusObject(status_type=StatusType.PASSED, status_name="Passed") + new_properties = {new_key: "newValue"} + create_response: CreateResultsPartialSuccess = create_results( + [ + Result( + part_number=unique_identifier, + properties=original_properties, + program_name=program_name, + status=status, + ) + ] + ) + assert create_response is not None + assert len(create_response.results) == 1 + + updated_result = create_response.results[0] + updated_result.properties = new_properties + update_response = client.update_results([updated_result], replace=False) + + assert update_response is not None + assert len(update_response.results) == 1 + updated_result = update_response.results[0] + assert ( + updated_result.properties is not None + and len(updated_result.properties) == 2 + ) + assert original_key in updated_result.properties.keys() + assert new_key in updated_result.properties.keys() + assert ( + updated_result.properties[original_key] == original_properties[original_key] + ) + assert updated_result.properties[new_key] == new_properties[new_key] From dbb905575a3a91ed61310f97730d03b4003bec5b Mon Sep 17 00:00:00 2001 From: Madhan Mohan Reddy Peram <madhan.mohan.reddy.peram@ni.com> Date: Thu, 7 Nov 2024 09:32:15 +0530 Subject: [PATCH 2/5] Added the examples --- docs/api_reference.rst | 1 + docs/api_reference/result.rst | 23 +++++++++ docs/getting_started.rst | 27 +++++++++++ examples/result/results.py | 87 +++++++++++++++++++++++++++++++++++ 4 files changed, 138 insertions(+) create mode 100644 docs/api_reference/result.rst create mode 100644 examples/result/results.py diff --git a/docs/api_reference.rst b/docs/api_reference.rst index b9181186..47e11445 100644 --- a/docs/api_reference.rst +++ b/docs/api_reference.rst @@ -10,6 +10,7 @@ API Reference api_reference/core api_reference/tag api_reference/testmonitor + api_reference/result api_reference/dataframe api_reference/spec api_reference/file diff --git a/docs/api_reference/result.rst b/docs/api_reference/result.rst new file mode 100644 index 00000000..5893f7b0 --- /dev/null +++ b/docs/api_reference/result.rst @@ -0,0 +1,23 @@ +.. _api_tag_page: + +nisystemlink.clients.result +====================== + +.. autoclass:: nisystemlink.clients.result.ResultClient + :exclude-members: __init__ + + .. automethod:: __init__ + .. automethod:: create_results + .. automethod:: get_results + .. automethod:: query_results + .. automethod:: query_result_values + .. automethod:: update_results + .. automethod:: delete_result + .. automethod:: delete_results + +.. automodule:: nisystemlink.clients.result.models + :members: + :imported-members: + +.. automodule:: nisystemlink.clients.result.utilities + :members: \ No newline at end of file diff --git a/docs/getting_started.rst b/docs/getting_started.rst index 0efe9358..06a72061 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -182,5 +182,32 @@ Examples Get the metadata of a File using its Id and download it. .. literalinclude:: ../examples/file/download_file.py + :language: python + :linenos: + + +Result API +------- + +Overview +~~~~~~~~ + +The :class:`.ResultClient` class is the primary entry point of the Result API. + +When constructing a :class:`.ResultClient`, you can pass an +:class:`.HttpConfiguration` (like one retrieved from the +:class:`.HttpConfigurationManager`), or let :class:`.ResultClient` use the +default connection. The default connection depends on your environment. + +With a :class:`.ResultClient` object, you can: + +* Create, update, query, and delete results + +Examples +~~~~~~~~ + +Create, query, update, and delete some results + +.. literalinclude:: ../examples/result/results.py :language: python :linenos: \ No newline at end of file diff --git a/examples/result/results.py b/examples/result/results.py new file mode 100644 index 00000000..944c5369 --- /dev/null +++ b/examples/result/results.py @@ -0,0 +1,87 @@ +from nisystemlink.clients.core import HttpConfiguration +from nisystemlink.clients.result import ResultClient +from nisystemlink.clients.result.models import ( + QueryResultsRequest, + QueryResultValuesRequest, + Result, + ResultField, + StatusObject, + StatusType, +) + +program_name = "Example Name" +host_name = "Example Host" + + +def create_some_results(): + """Create two example results on your server.""" + new_results = [ + Result( + part_number="Example 123 AA", + program_name=program_name, + host_name=host_name, + status=StatusObject(status_type=StatusType.PASSED, status_name="Passed"), + keywords=["original keyword"], + properties={"original property key": "yes"}, + ), + Result( + part_number="Example 123 AA1", + program_name=program_name, + host_name=host_name, + status=StatusObject(status_type=StatusType.FAILED, status_name="Failed"), + keywords=["original keyword"], + properties={"original property key": "original"}, + ), + ] + create_response = client.create_results(new_results) + return create_response + + +# Setup the server configuration to point to your instance of SystemLink Enterprise +server_configuration = HttpConfiguration( + server_uri="https://dev-api.lifecyclesolutions.ni.com", + api_key="30IUOX8btHdgziA8hgya502zVH8wp2tWGEDH-yMaF6", +) +client = ResultClient(configuration=server_configuration) + +# Get all the results using the continuation token in batches of 100 at a time. +response = client.get_results(take=100, return_count=True) +all_results = response.results +while response.continuation_token: + response = client.get_results( + take=100, continuation_token=response.continuation_token, return_count=True + ) + all_results.extend(response.results) + +create_response = create_some_results() + +# use get for first result created +created_result = client.get_result(create_response.results[0].id) + +# Query results without continuation +query_request = QueryResultsRequest( + filter=f'programName="{program_name}" && hostName="{host_name}"', + return_count=True, + order_by=ResultField.HOST_NAME, +) +response = client.query_results(query_request) + +# Update the first result that you just created and replace the keywords +updated_result = create_response.results[0] +updated_result.keywords = ["new keyword"] +updated_result.properties = {"new property key": "new value"} +update_response = client.update_results([create_response.results[0]], replace=True) + +# Query for just the ids of results that match the family +values_query = QueryResultValuesRequest( + filter=f'programName="{program_name}"', field=ResultField.ID +) +values_response = client.query_result_values(query=values_query) + +# delete each created result individually by id +for result in create_response.results: + client.delete_result(result.id) + +# Create some more and delete them with a single call to delete. +create_response = create_some_results() +client.delete_results([result.id for result in create_response.results]) From c31dfc5365d70be83ebbec56ddc587aa6aa8c4b6 Mon Sep 17 00:00:00 2001 From: Madhan Mohan Reddy Peram <madhan.mohan.reddy.peram@ni.com> Date: Thu, 7 Nov 2024 09:33:22 +0530 Subject: [PATCH 3/5] Removed the unnecessary changes --- examples/result/results.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/result/results.py b/examples/result/results.py index 944c5369..400cb4c9 100644 --- a/examples/result/results.py +++ b/examples/result/results.py @@ -39,8 +39,8 @@ def create_some_results(): # Setup the server configuration to point to your instance of SystemLink Enterprise server_configuration = HttpConfiguration( - server_uri="https://dev-api.lifecyclesolutions.ni.com", - api_key="30IUOX8btHdgziA8hgya502zVH8wp2tWGEDH-yMaF6", + server_uri="https://yourserver.yourcompany.com", + api_key="YourAPIKeyGeneratedFromSystemLink", ) client = ResultClient(configuration=server_configuration) From 5c6139f1ac86a0ba1aee3051f920c97e58d1ceac Mon Sep 17 00:00:00 2001 From: Madhan Mohan Reddy Peram <madhan.mohan.reddy.peram@ni.com> Date: Tue, 12 Nov 2024 12:15:11 +0530 Subject: [PATCH 4/5] Fix: PR Comments --- examples/result/results.py | 10 +++++----- nisystemlink/clients/result/_result_client.py | 8 ++++---- tests/integration/result/test_result_client.py | 8 ++++---- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/examples/result/results.py b/examples/result/results.py index 400cb4c9..60514f14 100644 --- a/examples/result/results.py +++ b/examples/result/results.py @@ -44,17 +44,17 @@ def create_some_results(): ) client = ResultClient(configuration=server_configuration) +create_response = create_some_results() + # Get all the results using the continuation token in batches of 100 at a time. -response = client.get_results(take=100, return_count=True) +response = client.get_results_paged(take=100, return_count=True) all_results = response.results while response.continuation_token: - response = client.get_results( + response = client.get_results_paged( take=100, continuation_token=response.continuation_token, return_count=True ) all_results.extend(response.results) -create_response = create_some_results() - # use get for first result created created_result = client.get_result(create_response.results[0].id) @@ -64,7 +64,7 @@ def create_some_results(): return_count=True, order_by=ResultField.HOST_NAME, ) -response = client.query_results(query_request) +response = client.query_results_paged(query_request) # Update the first result that you just created and replace the keywords updated_result = create_response.results[0] diff --git a/nisystemlink/clients/result/_result_client.py b/nisystemlink/clients/result/_result_client.py index 5b6ee2c8..de699407 100644 --- a/nisystemlink/clients/result/_result_client.py +++ b/nisystemlink/clients/result/_result_client.py @@ -52,7 +52,7 @@ def create_results( "results", args=[Query("continuationToken"), Query("take"), Query("returnCount")], ) - def get_results( + def get_results_paged( self, continuation_token: Optional[str] = None, take: Optional[int] = None, @@ -91,7 +91,7 @@ def get_result(self, id: str) -> models.Result: ... @post("query-results") - def query_results(self, query: models.QueryResultsRequest) -> models.PagedResults: + def query_results_paged(self, query: models.QueryResultsRequest) -> models.PagedResults: """Queries for results that match the filter. Args: @@ -113,10 +113,10 @@ def query_result_values(self, query: models.QueryResultValuesRequest) -> List[st """Queries for results that match the query and returns a list of the requested field. Args: - query : The query for the fields you want. + query : The query for the fields. Returns: - A list of the values of the field you requested. + A list of the values of the queried field. Raises: ApiException: if unable to communicate with the ``/nitestmonitor`` Service or provided diff --git a/tests/integration/result/test_result_client.py b/tests/integration/result/test_result_client.py index 1db5c667..9adc9a59 100644 --- a/tests/integration/result/test_result_client.py +++ b/tests/integration/result/test_result_client.py @@ -121,7 +121,7 @@ def test__create_single_result_and_get_results__at_least_one_result_exists( ] create_results(results) - get_response = client.get_results() + get_response = client.get_results_paged() assert get_response is not None assert len(get_response.results) >= 1 @@ -141,7 +141,7 @@ def test__create_multiple_results_and_get_results_with_take__only_take_returned( ] create_results(results) - get_response = client.get_results(take=1) + get_response = client.get_results_paged(take=1) assert get_response is not None assert len(get_response.results) == 1 @@ -161,7 +161,7 @@ def test__create_multiple_results_and_get_results_with_count_at_least_one_count( ] create_results(results) - get_response: PagedResults = client.get_results(return_count=True) + get_response: PagedResults = client.get_results_paged(return_count=True) assert get_response is not None assert get_response.total_count is not None and get_response.total_count >= 2 @@ -202,7 +202,7 @@ def test__query_result_by_part_number__matches_expected( query_request = QueryResultsRequest( filter=f'partNumber="{part_number}"', return_count=True ) - query_response: PagedResults = client.query_results(query_request) + query_response: PagedResults = client.query_results_paged(query_request) assert query_response.total_count == 1 assert query_response.results[0].part_number == part_number From a87f71dfee740353b91822ee95dbef8e5a938c52 Mon Sep 17 00:00:00 2001 From: Madhan Mohan Reddy Peram <madhan.mohan.reddy.peram@ni.com> Date: Tue, 12 Nov 2024 12:30:13 +0530 Subject: [PATCH 5/5] Fix: Linting --- nisystemlink/clients/result/_result_client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nisystemlink/clients/result/_result_client.py b/nisystemlink/clients/result/_result_client.py index de699407..8b8bc857 100644 --- a/nisystemlink/clients/result/_result_client.py +++ b/nisystemlink/clients/result/_result_client.py @@ -91,7 +91,9 @@ def get_result(self, id: str) -> models.Result: ... @post("query-results") - def query_results_paged(self, query: models.QueryResultsRequest) -> models.PagedResults: + def query_results_paged( + self, query: models.QueryResultsRequest + ) -> models.PagedResults: """Queries for results that match the filter. Args: