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,22 @@
1
+ """
2
+ STEP XML Client Library.
3
+
4
+ A Python client library for integrating with Stibo Systems STEP MDM (Master Data Management).
5
+ Supports standard STEP-XML protocol (OIEP/IIEP) for parsing and creating STEP-XML documents,
6
+ and wraps the STEP REST API v2 for direct system integration.
7
+
8
+ This library provides:
9
+ - Parser: Deserialize STEP-XML documents into Python dictionaries
10
+ - Creator: Serialize Python objects into STEP-XML documents
11
+ - REST Client: Direct integration with STEP REST API v2
12
+ - Asset Uploader: Binary file upload support for asset management
13
+
14
+ Namespaces:
15
+ STEPXML_NS: Dictionary mapping namespace prefixes to their URIs used throughout STEP-XML documents
16
+ """
17
+
18
+ #: XML namespace URIs used in STEP documents
19
+ STEPXML_NS = {
20
+ 'step' : 'http://www.stibosystems.com/step',
21
+ 'outputtemplate' : 'http://www.stibosystems.com/step/outputtemplate'
22
+ }
@@ -0,0 +1,373 @@
1
+ import requests
2
+ import requests_openapi as roa
3
+ import logging
4
+ import os
5
+ import json
6
+ from pathlib import Path
7
+ from urllib.parse import urlparse
8
+
9
+
10
+ def _lazy_import_inbound_operations():
11
+ """Lazy import InboundOperations to avoid circular imports.
12
+
13
+ Returns:
14
+ InboundOperations class or None if import fails
15
+ """
16
+ try:
17
+ from stepxmlclient.rest_client.operations.inbound import InboundOperations
18
+ return InboundOperations
19
+ except ImportError:
20
+ return None
21
+
22
+ class StiboStepRestClient:
23
+
24
+ _OPENAPI_CACHE_FILE = "openapi.json"
25
+
26
+ def __init__(
27
+ self, location_base, authorisation,
28
+ openapi_file=None, logger=None,
29
+ proxies = None, ssl_version = None,
30
+ cache_openapi = False, nocert=False,
31
+ context=None, workspace=None):
32
+ self.ssl_version = ssl_version
33
+ self.context = context
34
+ self.workspace = workspace
35
+
36
+ if logger is None:
37
+ self.logger = logging.getLogger(self.__class__.__name__)
38
+ else:
39
+ self.logger = logger
40
+ # build location base
41
+ location_base = location_base.lower()
42
+ if not location_base.startswith('http'):
43
+ location_base = '%s://%s' % ('https', location_base)
44
+ if location_base.endswith('/'):
45
+ location_base = location_base[:-1]
46
+ # create client
47
+ self.__create_client(location_base, openapi_file, cache_openapi)
48
+ # set server
49
+ self.client.set_server(roa.Server(url=location_base))
50
+ if isinstance(self.client.requestor, requests.Session):
51
+ # no ssl verify?
52
+ if nocert or os.path.exists('nocert'):
53
+ self.client.requestor.verify=False
54
+ # proxies
55
+ if isinstance(proxies, dict):
56
+ self.client.requestor.proxies.update(proxies)
57
+ self.proxies = proxies
58
+ # authorisation (Always after creating client!)
59
+ authorisation.adapt_client(self.client)
60
+ authorisation.do_authorisation(self.client)
61
+
62
+ # Initialize inbound operations (lazy import to avoid circular dependencies)
63
+ InboundOperations = _lazy_import_inbound_operations()
64
+ if InboundOperations:
65
+ self.inbound = InboundOperations(
66
+ self.client,
67
+ logger=self.logger,
68
+ context=context,
69
+ workspace=workspace
70
+ )
71
+ else:
72
+ self.inbound = None
73
+
74
+ return
75
+
76
+ @staticmethod
77
+ def _get_bundled_openapi_file():
78
+ """Get the path to the bundled openapi.json file.
79
+
80
+ Returns:
81
+ Path to bundled openapi.json if it exists, None otherwise
82
+ """
83
+ try:
84
+ # Get the directory of this module
85
+ module_dir = Path(__file__).parent
86
+ bundled_file = module_dir / 'openapi.json'
87
+ if bundled_file.exists():
88
+ return str(bundled_file)
89
+ except Exception:
90
+ pass
91
+ return None
92
+
93
+ def __create_client(self, location_base, openapi_file, cache_openapi):
94
+ """Create and initialize the OpenAPI client.
95
+
96
+ Internal method that loads the OpenAPI specification and creates the
97
+ requests-openapi client instance.
98
+
99
+ Priority order for loading spec:
100
+ 1. Specified openapi_file (if exists)
101
+ 2. Cache file (if caching enabled and exists)
102
+ 3. Bundled openapi.json (shipped with package)
103
+ 4. Fetch from STEP server (if caching enabled, save to cache)
104
+
105
+ Args:
106
+ location_base: Base URL of the STEP API server
107
+ openapi_file: Path to OpenAPI specification file (if available)
108
+ cache_openapi: Whether to cache the OpenAPI spec if fetched
109
+
110
+ Raises:
111
+ FileNotFoundError: If specified openapi_file doesn't exist and no other source available
112
+ """
113
+ # Determine which file to try to load
114
+ cache_file = self._OPENAPI_CACHE_FILE if cache_openapi else None
115
+ bundled_file = self._get_bundled_openapi_file()
116
+
117
+ # Try specified openapi_file first
118
+ if openapi_file:
119
+ if os.path.exists(openapi_file):
120
+ self.client = roa.Client().load_spec_from_file(openapi_file)
121
+ return
122
+ elif not cache_openapi:
123
+ # User specified a file that doesn't exist and isn't caching, so fail
124
+ raise FileNotFoundError(
125
+ f"Open API JSON File '{openapi_file}' not found!")
126
+ # else: continue to try other sources
127
+
128
+ # Try cache file if caching is enabled
129
+ if cache_file and os.path.exists(cache_file):
130
+ self.logger.debug(f"Loading OpenAPI spec from cache: {cache_file}")
131
+ self.client = roa.Client().load_spec_from_file(cache_file)
132
+ return
133
+
134
+ # Try bundled file (always available as fallback)
135
+ if bundled_file:
136
+ self.logger.debug(f"Loading OpenAPI spec from bundled file: {bundled_file}")
137
+ self.client = roa.Client().load_spec_from_file(bundled_file)
138
+ return
139
+
140
+ # Last resort: fetch from server
141
+ self.logger.debug(f"Loading OpenAPI spec from server: {location_base}/specification/openapi.json")
142
+ spec = roa.core.load_spec_from_url(
143
+ f'{location_base}/specification/openapi.json')
144
+
145
+ # If caching is enabled, save the spec
146
+ if cache_openapi and cache_file:
147
+ self.logger.debug(f"Caching OpenAPI spec to: {cache_file}")
148
+ with open(cache_file, 'w') as f:
149
+ json.dump(spec, f)
150
+
151
+ self.client = roa.Client()
152
+ self.client.load_spec(spec)
153
+ return
154
+
155
+ def _get_server_base_url(self):
156
+ """Get configured server base URL from the OpenAPI client.
157
+
158
+ Returns:
159
+ Configured server URL
160
+
161
+ Raises:
162
+ ValueError: If server URL cannot be determined
163
+ """
164
+ server = getattr(self.client, 'server', None) or getattr(self.client, '_server', None)
165
+ base_url = getattr(server, 'url', None)
166
+ if not base_url:
167
+ raise ValueError("Cannot determine server URL from client configuration")
168
+ return base_url
169
+
170
+ def resolve_download_url(self, uri_or_url):
171
+ """Resolve absolute URL or STEP-relative ResultURI to absolute URL.
172
+
173
+ STEP OIEP REST delivery often returns a payload such as:
174
+ ``{"ResultURI": "restapi/integrationendpoints/<endpoint>/<job>/result.zip"}``
175
+ This helper converts that relative URI into an absolute URL using the
176
+ configured server host while preserving already absolute URLs.
177
+
178
+ Args:
179
+ uri_or_url: Absolute URL or relative ResultURI
180
+
181
+ Returns:
182
+ Absolute download URL
183
+
184
+ Raises:
185
+ ValueError: If uri_or_url is empty or server URL is unavailable
186
+ """
187
+ if not uri_or_url or not str(uri_or_url).strip():
188
+ raise ValueError("uri_or_url must not be empty")
189
+
190
+ value = str(uri_or_url).strip()
191
+ parsed = urlparse(value)
192
+ if parsed.scheme in ('http', 'https'):
193
+ return value
194
+
195
+ base_url = self._get_server_base_url()
196
+ base_parts = urlparse(base_url)
197
+ if not base_parts.scheme or not base_parts.netloc:
198
+ raise ValueError(f"Invalid server URL: {base_url}")
199
+
200
+ root_url = f"{base_parts.scheme}://{base_parts.netloc}"
201
+ return f"{root_url}/{value.lstrip('/')}"
202
+
203
+ def download_result_uri(self, uri_or_url, target_dir,
204
+ filename=None, overwrite=False,
205
+ chunk_size=1024 * 1024, timeout=120):
206
+ """Download a file from OIEP ResultURI/URL using configured credentials.
207
+
208
+ Args:
209
+ uri_or_url: Absolute URL or relative ResultURI from STEP
210
+ target_dir: Directory where the file should be stored
211
+ filename: Optional output filename (defaults to URL basename)
212
+ overwrite: Whether to overwrite an existing local file
213
+ chunk_size: Download chunk size in bytes
214
+ timeout: HTTP timeout in seconds
215
+
216
+ Returns:
217
+ Absolute path to the downloaded file
218
+
219
+ Raises:
220
+ ValueError: If input parameters are invalid
221
+ FileExistsError: If destination exists and overwrite is False
222
+ requests.exceptions.RequestException: On HTTP/network failures
223
+ OSError: On file system write errors
224
+ """
225
+ if not target_dir or not str(target_dir).strip():
226
+ raise ValueError("target_dir must not be empty")
227
+ if chunk_size <= 0:
228
+ raise ValueError("chunk_size must be > 0")
229
+
230
+ download_url = self.resolve_download_url(uri_or_url)
231
+ parsed_url = urlparse(download_url)
232
+ default_name = os.path.basename(parsed_url.path.rstrip('/')) or 'download.bin'
233
+ output_name = filename or default_name
234
+
235
+ os.makedirs(target_dir, exist_ok=True)
236
+ output_path = os.path.abspath(os.path.join(target_dir, output_name))
237
+ if os.path.exists(output_path) and not overwrite:
238
+ raise FileExistsError(f"Target file already exists: {output_path}")
239
+
240
+ requestor = getattr(self.client, 'requestor', None)
241
+ if requestor is None or not hasattr(requestor, 'get'):
242
+ raise ValueError("Client requestor does not support HTTP GET")
243
+
244
+ self.logger.info(f"Downloading result from {download_url} to {output_path}")
245
+ response = requestor.get(
246
+ download_url,
247
+ stream=True,
248
+ allow_redirects=True,
249
+ timeout=timeout
250
+ )
251
+ response.raise_for_status()
252
+
253
+ try:
254
+ with open(output_path, 'wb') as f:
255
+ for chunk in response.iter_content(chunk_size=chunk_size):
256
+ if chunk:
257
+ f.write(chunk)
258
+ finally:
259
+ response.close()
260
+
261
+ return output_path
262
+
263
+ def download_oiep_result(self, result_payload, target_dir,
264
+ filename=None, overwrite=False,
265
+ chunk_size=1024 * 1024, timeout=120):
266
+ """Download OIEP REST delivery result from response payload.
267
+
268
+ Expects payload containing ``ResultURI``, for example:
269
+ ``{"ResultURI": "restapi/integrationendpoints/<endpoint>/<job>/result.zip"}``
270
+
271
+ Args:
272
+ result_payload: Dict payload returned by OIEP REST delivery endpoint
273
+ target_dir: Directory where the file should be stored
274
+ filename: Optional output filename override
275
+ overwrite: Whether to overwrite an existing local file
276
+ chunk_size: Download chunk size in bytes
277
+ timeout: HTTP timeout in seconds
278
+
279
+ Returns:
280
+ Absolute path to the downloaded file
281
+
282
+ Raises:
283
+ ValueError: If payload is invalid or missing ResultURI
284
+ FileExistsError: If destination exists and overwrite is False
285
+ requests.exceptions.RequestException: On HTTP/network failures
286
+ OSError: On file system write errors
287
+ """
288
+ if not isinstance(result_payload, dict):
289
+ raise ValueError("result_payload must be a dict containing ResultURI")
290
+
291
+ result_uri = (
292
+ result_payload.get("ResultURI")
293
+ or result_payload.get("resultURI")
294
+ or result_payload.get("result_uri")
295
+ )
296
+ if not result_uri:
297
+ raise ValueError("result_payload does not contain ResultURI")
298
+
299
+ return self.download_result_uri(
300
+ uri_or_url=result_uri,
301
+ target_dir=target_dir,
302
+ filename=filename,
303
+ overwrite=overwrite,
304
+ chunk_size=chunk_size,
305
+ timeout=timeout,
306
+ )
307
+
308
+ class __Authorisation:
309
+ """Abstract base class for authentication handlers.
310
+
311
+ Provides the interface that all authentication implementations must follow.
312
+ Subclasses should override adapt_client() and do_authorisation() as needed.
313
+ """
314
+
315
+ #: Authentication type identifier (override in subclasses)
316
+ TYPE = None
317
+
318
+ def adapt_client(self, client):
319
+ """Adapt the client with authentication-specific configuration.
320
+
321
+ Called to modify the client's request session or configuration
322
+ before making authenticated requests.
323
+
324
+ Args:
325
+ client: The requests_openapi.Client instance to configure
326
+ """
327
+ return
328
+
329
+ def do_authorisation(self, client):
330
+ """Perform any post-initialization authorization setup.
331
+
332
+ Called after client creation and adaptation to perform additional
333
+ authorization steps if needed (e.g., token refresh, permission checks).
334
+
335
+ Args:
336
+ client: The requests_openapi.Client instance
337
+ """
338
+ return
339
+
340
+ def get_type(self):
341
+ """Get the authentication type identifier.
342
+
343
+ Returns:
344
+ String identifying the authentication type (e.g., 'BASIC')
345
+ """
346
+ return self.TYPE
347
+
348
+ class BasicAuthorisation(__Authorisation):
349
+ """HTTP Basic Authentication handler.
350
+
351
+ Implements Basic Authentication (username/password) for STEP REST API requests.
352
+ Credentials are sent in the Authorization header with each request.
353
+ """
354
+
355
+ #: Authentication type identifier
356
+ TYPE = "BASIC"
357
+
358
+ def __init__(self, user, pwd):
359
+ """Initialize BasicAuthorisation with credentials.
360
+
361
+ Args:
362
+ user: Username for Basic Auth
363
+ pwd: Password for Basic Auth
364
+ """
365
+ self.user = user
366
+ self.pwd = pwd
367
+ return
368
+
369
+ def adapt_client(self, client):
370
+ client.requestor.auth = (self.user, self.pwd)
371
+ return
372
+
373
+