Skip to content

Commit

Permalink
feat: Add client for SystemLink products API (#69)
Browse files Browse the repository at this point in the history
  • Loading branch information
adamarnesen authored Nov 8, 2024
1 parent e1dc1c6 commit d53a9f4
Show file tree
Hide file tree
Showing 17 changed files with 855 additions and 3 deletions.
1 change: 1 addition & 0 deletions docs/api_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ API Reference

api_reference/core
api_reference/tag
api_reference/product
api_reference/testmonitor
api_reference/dataframe
api_reference/spec
Expand Down
23 changes: 23 additions & 0 deletions docs/api_reference/product.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.. _api_tag_page:

nisystemlink.clients.product
======================

.. autoclass:: nisystemlink.clients.product.ProductClient
:exclude-members: __init__

.. automethod:: __init__
.. automethod:: create_products
.. automethod:: get_products
.. automethod:: query_products
.. automethod:: query_product_values
.. automethod:: update_products
.. automethod:: delete_product
.. automethod:: delete_products

.. automodule:: nisystemlink.clients.product.models
:members:
:imported-members:

.. automodule:: nisystemlink.clients.product.utilities
:members:
28 changes: 28 additions & 0 deletions docs/getting_started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,34 @@ Subscribe to tag changes
:language: python
:linenos:


Product API
-------

Overview
~~~~~~~~

The :class:`.ProductClient` class is the primary entry point of the Product API.

When constructing a :class:`.ProductClient`, you can pass an
:class:`.HttpConfiguration` (like one retrieved from the
:class:`.HttpConfigurationManager`), or let :class:`.ProductClient` use the
default connection. The default connection depends on your environment.

With a :class:`.ProductClient` object, you can:

* Create, update, query, and delete Products

Examples
~~~~~~~~

Create, query, update, and delete some products

.. literalinclude:: ../examples/product/products.py
:language: python
:linenos:


DataFrame API
-------

Expand Down
83 changes: 83 additions & 0 deletions examples/product/products.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from nisystemlink.clients.core import HttpConfiguration
from nisystemlink.clients.product import ProductClient
from nisystemlink.clients.product.models import (
Product,
ProductField,
QueryProductsRequest,
QueryProductValuesRequest,
)

name = "Example Name"
family = "Example Family"


def create_some_products():
"""Create two example products on your server."""
new_products = [
Product(
part_number="Example 123 AA",
name=name,
family=family,
keywords=["original keyword"],
properties={"original property key": "yes"},
),
Product(
part_number="Example 123 AA1",
name=name,
family=family,
keywords=["original keyword"],
properties={"original property key": "original"},
),
]
create_response = client.create_products(new_products)
return create_response


# Setup the server configuration to point to your instance of SystemLink Enterprise
server_configuration = HttpConfiguration(
server_uri="https://yourserver.yourcompany.com",
api_key="YourAPIKeyGeneratedFromSystemLink",
)
client = ProductClient(configuration=server_configuration)

# Get all the products using the continuation token in batches of 100 at a time.
response = client.get_products_paged(take=100, return_count=True)
all_products = response.products
while response.continuation_token:
response = client.get_products_paged(
take=100, continuation_token=response.continuation_token, return_count=True
)
all_products.extend(response.products)

create_response = create_some_products()

# use get for first product created
created_product = client.get_product(create_response.products[0].id)

# Query products without continuation
query_request = QueryProductsRequest(
filter=f'family="{family}" && name="{name}"',
return_count=True,
order_by=ProductField.FAMILY,
)
response = client.query_products_paged(query_request)

# Update the first product that you just created and replace the keywords
updated_product = create_response.products[0]
updated_product.keywords = ["new keyword"]
updated_product.properties = {"new property key": "new value"}
update_response = client.update_products([create_response.products[0]], replace=True)

# Query for just the ids of products that match the family
values_query = QueryProductValuesRequest(
filter=f'family="{family}"', field=ProductField.ID
)
values_response = client.query_product_values(query=values_query)

# delete each created product individually by id
for product in create_response.products:
client.delete_product(product.id)

# Create some more and delete them with a single call to delete.
create_response = create_some_products()
client.delete_products([product.id for product in create_response.products])
3 changes: 3 additions & 0 deletions nisystemlink/clients/product/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from ._product_client import ProductClient

# flake8: noqa
182 changes: 182 additions & 0 deletions nisystemlink/clients/product/_product_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
"""Implementation of Product 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.product.models import Product
from uplink import Field, Query, returns

from . import models


class ProductClient(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 Product Service.
"""
if configuration is None:
configuration = core.HttpConfigurationManager.get_configuration()
super().__init__(configuration, base_path="/nitestmonitor/v2/")

@post("products", args=[Field("products")])
def create_products(
self, products: List[Product]
) -> models.CreateProductsPartialSuccess:
"""Creates one or more products and returns errors for failed creations.
Args:
products: A list of products to attempt to create.
Returns: A list of created products, products that failed to create, and errors for
failures.
Raises:
ApiException: if unable to communicate with the ``/nitestmonitor`` service of provided invalid
arguments.
"""
...

@get(
"products",
args=[Query("continuationToken"), Query("take"), Query("returnCount")],
)
def get_products_paged(
self,
continuation_token: Optional[str] = None,
take: Optional[int] = None,
return_count: Optional[bool] = None,
) -> models.PagedProducts:
"""Reads a list of products.
Args:
continuation_token: The token used to paginate results.
take: The number of products to get in this request.
return_count: Whether or not to return the total number of products available.
Returns:
A list of products.
Raises:
ApiException: if unable to communicate with the ``/nitestmonitor`` Service
or provided an invalid argument.
"""
...

@get("products/{id}")
def get_product(self, id: str) -> models.Product:
"""Retrieves a single product by id.
Args:
id (str): Unique ID of a products.
Returns:
The single product matching `id`
Raises:
ApiException: if unable to communicate with the ``/nitestmonitor`` Service
or provided an invalid argument.
"""
...

@post("query-products")
def query_products_paged(
self, query: models.QueryProductsRequest
) -> models.PagedProducts:
"""Queries for products 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 products.
Returns:
A paged list of products 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-product-values")
def query_product_values(
self, query: models.QueryProductValuesRequest
) -> List[str]:
"""Queries for products 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-products", args=[Field("products"), Field("replace")])
def update_products(
self, products: List[Product], replace: bool = False
) -> models.CreateProductsPartialSuccess:
"""Updates a list of products with optional field replacement.
Args:
`products`: A list of products to update. Products 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 product will be
replaced by what is in the `products` 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 products, products that failed to update, and errors for
failures.
Raises:
ApiException: if unable to communicate with the ``/nitestmonitor`` Service
or provided an invalid argument.
"""
...

@delete("products/{id}")
def delete_product(self, id: str) -> None:
"""Deletes a single product by id.
Args:
id (str): Unique ID of a product.
Raises:
ApiException: if unable to communicate with the ``/nitestmonitor`` Service
or provided an invalid argument.
"""
...

@post("delete-products", args=[Field("ids")])
def delete_products(
self, ids: List[str]
) -> Optional[models.DeleteProductsPartialSuccess]:
"""Deletes multiple products.
Args:
ids (List[str]): List of unique IDs of products.
Returns:
A partial success if any products failed to delete, or None if all
products were deleted successfully.
Raises:
ApiException: if unable to communicate with the ``/nitestmonitor`` Service
or provided an invalid argument.
"""
...
11 changes: 11 additions & 0 deletions nisystemlink/clients/product/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from ._product import Product
from ._create_products_partial_success import CreateProductsPartialSuccess
from ._delete_products_partial_success import DeleteProductsPartialSuccess
from ._paged_products import PagedProducts
from ._query_products_request import (
QueryProductsRequest,
ProductField,
QueryProductValuesRequest,
)

# flake8: noqa
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from typing import List, Optional

from nisystemlink.clients.core import ApiError
from nisystemlink.clients.core._uplink._json_model import JsonModel
from nisystemlink.clients.product.models import Product


class CreateProductsPartialSuccess(JsonModel):
products: List[Product]
"""The list of products that were successfully created."""

failed: Optional[List[Product]] = None
"""The list of products that were not created.
If this is `None`, then all products were successfully created.
"""

error: Optional[ApiError] = None
"""Error messages for products that were not created.
If this is `None`, then all products were successfully created.
"""
Original file line number Diff line number Diff line change
@@ -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 DeleteProductsPartialSuccess(JsonModel):
"""The result of deleting multiple products when one or more products could not be deleted."""

ids: List[str]
"""The IDs of the products that were successfully deleted."""

failed: Optional[List[str]]
"""The IDs of the products that could not be deleted."""

error: Optional[ApiError]
"""The error that occurred when deleting the products."""
Loading

0 comments on commit d53a9f4

Please sign in to comment.