diff --git a/examples/product/products.py b/examples/product/products.py index 756cc54c..fb77e36b 100644 --- a/examples/product/products.py +++ b/examples/product/products.py @@ -1,8 +1,9 @@ from nisystemlink.clients.core import HttpConfiguration from nisystemlink.clients.product import ProductClient from nisystemlink.clients.product.models import ( - Product, + CreateProductRequest, ProductField, + ProductOrderBy, QueryProductsRequest, QueryProductValuesRequest, ) @@ -14,14 +15,14 @@ def create_some_products(): """Create two example products on your server.""" new_products = [ - Product( + CreateProductRequest( part_number="Example 123 AA", name=name, family=family, keywords=["original keyword"], properties={"original property key": "yes"}, ), - Product( + CreateProductRequest( part_number="Example 123 AA1", name=name, family=family, @@ -58,9 +59,9 @@ def create_some_products(): query_request = QueryProductsRequest( filter=f'family="{family}" && name="{name}"', return_count=True, - order_by=ProductField.FAMILY, + order_by=ProductOrderBy.FAMILY, ) -response = client.query_products_paged(query_request) +query_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] diff --git a/examples/spec/update_and_delete_specs.py b/examples/spec/update_and_delete_specs.py index 03c09a56..0cbc4c2f 100644 --- a/examples/spec/update_and_delete_specs.py +++ b/examples/spec/update_and_delete_specs.py @@ -56,4 +56,4 @@ # query all specs response = client.query_specs(QuerySpecificationsRequest(product_ids=[product])) if response.specs: - client.delete_specs(ids=[spec.id for spec in response.specs]) + client.delete_specs(ids=[spec.id for spec in response.specs if spec.id]) diff --git a/nisystemlink/clients/product/_product_client.py b/nisystemlink/clients/product/_product_client.py index de627c5e..9f10de99 100644 --- a/nisystemlink/clients/product/_product_client.py +++ b/nisystemlink/clients/product/_product_client.py @@ -5,12 +5,14 @@ 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 uplink import Field, Query, retry, returns from . import models +@retry( + when=retry.when.status([408, 429, 502, 503, 504]), stop=retry.stop.after_attempt(5) +) class ProductClient(BaseClient): def __init__(self, configuration: Optional[core.HttpConfiguration] = None): """Initialize an instance. @@ -30,7 +32,7 @@ def __init__(self, configuration: Optional[core.HttpConfiguration] = None): @post("products", args=[Field("products")]) def create_products( - self, products: List[Product] + self, products: List[models.CreateProductRequest] ) -> models.CreateProductsPartialSuccess: """Creates one or more products and returns errors for failed creations. @@ -128,7 +130,7 @@ def query_product_values( @post("update-products", args=[Field("products"), Field("replace")]) def update_products( - self, products: List[Product], replace: bool = False + self, products: List[models.UpdateProductRequest], replace: bool = False ) -> models.CreateProductsPartialSuccess: """Updates a list of products with optional field replacement. diff --git a/nisystemlink/clients/product/models/__init__.py b/nisystemlink/clients/product/models/__init__.py index 88f6f68d..4a21c838 100644 --- a/nisystemlink/clients/product/models/__init__.py +++ b/nisystemlink/clients/product/models/__init__.py @@ -1,11 +1,14 @@ -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, + ProductOrderBy, + ProductProjection, + QueryProductsRequest, QueryProductValuesRequest, ) +from ._product import Product +from ._product_request import CreateProductRequest, UpdateProductRequest # flake8: noqa diff --git a/nisystemlink/clients/product/models/_create_products_partial_success.py b/nisystemlink/clients/product/models/_create_products_partial_success.py index f8eee6d7..cd529145 100644 --- a/nisystemlink/clients/product/models/_create_products_partial_success.py +++ b/nisystemlink/clients/product/models/_create_products_partial_success.py @@ -2,20 +2,21 @@ from nisystemlink.clients.core import ApiError from nisystemlink.clients.core._uplink._json_model import JsonModel -from nisystemlink.clients.product.models import Product +from nisystemlink.clients.product.models._product import Product +from nisystemlink.clients.product.models._product_request import CreateProductRequest class CreateProductsPartialSuccess(JsonModel): products: List[Product] """The list of products that were successfully created.""" - failed: Optional[List[Product]] = None + failed: Optional[List[CreateProductRequest]] """The list of products that were not created. If this is `None`, then all products were successfully created. """ - error: Optional[ApiError] = None + error: Optional[ApiError] """Error messages for products that were not created. If this is `None`, then all products were successfully created. diff --git a/nisystemlink/clients/product/models/_paged_products.py b/nisystemlink/clients/product/models/_paged_products.py index 7af39716..c615a5c4 100644 --- a/nisystemlink/clients/product/models/_paged_products.py +++ b/nisystemlink/clients/product/models/_paged_products.py @@ -1,11 +1,13 @@ from typing import List, Optional from nisystemlink.clients.core._uplink._with_paging import WithPaging -from nisystemlink.clients.product.models import Product +from nisystemlink.clients.product.models._product import Product class PagedProducts(WithPaging): - """The response for a Products query containing matched products.""" + """The response containing the list of products, total count of products and the continuation + token if applicable. + """ products: List[Product] """A list of all the products in this page.""" diff --git a/nisystemlink/clients/product/models/_product.py b/nisystemlink/clients/product/models/_product.py index 23058d65..0435bbac 100644 --- a/nisystemlink/clients/product/models/_product.py +++ b/nisystemlink/clients/product/models/_product.py @@ -10,7 +10,7 @@ class Product(JsonModel): id: Optional[str] """The globally unique id of the product.""" - part_number: str + part_number: Optional[str] """The part number is the unique identifier of a product within a single org. Usually the part number refers to a specific revision or version of a given product.""" diff --git a/nisystemlink/clients/product/models/_product_request.py b/nisystemlink/clients/product/models/_product_request.py new file mode 100644 index 00000000..b2d42255 --- /dev/null +++ b/nisystemlink/clients/product/models/_product_request.py @@ -0,0 +1,48 @@ +from typing import Dict, List, Optional + +from nisystemlink.clients.core._uplink._json_model import JsonModel + + +class BaseProductRequest(JsonModel): + """Contains information about a product.""" + + name: Optional[str] + """The name of the product. + + Usually the name is used to refer to several part numbers that all have the same name but + different revisions or versions. + """ + + family: Optional[str] + """The family that that this product belongs to. + + Usually the family is a grouping above product name. A family usually has multiple product + names within it. + """ + + file_ids: Optional[List[str]] + """A list of file ids that are attached to this product.""" + + keywords: Optional[List[str]] + """A list of keywords that categorize this product.""" + + properties: Optional[Dict[str, str]] + """A list of custom properties for this product.""" + + workspace: Optional[str] + """The id of the workspace that this product belongs to.""" + + +class CreateProductRequest(BaseProductRequest): + + part_number: str + """The part number is the unique identifier of a product within a single org. + + Usually the part number refers to a specific revision or version of a given product.""" + + +class UpdateProductRequest(BaseProductRequest): + """This is the request model to update a product.""" + + id: str + """The globally unique id of the product.""" diff --git a/nisystemlink/clients/product/models/_query_products_request.py b/nisystemlink/clients/product/models/_query_products_request.py index aca8b919..2c759b1f 100644 --- a/nisystemlink/clients/product/models/_query_products_request.py +++ b/nisystemlink/clients/product/models/_query_products_request.py @@ -5,7 +5,7 @@ from pydantic import Field -class ProductField(str, Enum): +class ProductOrderBy(str, Enum): """The valid ways to order a product query.""" ID = "ID" @@ -15,6 +15,32 @@ class ProductField(str, Enum): UPDATED_AT = "UPDATED_AT" +class ProductField(str, Enum): + """An enumeration of product fields for which the values can be queried for.""" + + ID = "ID" + FAMILY = "FAMILY" + PART_NUMBER = "PART_NUMBER" + NAME = "NAME" + UPDATED_AT = "UPDATED_AT" + + +class ProductProjection(str, Enum): + """An enumeration of all fields in a Product. These are used to project the required fields + from the API response. + """ + + ID = "ID" + FAMILY = "FAMILY" + PART_NUMBER = "PART_NUMBER" + NAME = "NAME" + UPDATED_AT = "UPDATED_AT" + WORKSPACE = "WORKSPACE" + KEYWORDS = "KEYWORDS" + PROPERTIES = "PROPERTIES" + FILE_IDS = "FILE_IDS" + + class QueryProductsBase(JsonModel): filter: Optional[str] = None """ @@ -50,7 +76,7 @@ class QueryProductsBase(JsonModel): class QueryProductsRequest(QueryProductsBase): - order_by: Optional[ProductField] = Field(None, alias="orderBy") + order_by: Optional[ProductOrderBy] = Field(None, alias="orderBy") """Specifies the fields to use to sort the products. By default, products are sorted by `id` @@ -61,11 +87,21 @@ class QueryProductsRequest(QueryProductsBase): By default, this value is `false` and products are sorted in ascending order. """ + + projection: Optional[List[ProductProjection]] = None + """Specifies the product fields to project. + + When a field value is given here, the corresponding field will be present in all returned products, + and all unspecified fields will be excluded. If no projection is specified, all product fields + will be returned. + """ + take: Optional[int] = None """Maximum number of products to return in the current API response. Uses the default if the specified value is negative. The default value is `1000` products. """ + continuation_token: Optional[str] = None """Allows users to continue the query at the next product that matches the given criteria. diff --git a/nisystemlink/clients/product/utilities/_file_utilities.py b/nisystemlink/clients/product/utilities/_file_utilities.py index 70f8189b..e86a6fc7 100644 --- a/nisystemlink/clients/product/utilities/_file_utilities.py +++ b/nisystemlink/clients/product/utilities/_file_utilities.py @@ -2,7 +2,9 @@ from nisystemlink.clients.product._product_client import ProductClient from nisystemlink.clients.product.models._paged_products import PagedProducts -from nisystemlink.clients.product.models._product import Product +from nisystemlink.clients.product.models._product import ( + Product, +) from nisystemlink.clients.product.models._query_products_request import ( QueryProductsRequest, ) diff --git a/nisystemlink/clients/spec/_spec_client.py b/nisystemlink/clients/spec/_spec_client.py index e401932c..ff6de3cd 100644 --- a/nisystemlink/clients/spec/_spec_client.py +++ b/nisystemlink/clients/spec/_spec_client.py @@ -5,11 +5,14 @@ from nisystemlink.clients import core from nisystemlink.clients.core._uplink._base_client import BaseClient from nisystemlink.clients.core._uplink._methods import get, post -from uplink import Field +from uplink import Field, retry from . import models +@retry( + when=retry.when.status([408, 429, 502, 503, 504]), stop=retry.stop.after_attempt(5) +) class SpecClient(BaseClient): def __init__(self, configuration: Optional[core.HttpConfiguration] = None): """Initialize an instance. @@ -80,7 +83,7 @@ def delete_specs( @post("query-specs") def query_specs( self, query: models.QuerySpecificationsRequest - ) -> models.QuerySpecifications: + ) -> models.PagedSpecifications: """Queries for specs that match the filters. Args: diff --git a/nisystemlink/clients/spec/models/__init__.py b/nisystemlink/clients/spec/models/__init__.py index 7a7fff4b..b30499ec 100644 --- a/nisystemlink/clients/spec/models/__init__.py +++ b/nisystemlink/clients/spec/models/__init__.py @@ -10,24 +10,25 @@ CreatedSpecification, CreateSpecificationsPartialSuccess, CreateSpecificationsRequest, + CreateSpecificationsRequestObject, ) from ._delete_specs_request import DeleteSpecificationsPartialSuccess -from ._query_specs import QuerySpecificationsRequest, QuerySpecifications +from ._query_specs import ( + QuerySpecificationsRequest, + PagedSpecifications, + SpecificationProjection, +) from ._specification import ( Specification, - SpecificationCreation, SpecificationDefinition, SpecificationLimit, - SpecificationServerManaged, SpecificationType, - SpecificationUpdated, - SpecificationUserManaged, - SpecificationWithHistory, ) from ._update_specs_request import ( UpdatedSpecification, UpdateSpecificationsPartialSuccess, UpdateSpecificationsRequest, + UpdateSpecificationsRequestObject, ) # flake8: noqa diff --git a/nisystemlink/clients/spec/models/_create_specs_request.py b/nisystemlink/clients/spec/models/_create_specs_request.py index 81c5d907..28f58b14 100644 --- a/nisystemlink/clients/spec/models/_create_specs_request.py +++ b/nisystemlink/clients/spec/models/_create_specs_request.py @@ -1,27 +1,73 @@ +from datetime import datetime from typing import List, Optional from nisystemlink.clients.core import ApiError from nisystemlink.clients.core._uplink._json_model import JsonModel from nisystemlink.clients.spec.models._specification import ( - SpecificationCreation, SpecificationDefinition, - SpecificationServerManaged, - SpecificationUserManaged, + SpecificationType, ) +class CreateSpecificationsRequestObject(SpecificationDefinition): + product_id: str + """Id of the product to which the specification will be associated.""" + + spec_id: str + """User provided value using which the specification will be identified. + + This should be unique for a product and workspace combination. + """ + + type: SpecificationType + """Type of the specification.""" + + class CreateSpecificationsRequest(JsonModel): """Create multiple specifications.""" - specs: Optional[List[SpecificationDefinition]] = None + specs: Optional[List[CreateSpecificationsRequestObject]] = None """List of specifications to be created.""" -class CreatedSpecification( - SpecificationServerManaged, SpecificationUserManaged, SpecificationCreation -): +class BaseSpecificationResponse(JsonModel): + """Base Response Model for create specs response and update specs response.""" + + id: str + """The global Id of the specification.""" + + product_id: str + """Id of the product to which the specification will be associated.""" + + spec_id: str + """User provided value using which the specification will be identified. + + This should be unique for a product and workspace combination. + """ + + workspace: str + """Id of the workspace to which the specification will be associated. + + Default workspace will be taken if the value is not given. + """ + + version: int + """ + Current version of the specification. + + When an update is applied, the version is automatically incremented. + """ + + +class CreatedSpecification(BaseSpecificationResponse): """A specification successfully created on the server.""" + created_at: datetime + """ISO-8601 formatted timestamp indicating when the specification was created.""" + + created_by: str + """Id of the user who created the specification.""" + class CreateSpecificationsPartialSuccess(JsonModel): """When some specs can not be created, this contains the list that was and was not created.""" @@ -29,7 +75,7 @@ class CreateSpecificationsPartialSuccess(JsonModel): created_specs: Optional[List[CreatedSpecification]] = None """Information about the created specification(s)""" - failed_specs: Optional[List[SpecificationDefinition]] = None + failed_specs: Optional[List[CreateSpecificationsRequestObject]] = None """List of specification requests that failed during creation.""" error: Optional[ApiError] = None diff --git a/nisystemlink/clients/spec/models/_query_specs.py b/nisystemlink/clients/spec/models/_query_specs.py index 61300ac1..cd06dcd6 100644 --- a/nisystemlink/clients/spec/models/_query_specs.py +++ b/nisystemlink/clients/spec/models/_query_specs.py @@ -3,10 +3,10 @@ from nisystemlink.clients.core._uplink._json_model import JsonModel from nisystemlink.clients.core._uplink._with_paging import WithPaging -from nisystemlink.clients.spec.models._specification import SpecificationWithHistory +from nisystemlink.clients.spec.models._specification import Specification -class Projection(str, Enum): +class SpecificationProjection(str, Enum): """The allowed projections for query. When using projection, only the fields specified by the projection element will be included in @@ -34,7 +34,7 @@ class Projection(str, Enum): CREATED_BY = "CREATED_BY" -class OrderBy(Enum): +class SpecificationOrderBy(Enum): """The valid ways to order the response to a spec query.""" ID = "ID" @@ -85,13 +85,13 @@ class QuerySpecificationsRequest(JsonModel): documentation for more details. """ - projection: Optional[List[Projection]] = None + projection: Optional[List[SpecificationProjection]] = None """Specifies the fields to include in the returned specifications. Fields you do not specify are excluded. Returns all fields if no value is specified. """ - order_by: Optional[OrderBy] = None + order_by: Optional[SpecificationOrderBy] = None """Specifies the field to use to sort specifications. By default, specifications are sorted by `ID`. @@ -103,10 +103,10 @@ class QuerySpecificationsRequest(JsonModel): """ -class QuerySpecifications(WithPaging): +class PagedSpecifications(WithPaging): """The list of matching specifications and a continuation token to get the next items.""" - specs: Optional[List[SpecificationWithHistory]] = None + specs: Optional[List[Specification]] = None """List of queried specifications. An empty list indicates that there are no specifications meeting the criteria provided in the diff --git a/nisystemlink/clients/spec/models/_specification.py b/nisystemlink/clients/spec/models/_specification.py index 813612ae..36e36fc8 100644 --- a/nisystemlink/clients/spec/models/_specification.py +++ b/nisystemlink/clients/spec/models/_specification.py @@ -40,44 +40,24 @@ class SpecificationType(Enum): """Functional specs only have pass/fail status.""" -class SpecificationUserManaged(JsonModel): - product_id: str +class SpecificationDefinition(JsonModel): + + product_id: Optional[str] = None """Id of the product to which the specification will be associated.""" - spec_id: str + spec_id: Optional[str] = None """User provided value using which the specification will be identified. This should be unique for a product and workspace combination. """ - workspace: Optional[str] = None - """Id of the workspace to which the specification will be associated. - - Default workspace will be taken if the value is not given. - """ - - -class SpecificationServerManaged(JsonModel): - id: str - """The global Id of the specification.""" - - version: int - """ - Current version of the specification. - - When an update is applied, the version is automatically incremented. - """ - - -class SpecificationDefinition(SpecificationUserManaged): - name: Optional[str] = None """Name of the specification.""" category: Optional[str] = None """Category of the specification.""" - type: SpecificationType + type: Optional[SpecificationType] = None """Type of the specification.""" symbol: Optional[str] = None @@ -104,13 +84,18 @@ class SpecificationDefinition(SpecificationUserManaged): properties: Optional[Dict[str, str]] = None """Additional properties associated with the specification.""" + workspace: Optional[str] = None + """Id of the workspace to which the specification will be associated. + + Default workspace will be taken if the value is not given. + """ -class Specification(SpecificationDefinition, SpecificationServerManaged): - """The complete definition of a specification.""" +class Specification(SpecificationDefinition): + """The complete definition of a specification.""" -class SpecificationCreation(JsonModel): - """When the spec was created and when.""" + id: Optional[str] = None + """The global Id of the specification.""" created_at: Optional[datetime] = None """ISO-8601 formatted timestamp indicating when the specification was created.""" @@ -118,18 +103,15 @@ class SpecificationCreation(JsonModel): created_by: Optional[str] = None """Id of the user who created the specification.""" - -class SpecificationUpdated(JsonModel): - """When the spec was updated and when.""" - updated_at: Optional[datetime] = None """ISO-8601 formatted timestamp indicating when the specification was last updated.""" updated_by: Optional[str] = None """Id of the user who last updated the specification.""" + version: Optional[int] = None + """ + Current version of the specification. -class SpecificationWithHistory( - Specification, SpecificationCreation, SpecificationUpdated -): - """A full specification with update and create history.""" + When an update is applied, the version is automatically incremented. + """ diff --git a/nisystemlink/clients/spec/models/_update_specs_request.py b/nisystemlink/clients/spec/models/_update_specs_request.py index e2a11b9d..1301015a 100644 --- a/nisystemlink/clients/spec/models/_update_specs_request.py +++ b/nisystemlink/clients/spec/models/_update_specs_request.py @@ -1,35 +1,69 @@ +from datetime import datetime from typing import List, Optional from nisystemlink.clients.core import ApiError from nisystemlink.clients.core._uplink._json_model import JsonModel +from nisystemlink.clients.spec.models._create_specs_request import ( + BaseSpecificationResponse, +) from nisystemlink.clients.spec.models._specification import ( - Specification, - SpecificationServerManaged, - SpecificationUpdated, - SpecificationUserManaged, + SpecificationDefinition, + SpecificationType, ) +class UpdateSpecificationsRequestObject(SpecificationDefinition): + id: str + """The global Id of the specification.""" + + product_id: str + """Id of the product to which the specification will be associated.""" + + spec_id: str + """User provided value using which the specification will be identified. + + This should be unique for a product and workspace combination. + """ + + type: SpecificationType + """Type of the specification.""" + + workspace: str + """Id of the workspace to which the specification will be associated. + + Default workspace will be taken if the value is not given. + """ + + version: int + """ + Current version of the specification. + + When an update is applied, the version is automatically incremented. + """ + + class UpdateSpecificationsRequest(JsonModel): - specs: Optional[List[Specification]] = None + specs: Optional[List[UpdateSpecificationsRequestObject]] = None """List of specifications to be updated.""" -class UpdatedSpecification( - SpecificationUserManaged, - SpecificationServerManaged, - SpecificationUpdated, -): +class UpdatedSpecification(BaseSpecificationResponse): """A specification that was updated on the server.""" + updated_at: datetime + """ISO-8601 formatted timestamp indicating when the specification was last updated.""" + + updated_by: str + """Id of the user who last updated the specification.""" + class UpdateSpecificationsPartialSuccess(JsonModel): updated_specs: Optional[List[UpdatedSpecification]] = None """Information about each of the updated specification(s).""" - failed_specs: Optional[List[Specification]] = None + failed_specs: Optional[List[UpdateSpecificationsRequestObject]] = None """Information about each of the specification request(s) that failed during the update.""" error: Optional[ApiError] = None diff --git a/tests/integration/product/test_product_client.py b/tests/integration/product/test_product_client.py index e4b159e2..e4ccb163 100644 --- a/tests/integration/product/test_product_client.py +++ b/tests/integration/product/test_product_client.py @@ -5,12 +5,15 @@ from nisystemlink.clients.core._http_configuration import HttpConfiguration from nisystemlink.clients.product._product_client import ProductClient from nisystemlink.clients.product.models import ( + CreateProductRequest, CreateProductsPartialSuccess, Product, + ProductField, + ProductProjection, + UpdateProductRequest, ) from nisystemlink.clients.product.models._paged_products import PagedProducts from nisystemlink.clients.product.models._query_products_request import ( - ProductField, QueryProductsRequest, QueryProductValuesRequest, ) @@ -30,12 +33,31 @@ def unique_identifier() -> str: return product_id +@pytest.fixture +def create_update_product_request(): + """Fixture to create a request object for updating products, from a product response""" + + def _create_update_product_request( + product_response: Product, + ) -> UpdateProductRequest: + product_data = product_response.dict() + + product_data.pop("updated_at", None) + product_data.pop("part_number", None) + + return UpdateProductRequest(**product_data) + + return _create_update_product_request + + @pytest.fixture def create_products(client: ProductClient): """Fixture to return a factory that creates specs.""" responses: List[CreateProductsPartialSuccess] = [] - def _create_products(products: List[Product]) -> CreateProductsPartialSuccess: + def _create_products( + products: List[CreateProductRequest], + ) -> CreateProductsPartialSuccess: response = client.create_products(products) responses.append(response) return response @@ -61,7 +83,7 @@ def test__create_single_product__one_product_created_with_right_field_values( family = "Example Family" keywords = ["testing"] properties = {"test_property": "yes"} - product = Product( + product = CreateProductRequest( part_number=part_number, name=name, family=family, @@ -83,8 +105,8 @@ def test__create_multiple_products__multiple_creates_succeed( self, client: ProductClient, create_products ): products = [ - Product(part_number=uuid.uuid1().hex), - Product(part_number=uuid.uuid1().hex), + CreateProductRequest(part_number=uuid.uuid1().hex), + CreateProductRequest(part_number=uuid.uuid1().hex), ] response: CreateProductsPartialSuccess = create_products(products) assert response is not None @@ -93,7 +115,7 @@ def test__create_multiple_products__multiple_creates_succeed( def test__create_single_product_and_get_products__at_least_one_product_exists( self, client: ProductClient, create_products, unique_identifier ): - products = [Product(part_number=unique_identifier)] + products = [CreateProductRequest(part_number=unique_identifier)] create_products(products) get_response = client.get_products_paged() assert get_response is not None @@ -103,8 +125,8 @@ def test__create_multiple_products_and_get_products_with_take__only_take_returne self, client: ProductClient, create_products, unique_identifier ): products = [ - Product(part_number=unique_identifier), - Product(part_number=unique_identifier), + CreateProductRequest(part_number=unique_identifier), + CreateProductRequest(part_number=unique_identifier), ] create_products(products) get_response = client.get_products_paged(take=1) @@ -115,8 +137,8 @@ def test__create_multiple_products_and_get_products_with_count_at_least_one_coun self, client: ProductClient, create_products, unique_identifier ): products = [ - Product(part_number=unique_identifier), - Product(part_number=unique_identifier), + CreateProductRequest(part_number=unique_identifier), + CreateProductRequest(part_number=unique_identifier), ] create_products(products) get_response: PagedProducts = client.get_products_paged(return_count=True) @@ -127,7 +149,7 @@ def test__get_product_by_id__product_matches_expected( self, client: ProductClient, create_products, unique_identifier ): part_number = unique_identifier - products = [Product(part_number=part_number)] + products = [CreateProductRequest(part_number=part_number)] create_response: CreateProductsPartialSuccess = create_products(products) assert create_response is not None id = str(create_response.products[0].id) @@ -139,7 +161,7 @@ def test__query_product_by_part_number__matches_expected( self, client: ProductClient, create_products, unique_identifier ): part_number = unique_identifier - products = [Product(part_number=part_number)] + products = [CreateProductRequest(part_number=part_number)] create_response: CreateProductsPartialSuccess = create_products(products) assert create_response is not None query_request = QueryProductsRequest( @@ -155,7 +177,7 @@ def test__query_product_values_for_name__name_matches( part_number = unique_identifier test_name = "query values test" create_response: CreateProductsPartialSuccess = create_products( - [Product(part_number=part_number, name=test_name)] + [CreateProductRequest(part_number=part_number, name=test_name)] ) assert create_response is not None query_request = QueryProductValuesRequest( @@ -167,18 +189,29 @@ def test__query_product_values_for_name__name_matches( assert query_response[0] == test_name def test__update_keywords_with_replace__keywords_replaced( - self, client: ProductClient, create_products, unique_identifier + self, + client: ProductClient, + create_products, + unique_identifier, + create_update_product_request, ): original_keyword = "originalKeyword" updated_keyword = "updatedKeyword" create_response: CreateProductsPartialSuccess = create_products( - [Product(part_number=unique_identifier, keywords=[original_keyword])] + [ + CreateProductRequest( + part_number=unique_identifier, keywords=[original_keyword] + ) + ] ) assert create_response is not None assert len(create_response.products) == 1 updated_product = create_response.products[0] updated_product.keywords = [updated_keyword] - update_response = client.update_products([updated_product], replace=True) + update_response = client.update_products( + [create_update_product_request(product_response=updated_product)], + replace=True, + ) assert update_response is not None assert len(update_response.products) == 1 assert ( @@ -188,18 +221,29 @@ def test__update_keywords_with_replace__keywords_replaced( assert original_keyword not in update_response.products[0].keywords def test__update_keywords_no_replace__keywords_appended( - self, client: ProductClient, create_products, unique_identifier + self, + client: ProductClient, + create_products, + unique_identifier, + create_update_product_request, ): original_keyword = "originalKeyword" additional_keyword = "additionalKeyword" create_response: CreateProductsPartialSuccess = create_products( - [Product(part_number=unique_identifier, keywords=[original_keyword])] + [ + CreateProductRequest( + part_number=unique_identifier, keywords=[original_keyword] + ) + ] ) assert create_response is not None assert len(create_response.products) == 1 updated_product = create_response.products[0] updated_product.keywords = [additional_keyword] - update_response = client.update_products([updated_product], replace=False) + update_response = client.update_products( + [create_update_product_request(product_response=updated_product)], + replace=False, + ) assert update_response is not None assert len(update_response.products) == 1 assert ( @@ -212,19 +256,30 @@ def test__update_keywords_no_replace__keywords_appended( ) def test__update_properties_with_replace__properties_replaced( - self, client: ProductClient, create_products, unique_identifier + self, + client: ProductClient, + create_products, + unique_identifier, + create_update_product_request, ): new_key = "newKey" original_properties = {"originalKey": "originalValue"} new_properties = {new_key: "newValue"} create_response: CreateProductsPartialSuccess = create_products( - [Product(part_number=unique_identifier, properties=original_properties)] + [ + CreateProductRequest( + part_number=unique_identifier, properties=original_properties + ) + ] ) assert create_response is not None assert len(create_response.products) == 1 updated_product = create_response.products[0] updated_product.properties = new_properties - update_response = client.update_products([updated_product], replace=True) + update_response = client.update_products( + [create_update_product_request(product_response=updated_product)], + replace=True, + ) assert update_response is not None assert len(update_response.products) == 1 assert ( @@ -237,20 +292,31 @@ def test__update_properties_with_replace__properties_replaced( ) def test__update_properties_append__properties_appended( - self, client: ProductClient, create_products, unique_identifier + self, + client: ProductClient, + create_products, + unique_identifier, + create_update_product_request, ): original_key = "originalKey" new_key = "newKey" original_properties = {original_key: "originalValue"} new_properties = {new_key: "newValue"} create_response: CreateProductsPartialSuccess = create_products( - [Product(part_number=unique_identifier, properties=original_properties)] + [ + CreateProductRequest( + part_number=unique_identifier, properties=original_properties + ) + ] ) assert create_response is not None assert len(create_response.products) == 1 updated_product = create_response.products[0] updated_product.properties = new_properties - update_response = client.update_products([updated_product], replace=False) + update_response = client.update_products( + [create_update_product_request(product_response=updated_product)], + replace=False, + ) assert update_response is not None assert len(update_response.products) == 1 updated_product = update_response.products[0] @@ -272,12 +338,12 @@ def test__query_products_linked_to_files_correct_products_returned( file_id = uuid.uuid1().hex product_name_with_file = "Has File" products = [ - Product( + CreateProductRequest( part_number=uuid.uuid1().hex, name=product_name_with_file, file_ids=[file_id], ), - Product(part_number=uuid.uuid1().hex, name="No File Link"), + CreateProductRequest(part_number=uuid.uuid1().hex, name="No File Link"), ] print(products) create_response: CreateProductsPartialSuccess = create_products(products) @@ -286,3 +352,36 @@ def test__query_products_linked_to_files_correct_products_returned( linked_products = get_products_linked_to_file(client, file_id) names = [product.name for product in linked_products] assert product_name_with_file in names + + def test__query_products_with_projection__returns_only_specified_fields( + self, client: ProductClient, create_products, unique_identifier + ): + part_number = unique_identifier + name = "Test Name" + family = "Example Family" + keywords = ["testing"] + properties = {"test_property": "yes"} + product = CreateProductRequest( + part_number=part_number, + name=name, + family=family, + keywords=keywords, + properties=properties, + ) + response: CreateProductsPartialSuccess = create_products([product]) + assert response is not None + + query_request = QueryProductsRequest( + filter=f'partNumber=="{part_number}"', + projection=[ProductProjection.FAMILY, ProductProjection.NAME], + ) + query_response: PagedProducts = client.query_products_paged(query_request) + queried_product = query_response.products[0] + + # Assert that the projected fields are returned as expected. + assert queried_product.family == family + assert queried_product.name == name + # Assert that non-projected fields are not returned. + assert queried_product.part_number is None + assert queried_product.keywords is None + assert queried_product.properties is None diff --git a/tests/integration/spec/test_spec.py b/tests/integration/spec/test_spec.py index 4d126c6d..49afc40f 100644 --- a/tests/integration/spec/test_spec.py +++ b/tests/integration/spec/test_spec.py @@ -11,13 +11,14 @@ CreatedSpecification, CreateSpecificationsPartialSuccess, CreateSpecificationsRequest, + CreateSpecificationsRequestObject, NumericConditionValue, QuerySpecificationsRequest, - Specification, - SpecificationDefinition, SpecificationLimit, + SpecificationProjection, SpecificationType, UpdateSpecificationsRequest, + UpdateSpecificationsRequestObject, ) @@ -59,7 +60,7 @@ def _create_specs( def create_specs_for_query(create_specs, product): """Fixture for creating a set of specs that can be used to test query operations.""" spec_requests = [ - SpecificationDefinition( + CreateSpecificationsRequestObject( product_id=product, spec_id=uuid.uuid1().hex, type=SpecificationType.PARAMETRIC, @@ -68,7 +69,7 @@ def create_specs_for_query(create_specs, product): limit=SpecificationLimit(min=1.2, max=1.5), unit="mV", ), - SpecificationDefinition( + CreateSpecificationsRequestObject( product_id=product, spec_id=uuid.uuid1().hex, type=SpecificationType.PARAMETRIC, @@ -95,7 +96,7 @@ def create_specs_for_query(create_specs, product): ), ], ), - SpecificationDefinition( + CreateSpecificationsRequestObject( product_id=product, spec_id=uuid.uuid1().hex, type=SpecificationType.FUNCTIONAL, @@ -118,7 +119,7 @@ def test__create_single_spec__one_created_with_right_field_values( ): specId = uuid.uuid1().hex productId = product - spec = SpecificationDefinition( + spec = CreateSpecificationsRequestObject( product_id=productId, spec_id=specId, type=SpecificationType.FUNCTIONAL, @@ -140,7 +141,7 @@ def test__create_multiple_specs__all_succeed( productId = product specs = [] for id in specIds: - spec = SpecificationDefinition( + spec = CreateSpecificationsRequestObject( product_id=productId, spec_id=id, type=SpecificationType.FUNCTIONAL, @@ -158,7 +159,7 @@ def test__create_duplicate_spec__errors( ): duplicate_id = uuid.uuid1().hex productId = product - spec = SpecificationDefinition( + spec = CreateSpecificationsRequestObject( product_id=productId, spec_id=duplicate_id, type=SpecificationType.FUNCTIONAL, @@ -179,7 +180,7 @@ def test__delete_existing_spec__succeeds(self, client: SpecClient, product): # Not using the fixture here so that we can inspect delete response. specId = uuid.uuid1().hex productId = product - spec = SpecificationDefinition( + spec = CreateSpecificationsRequestObject( product_id=productId, spec_id=specId, type=SpecificationType.FUNCTIONAL, @@ -201,7 +202,7 @@ def test__delete_non_existant_spec__delete_fails(self, client: SpecClient): def test__update_single_same_version__version_updates( self, client: SpecClient, create_specs, product ): - spec = SpecificationDefinition( + spec = CreateSpecificationsRequestObject( product_id=product, spec_id="spec1", type=SpecificationType.FUNCTIONAL, @@ -215,7 +216,7 @@ def test__update_single_same_version__version_updates( created_spec = response.created_specs[0] assert created_spec.version == 0 - update_spec = Specification( + update_spec = UpdateSpecificationsRequestObject( id=created_spec.id, product_id=created_spec.product_id, spec_id=created_spec.spec_id, @@ -276,3 +277,23 @@ def test__query_input_voltage__conditions_match( voltage_spec = response.specs[0] assert voltage_spec.conditions assert len(voltage_spec.conditions) == 2 + + def test__query_spec_projection_columns__columns_returned( + self, client: SpecClient, create_specs, create_specs_for_query, product + ): + request = QuerySpecificationsRequest( + product_ids=[product], + projection=[SpecificationProjection.SPEC_ID, SpecificationProjection.NAME], + ) + + response = client.query_specs(request) + specs = [vars(spec) for spec in response.specs or []] + spec_columns = { + key for spec in specs for key in spec.keys() if spec[key] is not None + } + + assert response.specs + assert len(response.specs) == 3 + assert len(spec_columns) == 2 + assert "spec_id" in spec_columns + assert "name" in spec_columns