Skip to content

Commit 2684b93

Browse files
authored
Add single-item convenience wrappers for bulk partial-success APIs (#211)
1 parent 01403b7 commit 2684b93

11 files changed

Lines changed: 1267 additions & 50 deletions

File tree

examples/assetmanagement/assets.py

Lines changed: 45 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -31,61 +31,56 @@
3131

3232
client = AssetManagementClient(configuration=server_configuration)
3333

34-
create_assets_request = [
35-
CreateAssetRequest(
36-
model_number=4000,
37-
model_name="NI PXIe-6368",
38-
serial_number="01BB877A",
39-
vendor_name="NI",
40-
vendor_number=4244,
41-
bus_type=AssetBusType.ACCESSORY,
42-
name="PCISlot2",
43-
asset_type=AssetType.DEVICE_UNDER_TEST,
44-
firmware_version="A1",
45-
hardware_version="12A",
46-
visa_resource_name="vs-3144",
34+
create_asset_request = CreateAssetRequest(
35+
model_number=4000,
36+
model_name="NI PXIe-6368",
37+
serial_number="01BB877A",
38+
vendor_name="NI",
39+
vendor_number=4244,
40+
bus_type=AssetBusType.ACCESSORY,
41+
name="PCISlot2",
42+
asset_type=AssetType.DEVICE_UNDER_TEST,
43+
firmware_version="A1",
44+
hardware_version="12A",
45+
visa_resource_name="vs-3144",
46+
temperature_sensors=[TemperatureSensor(name="Sensor0", reading=25.8)],
47+
supports_self_calibration=True,
48+
supports_external_calibration=True,
49+
custom_calibration_interval=24,
50+
self_calibration=SelfCalibration(
4751
temperature_sensors=[TemperatureSensor(name="Sensor0", reading=25.8)],
48-
supports_self_calibration=True,
49-
supports_external_calibration=True,
50-
custom_calibration_interval=24,
51-
self_calibration=SelfCalibration(
52-
temperature_sensors=[TemperatureSensor(name="Sensor0", reading=25.8)],
53-
is_limited=False,
54-
date=datetime(2022, 6, 7, 18, 58, 5, tzinfo=timezone.utc),
55-
),
56-
is_NI_asset=True,
57-
workspace=workspace_id,
58-
location=AssetLocationForCreate(
59-
state=AssetPresence(asset_presence=AssetPresenceStatus.PRESENT)
52+
is_limited=False,
53+
date=datetime(2022, 6, 7, 18, 58, 5, tzinfo=timezone.utc),
54+
),
55+
is_NI_asset=True,
56+
workspace=workspace_id,
57+
location=AssetLocationForCreate(
58+
state=AssetPresence(asset_presence=AssetPresenceStatus.PRESENT)
59+
),
60+
external_calibration=ExternalCalibration(
61+
temperature_sensors=[TemperatureSensor(name="Sensor0", reading=25.8)],
62+
date=datetime(2022, 6, 7, 18, 58, 5, tzinfo=timezone.utc),
63+
recommended_interval=10,
64+
next_recommended_date=datetime(
65+
2023, 11, 14, 20, 42, 11, 583000, tzinfo=timezone.utc
6066
),
61-
external_calibration=ExternalCalibration(
62-
temperature_sensors=[TemperatureSensor(name="Sensor0", reading=25.8)],
63-
date=datetime(2022, 6, 7, 18, 58, 5, tzinfo=timezone.utc),
64-
recommended_interval=10,
65-
next_recommended_date=datetime(
66-
2023, 11, 14, 20, 42, 11, 583000, tzinfo=timezone.utc
67-
),
68-
next_custom_due_date=datetime(
69-
2024, 11, 14, 20, 42, 11, 583000, tzinfo=timezone.utc
70-
),
71-
resolved_due_date=datetime(2022, 6, 7, 18, 58, 5, tzinfo=timezone.utc),
67+
next_custom_due_date=datetime(
68+
2024, 11, 14, 20, 42, 11, 583000, tzinfo=timezone.utc
7269
),
73-
properties={"Key1": "Value1"},
74-
keywords=["Keyword1"],
75-
discovery_type=AssetDiscoveryType.MANUAL,
76-
file_ids=["608a5684800e325b48837c2a"],
77-
supports_self_test=True,
78-
supports_reset=True,
79-
part_number="A1234 B5",
80-
)
81-
]
70+
resolved_due_date=datetime(2022, 6, 7, 18, 58, 5, tzinfo=timezone.utc),
71+
),
72+
properties={"Key1": "Value1"},
73+
keywords=["Keyword1"],
74+
discovery_type=AssetDiscoveryType.MANUAL,
75+
file_ids=["608a5684800e325b48837c2a"],
76+
supports_self_test=True,
77+
supports_reset=True,
78+
part_number="A1234 B5",
79+
)
8280

8381
# Create an asset.
84-
create_assets_response = client.create_assets(assets=create_assets_request)
85-
86-
created_asset_id = None
87-
if create_assets_response.assets and len(create_assets_response.assets) > 0:
88-
created_asset_id = str(create_assets_response.assets[0].id)
82+
created_asset = client.create_asset(asset=create_asset_request)
83+
created_asset_id = str(created_asset.id)
8984

9085
# Query assets using id.
9186
query_asset_request = QueryAssetsRequest(

nisystemlink/clients/assetmanagement/_asset_management_client.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from uplink import Field, Path, retry
1414

1515
from . import models
16+
from ..core.helpers._partial_success import unwrap_single_item_partial_success
1617

1718

1819
@retry(
@@ -56,6 +57,30 @@ def create_assets(
5657
"""
5758
...
5859

60+
def create_asset(self, asset: models.CreateAssetRequest) -> models.Asset:
61+
"""Create a single asset.
62+
63+
Args:
64+
asset: The asset to create.
65+
66+
Returns:
67+
The created asset.
68+
69+
Raises:
70+
ApiException: if the asset could not be created or the service returns an
71+
unexpected partial-success payload.
72+
"""
73+
response = self.create_assets([asset])
74+
75+
return unwrap_single_item_partial_success(
76+
response=response,
77+
items=response.assets,
78+
failed=response.failed,
79+
error=response.error,
80+
failure_message="Failed to create asset.",
81+
empty_message="Server returned no created assets.",
82+
)
83+
5984
@post("query-assets")
6085
def __query_assets(
6186
self, query: models._QueryAssetsRequest
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
from typing import Any, Sequence, TypeVar
2+
3+
from nisystemlink.clients import core
4+
5+
_ItemT = TypeVar("_ItemT")
6+
_ONE_OR_MORE_ERRORS_OCCURRED_NAME = "Skyline.OneOrMoreErrorsOccurred"
7+
_ONE_OR_MORE_ERRORS_OCCURRED_CODE = -251041
8+
9+
10+
def _unwrap_single_inner_error(error: core.ApiError | None) -> core.ApiError | None:
11+
if error is None:
12+
return None
13+
14+
if len(error.inner_errors) != 1:
15+
return error
16+
17+
if (
18+
error.name == _ONE_OR_MORE_ERRORS_OCCURRED_NAME
19+
or error.code == _ONE_OR_MORE_ERRORS_OCCURRED_CODE
20+
):
21+
return error.inner_errors[0]
22+
23+
return error
24+
25+
26+
def unwrap_single_item_partial_success(
27+
*,
28+
response: Any | None,
29+
items: Sequence[_ItemT] | None,
30+
failed: Sequence[Any] | None,
31+
error: core.ApiError | None,
32+
failure_message: str,
33+
empty_message: str,
34+
) -> _ItemT:
35+
"""Return the first successful item from a partial-success response.
36+
37+
Raises:
38+
ApiException: if the response reports a failure or contains no successful item.
39+
"""
40+
response_data = (
41+
response.model_dump(mode="json", by_alias=True)
42+
if response is not None
43+
else None
44+
)
45+
46+
if failed or error:
47+
raise core.ApiException(
48+
failure_message,
49+
error=_unwrap_single_inner_error(error),
50+
response_data=response_data,
51+
)
52+
53+
if not items:
54+
raise core.ApiException(
55+
empty_message,
56+
response_data=response_data,
57+
)
58+
59+
if len(items) != 1:
60+
raise core.ApiException(
61+
f"Expected exactly one successful item but received {len(items)}.",
62+
response_data=response_data,
63+
)
64+
65+
return items[0]

nisystemlink/clients/product/_product_client.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from uplink import Field, Query, retry, returns
99

1010
from . import models
11+
from ..core.helpers._partial_success import unwrap_single_item_partial_success
1112

1213

1314
@retry(
@@ -50,6 +51,30 @@ def create_products(
5051
"""
5152
...
5253

54+
def create_product(self, product: models.CreateProductRequest) -> models.Product:
55+
"""Creates a single product.
56+
57+
Args:
58+
product: The product to create.
59+
60+
Returns:
61+
The created product.
62+
63+
Raises:
64+
ApiException: if the product could not be created or the service returns an
65+
unexpected partial-success payload.
66+
"""
67+
response = self.create_products([product])
68+
69+
return unwrap_single_item_partial_success(
70+
response=response,
71+
items=response.products,
72+
failed=response.failed,
73+
error=response.error,
74+
failure_message="Failed to create product.",
75+
empty_message="Server returned no created products.",
76+
)
77+
5378
@get(
5479
"products",
5580
args=[Query("continuationToken"), Query("take"), Query("returnCount")],
@@ -153,6 +178,33 @@ def update_products(
153178
"""
154179
...
155180

181+
def update_product(
182+
self, product: models.UpdateProductRequest, replace: bool = False
183+
) -> models.Product:
184+
"""Updates a single product.
185+
186+
Args:
187+
product: The product to update.
188+
replace: Replace the existing fields instead of merging them.
189+
190+
Returns:
191+
The updated product.
192+
193+
Raises:
194+
ApiException: if the product could not be updated or the service returns an
195+
unexpected partial-success payload.
196+
"""
197+
response = self.update_products([product], replace=replace)
198+
199+
return unwrap_single_item_partial_success(
200+
response=response,
201+
items=response.products,
202+
failed=response.failed,
203+
error=response.error,
204+
failure_message="Failed to update product.",
205+
empty_message="Server returned no updated products.",
206+
)
207+
156208
@delete("products/{id}")
157209
def delete_product(self, id: str) -> None:
158210
"""Deletes a single product by id.

nisystemlink/clients/spec/_spec_client.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from uplink import Field, retry
99

1010
from . import models
11+
from ..core.helpers._partial_success import unwrap_single_item_partial_success
1112

1213

1314
@retry(
@@ -63,6 +64,32 @@ def create_specs(
6364
"""
6465
...
6566

67+
def create_spec(
68+
self, spec: models.CreateSpecificationsRequestObject
69+
) -> models.CreatedSpecification:
70+
"""Creates a single specification.
71+
72+
Args:
73+
spec: The specification to create.
74+
75+
Returns:
76+
The created specification.
77+
78+
Raises:
79+
ApiException: if the specification could not be created or the service returns an
80+
unexpected partial-success payload.
81+
"""
82+
response = self.create_specs(models.CreateSpecificationsRequest(specs=[spec]))
83+
84+
return unwrap_single_item_partial_success(
85+
response=response,
86+
items=response.created_specs,
87+
failed=response.failed_specs,
88+
error=response.error,
89+
failure_message="Failed to create spec.",
90+
empty_message="Server returned no created specs.",
91+
)
92+
6693
@post("delete-specs", args=[Field("ids")])
6794
def delete_specs(
6895
self, ids: List[str]
@@ -129,3 +156,29 @@ def update_specs(
129156
with error messages for updates that failed.
130157
"""
131158
...
159+
160+
def update_spec(
161+
self, spec: models.UpdateSpecificationsRequestObject
162+
) -> models.UpdatedSpecification:
163+
"""Updates a single specification.
164+
165+
Args:
166+
spec: The specification to update.
167+
168+
Returns:
169+
The updated specification.
170+
171+
Raises:
172+
ApiException: if the specification could not be updated or the service returns an
173+
unexpected partial-success payload.
174+
"""
175+
response = self.update_specs(models.UpdateSpecificationsRequest(specs=[spec]))
176+
177+
return unwrap_single_item_partial_success(
178+
response=response,
179+
items=response.updated_specs if response is not None else None,
180+
failed=response.failed_specs if response is not None else None,
181+
error=response.error if response is not None else None,
182+
failure_message="Failed to update spec.",
183+
empty_message="Server returned no updated specs.",
184+
)

0 commit comments

Comments
 (0)