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,16 @@
|
|
|
1
|
+
"""REST API client for STEP REST API v2.
|
|
2
|
+
|
|
3
|
+
Provides comprehensive, typed access to STEP REST API operations through
|
|
4
|
+
organized operation groups with shared authentication.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from . import exceptions
|
|
8
|
+
from .base import BaseOperation, handle_rest_errors
|
|
9
|
+
from .client import StepRestClient
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
'BaseOperation',
|
|
13
|
+
'StepRestClient',
|
|
14
|
+
'exceptions',
|
|
15
|
+
'handle_rest_errors',
|
|
16
|
+
]
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
"""Base operation class for REST API operations.
|
|
2
|
+
|
|
3
|
+
Provides common functionality for all operation classes including error handling,
|
|
4
|
+
pagination, logging, and request/response processing.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from functools import wraps
|
|
9
|
+
from typing import Any, Callable, Dict, Optional, TypeVar
|
|
10
|
+
|
|
11
|
+
import requests
|
|
12
|
+
import requests_openapi as roa
|
|
13
|
+
|
|
14
|
+
from . import exceptions as exc
|
|
15
|
+
from .models.common import PageInfo
|
|
16
|
+
|
|
17
|
+
T = TypeVar('T')
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def handle_rest_errors(func: Callable) -> Callable:
|
|
21
|
+
"""Decorator to handle common REST API errors.
|
|
22
|
+
|
|
23
|
+
Converts HTTP errors to appropriate exception types.
|
|
24
|
+
Allows FileNotFoundError and other file system exceptions to pass through.
|
|
25
|
+
|
|
26
|
+
Also detects STEP API error responses returned as dicts when
|
|
27
|
+
requests_openapi does not raise an exception for non-2xx HTTP responses.
|
|
28
|
+
The STEP REST API returns error payloads with the form
|
|
29
|
+
``{"httpStatus": <int>, "message": "...", "httpReason": "..."}``.
|
|
30
|
+
"""
|
|
31
|
+
@wraps(func)
|
|
32
|
+
def wrapper(*args, **kwargs):
|
|
33
|
+
try:
|
|
34
|
+
result = func(*args, **kwargs)
|
|
35
|
+
# Detect STEP error responses returned as plain dicts instead of
|
|
36
|
+
# raised exceptions (requests_openapi may not call raise_for_status).
|
|
37
|
+
if isinstance(result, dict) and 'httpStatus' in result:
|
|
38
|
+
status = result.get('httpStatus', 0)
|
|
39
|
+
message = result.get('message') or result.get('httpReason', 'API error')
|
|
40
|
+
if status == 401:
|
|
41
|
+
raise exc.AuthenticationError(f"Authentication failed: {message}")
|
|
42
|
+
elif status == 403:
|
|
43
|
+
raise exc.AuthorizationError(f"Access denied: {message}")
|
|
44
|
+
elif status == 404:
|
|
45
|
+
raise exc.NotFoundError(None, details=result)
|
|
46
|
+
elif status == 409:
|
|
47
|
+
raise exc.ConflictError(status, message)
|
|
48
|
+
elif status == 429:
|
|
49
|
+
raise exc.RateLimitError(details=result)
|
|
50
|
+
elif 500 <= status < 600:
|
|
51
|
+
raise exc.ServerError(status, message)
|
|
52
|
+
elif 400 <= status < 500:
|
|
53
|
+
raise exc.RestApiError(status, message)
|
|
54
|
+
return result
|
|
55
|
+
except requests.exceptions.RequestException as e:
|
|
56
|
+
# Handle requests library exceptions
|
|
57
|
+
if hasattr(e, 'response') and e.response is not None:
|
|
58
|
+
status = e.response.status_code
|
|
59
|
+
try:
|
|
60
|
+
error_data = e.response.json()
|
|
61
|
+
except Exception:
|
|
62
|
+
error_data = {"message": e.response.text}
|
|
63
|
+
|
|
64
|
+
error_msg = error_data.get('error_message') or error_data.get('message', str(e))
|
|
65
|
+
error_code = error_data.get('error_code')
|
|
66
|
+
|
|
67
|
+
if status == 401:
|
|
68
|
+
raise exc.AuthenticationError(f"Authentication failed: {error_msg}")
|
|
69
|
+
elif status == 403:
|
|
70
|
+
raise exc.AuthorizationError(f"Access denied: {error_msg}")
|
|
71
|
+
elif status == 404:
|
|
72
|
+
raise exc.NotFoundError(None, details=error_data)
|
|
73
|
+
elif status == 409:
|
|
74
|
+
raise exc.ConflictError(status, error_msg, error_code=error_code)
|
|
75
|
+
elif status == 429:
|
|
76
|
+
retry_after = e.response.headers.get('Retry-After')
|
|
77
|
+
raise exc.RateLimitError(retry_after=retry_after, details=error_data)
|
|
78
|
+
elif 500 <= status < 600:
|
|
79
|
+
raise exc.ServerError(status, error_msg, error_code=error_code)
|
|
80
|
+
else:
|
|
81
|
+
raise exc.RestApiError(status, error_msg, error_code=error_code)
|
|
82
|
+
else:
|
|
83
|
+
raise exc.RestClientError(f"Request failed: {str(e)}")
|
|
84
|
+
except (FileNotFoundError, OSError):
|
|
85
|
+
# Let file system errors pass through unchanged
|
|
86
|
+
raise
|
|
87
|
+
except Exception as e:
|
|
88
|
+
if isinstance(e, exc.RestClientError):
|
|
89
|
+
raise
|
|
90
|
+
raise exc.RestClientError(f"Unexpected error: {e!s}")
|
|
91
|
+
|
|
92
|
+
return wrapper
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class BaseOperation:
|
|
96
|
+
"""Base class for REST API operation groups.
|
|
97
|
+
|
|
98
|
+
Provides common functionality for all operation classes:
|
|
99
|
+
- Error handling
|
|
100
|
+
- Pagination helpers
|
|
101
|
+
- Logging
|
|
102
|
+
- Response processing
|
|
103
|
+
|
|
104
|
+
Attributes:
|
|
105
|
+
client: The requests_openapi.Client instance
|
|
106
|
+
logger: Logger instance for this operation group
|
|
107
|
+
operation_name: Name of this operation group (for logging)
|
|
108
|
+
context: STEP context ID passed to every API call
|
|
109
|
+
workspace: STEP workspace ID passed to every API call (defaults to "Main" server-side)
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
def __init__(self, client: roa.Client, operation_name: str | None = None,
|
|
113
|
+
logger: Optional[logging.Logger] = None,
|
|
114
|
+
context: str | None = None, workspace: str | None = None):
|
|
115
|
+
"""Initialize base operation.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
client: The requests-openapi Client instance
|
|
119
|
+
operation_name: Name of this operation group (defaults to class name)
|
|
120
|
+
logger: Logger instance (defaults to class logger)
|
|
121
|
+
context: STEP context ID to use for all API calls (required by STEP REST API)
|
|
122
|
+
workspace: STEP workspace ID (server defaults to "Main" if not supplied)
|
|
123
|
+
"""
|
|
124
|
+
self.client = client
|
|
125
|
+
self.operation_name = operation_name or self.__class__.__name__
|
|
126
|
+
self.context = context
|
|
127
|
+
self.workspace = workspace
|
|
128
|
+
|
|
129
|
+
if logger is None:
|
|
130
|
+
self.logger = logging.getLogger(f"{self.__class__.__module__}.{self.operation_name}")
|
|
131
|
+
else:
|
|
132
|
+
self.logger = logger
|
|
133
|
+
|
|
134
|
+
def _build_context_params(self) -> Dict[str, Any]:
|
|
135
|
+
"""Build context and workspace parameters for API calls.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Dictionary containing context and/or workspace keys if set.
|
|
139
|
+
"""
|
|
140
|
+
params: Dict[str, Any] = {}
|
|
141
|
+
if self.context is not None:
|
|
142
|
+
params['context'] = self.context
|
|
143
|
+
if self.workspace is not None:
|
|
144
|
+
params['workspace'] = self.workspace
|
|
145
|
+
return params
|
|
146
|
+
|
|
147
|
+
def _check_for_api_error(self, response: Any, resource_id: str | None = None,
|
|
148
|
+
resource_type: str | None = None) -> None:
|
|
149
|
+
"""Detect STEP error responses returned as dicts instead of raised exceptions.
|
|
150
|
+
|
|
151
|
+
The STEP REST API may return an error body like
|
|
152
|
+
``{"httpStatus": 404, "message": "...", "httpReason": "..."}`` on a
|
|
153
|
+
transport-level 200 when using requests_openapi without raise_for_status.
|
|
154
|
+
This method raises the appropriate library exception in those cases.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
response: The API response (dict, list, or object)
|
|
158
|
+
resource_id: Resource ID for error messages
|
|
159
|
+
resource_type: Resource type name for error messages
|
|
160
|
+
|
|
161
|
+
Raises:
|
|
162
|
+
AuthenticationError: If httpStatus is 401
|
|
163
|
+
AuthorizationError: If httpStatus is 403
|
|
164
|
+
NotFoundError: If httpStatus is 404
|
|
165
|
+
ConflictError: If httpStatus is 409
|
|
166
|
+
RestApiError: For other 4xx/5xx status codes
|
|
167
|
+
"""
|
|
168
|
+
if not isinstance(response, dict) or 'httpStatus' not in response:
|
|
169
|
+
return
|
|
170
|
+
status = response.get('httpStatus', 0)
|
|
171
|
+
message = response.get('message') or response.get('httpReason', 'API error')
|
|
172
|
+
if status == 401:
|
|
173
|
+
raise exc.AuthenticationError(f"Authentication failed: {message}")
|
|
174
|
+
elif status == 403:
|
|
175
|
+
raise exc.AuthorizationError(f"Access denied: {message}")
|
|
176
|
+
elif status == 404:
|
|
177
|
+
raise exc.NotFoundError(resource_id, resource_type)
|
|
178
|
+
elif status == 409:
|
|
179
|
+
raise exc.ConflictError(status, message)
|
|
180
|
+
elif 400 <= status < 600:
|
|
181
|
+
raise exc.RestApiError(status, message)
|
|
182
|
+
|
|
183
|
+
def _check_response_status(self, response: Any, resource_id: str | None = None,
|
|
184
|
+
resource_type: str | None = None) -> None:
|
|
185
|
+
"""Check response for HTTP error status codes returned without raising an exception.
|
|
186
|
+
|
|
187
|
+
Some STEP endpoints (e.g. binary content) return an HTTP error response body
|
|
188
|
+
(e.g. an HTML 404 page) with a non-error HTTP status from the transport layer.
|
|
189
|
+
This method inspects the underlying response object and raises the appropriate
|
|
190
|
+
exception when an error status is detected.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
response: The API response object
|
|
194
|
+
resource_id: Resource ID for error messages
|
|
195
|
+
resource_type: Resource type name for error messages
|
|
196
|
+
|
|
197
|
+
Raises:
|
|
198
|
+
NotFoundError: If status is 404
|
|
199
|
+
RestApiError: For other 4xx/5xx status codes
|
|
200
|
+
"""
|
|
201
|
+
# First check for STEP error dicts (JSON body with httpStatus field)
|
|
202
|
+
self._check_for_api_error(response, resource_id, resource_type)
|
|
203
|
+
|
|
204
|
+
raw_response = getattr(response, '_response', None) or getattr(response, 'response', None)
|
|
205
|
+
status_code = (
|
|
206
|
+
getattr(raw_response, 'status_code', None)
|
|
207
|
+
or getattr(response, 'status_code', None)
|
|
208
|
+
)
|
|
209
|
+
if status_code is not None and isinstance(status_code, int) and status_code >= 400:
|
|
210
|
+
if status_code == 404:
|
|
211
|
+
raise exc.NotFoundError(resource_id, resource_type)
|
|
212
|
+
raise exc.RestApiError(status_code, f"HTTP {status_code} for {resource_type} {resource_id}")
|
|
213
|
+
|
|
214
|
+
def _log_operation(self, operation_id: str, params: Optional[Dict[str, Any]] = None) -> None:
|
|
215
|
+
"""Log an API operation.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
operation_id: The OpenAPI operation ID
|
|
219
|
+
params: Request parameters
|
|
220
|
+
"""
|
|
221
|
+
self.logger.debug(
|
|
222
|
+
f"Calling {self.operation_name}.{operation_id}",
|
|
223
|
+
extra={"params": params}
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
@staticmethod
|
|
227
|
+
def _coerce_list_payload(payload: Any) -> list[Any]:
|
|
228
|
+
"""Normalize list-like API payloads from various response shapes.
|
|
229
|
+
|
|
230
|
+
Supports:
|
|
231
|
+
- native lists
|
|
232
|
+
- dicts with common wrapper keys such as items/results/data/values
|
|
233
|
+
- response-like objects exposing json() or text
|
|
234
|
+
- single-object payloads, which are returned as a one-item list
|
|
235
|
+
"""
|
|
236
|
+
if payload is None:
|
|
237
|
+
return []
|
|
238
|
+
|
|
239
|
+
if isinstance(payload, list):
|
|
240
|
+
return payload
|
|
241
|
+
|
|
242
|
+
if isinstance(payload, dict):
|
|
243
|
+
for key in ("items", "results", "data", "values"):
|
|
244
|
+
value = payload.get(key)
|
|
245
|
+
if isinstance(value, list):
|
|
246
|
+
return value
|
|
247
|
+
if payload:
|
|
248
|
+
return [payload]
|
|
249
|
+
return []
|
|
250
|
+
|
|
251
|
+
if hasattr(payload, "json"):
|
|
252
|
+
try:
|
|
253
|
+
parsed = payload.json()
|
|
254
|
+
except Exception:
|
|
255
|
+
parsed = None
|
|
256
|
+
if parsed is not None:
|
|
257
|
+
return BaseOperation._coerce_list_payload(parsed)
|
|
258
|
+
|
|
259
|
+
if hasattr(payload, "text"):
|
|
260
|
+
try:
|
|
261
|
+
import json
|
|
262
|
+
parsed = json.loads(payload.text)
|
|
263
|
+
except Exception:
|
|
264
|
+
parsed = None
|
|
265
|
+
if parsed is not None:
|
|
266
|
+
return BaseOperation._coerce_list_payload(parsed)
|
|
267
|
+
|
|
268
|
+
return []
|
|
269
|
+
|
|
270
|
+
def _process_response(self, response: Any) -> Any:
|
|
271
|
+
"""Process API response (hook for subclasses).
|
|
272
|
+
|
|
273
|
+
Override this method in subclasses to add custom response processing.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
response: Raw API response
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
Processed response
|
|
280
|
+
"""
|
|
281
|
+
return response
|
|
282
|
+
|
|
283
|
+
def _build_pagination_params(self, limit: int = 20, offset: int = 0) -> Dict[str, Any]:
|
|
284
|
+
"""Build pagination parameters for requests.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
limit: Number of items per page
|
|
288
|
+
offset: Result offset
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
Dictionary of pagination parameters
|
|
292
|
+
"""
|
|
293
|
+
return {
|
|
294
|
+
"limit": max(1, min(limit, 1000)), # Clamp between 1 and 1000
|
|
295
|
+
"offset": max(0, offset)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
def _build_page_info(self, limit: int, offset: int,
|
|
299
|
+
total: Optional[int] = None) -> PageInfo:
|
|
300
|
+
"""Build PageInfo object from pagination parameters.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
limit: Items per page
|
|
304
|
+
offset: Current offset
|
|
305
|
+
total: Total items (if known)
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
PageInfo instance
|
|
309
|
+
"""
|
|
310
|
+
return PageInfo(
|
|
311
|
+
limit=limit,
|
|
312
|
+
offset=offset,
|
|
313
|
+
total=total
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
def _get_paginated_results(self, operation_id: str, params: Optional[Dict[str, Any]] = None,
|
|
317
|
+
item_key: str = "items",
|
|
318
|
+
total_key: str = "total") -> tuple:
|
|
319
|
+
"""Execute paginated API call and extract items and total count.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
operation_id: OpenAPI operation ID to call
|
|
323
|
+
params: Request parameters (should include limit and offset)
|
|
324
|
+
item_key: Key in response containing items list
|
|
325
|
+
total_key: Key in response containing total count
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
Tuple of (items, total) where total may be None if not in response
|
|
329
|
+
"""
|
|
330
|
+
self._log_operation(operation_id, params)
|
|
331
|
+
|
|
332
|
+
operation = getattr(self.client, operation_id)
|
|
333
|
+
response = operation(**(params or {}))
|
|
334
|
+
|
|
335
|
+
# Handle different response formats
|
|
336
|
+
if isinstance(response, dict):
|
|
337
|
+
items = response.get(item_key, [])
|
|
338
|
+
total = response.get(total_key)
|
|
339
|
+
else:
|
|
340
|
+
# Handle object responses
|
|
341
|
+
items = getattr(response, item_key, [])
|
|
342
|
+
total = getattr(response, total_key, None)
|
|
343
|
+
|
|
344
|
+
return items, total
|
|
345
|
+
|
|
346
|
+
@handle_rest_errors
|
|
347
|
+
def _call_operation(self, operation_id: str, **params) -> Any:
|
|
348
|
+
"""Call an OpenAPI operation with error handling.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
operation_id: OpenAPI operation ID to call
|
|
352
|
+
**params: Operation parameters
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
Operation response
|
|
356
|
+
"""
|
|
357
|
+
self._log_operation(operation_id, params)
|
|
358
|
+
|
|
359
|
+
operation = getattr(self.client, operation_id)
|
|
360
|
+
response = operation(**params)
|
|
361
|
+
|
|
362
|
+
return self._process_response(response)
|
|
363
|
+
|
|
364
|
+
def _validate_required_params(self, params: Dict[str, Any],
|
|
365
|
+
required: list) -> None:
|
|
366
|
+
"""Validate that required parameters are present.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
params: Parameters to validate
|
|
370
|
+
required: List of required parameter names
|
|
371
|
+
|
|
372
|
+
Raises:
|
|
373
|
+
ValidationError: If required parameters are missing
|
|
374
|
+
"""
|
|
375
|
+
missing = [p for p in required if p not in params or params[p] is None]
|
|
376
|
+
if missing:
|
|
377
|
+
raise exc.ValidationError(f"Missing required parameters: {missing}")
|
|
378
|
+
|
|
379
|
+
def _clean_params(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
380
|
+
"""Remove None values from parameters.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
params: Parameters to clean
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
Cleaned parameters
|
|
387
|
+
"""
|
|
388
|
+
return {k: v for k, v in params.items() if v is not None}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Extended STEP REST API client with operation groups.
|
|
2
|
+
|
|
3
|
+
Extends the base StiboStepRestClient to provide organized access to
|
|
4
|
+
operation groups for different STEP entities.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from ..client import StiboStepRestClient
|
|
11
|
+
from .base import BaseOperation
|
|
12
|
+
from .operations.assets import AssetOperations
|
|
13
|
+
from .operations.attributes import AttributeOperations
|
|
14
|
+
from .operations.background_processes import BackgroundProcessOperations
|
|
15
|
+
from .operations.batch import BatchOperations
|
|
16
|
+
from .operations.classifications import ClassificationOperations
|
|
17
|
+
from .operations.collection_groups import CollectionGroupOperations
|
|
18
|
+
from .operations.collections import CollectionOperations
|
|
19
|
+
from .operations.contexts import ContextOperations
|
|
20
|
+
from .operations.data_type_groups import DataTypeGroupOperations
|
|
21
|
+
from .operations.entities import EntityOperations
|
|
22
|
+
from .operations.event_processors import EventProcessorOperations
|
|
23
|
+
from .operations.exports import ExportOperations
|
|
24
|
+
from .operations.gateway import GatewayOperations
|
|
25
|
+
from .operations.inbound import InboundOperations
|
|
26
|
+
from .operations.outbound import OutboundOperations
|
|
27
|
+
from .operations.products import ProductOperations
|
|
28
|
+
from .operations.relations import RelationOperations
|
|
29
|
+
from .operations.reports import ReportOperations
|
|
30
|
+
from .operations.system_information import SystemInformationOperations
|
|
31
|
+
from .operations.user_groups import UserGroupOperations
|
|
32
|
+
from .operations.users import UserOperations
|
|
33
|
+
from .operations.workflow_tasks import WorkflowTaskOperations
|
|
34
|
+
from .operations.workflows import WorkflowOperations
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class StepRestClient(StiboStepRestClient):
|
|
38
|
+
"""Extended REST client with organized operation groups.
|
|
39
|
+
|
|
40
|
+
Inherits authentication and base functionality from StiboStepRestClient,
|
|
41
|
+
adding domain-specific operation groups for easier API access.
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
products: ProductOperations instance
|
|
45
|
+
assets: AssetOperations instance
|
|
46
|
+
classifications: ClassificationOperations instance
|
|
47
|
+
entities: EntityOperations instance
|
|
48
|
+
attributes: AttributeOperations instance
|
|
49
|
+
exports: ExportOperations instance
|
|
50
|
+
relations: RelationOperations instance
|
|
51
|
+
inbound: InboundOperations instance (for IIEP uploads)
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, location_base, authorisation,
|
|
55
|
+
openapi_file=None, logger=None,
|
|
56
|
+
proxies=None, ssl_version=None,
|
|
57
|
+
cache_openapi=False, nocert=False,
|
|
58
|
+
context: str | None = None, workspace: str | None = None):
|
|
59
|
+
"""Initialize StepRestClient with operation groups.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
location_base: Base URL of STEP REST API
|
|
63
|
+
authorisation: Authentication handler (e.g., BasicAuthorisation)
|
|
64
|
+
openapi_file: Path to OpenAPI specification file
|
|
65
|
+
logger: Logger instance
|
|
66
|
+
proxies: Proxy configuration
|
|
67
|
+
ssl_version: SSL version configuration
|
|
68
|
+
cache_openapi: Whether to cache OpenAPI spec
|
|
69
|
+
nocert: Whether to skip SSL certificate verification
|
|
70
|
+
context: STEP context ID passed to all context-aware API calls
|
|
71
|
+
workspace: STEP workspace ID (server defaults to "Main" if not supplied)
|
|
72
|
+
"""
|
|
73
|
+
# Initialize parent class
|
|
74
|
+
super().__init__(
|
|
75
|
+
location_base=location_base,
|
|
76
|
+
authorisation=authorisation,
|
|
77
|
+
openapi_file=openapi_file,
|
|
78
|
+
logger=logger,
|
|
79
|
+
proxies=proxies,
|
|
80
|
+
ssl_version=ssl_version,
|
|
81
|
+
cache_openapi=cache_openapi,
|
|
82
|
+
nocert=nocert
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Get logger for operation groups
|
|
86
|
+
if logger is None:
|
|
87
|
+
op_logger = logging.getLogger(f"{self.__class__.__module__}.operations")
|
|
88
|
+
else:
|
|
89
|
+
op_logger = logger
|
|
90
|
+
|
|
91
|
+
# Initialize operation groups (context/workspace passed to entity-level operations)
|
|
92
|
+
self.products = ProductOperations(self.client, logger=op_logger, context=context, workspace=workspace)
|
|
93
|
+
self.assets = AssetOperations(self.client, logger=op_logger, context=context, workspace=workspace)
|
|
94
|
+
self.classifications = ClassificationOperations(self.client, logger=op_logger, context=context, workspace=workspace)
|
|
95
|
+
self.entities = EntityOperations(self.client, logger=op_logger, context=context, workspace=workspace)
|
|
96
|
+
self.attributes = AttributeOperations(self.client, logger=op_logger, context=context, workspace=workspace)
|
|
97
|
+
self.relations = RelationOperations(self.client, logger=op_logger, context=context, workspace=workspace)
|
|
98
|
+
self.inbound = InboundOperations(self.client, logger=op_logger, context=context, workspace=workspace)
|
|
99
|
+
self.exports = ExportOperations(self.client, logger=op_logger, context=context, workspace=workspace)
|
|
100
|
+
self.contexts = ContextOperations(self.client, logger=op_logger)
|
|
101
|
+
self.system_information = SystemInformationOperations(self.client, logger=op_logger)
|
|
102
|
+
self.users = UserOperations(self.client, logger=op_logger, context=context, workspace=workspace)
|
|
103
|
+
self.user_groups = UserGroupOperations(self.client, logger=op_logger, context=context, workspace=workspace)
|
|
104
|
+
self.collections = CollectionOperations(self.client, logger=op_logger, context=context, workspace=workspace)
|
|
105
|
+
self.collection_groups = CollectionGroupOperations(self.client, logger=op_logger, context=context, workspace=workspace)
|
|
106
|
+
self.batch = BatchOperations(self.client, logger=op_logger, context=context, workspace=workspace)
|
|
107
|
+
self.data_type_groups = DataTypeGroupOperations(self.client, logger=op_logger)
|
|
108
|
+
self.background_processes = BackgroundProcessOperations(self.client, logger=op_logger)
|
|
109
|
+
self.event_processors = EventProcessorOperations(self.client, logger=op_logger)
|
|
110
|
+
self.outbound = OutboundOperations(self.client, logger=op_logger)
|
|
111
|
+
self.gateway = GatewayOperations(self.client, logger=op_logger)
|
|
112
|
+
self.workflows = WorkflowOperations(self.client, logger=op_logger, context=context, workspace=workspace)
|
|
113
|
+
self.workflow_tasks = WorkflowTaskOperations(self.client, logger=op_logger, context=context, workspace=workspace)
|
|
114
|
+
self.reports = ReportOperations(self.client, logger=op_logger, context=context, workspace=workspace)
|
|
115
|
+
|
|
116
|
+
def get_operation_group(self, group_name: str) -> BaseOperation | None:
|
|
117
|
+
"""Get an operation group by name.
|
|
118
|
+
|
|
119
|
+
Useful for dynamic access to operation groups.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
group_name: Name of operation group (e.g., 'products', 'assets')
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Operation group instance or None if not found
|
|
126
|
+
"""
|
|
127
|
+
return getattr(self, group_name, None)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""REST API client exceptions.
|
|
2
|
+
|
|
3
|
+
Provides REST-specific exception classes for error handling in rest_client operations.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class RestClientError(Exception):
|
|
8
|
+
"""Base exception for REST API client errors."""
|
|
9
|
+
pass # noqa: PIE790
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RestApiError(RestClientError):
|
|
13
|
+
"""Exception raised for API response errors.
|
|
14
|
+
|
|
15
|
+
Attributes:
|
|
16
|
+
status_code: HTTP status code from the API response
|
|
17
|
+
error_message: Error message from the API
|
|
18
|
+
error_code: API-specific error code if available
|
|
19
|
+
details: Additional error details from response body
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, status_code, error_message, error_code=None, details=None):
|
|
23
|
+
self.status_code = status_code
|
|
24
|
+
self.error_message = error_message
|
|
25
|
+
self.error_code = error_code
|
|
26
|
+
self.details = details
|
|
27
|
+
message = f"API Error ({status_code}): {error_message}"
|
|
28
|
+
if error_code:
|
|
29
|
+
message += f" [Code: {error_code}]"
|
|
30
|
+
super().__init__(message)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AuthenticationError(RestClientError):
|
|
34
|
+
"""Exception raised for authentication failures."""
|
|
35
|
+
|
|
36
|
+
pass # noqa: PIE790
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class AuthorizationError(RestClientError):
|
|
40
|
+
"""Exception raised for authorization failures (insufficient permissions)."""
|
|
41
|
+
|
|
42
|
+
pass # noqa: PIE790 # noqa: PIE790
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class NotFoundError(RestApiError):
|
|
46
|
+
"""Exception raised when a requested resource is not found (HTTP 404)."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, resource_id, resource_type=None, details=None):
|
|
49
|
+
message = "Resource not found"
|
|
50
|
+
if resource_type:
|
|
51
|
+
message += f": {resource_type}"
|
|
52
|
+
if resource_id:
|
|
53
|
+
message += f" (ID: {resource_id})"
|
|
54
|
+
super().__init__(404, message, error_code="NOT_FOUND", details=details)
|
|
55
|
+
self.resource_id = resource_id
|
|
56
|
+
self.resource_type = resource_type
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ValidationError(RestClientError):
|
|
60
|
+
"""Exception raised for request validation errors."""
|
|
61
|
+
|
|
62
|
+
def __init__(self, validation_errors):
|
|
63
|
+
self.validation_errors = validation_errors
|
|
64
|
+
message = f"Validation error: {validation_errors}"
|
|
65
|
+
super().__init__(message)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ConflictError(RestApiError):
|
|
69
|
+
"""Exception raised for resource conflicts (HTTP 409)."""
|
|
70
|
+
|
|
71
|
+
pass # noqa: PIE790
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class RateLimitError(RestApiError):
|
|
75
|
+
"""Exception raised when rate limit is exceeded (HTTP 429)."""
|
|
76
|
+
|
|
77
|
+
def __init__(self, retry_after=None, details=None):
|
|
78
|
+
super().__init__(429, "Rate limit exceeded", error_code="RATE_LIMITED", details=details)
|
|
79
|
+
self.retry_after = retry_after
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class ServerError(RestApiError):
|
|
83
|
+
"""Exception raised for server errors (HTTP 5xx)."""
|
|
84
|
+
|
|
85
|
+
pass # noqa: PIE790
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class TimeoutError(RestClientError):
|
|
89
|
+
"""Exception raised when request times out."""
|
|
90
|
+
|
|
91
|
+
pass # noqa: PIE790
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class OperationError(RestClientError):
|
|
95
|
+
"""Exception raised for operation-specific errors."""
|
|
96
|
+
|
|
97
|
+
pass # noqa: PIE790
|