Skip to content

Commit d53a9f4

Browse files
authored
feat: Add client for SystemLink products API (#69)
1 parent e1dc1c6 commit d53a9f4

17 files changed

+855
-3
lines changed

docs/api_reference.rst

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ API Reference
99

1010
api_reference/core
1111
api_reference/tag
12+
api_reference/product
1213
api_reference/testmonitor
1314
api_reference/dataframe
1415
api_reference/spec

docs/api_reference/product.rst

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
.. _api_tag_page:
2+
3+
nisystemlink.clients.product
4+
======================
5+
6+
.. autoclass:: nisystemlink.clients.product.ProductClient
7+
:exclude-members: __init__
8+
9+
.. automethod:: __init__
10+
.. automethod:: create_products
11+
.. automethod:: get_products
12+
.. automethod:: query_products
13+
.. automethod:: query_product_values
14+
.. automethod:: update_products
15+
.. automethod:: delete_product
16+
.. automethod:: delete_products
17+
18+
.. automodule:: nisystemlink.clients.product.models
19+
:members:
20+
:imported-members:
21+
22+
.. automodule:: nisystemlink.clients.product.utilities
23+
:members:

docs/getting_started.rst

+28
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,34 @@ Subscribe to tag changes
7676
:language: python
7777
:linenos:
7878

79+
80+
Product API
81+
-------
82+
83+
Overview
84+
~~~~~~~~
85+
86+
The :class:`.ProductClient` class is the primary entry point of the Product API.
87+
88+
When constructing a :class:`.ProductClient`, you can pass an
89+
:class:`.HttpConfiguration` (like one retrieved from the
90+
:class:`.HttpConfigurationManager`), or let :class:`.ProductClient` use the
91+
default connection. The default connection depends on your environment.
92+
93+
With a :class:`.ProductClient` object, you can:
94+
95+
* Create, update, query, and delete Products
96+
97+
Examples
98+
~~~~~~~~
99+
100+
Create, query, update, and delete some products
101+
102+
.. literalinclude:: ../examples/product/products.py
103+
:language: python
104+
:linenos:
105+
106+
79107
DataFrame API
80108
-------
81109

examples/product/products.py

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
from nisystemlink.clients.core import HttpConfiguration
2+
from nisystemlink.clients.product import ProductClient
3+
from nisystemlink.clients.product.models import (
4+
Product,
5+
ProductField,
6+
QueryProductsRequest,
7+
QueryProductValuesRequest,
8+
)
9+
10+
name = "Example Name"
11+
family = "Example Family"
12+
13+
14+
def create_some_products():
15+
"""Create two example products on your server."""
16+
new_products = [
17+
Product(
18+
part_number="Example 123 AA",
19+
name=name,
20+
family=family,
21+
keywords=["original keyword"],
22+
properties={"original property key": "yes"},
23+
),
24+
Product(
25+
part_number="Example 123 AA1",
26+
name=name,
27+
family=family,
28+
keywords=["original keyword"],
29+
properties={"original property key": "original"},
30+
),
31+
]
32+
create_response = client.create_products(new_products)
33+
return create_response
34+
35+
36+
# Setup the server configuration to point to your instance of SystemLink Enterprise
37+
server_configuration = HttpConfiguration(
38+
server_uri="https://yourserver.yourcompany.com",
39+
api_key="YourAPIKeyGeneratedFromSystemLink",
40+
)
41+
client = ProductClient(configuration=server_configuration)
42+
43+
# Get all the products using the continuation token in batches of 100 at a time.
44+
response = client.get_products_paged(take=100, return_count=True)
45+
all_products = response.products
46+
while response.continuation_token:
47+
response = client.get_products_paged(
48+
take=100, continuation_token=response.continuation_token, return_count=True
49+
)
50+
all_products.extend(response.products)
51+
52+
create_response = create_some_products()
53+
54+
# use get for first product created
55+
created_product = client.get_product(create_response.products[0].id)
56+
57+
# Query products without continuation
58+
query_request = QueryProductsRequest(
59+
filter=f'family="{family}" && name="{name}"',
60+
return_count=True,
61+
order_by=ProductField.FAMILY,
62+
)
63+
response = client.query_products_paged(query_request)
64+
65+
# Update the first product that you just created and replace the keywords
66+
updated_product = create_response.products[0]
67+
updated_product.keywords = ["new keyword"]
68+
updated_product.properties = {"new property key": "new value"}
69+
update_response = client.update_products([create_response.products[0]], replace=True)
70+
71+
# Query for just the ids of products that match the family
72+
values_query = QueryProductValuesRequest(
73+
filter=f'family="{family}"', field=ProductField.ID
74+
)
75+
values_response = client.query_product_values(query=values_query)
76+
77+
# delete each created product individually by id
78+
for product in create_response.products:
79+
client.delete_product(product.id)
80+
81+
# Create some more and delete them with a single call to delete.
82+
create_response = create_some_products()
83+
client.delete_products([product.id for product in create_response.products])
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from ._product_client import ProductClient
2+
3+
# flake8: noqa
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
"""Implementation of Product Client"""
2+
3+
from typing import List, Optional
4+
5+
from nisystemlink.clients import core
6+
from nisystemlink.clients.core._uplink._base_client import BaseClient
7+
from nisystemlink.clients.core._uplink._methods import delete, get, post
8+
from nisystemlink.clients.product.models import Product
9+
from uplink import Field, Query, returns
10+
11+
from . import models
12+
13+
14+
class ProductClient(BaseClient):
15+
def __init__(self, configuration: Optional[core.HttpConfiguration] = None):
16+
"""Initialize an instance.
17+
18+
Args:
19+
configuration: Defines the web server to connect to and information about
20+
how to connect. If not provided, the
21+
:class:`HttpConfigurationManager <nisystemlink.clients.core.HttpConfigurationManager>`
22+
is used to obtain the configuration.
23+
24+
Raises:
25+
ApiException: if unable to communicate with the Product Service.
26+
"""
27+
if configuration is None:
28+
configuration = core.HttpConfigurationManager.get_configuration()
29+
super().__init__(configuration, base_path="/nitestmonitor/v2/")
30+
31+
@post("products", args=[Field("products")])
32+
def create_products(
33+
self, products: List[Product]
34+
) -> models.CreateProductsPartialSuccess:
35+
"""Creates one or more products and returns errors for failed creations.
36+
37+
Args:
38+
products: A list of products to attempt to create.
39+
40+
Returns: A list of created products, products that failed to create, and errors for
41+
failures.
42+
43+
Raises:
44+
ApiException: if unable to communicate with the ``/nitestmonitor`` service of provided invalid
45+
arguments.
46+
"""
47+
...
48+
49+
@get(
50+
"products",
51+
args=[Query("continuationToken"), Query("take"), Query("returnCount")],
52+
)
53+
def get_products_paged(
54+
self,
55+
continuation_token: Optional[str] = None,
56+
take: Optional[int] = None,
57+
return_count: Optional[bool] = None,
58+
) -> models.PagedProducts:
59+
"""Reads a list of products.
60+
61+
Args:
62+
continuation_token: The token used to paginate results.
63+
take: The number of products to get in this request.
64+
return_count: Whether or not to return the total number of products available.
65+
66+
Returns:
67+
A list of products.
68+
69+
Raises:
70+
ApiException: if unable to communicate with the ``/nitestmonitor`` Service
71+
or provided an invalid argument.
72+
"""
73+
...
74+
75+
@get("products/{id}")
76+
def get_product(self, id: str) -> models.Product:
77+
"""Retrieves a single product by id.
78+
79+
Args:
80+
id (str): Unique ID of a products.
81+
82+
Returns:
83+
The single product matching `id`
84+
85+
Raises:
86+
ApiException: if unable to communicate with the ``/nitestmonitor`` Service
87+
or provided an invalid argument.
88+
"""
89+
...
90+
91+
@post("query-products")
92+
def query_products_paged(
93+
self, query: models.QueryProductsRequest
94+
) -> models.PagedProducts:
95+
"""Queries for products that match the filter.
96+
97+
Args:
98+
query : The query contains a DynamicLINQ query string in addition to other details
99+
about how to filter and return the list of products.
100+
101+
Returns:
102+
A paged list of products with a continuation token to get the next page.
103+
104+
Raises:
105+
ApiException: if unable to communicate with the ``/nitestmonitor`` Service or provided invalid
106+
arguments.
107+
"""
108+
...
109+
110+
@returns.json # type: ignore
111+
@post("query-product-values")
112+
def query_product_values(
113+
self, query: models.QueryProductValuesRequest
114+
) -> List[str]:
115+
"""Queries for products that match the query and returns a list of the requested field.
116+
117+
Args:
118+
query : The query for the fields you want.
119+
120+
Returns:
121+
A list of the values of the field you requested.
122+
123+
Raises:
124+
ApiException: if unable to communicate with the ``/nitestmonitor`` Service or provided
125+
invalid arguments.
126+
"""
127+
...
128+
129+
@post("update-products", args=[Field("products"), Field("replace")])
130+
def update_products(
131+
self, products: List[Product], replace: bool = False
132+
) -> models.CreateProductsPartialSuccess:
133+
"""Updates a list of products with optional field replacement.
134+
135+
Args:
136+
`products`: A list of products to update. Products are matched for update by id.
137+
`replace`: Replace the existing fields instead of merging them. Defaults to `False`.
138+
If this is `True`, then `keywords` and `properties` for the product will be
139+
replaced by what is in the `products` provided in this request.
140+
If this is `False`, then the `keywords` and `properties` in this request will
141+
merge with what is already present in the server resource.
142+
143+
Returns: A list of updates products, products that failed to update, and errors for
144+
failures.
145+
146+
Raises:
147+
ApiException: if unable to communicate with the ``/nitestmonitor`` Service
148+
or provided an invalid argument.
149+
"""
150+
...
151+
152+
@delete("products/{id}")
153+
def delete_product(self, id: str) -> None:
154+
"""Deletes a single product by id.
155+
156+
Args:
157+
id (str): Unique ID of a product.
158+
159+
Raises:
160+
ApiException: if unable to communicate with the ``/nitestmonitor`` Service
161+
or provided an invalid argument.
162+
"""
163+
...
164+
165+
@post("delete-products", args=[Field("ids")])
166+
def delete_products(
167+
self, ids: List[str]
168+
) -> Optional[models.DeleteProductsPartialSuccess]:
169+
"""Deletes multiple products.
170+
171+
Args:
172+
ids (List[str]): List of unique IDs of products.
173+
174+
Returns:
175+
A partial success if any products failed to delete, or None if all
176+
products were deleted successfully.
177+
178+
Raises:
179+
ApiException: if unable to communicate with the ``/nitestmonitor`` Service
180+
or provided an invalid argument.
181+
"""
182+
...
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from ._product import Product
2+
from ._create_products_partial_success import CreateProductsPartialSuccess
3+
from ._delete_products_partial_success import DeleteProductsPartialSuccess
4+
from ._paged_products import PagedProducts
5+
from ._query_products_request import (
6+
QueryProductsRequest,
7+
ProductField,
8+
QueryProductValuesRequest,
9+
)
10+
11+
# flake8: noqa
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from typing import List, Optional
2+
3+
from nisystemlink.clients.core import ApiError
4+
from nisystemlink.clients.core._uplink._json_model import JsonModel
5+
from nisystemlink.clients.product.models import Product
6+
7+
8+
class CreateProductsPartialSuccess(JsonModel):
9+
products: List[Product]
10+
"""The list of products that were successfully created."""
11+
12+
failed: Optional[List[Product]] = None
13+
"""The list of products that were not created.
14+
15+
If this is `None`, then all products were successfully created.
16+
"""
17+
18+
error: Optional[ApiError] = None
19+
"""Error messages for products that were not created.
20+
21+
If this is `None`, then all products were successfully created.
22+
"""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from typing import List, Optional
2+
3+
from nisystemlink.clients.core import ApiError
4+
from nisystemlink.clients.core._uplink._json_model import JsonModel
5+
6+
7+
class DeleteProductsPartialSuccess(JsonModel):
8+
"""The result of deleting multiple products when one or more products could not be deleted."""
9+
10+
ids: List[str]
11+
"""The IDs of the products that were successfully deleted."""
12+
13+
failed: Optional[List[str]]
14+
"""The IDs of the products that could not be deleted."""
15+
16+
error: Optional[ApiError]
17+
"""The error that occurred when deleting the products."""

0 commit comments

Comments
 (0)