stepxmlclient 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. stepxmlclient/__init__.py +22 -0
  2. stepxmlclient/client.py +373 -0
  3. stepxmlclient/creator.py +317 -0
  4. stepxmlclient/model/__init__.py +418 -0
  5. stepxmlclient/model/asset.py +250 -0
  6. stepxmlclient/model/attribute.py +288 -0
  7. stepxmlclient/model/business.py +233 -0
  8. stepxmlclient/model/classification.py +409 -0
  9. stepxmlclient/model/complex_elements.py +260 -0
  10. stepxmlclient/model/deletion.py +87 -0
  11. stepxmlclient/model/entity.py +368 -0
  12. stepxmlclient/model/metadata.py +199 -0
  13. stepxmlclient/model/organization.py +167 -0
  14. stepxmlclient/model/product.py +519 -0
  15. stepxmlclient/model/relations.py +105 -0
  16. stepxmlclient/model/replacement.py +417 -0
  17. stepxmlclient/parser.py +2805 -0
  18. stepxmlclient/rest_client/__init__.py +16 -0
  19. stepxmlclient/rest_client/base.py +388 -0
  20. stepxmlclient/rest_client/client.py +127 -0
  21. stepxmlclient/rest_client/exceptions.py +97 -0
  22. stepxmlclient/rest_client/models/__init__.py +175 -0
  23. stepxmlclient/rest_client/models/asset.py +253 -0
  24. stepxmlclient/rest_client/models/attribute.py +112 -0
  25. stepxmlclient/rest_client/models/background_process.py +23 -0
  26. stepxmlclient/rest_client/models/batch.py +41 -0
  27. stepxmlclient/rest_client/models/classification.py +57 -0
  28. stepxmlclient/rest_client/models/collection.py +26 -0
  29. stepxmlclient/rest_client/models/common.py +192 -0
  30. stepxmlclient/rest_client/models/context.py +16 -0
  31. stepxmlclient/rest_client/models/data_type_group.py +15 -0
  32. stepxmlclient/rest_client/models/entity.py +63 -0
  33. stepxmlclient/rest_client/models/event_processor.py +15 -0
  34. stepxmlclient/rest_client/models/import_export.py +43 -0
  35. stepxmlclient/rest_client/models/integration_endpoint.py +16 -0
  36. stepxmlclient/rest_client/models/product.py +200 -0
  37. stepxmlclient/rest_client/models/relations.py +280 -0
  38. stepxmlclient/rest_client/models/system_information.py +11 -0
  39. stepxmlclient/rest_client/models/user.py +28 -0
  40. stepxmlclient/rest_client/models/workflow.py +30 -0
  41. stepxmlclient/rest_client/operations/__init__.py +54 -0
  42. stepxmlclient/rest_client/operations/assets.py +652 -0
  43. stepxmlclient/rest_client/operations/attributes.py +252 -0
  44. stepxmlclient/rest_client/operations/background_processes.py +84 -0
  45. stepxmlclient/rest_client/operations/batch.py +28 -0
  46. stepxmlclient/rest_client/operations/classifications.py +248 -0
  47. stepxmlclient/rest_client/operations/collection_groups.py +46 -0
  48. stepxmlclient/rest_client/operations/collections.py +67 -0
  49. stepxmlclient/rest_client/operations/contexts.py +88 -0
  50. stepxmlclient/rest_client/operations/data_type_groups.py +68 -0
  51. stepxmlclient/rest_client/operations/entities.py +295 -0
  52. stepxmlclient/rest_client/operations/event_processors.py +109 -0
  53. stepxmlclient/rest_client/operations/exports.py +153 -0
  54. stepxmlclient/rest_client/operations/gateway.py +53 -0
  55. stepxmlclient/rest_client/operations/inbound.py +472 -0
  56. stepxmlclient/rest_client/operations/outbound.py +64 -0
  57. stepxmlclient/rest_client/operations/products.py +489 -0
  58. stepxmlclient/rest_client/operations/relations.py +349 -0
  59. stepxmlclient/rest_client/operations/reports.py +27 -0
  60. stepxmlclient/rest_client/operations/system_information.py +28 -0
  61. stepxmlclient/rest_client/operations/user_groups.py +79 -0
  62. stepxmlclient/rest_client/operations/users.py +119 -0
  63. stepxmlclient/rest_client/operations/workflow_tasks.py +61 -0
  64. stepxmlclient/rest_client/operations/workflows.py +50 -0
  65. stepxmlclient/upload.py +293 -0
  66. stepxmlclient-1.0.0.dist-info/METADATA +666 -0
  67. stepxmlclient-1.0.0.dist-info/RECORD +70 -0
  68. stepxmlclient-1.0.0.dist-info/WHEEL +5 -0
  69. stepxmlclient-1.0.0.dist-info/licenses/LICENSE +21 -0
  70. stepxmlclient-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,64 @@
