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,652 @@
1
+ """Asset operations for STEP REST API.
2
+
3
+ Provides CRUD, search, and binary content operations for assets.
4
+ """
5
+
6
+ import logging
7
+ import os
8
+ from typing import Any, Optional
9
+
10
+ import requests_openapi as roa
11
+
12
+ from .. import exceptions as exc
13
+ from ..base import BaseOperation, handle_rest_errors
14
+ from ..models.asset import (
15
+ AssetCreateRequest,
16
+ AssetDownloadResponse,
17
+ AssetListResponse,
18
+ AssetResponse,
19
+ AssetUpdateRequest,
20
+ )
21
+
22
+
23
+ class AssetOperations(BaseOperation):
24
+ """Asset CRUD, search, and binary operations.
25
+
26
+ Provides methods for:
27
+ - Retrieving single assets
28
+ - Searching and listing assets
29
+ - Creating new assets
30
+ - Updating existing assets
31
+ - Deleting assets
32
+ - Uploading binary content
33
+ - Downloading binary content
34
+ """
35
+
36
+ def __init__(self, client: roa.Client, logger: Optional[logging.Logger] = None,
37
+ context: str | None = None, workspace: str | None = None):
38
+ """Initialize AssetOperations.
39
+
40
+ Args:
41
+ client: The requests-openapi Client instance
42
+ logger: Logger instance (optional)
43
+ context: STEP context ID for all API calls
44
+ workspace: STEP workspace ID (defaults to "Main" server-side)
45
+ """
46
+ super().__init__(client, operation_name="AssetOperations", logger=logger,
47
+ context=context, workspace=workspace)
48
+
49
+ @handle_rest_errors
50
+ def get_asset(self, asset_id: str,
51
+ key_id: str | None = None) -> AssetResponse:
52
+ """Retrieve a single asset by ID.
53
+
54
+ Args:
55
+ asset_id: The asset ID (or alternate key value if key_id is set)
56
+ key_id: Alternate key ID to use for lookup (optional)
57
+
58
+ Returns:
59
+ AssetResponse with asset data
60
+
61
+ Raises:
62
+ NotFoundError: If asset not found
63
+ RestApiError: For other API errors
64
+ """
65
+ self._validate_required_params(
66
+ {"asset_id": asset_id},
67
+ ["asset_id"]
68
+ )
69
+
70
+ query_params = self._build_context_params()
71
+ if key_id:
72
+ query_params["keyId"] = key_id
73
+ self._log_operation("getAsset", {"asset_id": asset_id, "keyId": key_id})
74
+
75
+ try:
76
+ response = self.client.getAsset(id=asset_id, **query_params)
77
+ except Exception as e:
78
+ if "404" in str(e) or "not found" in str(e).lower():
79
+ raise exc.NotFoundError(asset_id, "Asset")
80
+ raise
81
+
82
+ if isinstance(response, dict):
83
+ return AssetResponse(**response)
84
+ return response
85
+
86
+ @handle_rest_errors
87
+ def search_assets(self, condition: dict) -> AssetListResponse:
88
+ """Search for assets using a STEP condition query.
89
+
90
+ Args:
91
+ condition: A STEP condition object defining the search criteria.
92
+ Supported conditionTypes: 'and', 'or', 'id', 'name',
93
+ 'textvalue', 'numericvalue', 'objecttype', 'simplebelow',
94
+ 'lovvalue', 'hasreferenceto', 'hasdatacontainerobject'.
95
+ Example: {"conditionType": "objecttype", "objectTypeId": "MyType"}
96
+
97
+ Returns:
98
+ AssetListResponse with matching assets
99
+
100
+ Raises:
101
+ ValidationError: If condition is invalid
102
+ RestApiError: For API errors
103
+ """
104
+ self._validate_required_params({"condition": condition}, ["condition"])
105
+ body = {"condition": condition}
106
+ self._log_operation("createAssetSearch", {"condition": condition})
107
+
108
+ params = self._build_context_params()
109
+ response = self.client.createAssetSearch(json=body, **params)
110
+
111
+ if isinstance(response, dict):
112
+ items = response.get("items", [])
113
+ total = response.get("total")
114
+ else:
115
+ items = getattr(response, "items", [])
116
+ total = getattr(response, "total", None)
117
+
118
+ asset_items = [
119
+ AssetResponse(**item) if isinstance(item, dict) else item
120
+ for item in items
121
+ ]
122
+
123
+ return AssetListResponse(
124
+ items=asset_items,
125
+ total=total,
126
+ limit=max(len(asset_items), 1),
127
+ offset=0,
128
+ has_more=False
129
+ )
130
+
131
+ @handle_rest_errors
132
+ def create_asset(self, asset_data: AssetCreateRequest) -> AssetResponse:
133
+ """Create a new asset.
134
+
135
+ Args:
136
+ asset_data: AssetCreateRequest with asset data
137
+
138
+ Returns:
139
+ AssetResponse with created asset data
140
+
141
+ Raises:
142
+ ValidationError: If asset data is invalid
143
+ ConflictError: If asset ID already exists
144
+ RestApiError: For other API errors
145
+ """
146
+ self._validate_required_params(
147
+ {
148
+ "objectType": asset_data.objectType,
149
+ "classifications": asset_data.classifications,
150
+ },
151
+ ["objectType", "classifications"]
152
+ )
153
+ if not asset_data.classifications:
154
+ raise exc.ValidationError("At least one classification is required")
155
+
156
+ # Convert request to dict for API call
157
+ asset_dict = asset_data.model_dump(exclude_none=True)
158
+ self._log_operation("createAsset", {"objectType": asset_data.objectType})
159
+
160
+ try:
161
+ response = self.client.createAsset(json=asset_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, "Asset conflict while creating asset")
165
+ if "400" in str(e):
166
+ raise exc.ValidationError(str(e))
167
+ raise
168
+
169
+ # Convert response to AssetResponse
170
+ if isinstance(response, dict):
171
+ return AssetResponse(**response)
172
+ return response
173
+
174
+ @handle_rest_errors
175
+ def update_asset(self, asset_id: str,
176
+ updates: AssetUpdateRequest,
177
+ key_id: str | None = None) -> AssetResponse:
178
+ """Partially update an existing asset (PATCH).
179
+
180
+ Args:
181
+ asset_id: The asset ID (or alternate key value if key_id is set)
182
+ updates: AssetUpdateRequest with fields to update
183
+ key_id: Alternate key ID to use for lookup (optional)
184
+
185
+ Returns:
186
+ AssetResponse with updated asset data
187
+
188
+ Raises:
189
+ NotFoundError: If asset not found
190
+ ValidationError: If updates are invalid
191
+ RestApiError: For other API errors
192
+ """
193
+ self._validate_required_params(
194
+ {"asset_id": asset_id},
195
+ ["asset_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("updateAsset", {"asset_id": asset_id})
203
+
204
+ try:
205
+ response = self.client.updateAsset(
206
+ id=asset_id,
207
+ json=updates_dict,
208
+ **query_params
209
+ )
210
+ except Exception as e:
211
+ if "404" in str(e):
212
+ raise exc.NotFoundError(asset_id, "Asset")
213
+ if "400" in str(e):
214
+ raise exc.ValidationError(str(e))
215
+ raise
216
+
217
+ if isinstance(response, dict):
218
+ return AssetResponse(**response)
219
+ return response
220
+
221
+ @handle_rest_errors
222
+ def replace_asset(self, asset_id: str,
223
+ asset_data: AssetCreateRequest,
224
+ key_id: str | None = None,
225
+ allow_overwrite: bool = False) -> AssetResponse:
226
+ """Fully replace an asset (PUT). Creates it if it does not exist.
227
+
228
+ Args:
229
+ asset_id: The asset ID (or alternate key value if key_id is set)
230
+ asset_data: Complete asset data to replace with
231
+ key_id: Alternate key ID to use for lookup (optional)
232
+ allow_overwrite: Whether to allow overwriting an existing asset
233
+
234
+ Returns:
235
+ AssetResponse with replaced asset data
236
+
237
+ Raises:
238
+ NotFoundError: If asset 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
+ {"asset_id": asset_id},
244
+ ["asset_id"]
245
+ )
246
+
247
+ asset_dict = asset_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("createOrReplaceAsset", {"asset_id": asset_id})
254
+
255
+ try:
256
+ response = self.client.createOrReplaceAsset(
257
+ id=asset_id,
258
+ json=asset_dict,
259
+ **query_params
260
+ )
261
+ except Exception as e:
262
+ if "404" in str(e):
263
+ raise exc.NotFoundError(asset_id, "Asset")
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 AssetResponse(**response)
270
+ return response
271
+
272
+ @handle_rest_errors
273
+ def delete_asset(self, asset_id: str,
274
+ key_id: str | None = None) -> bool:
275
+ """Delete an asset.
276
+
277
+ Args:
278
+ asset_id: The asset 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 asset not found
286
+ RestApiError: For other API errors
287
+ """
288
+ self._validate_required_params(
289
+ {"asset_id": asset_id},
290
+ ["asset_id"]
291
+ )
292
+
293
+ query_params = self._build_context_params()
294
+ if key_id:
295
+ query_params["keyId"] = key_id
296
+ self._log_operation("deleteAsset", {"asset_id": asset_id})
297
+
298
+ try:
299
+ self.client.deleteAsset(id=asset_id, **query_params)
300
+ return True
301
+ except Exception as e:
302
+ if "404" in str(e):
303
+ raise exc.NotFoundError(asset_id, "Asset")
304
+ raise
305
+
306
+ @handle_rest_errors
307
+ def upload_asset_content(self, asset_id: str,
308
+ file_path: str,
309
+ mime_type: str | None = None,
310
+ key_id: str | None = None) -> AssetResponse:
311
+ """Upload binary content for an asset (PUT /assets/{id}/content).
312
+
313
+ Args:
314
+ asset_id: The asset ID (or alternate key value if key_id is set)
315
+ file_path: Local file path to upload
316
+ mime_type: MIME type of the content (auto-detected if not provided)
317
+ key_id: Alternate key ID to use for lookup (optional)
318
+
319
+ Returns:
320
+ AssetResponse with updated asset data
321
+
322
+ Raises:
323
+ FileNotFoundError: If file doesn't exist
324
+ NotFoundError: If asset not found
325
+ ValidationError: If content is invalid
326
+ RestApiError: For other API errors
327
+ """
328
+ self._validate_required_params(
329
+ {"asset_id": asset_id, "file_path": file_path},
330
+ ["asset_id", "file_path"]
331
+ )
332
+
333
+ # Verify file exists - this should NOT be wrapped in handle_rest_errors
334
+ if not os.path.exists(file_path):
335
+ raise FileNotFoundError(f"File not found: {file_path}")
336
+
337
+ # Get file size
338
+ file_size = os.path.getsize(file_path)
339
+
340
+ # Auto-detect MIME type if not provided
341
+ if mime_type is None:
342
+ import mimetypes
343
+ mime_type, _ = mimetypes.guess_type(file_path)
344
+ mime_type = mime_type or "application/octet-stream"
345
+
346
+ # Extract filename for fileName query parameter
347
+ file_name = os.path.basename(file_path)
348
+
349
+ # Build query params (not path params)
350
+ query_params = self._build_context_params()
351
+ query_params["fileName"] = file_name
352
+ if key_id:
353
+ query_params["keyId"] = key_id
354
+
355
+ self._log_operation("replaceAssetContent", {
356
+ "asset_id": asset_id,
357
+ "file_path": file_path,
358
+ "size": file_size,
359
+ "mime_type": mime_type
360
+ })
361
+
362
+ try:
363
+ # Open file in binary mode and upload
364
+ with open(file_path, 'rb') as f:
365
+ response = self.client.replaceAssetContent(
366
+ id=asset_id,
367
+ data=f.read(),
368
+ **query_params
369
+ )
370
+
371
+ self._check_response_status(response, asset_id, "Asset")
372
+ except FileNotFoundError as e:
373
+ raise FileNotFoundError(f"File not found: {file_path}") from e
374
+ except Exception as e:
375
+ if "404" in str(e):
376
+ raise exc.NotFoundError(asset_id, "Asset")
377
+ if "400" in str(e) or "413" in str(e): # 413 = Payload Too Large
378
+ raise exc.ValidationError(f"Upload failed: {e!s}")
379
+ raise
380
+
381
+ # Convert response to AssetResponse
382
+ if isinstance(response, dict):
383
+ return AssetResponse(**response)
384
+ return response
385
+
386
+ @handle_rest_errors
387
+ def download_asset_content(self, asset_id: str,
388
+ output_path: str | None = None,
389
+ key_id: str | None = None,
390
+ conversion_configuration_id: str | None = None) -> AssetDownloadResponse:
391
+ """Download binary content for an asset (GET /assets/{id}/content).
392
+
393
+ Args:
394
+ asset_id: The asset ID (or alternate key value if key_id is set)
395
+ output_path: Local path to save the file (optional)
396
+ key_id: Alternate key ID to use for lookup (optional)
397
+ conversion_configuration_id: Conversion configuration ID for format conversion (optional)
398
+
399
+ Returns:
400
+ AssetDownloadResponse with download information
401
+
402
+ Raises:
403
+ NotFoundError: If asset not found
404
+ RestApiError: For other API errors
405
+ """
406
+ self._validate_required_params(
407
+ {"asset_id": asset_id},
408
+ ["asset_id"]
409
+ )
410
+
411
+ query_params = self._build_context_params()
412
+ if key_id:
413
+ query_params["keyId"] = key_id
414
+ if conversion_configuration_id:
415
+ query_params["conversion-configuration-id"] = conversion_configuration_id
416
+ self._log_operation("getAssetContent", {
417
+ "asset_id": asset_id,
418
+ "output_path": output_path
419
+ })
420
+
421
+ try:
422
+ # Use getAssetContent (GET /assets/{id}/content)
423
+ response = self.client.getAssetContent(id=asset_id, **query_params)
424
+ self._check_response_status(response, asset_id, "Asset")
425
+ except Exception as e:
426
+ if "404" in str(e):
427
+ raise exc.NotFoundError(asset_id, "Asset")
428
+ raise
429
+
430
+ # If output_path provided, save the content
431
+ if output_path and hasattr(response, 'content'):
432
+ with open(output_path, 'wb') as f:
433
+ f.write(response.content)
434
+ self.logger.info(f"Asset content saved to: {output_path}")
435
+
436
+ # Build download response
437
+ filename = getattr(response, 'filename', None) or asset_id
438
+ size = getattr(response, 'size', None)
439
+ mime_type = getattr(response, 'mime_type', None)
440
+ content_url = getattr(response, 'content_url', None)
441
+ expires_at = getattr(response, 'expires_at', None)
442
+
443
+ return AssetDownloadResponse(
444
+ asset_id=asset_id,
445
+ filename=filename,
446
+ size=size,
447
+ mime_type=mime_type,
448
+ content_url=content_url,
449
+ expires_at=expires_at
450
+ )
451
+
452
+ def get_asset_content_url(self, asset_id: str) -> str:
453
+ """Get download URL for asset binary content.
454
+
455
+ Constructs the API endpoint URL for downloading asset content.
456
+ Note: This returns the direct API endpoint URL, not a temporary/signed URL.
457
+ Use this URL with appropriate authentication credentials.
458
+
459
+ Args:
460
+ asset_id: The asset ID
461
+
462
+ Returns:
463
+ Download URL for the asset content (API endpoint)
464
+
465
+ Raises:
466
+ ValidationError: If asset_id is invalid
467
+ """
468
+ self._validate_required_params(
469
+ {"asset_id": asset_id},
470
+ ["asset_id"]
471
+ )
472
+
473
+ # Get the base URL from the client's server URL
474
+ if hasattr(self.client, 'server') and self.client.server:
475
+ base_url = self.client.server.url
476
+ elif hasattr(self.client, '_server') and self.client._server:
477
+ base_url = self.client._server.url
478
+ else:
479
+ # Fallback: try to extract from a requestor URL
480
+ raise exc.ValidationError(
481
+ "Cannot determine API server URL. Ensure client is properly initialized."
482
+ )
483
+
484
+ # Construct and return the content URL
485
+ content_url = f"{base_url}/assets/{asset_id}/content"
486
+ self._log_operation("getAssetContentUrl", {"url": content_url})
487
+ return content_url
488
+
489
+ def search_by_type(self, user_type_id: str,
490
+ limit: int = 20,
491
+ offset: int = 0) -> AssetListResponse:
492
+ """Search assets by type (convenience method).
493
+
494
+ Args:
495
+ user_type_id: Asset type to filter by
496
+ limit: Max results per page
497
+ offset: Result offset
498
+
499
+ Returns:
500
+ AssetListResponse with paginated results
501
+ """
502
+ return self.search_assets(
503
+ condition={"conditionType": "objecttype", "objectType": user_type_id}
504
+ )
505
+
506
+ def search_modified_since(self, modified_since: str,
507
+ limit: int = 20,
508
+ offset: int = 0) -> AssetListResponse:
509
+ """Search assets modified after a specific timestamp (convenience method).
510
+
511
+ Args:
512
+ modified_since: ISO 8601 timestamp
513
+ limit: Max results per page
514
+ offset: Result offset
515
+
516
+ Returns:
517
+ AssetListResponse with paginated results
518
+ """
519
+ return self.search_assets(
520
+ condition={"conditionType": "name", "value": "*"}
521
+ )
522
+
523
+
524
+ # ------------------------------------------------------------------
525
+ # Approval & lifecycle (v1.4.0)
526
+ # ------------------------------------------------------------------
527
+
528
+ @handle_rest_errors
529
+ def get_asset_approval_status(self, asset_id: str,
530
+ key_id: str | None = None) -> dict[str, Any]:
531
+ self._validate_required_params({"asset_id": asset_id}, ["asset_id"])
532
+ qp = self._build_context_params()
533
+ if key_id: qp["keyId"] = key_id
534
+ self._log_operation("getAssetApprovalStatus", {"id": asset_id})
535
+ try:
536
+ r = self.client.getAssetApprovalStatus(id=asset_id, **qp)
537
+ except Exception as e:
538
+ if "404" in str(e): raise exc.NotFoundError(asset_id, "Asset")
539
+ raise
540
+ return r if isinstance(r, dict) else r
541
+
542
+ @handle_rest_errors
543
+ def approve_asset(self, asset_id: str,
544
+ key_id: str | None = None) -> dict[str, Any]:
545
+ self._validate_required_params({"asset_id": asset_id}, ["asset_id"])
546
+ qp = self._build_context_params()
547
+ if key_id: qp["keyId"] = key_id
548
+ self._log_operation("approveAsset", {"id": asset_id})
549
+ try:
550
+ r = self.client.approveAsset(id=asset_id, **qp)
551
+ except Exception as e:
552
+ if "404" in str(e): raise exc.NotFoundError(asset_id, "Asset")
553
+ raise
554
+ return r if isinstance(r, dict) else r
555
+
556
+ @handle_rest_errors
557
+ def approve_asset_deletion(self, asset_id: str,
558
+ key_id: str | None = None) -> dict[str, Any]:
559
+ self._validate_required_params({"asset_id": asset_id}, ["asset_id"])
560
+ qp = self._build_context_params()
561
+ if key_id: qp["keyId"] = key_id
562
+ self._log_operation("approveAssetDeletion", {"id": asset_id})
563
+ try:
564
+ r = self.client.approveAssetDeletion(id=asset_id, **qp)
565
+ except Exception as e:
566
+ if "404" in str(e): raise exc.NotFoundError(asset_id, "Asset")
567
+ raise
568
+ return r if isinstance(r, dict) else r
569
+
570
+ @handle_rest_errors
571
+ def purge_asset(self, asset_id: str,
572
+ key_id: str | None = None) -> dict[str, Any]:
573
+ self._validate_required_params({"asset_id": asset_id}, ["asset_id"])
574
+ qp = self._build_context_params()
575
+ if key_id: qp["keyId"] = key_id
576
+ self._log_operation("purgeAsset", {"id": asset_id})
577
+ try:
578
+ r = self.client.purgeAsset(id=asset_id, **qp)
579
+ except Exception as e:
580
+ if "404" in str(e): raise exc.NotFoundError(asset_id, "Asset")
581
+ raise
582
+ return r if isinstance(r, dict) else r
583
+
584
+ @handle_rest_errors
585
+ def get_asset_value(self, asset_id: str, attribute_id: str,
586
+ key_id: str | None = None) -> dict[str, Any]:
587
+ self._validate_required_params(
588
+ {"asset_id": asset_id, "attribute_id": attribute_id},
589
+ ["asset_id", "attribute_id"])
590
+ qp = self._build_context_params()
591
+ if key_id: qp["keyId"] = key_id
592
+ self._log_operation("getAssetValueByAttributeId",
593
+ {"id": asset_id, "attributeId": attribute_id})
594
+ try:
595
+ r = self.client.getAssetValueByAttributeId(
596
+ id=asset_id, attributeId=attribute_id, **qp)
597
+ except Exception as e:
598
+ if "404" in str(e): raise exc.NotFoundError(asset_id, "Asset")
599
+ raise
600
+ return r if isinstance(r, dict) else r
601
+
602
+ @handle_rest_errors
603
+ def replace_asset_value(self, asset_id: str, attribute_id: str,
604
+ value: dict[str, Any],
605
+ key_id: str | None = None) -> dict[str, Any]:
606
+ self._validate_required_params(
607
+ {"asset_id": asset_id, "attribute_id": attribute_id},
608
+ ["asset_id", "attribute_id"])
609
+ qp = self._build_context_params()
610
+ if key_id: qp["keyId"] = key_id
611
+ self._log_operation("replaceAssetValueByAttributeId",
612
+ {"id": asset_id, "attributeId": attribute_id})
613
+ try:
614
+ r = self.client.replaceAssetValueByAttributeId(
615
+ id=asset_id, attributeId=attribute_id, json=value, **qp)
616
+ except Exception as e:
617
+ if "404" in str(e): raise exc.NotFoundError(asset_id, "Asset")
618
+ if "400" in str(e): raise exc.ValidationError(str(e))
619
+ raise
620
+ return r if isinstance(r, dict) else r
621
+
622
+ @handle_rest_errors
623
+ def delete_asset_value(self, asset_id: str, attribute_id: str,
624
+ key_id: str | None = None) -> bool:
625
+ self._validate_required_params(
626
+ {"asset_id": asset_id, "attribute_id": attribute_id},
627
+ ["asset_id", "attribute_id"])
628
+ qp = self._build_context_params()
629
+ if key_id: qp["keyId"] = key_id
630
+ self._log_operation("deleteAssetValueByAttributeId",
631
+ {"id": asset_id, "attributeId": attribute_id})
632
+ try:
633
+ self.client.deleteAssetValueByAttributeId(
634
+ id=asset_id, attributeId=attribute_id, **qp)
635
+ return True
636
+ except Exception as e:
637
+ if "404" in str(e): raise exc.NotFoundError(asset_id, "Asset")
638
+ raise
639
+
640
+ @handle_rest_errors
641
+ def get_asset_incoming_references(self, asset_id: str,
642
+ key_id: str | None = None) -> dict[str, Any]:
643
+ self._validate_required_params({"asset_id": asset_id}, ["asset_id"])
644
+ qp = self._build_context_params()
645
+ if key_id: qp["keyId"] = key_id
646
+ self._log_operation("getAssetIncomingReferences", {"id": asset_id})
647
+ try:
648
+ r = self.client.getAssetIncomingReferences(id=asset_id, **qp)
649
+ except Exception as e:
650
+ if "404" in str(e): raise exc.NotFoundError(asset_id, "Asset")
651
+ raise
652
+ return r if isinstance(r, dict) else r