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.
- stepxmlclient/__init__.py +22 -0
- stepxmlclient/client.py +373 -0
- stepxmlclient/creator.py +317 -0
- stepxmlclient/model/__init__.py +418 -0
- stepxmlclient/model/asset.py +250 -0
- stepxmlclient/model/attribute.py +288 -0
- stepxmlclient/model/business.py +233 -0
- stepxmlclient/model/classification.py +409 -0
- stepxmlclient/model/complex_elements.py +260 -0
- stepxmlclient/model/deletion.py +87 -0
- stepxmlclient/model/entity.py +368 -0
- stepxmlclient/model/metadata.py +199 -0
- stepxmlclient/model/organization.py +167 -0
- stepxmlclient/model/product.py +519 -0
- stepxmlclient/model/relations.py +105 -0
- stepxmlclient/model/replacement.py +417 -0
- stepxmlclient/parser.py +2805 -0
- stepxmlclient/rest_client/__init__.py +16 -0
- stepxmlclient/rest_client/base.py +388 -0
- stepxmlclient/rest_client/client.py +127 -0
- stepxmlclient/rest_client/exceptions.py +97 -0
- stepxmlclient/rest_client/models/__init__.py +175 -0
- stepxmlclient/rest_client/models/asset.py +253 -0
- stepxmlclient/rest_client/models/attribute.py +112 -0
- stepxmlclient/rest_client/models/background_process.py +23 -0
- stepxmlclient/rest_client/models/batch.py +41 -0
- stepxmlclient/rest_client/models/classification.py +57 -0
- stepxmlclient/rest_client/models/collection.py +26 -0
- stepxmlclient/rest_client/models/common.py +192 -0
- stepxmlclient/rest_client/models/context.py +16 -0
- stepxmlclient/rest_client/models/data_type_group.py +15 -0
- stepxmlclient/rest_client/models/entity.py +63 -0
- stepxmlclient/rest_client/models/event_processor.py +15 -0
- stepxmlclient/rest_client/models/import_export.py +43 -0
- stepxmlclient/rest_client/models/integration_endpoint.py +16 -0
- stepxmlclient/rest_client/models/product.py +200 -0
- stepxmlclient/rest_client/models/relations.py +280 -0
- stepxmlclient/rest_client/models/system_information.py +11 -0
- stepxmlclient/rest_client/models/user.py +28 -0
- stepxmlclient/rest_client/models/workflow.py +30 -0
- stepxmlclient/rest_client/operations/__init__.py +54 -0
- stepxmlclient/rest_client/operations/assets.py +652 -0
- stepxmlclient/rest_client/operations/attributes.py +252 -0
- stepxmlclient/rest_client/operations/background_processes.py +84 -0
- stepxmlclient/rest_client/operations/batch.py +28 -0
- stepxmlclient/rest_client/operations/classifications.py +248 -0
- stepxmlclient/rest_client/operations/collection_groups.py +46 -0
- stepxmlclient/rest_client/operations/collections.py +67 -0
- stepxmlclient/rest_client/operations/contexts.py +88 -0
- stepxmlclient/rest_client/operations/data_type_groups.py +68 -0
- stepxmlclient/rest_client/operations/entities.py +295 -0
- stepxmlclient/rest_client/operations/event_processors.py +109 -0
- stepxmlclient/rest_client/operations/exports.py +153 -0
- stepxmlclient/rest_client/operations/gateway.py +53 -0
- stepxmlclient/rest_client/operations/inbound.py +472 -0
- stepxmlclient/rest_client/operations/outbound.py +64 -0
- stepxmlclient/rest_client/operations/products.py +489 -0
- stepxmlclient/rest_client/operations/relations.py +349 -0
- stepxmlclient/rest_client/operations/reports.py +27 -0
- stepxmlclient/rest_client/operations/system_information.py +28 -0
- stepxmlclient/rest_client/operations/user_groups.py +79 -0
- stepxmlclient/rest_client/operations/users.py +119 -0
- stepxmlclient/rest_client/operations/workflow_tasks.py +61 -0
- stepxmlclient/rest_client/operations/workflows.py +50 -0
- stepxmlclient/upload.py +293 -0
- stepxmlclient-1.0.0.dist-info/METADATA +666 -0
- stepxmlclient-1.0.0.dist-info/RECORD +70 -0
- stepxmlclient-1.0.0.dist-info/WHEEL +5 -0
- stepxmlclient-1.0.0.dist-info/licenses/LICENSE +21 -0
- 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
|