1
+ """Outbound integration endpoint operations for STEP REST API v2."""
2
+ from typing import Any
3
+
4
+ from ..base import BaseOperation, handle_rest_errors
5
+ from ..exceptions import NotFoundError
6
+ from ..models.integration_endpoint import IntegrationEndpointResponse
7
+
8
+
9
+ class OutboundOperations(BaseOperation):
10
+ def __init__(self, client, logger=None, context=None, workspace=None):
11
+ super().__init__(client, operation_name="OutboundOperations", logger=logger, context=context, workspace=workspace)
12
+
13
+ @handle_rest_errors
14
+ def get_outbound_endpoints(self) -> list[dict[str, Any]]:
15
+ self._log_operation("getOutboundIntegrationEndpoints", {})
16
+ r = self.client.getOutboundIntegrationEndpoints(**self._build_context_params())
17
+ return r if isinstance(r, list) else []
18
+
19
+ @handle_rest_errors
20
+ def get_outbound_endpoint(self, ep_id: str) -> IntegrationEndpointResponse:
21
+ self._validate_required_params({"ep_id": ep_id}, ["ep_id"])
22
+ self._log_operation("getOutboundIntegrationEndpoint", {"id": ep_id})
23
+ try:
24
+ r = self.client.getOutboundIntegrationEndpoint(id=ep_id, **self._build_context_params())
25
+ except Exception as e:
26
+ if "404" in str(e): raise NotFoundError(ep_id, "OutboundIntegrationEndpoint")
27
+ raise
28
+ return IntegrationEndpointResponse(**r) if isinstance(r, dict) else r
29
+
30
+ @handle_rest_errors
31
+ def get_outbound_endpoint_status(self, ep_id: str) -> IntegrationEndpointResponse:
32
+ self._validate_required_params({"ep_id": ep_id}, ["ep_id"])
33
+ self._log_operation("getOutboundIntegrationEndpointStatus", {"id": ep_id})
34
+ try:
35
+ r = self.client.getOutboundIntegrationEndpointStatus(id=ep_id, **self._build_context_params())
36
+ except Exception as e:
37
+ if "404" in str(e): raise NotFoundError(ep_id, "OutboundIntegrationEndpoint")
38
+ raise
39
+ return IntegrationEndpointResponse(**r) if isinstance(r, dict) else r
40
+
41
+ @handle_rest_errors
42
+ def enable_outbound_endpoint(self, ep_id: str) -> IntegrationEndpointResponse:
43
+ self._validate_required_params({"ep_id": ep_id}, ["ep_id"])
44
+ self._log_operation("enableOutboundIntegrationEndpoint", {"id": ep_id})
45
+ r = self.client.enableOutboundIntegrationEndpoint(id=ep_id, **self._build_context_params())
46
+ return IntegrationEndpointResponse(**r) if isinstance(r, dict) else r
47
+
48
+ @handle_rest_errors
49
+ def disable_outbound_endpoint(self, ep_id: str) -> IntegrationEndpointResponse:
50
+ self._validate_required_params({"ep_id": ep_id}, ["ep_id"])
51
+ self._log_operation("disableOutboundIntegrationEndpoint", {"id": ep_id})
52
+ r = self.client.disableOutboundIntegrationEndpoint(id=ep_id, **self._build_context_params())
53
+ return IntegrationEndpointResponse(**r) if isinstance(r, dict) else r
54
+
55
+ @handle_rest_errors
56
+ def get_outbound_endpoint_execution_report(self, ep_id: str) -> list[dict[str, Any]]:
57
+ self._validate_required_params({"ep_id": ep_id}, ["ep_id"])
58
+ self._log_operation("getOutboundIntegrationEndpointExecutionReport", {"id": ep_id})
59
+ try:
60
+ r = self.client.getOutboundIntegrationEndpointExecutionReport(id=ep_id, **self._build_context_params())
61
+ except Exception as e:
62
+ if "404" in str(e): raise NotFoundError(ep_id, "OutboundIntegrationEndpoint")
63
+ raise
64
+ return r if isinstance(r, list) else []
@@ -0,0 +1,489 @@
1
+ """Product operations for STEP REST API.
2
+
3
+ Provides CRUD and search operations for products.
4
+ """
5
+
6
+ import logging
7
+ from typing import Any, Optional
8
+
9
+ import requests_openapi as roa
10
+
11
+ from .. import exceptions as exc
12
+ from ..base import BaseOperation, handle_rest_errors
13
+ from ..models.product import (
14
+ ProductCreateRequest,
15
+ ProductListResponse,
16
+ ProductResponse,
17
+ ProductUpdateRequest,
18
+ )
19
+
20
+
21
+ class ProductOperations(BaseOperation):
22
+ """Product CRUD and search operations.
23
+
24
+ Provides methods for:
25
+ - Retrieving single products
26
+ - Searching and listing products
27
+ - Creating new products
28
+ - Updating existing products
29
+ - Deleting products
30
+ - Accessing product attributes and values
31
+ """
32
+
33
+ def __init__(self, client: roa.Client, logger: Optional[logging.Logger] = None,
34
+ context: Optional[str] = None, workspace: Optional[str] = None):
35
+ """Initialize ProductOperations.
36
+
37
+ Args:
38
+ client: The requests-openapi Client instance
39
+ logger: Logger instance (optional)
40
+ context: STEP context ID for all API calls
41
+ workspace: STEP workspace ID (defaults to "Main" server-side)
42
+ """
43
+ super().__init__(client, operation_name="ProductOperations", logger=logger,
44
+ context=context, workspace=workspace)
45
+
46
+ @handle_rest_errors
47
+ def get_product(self, product_id: str,
48
+ key_id: Optional[str] = None,
49
+ include_inherited_data: bool = False) -> ProductResponse:
50
+ """Retrieve a single product by ID.
51
+
52
+ Args:
53
+ product_id: The product ID (or alternate key value if key_id is set)
54
+ key_id: Alternate key ID to use for lookup (optional)
55
+ include_inherited_data: Whether to include inherited attribute values (default: False)
56
+
57
+ Returns:
58
+ ProductResponse with product data
59
+
60
+ Raises:
61
+ NotFoundError: If product not found
62
+ RestApiError: For other API errors
63
+ """
64
+ self._validate_required_params(
65
+ {"product_id": product_id},
66
+ ["product_id"]
67
+ )
68
+
69
+ query_params: dict[str, Any] = self._build_context_params()
70
+ if key_id:
71
+ query_params["keyId"] = key_id
72
+ if include_inherited_data:
73
+ query_params["includeInheritedData"] = include_inherited_data
74
+ self._log_operation("getProduct", {"product_id": product_id})
75
+
76
+ try:
77
+ response = self.client.getProduct(id=product_id, **query_params)
78
+ except Exception as e:
79
+ if "404" in str(e) or "not found" in str(e).lower():
80
+ raise exc.NotFoundError(product_id, "Product")
81
+ raise
82
+
83
+ # Convert response to ProductResponse if needed
84
+ if isinstance(response, dict):
85
+ return ProductResponse(**response)
86
+ return response
87
+
88
+ @handle_rest_errors
89
+ def search_products(self, condition: dict) -> ProductListResponse:
90
+ """Search for products using a STEP condition query.
91
+
92
+ Args:
93
+ condition: A STEP condition object defining the search criteria.
94
+ Supported conditionTypes: 'and', 'or', 'id', 'name',
95
+ 'textvalue', 'numericvalue', 'objecttype', 'simplebelow',
96
+ 'lovvalue', 'hasreferenceto', 'hasdatacontainerobject'.
97
+ Example: {"conditionType": "objecttype", "objectType": "MyType"}
98
+
99
+ Returns:
100
+ ProductListResponse with matching products
101
+
102
+ Raises:
103
+ ValidationError: If condition is invalid
104
+ RestApiError: For API errors
105
+ """
106
+ self._validate_required_params({"condition": condition}, ["condition"])
107
+ body = {"condition": condition}
108
+ self._log_operation("createProductSearch", {"condition": condition})
109
+
110
+ response = self.client.createProductSearch(json=body, **self._build_context_params())
111
+
112
+ if isinstance(response, dict):
113
+ items = response.get("items", [])
114
+ total = response.get("total")
115
+ else:
116
+ items = getattr(response, "items", [])
117
+ total = getattr(response, "total", None)
118
+
119
+ product_items = [
120
+ ProductResponse(**item) if isinstance(item, dict) else item
121
+ for item in items
122
+ ]
123
+
124
+ return ProductListResponse(
125
+ items=product_items,
126
+ total=total,
127
+ limit=max(len(product_items), 1),
128
+ offset=0,
129
+ has_more=False
130
+ )
131
+
132
+ @handle_rest_errors
133
+ def create_product(self, product_data: ProductCreateRequest) -> ProductResponse:
134
+ """Create a new product.
135
+
136
+ Args:
137
+ product_data: ProductCreateRequest with product data
138
+
139
+ Returns:
140
+ ProductResponse with created product data
141
+
142
+ Raises:
143
+ ValidationError: If product data is invalid
144
+ ConflictError: If product ID already exists
145
+ RestApiError: For other API errors
146
+ """
147
+ # POST /products requires objectType and parent. ID is auto-generated server-side.
148
+ self._validate_required_params(
149
+ {
150
+ "objectType": product_data.objectType,
151
+ "parent": product_data.parent,
152
+ },
153
+ ["objectType", "parent"]
154
+ )
155
+
156
+ # Convert request to dict for API call
157
+ product_dict = product_data.model_dump(exclude_none=True)
158
+ self._log_operation("createProduct", {"objectType": product_data.objectType, "parent": product_data.parent})
159
+
160
+ try:
161
+ response = self.client.createProduct(json=product_dict, **self._build_context_params())
162
+ except Exception as e:
163
+ if "409" in str(e) or "conflict" in str(e).lower():
164
+ raise exc.ConflictError(409, "Product conflict while creating product")
165
+ if "400" in str(e):
166
+ raise exc.ValidationError(str(e))
167
+ raise
168
+
169
+ # Convert response to ProductResponse
170
+ if isinstance(response, dict):
171
+ return ProductResponse(**response)
172
+ return response
173
+
174
+ @handle_rest_errors
175
+ def update_product(self, product_id: str,
176
+ updates: ProductUpdateRequest,
177
+ key_id: Optional[str] = None) -> ProductResponse:
178
+ """Partially update an existing product (PATCH).
179
+
180
+ Args:
181
+ product_id: The product ID (or alternate key value if key_id is set)
182
+ updates: ProductUpdateRequest with fields to update
183
+ key_id: Alternate key ID to use for lookup (optional)
184
+
185
+ Returns:
186
+ ProductResponse with updated product data
187
+
188
+ Raises:
189
+ NotFoundError: If product not found
190
+ ValidationError: If updates are invalid
191
+ RestApiError: For other API errors
192
+ """
193
+ self._validate_required_params(
194
+ {"product_id": product_id},
195
+ ["product_id"]
196
+ )
197
+
198
+ updates_dict = updates.model_dump(exclude_none=True)
199
+ query_params = self._build_context_params()
200
+ if key_id:
201
+ query_params["keyId"] = key_id
202
+ self._log_operation("updateProduct", {"product_id": product_id})
203
+
204
+ try:
205
+ response = self.client.updateProduct(
206
+ id=product_id,
207
+ json=updates_dict,
208
+ **query_params
209
+ )
210
+ except Exception as e:
211
+ if "404" in str(e):
212
+ raise exc.NotFoundError(product_id, "Product")
213
+ if "400" in str(e):
214
+ raise exc.ValidationError(str(e))
215
+ raise
216
+
217
+ if isinstance(response, dict):
218
+ return ProductResponse(**response)
219
+ return response
220
+
221
+ @handle_rest_errors
222
+ def replace_product(self, product_id: str,
223
+ product_data: ProductCreateRequest,
224
+ key_id: Optional[str] = None,
225
+ allow_overwrite: bool = False) -> ProductResponse:
226
+ """Fully replace a product (PUT). Creates it if it does not exist.
227
+
228
+ Args:
229
+ product_id: The product ID (or alternate key value if key_id is set)
230
+ product_data: Complete product data to replace with
231
+ key_id: Alternate key ID to use for lookup (optional)
232
+ allow_overwrite: Whether to allow overwriting an existing product
233
+
234
+ Returns:
235
+ ProductResponse with replaced product data
236
+
237
+ Raises:
238
+ NotFoundError: If product not found and allow_overwrite is False
239
+ ValidationError: If data is invalid
240
+ RestApiError: For other API errors
241
+ """
242
+ self._validate_required_params(
243
+ {"product_id": product_id},
244
+ ["product_id"]
245
+ )
246
+
247
+ product_dict = product_data.model_dump(exclude_none=True)
248
+ query_params: dict[str, Any] = self._build_context_params()
249
+ if key_id:
250
+ query_params["keyId"] = key_id
251
+ if allow_overwrite:
252
+ query_params["allow-overwrite"] = allow_overwrite
253
+ self._log_operation("createOrReplaceProduct", {"product_id": product_id})
254
+
255
+ try:
256
+ response = self.client.createOrReplaceProduct(
257
+ id=product_id,
258
+ json=product_dict,
259
+ **query_params
260
+ )
261
+ except Exception as e:
262
+ if "404" in str(e):
263
+ raise exc.NotFoundError(product_id, "Product")
264
+ if "400" in str(e) or "409" in str(e):
265
+ raise exc.ValidationError(str(e))
266
+ raise
267
+
268
+ if isinstance(response, dict):
269
+ return ProductResponse(**response)
270
+ return response
271
+
272
+ @handle_rest_errors
273
+ def delete_product(self, product_id: str,
274
+ key_id: Optional[str] = None) -> bool:
275
+ """Delete a product.
276
+
277
+ Args:
278
+ product_id: The product ID (or alternate key value if key_id is set)
279
+ key_id: Alternate key ID to use for lookup (optional)
280
+
281
+ Returns:
282
+ True if deletion was successful
283
+
284
+ Raises:
285
+ NotFoundError: If product not found
286
+ RestApiError: For other API errors
287
+ """
288
+ self._validate_required_params(
289
+ {"product_id": product_id},
290
+ ["product_id"]
291
+ )
292
+
293
+ query_params = self._build_context_params()
294
+ if key_id:
295
+ query_params["keyId"] = key_id
296
+ self._log_operation("deleteProduct", {"product_id": product_id})
297
+
298
+ try:
299
+ self.client.deleteProduct(id=product_id, **query_params)
300
+ return True
301
+ except Exception as e:
302
+ if "404" in str(e):
303
+ raise exc.NotFoundError(product_id, "Product")
304
+ raise
305
+
306
+ def search_by_type(self, user_type_id: str) -> ProductListResponse:
307
+ """Search products by object type (convenience method).
308
+
309
+ Args:
310
+ user_type_id: Object type ID to filter by
311
+
312
+ Returns:
313
+ ProductListResponse with matching products
314
+ """
315
+ return self.search_products(
316
+ condition={"conditionType": "objecttype", "objectType": user_type_id}
317
+ )
318
+
319
+ def search_by_id(self, product_id: str) -> ProductListResponse:
320
+ """Search products by ID condition (convenience method).
321
+
322
+ Args:
323
+ product_id: Product ID or pattern to search for
324
+
325
+ Returns:
326
+ ProductListResponse with matching products
327
+ """
328
+ return self.search_products(
329
+ condition={"conditionType": "id", "id": product_id}
330
+ )
331
+
332
+ # ------------------------------------------------------------------
333
+ # Approval & lifecycle (v1.4.0)
334
+ # ------------------------------------------------------------------
335
+
336
+ @handle_rest_errors
337
+ def get_product_approval_status(self, product_id: str,
338
+ key_id: Optional[str] = None) -> dict[str, Any]:
339
+ self._validate_required_params({"product_id": product_id}, ["product_id"])
340
+ qp = self._build_context_params()
341
+ if key_id: qp["keyId"] = key_id
342
+ self._log_operation("getProductApprovalStatus", {"id": product_id})
343
+ try:
344
+ r = self.client.getProductApprovalStatus(id=product_id, **qp)
345
+ except Exception as e:
346
+ if "404" in str(e): raise exc.NotFoundError(product_id, "Product")
347
+ raise
348
+ return r if isinstance(r, dict) else r
349
+
350
+ @handle_rest_errors
351
+ def approve_product(self, product_id: str,
352
+ key_id: Optional[str] = None) -> dict[str, Any]:
353
+ self._validate_required_params({"product_id": product_id}, ["product_id"])
354
+ qp = self._build_context_params()
355
+ if key_id: qp["keyId"] = key_id
356
+ self._log_operation("approveProduct", {"id": product_id})
357
+ try:
358
+ r = self.client.approveProduct(id=product_id, **qp)
359
+ except Exception as e:
360
+ if "404" in str(e): raise exc.NotFoundError(product_id, "Product")
361
+ raise
362
+ return r if isinstance(r, dict) else r
363
+
364
+ @handle_rest_errors
365
+ def approve_product_deletion(self, product_id: str,
366
+ key_id: Optional[str] = None) -> dict[str, Any]:
367
+ self._validate_required_params({"product_id": product_id}, ["product_id"])
368
+ qp = self._build_context_params()
369
+ if key_id: qp["keyId"] = key_id
370
+ self._log_operation("approveProductDeletion", {"id": product_id})
371
+ try:
372
+ r = self.client.approveProductDeletion(id=product_id, **qp)
373
+ except Exception as e:
374
+ if "404" in str(e): raise exc.NotFoundError(product_id, "Product")
375
+ raise
376
+ return r if isinstance(r, dict) else r
377
+
378
+ @handle_rest_errors
379
+ def purge_product(self, product_id: str,
380
+ key_id: Optional[str] = None) -> dict[str, Any]:
381
+ self._validate_required_params({"product_id": product_id}, ["product_id"])
382
+ qp = self._build_context_params()
383
+ if key_id: qp["keyId"] = key_id
384
+ self._log_operation("purgeProduct", {"id": product_id})
385
+ try:
386
+ r = self.client.purgeProduct(id=product_id, **qp)
387
+ except Exception as e:
388
+ if "404" in str(e): raise exc.NotFoundError(product_id, "Product")
389
+ raise
390
+ return r if isinstance(r, dict) else r
391
+
392
+ @handle_rest_errors
393
+ def get_product_children(self, product_id: str,
394
+ key_id: Optional[str] = None) -> list[str]:
395
+ self._validate_required_params({"product_id": product_id}, ["product_id"])
396
+ qp = self._build_context_params()
397
+ if key_id: qp["keyId"] = key_id
398
+ self._log_operation("getProductChildren", {"id": product_id})
399
+ try:
400
+ r = self.client.getProductChildren(id=product_id, **qp)
401
+ except Exception as e:
402
+ if "404" in str(e): raise exc.NotFoundError(product_id, "Product")
403
+ raise
404
+ return r if isinstance(r, list) else []
405
+
406
+ @handle_rest_errors
407
+ def get_product_data_containers(self, product_id: str,
408
+ key_id: Optional[str] = None) -> dict[str, Any]:
409
+ self._validate_required_params({"product_id": product_id}, ["product_id"])
410
+ qp = self._build_context_params()
411
+ if key_id: qp["keyId"] = key_id
412
+ self._log_operation("getProductDataContainers", {"id": product_id})
413
+ try:
414
+ r = self.client.getProductDataContainers(id=product_id, **qp)
415
+ except Exception as e:
416
+ if "404" in str(e): raise exc.NotFoundError(product_id, "Product")
417
+ raise
418
+ return r if isinstance(r, dict) else r
419
+
420
+ @handle_rest_errors
421
+ def get_product_incoming_references(self, product_id: str,
422
+ key_id: Optional[str] = None) -> dict[str, Any]:
423
+ self._validate_required_params({"product_id": product_id}, ["product_id"])
424
+ qp = self._build_context_params()
425
+ if key_id: qp["keyId"] = key_id
426
+ self._log_operation("getProductIncomingReferences", {"id": product_id})
427
+ try:
428
+ r = self.client.getProductIncomingReferences(id=product_id, **qp)
429
+ except Exception as e:
430
+ if "404" in str(e): raise exc.NotFoundError(product_id, "Product")
431
+ raise
432
+ return r if isinstance(r, dict) else r
433
+
434
+ @handle_rest_errors
435
+ def get_product_value(self, product_id: str, attribute_id: str,
436
+ key_id: Optional[str] = None) -> dict[str, Any]:
437
+ self._validate_required_params(
438
+ {"product_id": product_id, "attribute_id": attribute_id},
439
+ ["product_id", "attribute_id"])
440
+ qp = self._build_context_params()
441
+ if key_id: qp["keyId"] = key_id
442
+ self._log_operation("getProductValueByAttributeId",
443
+ {"id": product_id, "attributeId": attribute_id})
444
+ try:
445
+ r = self.client.getProductValueByAttributeId(
446
+ id=product_id, attributeId=attribute_id, **qp)
447
+ except Exception as e:
448
+ if "404" in str(e): raise exc.NotFoundError(product_id, "Product")
449
+ raise
450
+ return r if isinstance(r, dict) else r
451
+
452
+ @handle_rest_errors
453
+ def replace_product_value(self, product_id: str, attribute_id: str,
454
+ value: dict[str, Any],
455
+ key_id: Optional[str] = None) -> dict[str, Any]:
456
+ self._validate_required_params(
457
+ {"product_id": product_id, "attribute_id": attribute_id},
458
+ ["product_id", "attribute_id"])
459
+ qp = self._build_context_params()
460
+ if key_id: qp["keyId"] = key_id
461
+ self._log_operation("replaceProductValueByAttributeId",
462
+ {"id": product_id, "attributeId": attribute_id})
463
+ try:
464
+ r = self.client.replaceProductValueByAttributeId(
465
+ id=product_id, attributeId=attribute_id, json=value, **qp)
466
+ except Exception as e:
467
+ if "404" in str(e): raise exc.NotFoundError(product_id, "Product")
468
+ if "400" in str(e): raise exc.ValidationError(str(e))
469
+ raise
470
+ return r if isinstance(r, dict) else r
471
+
472
+ @handle_rest_errors
473
+ def delete_product_value(self, product_id: str, attribute_id: str,
474
+ key_id: Optional[str] = None) -> bool:
475
+ self._validate_required_params(
476
+ {"product_id": product_id, "attribute_id": attribute_id},
477
+ ["product_id", "attribute_id"])
478
+ qp = self._build_context_params()
479
+ if key_id: qp["keyId"] = key_id
480
+ self._log_operation("deleteProductValueByAttributeId",
481
+ {"id": product_id, "attributeId": attribute_id})
482
+ try:
483
+ self.client.deleteProductValueByAttributeId(
484
+ id=product_id, attributeId=attribute_id, **qp)
485
+ return True
486
+ except Exception as e:
487
+ if "404" in str(e): raise exc.NotFoundError(product_id, "Product")
488
+ raise
489
+