pyxecm 1.3.0__py3-none-any.whl → 1.5__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.

Potentially problematic release.


This version of pyxecm might be problematic. Click here for more details.

@@ -0,0 +1,503 @@
1
+ """
2
+ PHT is an OpenText internal application aiming at creating a common naming reference for Engineering Products and
3
+ track all product-related data. It also provides an approved reporting hierarchy.
4
+ See: https://pht.opentext.com
5
+
6
+ Class: PHT
7
+ Methods:
8
+
9
+ __init__ : class initializer
10
+ config : Returns config data set
11
+ get_data: Get the Data object that holds all processed PHT products
12
+ request_header: Returns the request header for ServiceNow API calls
13
+ parse_request_response: Parse the REST API responses and convert
14
+ them to Python dict in a safe way
15
+
16
+ authenticate : Authenticates at ServiceNow API
17
+
18
+ get_attributes: Get a list of all product attributes (schema) of PHT
19
+ get_business_units: Get the list of PHT Business Units
20
+ get_product_families: Get the list of PHT product families
21
+ get_products: Get the list of PHT products
22
+ get_master_products: Get the list of PHT master products
23
+ filter_products: Get a list of filtered PHT products
24
+ load_products: Load products into a data frame.
25
+
26
+ """
27
+
28
+ __author__ = "Dr. Marc Diefenbruch"
29
+ __copyright__ = "Copyright 2024, OpenText"
30
+ __credits__ = ["Kai-Philip Gatzweiler"]
31
+ __maintainer__ = "Dr. Marc Diefenbruch"
32
+ __email__ = "mdiefenb@opentext.com"
33
+
34
+ import json
35
+ import logging
36
+
37
+ import requests
38
+ from requests.auth import HTTPBasicAuth
39
+ from pyxecm.helper.data import Data
40
+
41
+ logger = logging.getLogger("pyxecm.customizer.pht")
42
+
43
+ REQUEST_HEADERS = {"Accept": "application/json", "Content-Type": "application/json"}
44
+
45
+ REQUEST_TIMEOUT = 60
46
+
47
+
48
+ class PHT(object):
49
+ """Used to retrieve data from OpenText PHT."""
50
+
51
+ _config: dict
52
+ _session = None
53
+
54
+ def __init__(
55
+ self,
56
+ base_url: str,
57
+ username: str,
58
+ password: str,
59
+ ):
60
+ """Initialize the PHT object
61
+
62
+ Args:
63
+ base_url (str): base URL of the ServiceNow tenant
64
+ username (str): user name in Saleforce
65
+ password (str): password of the user
66
+ """
67
+
68
+ pht_config = {}
69
+
70
+ # Store the credentials and parameters in a config dictionary:
71
+ pht_config["baseUrl"] = base_url
72
+ pht_config["username"] = username
73
+ pht_config["password"] = password
74
+
75
+ pht_config["restUrl"] = pht_config["baseUrl"] + "/api"
76
+ pht_config["attributeUrl"] = pht_config["restUrl"] + "/attribute"
77
+ pht_config["businessUnitUrl"] = pht_config["restUrl"] + "/business-unit"
78
+ pht_config["productFamilyUrl"] = pht_config["restUrl"] + "/product-family"
79
+ pht_config["productUrl"] = pht_config["restUrl"] + "/product"
80
+ pht_config["searchUrl"] = pht_config["productUrl"] + "/product/search"
81
+ pht_config["teamUrl"] = pht_config["restUrl"] + "/team"
82
+ pht_config["componentUrl"] = pht_config["restUrl"] + "/component"
83
+ pht_config["masterProductUrl"] = pht_config["restUrl"] + "/master-product"
84
+
85
+ self._config = pht_config
86
+
87
+ self._session = requests.Session()
88
+
89
+ self._data = Data()
90
+
91
+ # end method definition
92
+
93
+ def config(self) -> dict:
94
+ """Returns the configuration dictionary
95
+
96
+ Returns:
97
+ dict: Configuration dictionary
98
+ """
99
+ return self._config
100
+
101
+ # end method definition
102
+
103
+ def get_data(self) -> Data:
104
+ """Get the Data object that holds all processed PHT products
105
+
106
+ Returns:
107
+ Data: Datastructure with all processed PHT product data.
108
+ """
109
+
110
+ return self._data
111
+
112
+ # end method definition
113
+
114
+ def request_header(self, content_type: str = "") -> dict:
115
+ """Returns the request header used for Application calls.
116
+ Consists of Bearer access token and Content Type
117
+
118
+ Args:
119
+ content_type (str, optional): custom content type for the request
120
+ Return:
121
+ dict: request header values
122
+ """
123
+
124
+ request_header = {}
125
+
126
+ request_header = REQUEST_HEADERS
127
+
128
+ if content_type:
129
+ request_header["Content-Type"] = content_type
130
+
131
+ return request_header
132
+
133
+ # end method definition
134
+
135
+ def parse_request_response(
136
+ self,
137
+ response_object: requests.Response,
138
+ additional_error_message: str = "",
139
+ show_error: bool = True,
140
+ ) -> list | None:
141
+ """Converts the request response (JSon) to a Python list in a safe way
142
+ that also handles exceptions. It first tries to load the response.text
143
+ via json.loads() that produces a dict output. Only if response.text is
144
+ not set or is empty it just converts the response_object to a dict using
145
+ the vars() built-in method.
146
+
147
+ Args:
148
+ response_object (object): this is reponse object delivered by the request call
149
+ additional_error_message (str, optional): use a more specific error message
150
+ in case of an error
151
+ show_error (bool): True: write an error to the log file
152
+ False: write a warning to the log file
153
+ Returns:
154
+ list: response information or None in case of an error
155
+ """
156
+
157
+ if not response_object:
158
+ return None
159
+
160
+ try:
161
+ if response_object.text:
162
+ list_object = json.loads(response_object.text)
163
+ else:
164
+ list_object = vars(response_object)
165
+ except json.JSONDecodeError as exception:
166
+ if additional_error_message:
167
+ message = "Cannot decode response as JSON. {}; error -> {}".format(
168
+ additional_error_message, exception
169
+ )
170
+ else:
171
+ message = "Cannot decode response as JSON; error -> {}".format(
172
+ exception
173
+ )
174
+ if show_error:
175
+ logger.error(message)
176
+ else:
177
+ logger.warning(message)
178
+ return None
179
+ else:
180
+ return list_object
181
+
182
+ # end method definition
183
+
184
+ def authenticate(self) -> str | None:
185
+ """Authenticate at PHT with basic authentication."""
186
+
187
+ self._session.headers.update(self.request_header())
188
+
189
+ username = self.config()["username"]
190
+ password = self.config()["password"]
191
+ if not self._session:
192
+ self._session = requests.Session()
193
+ self._session.auth = HTTPBasicAuth(username, password)
194
+
195
+ return self._session.auth
196
+
197
+ # end method definition
198
+
199
+ def get_attributes(self) -> list | None:
200
+ """Get a list of all product attributes (schema) of PHT
201
+
202
+ Returns:
203
+ list | None: list of product attributes
204
+
205
+ Example:
206
+ [
207
+ {
208
+ 'id': 28,
209
+ 'uuid': '43ba5852-eb83-11ed-a752-00505682262c',
210
+ 'name': 'UBM SCM Migration JIRA/ValueEdge',
211
+ 'description': 'Identifies the Issue to track work for the SCM migration for this project.\nIts a free text field and no validation with JIRA/ValueEdge will take place',
212
+ 'type': 'TEXT',
213
+ 'attributeCategory': {
214
+ 'id': 2,
215
+ 'name': 'Auxiliary assignment'
216
+ },
217
+ 'showDefault': False,
218
+ 'restricted': True,
219
+ 'allowScopeChain': True,
220
+ 'visibleToAll': False,
221
+ 'deleted': False,
222
+ 'attributeOptions': [],
223
+ 'attributeScopes': [],
224
+ 'allowedTeams': []
225
+ }
226
+ ]
227
+ """
228
+
229
+ request_header = self.request_header()
230
+ request_url = self.config()["attributeUrl"]
231
+
232
+ retries = 0
233
+
234
+ while True:
235
+ response = self._session.get(url=request_url, headers=request_header)
236
+ if response.ok:
237
+ return self.parse_request_response(response)
238
+ # Check if Session has expired - then re-authenticate and try once more
239
+ elif response.status_code == 401 and retries == 0:
240
+ logger.debug("Session has expired - try to re-authenticate...")
241
+ self.authenticate()
242
+ retries += 1
243
+ else:
244
+ logger.error(
245
+ "Failed to get PHT attributes; error -> %s (%s)",
246
+ response.text,
247
+ response.status_code,
248
+ )
249
+ return None
250
+
251
+ # end method definition
252
+
253
+ def get_business_units(self) -> list | None:
254
+ """Get the list of PHT Business Units
255
+
256
+ Returns:
257
+ list | None: list of the known business units.
258
+
259
+ Example:
260
+ [
261
+ {
262
+ 'id': 1,
263
+ 'name': 'Content Services',
264
+ 'leaderModel': {
265
+ 'id': 219,
266
+ 'domain': 'mcybala',
267
+ 'email': 'mcybala@opentext.com',
268
+ 'name': 'Michael Cybala',
269
+ 'role': None,
270
+ 'status': 'ACTIVE',
271
+ 'location': 'Kempten, DEU',
272
+ 'title': 'VP, Software Engineering',
273
+ 'type': 'OTHERS'
274
+ },
275
+ 'pmLeaderModel': {
276
+ 'id': 350,
277
+ 'domain': 'mdiefenb',
278
+ 'email': 'mdiefenb@opentext.com',
279
+ 'name': 'Marc Diefenbruch',
280
+ 'role': None,
281
+ 'status': 'ACTIVE',
282
+ 'location': 'Virtual, DEU',
283
+ 'title': 'VP, Product Management',
284
+ 'type': 'OTHERS'
285
+ },
286
+ 'sltOwnerModel': {
287
+ 'id': 450,
288
+ 'domain': 'jradko',
289
+ 'email': 'jradko@opentext.com',
290
+ 'name': 'John Radko',
291
+ 'role': None,
292
+ 'status': 'ACTIVE',
293
+ 'location': 'Gaithersburg, MD, USA',
294
+ 'title': 'SVP, Software Engineering',
295
+ 'type': 'OTHERS'
296
+ },
297
+ 'status': 'ACTIVE',
298
+ 'engineering': True,
299
+ 'attributes': [{...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}],
300
+ 'leader': 'Michael Cybala',
301
+ 'leaderDomain': 'mcybala',
302
+ 'pmLeader': 'Marc Diefenbruch',
303
+ 'pmLeaderDomain': 'mdiefenb',
304
+ 'sltOwner': 'John Radko',
305
+ 'sltOwnerDomain': 'jradko'
306
+ }
307
+ ]
308
+ """
309
+
310
+ request_header = self.request_header()
311
+ request_url = self.config()["businessUnitUrl"]
312
+
313
+ retries = 0
314
+
315
+ while True:
316
+ response = self._session.get(url=request_url, headers=request_header)
317
+ if response.ok:
318
+ return self.parse_request_response(response)
319
+ # Check if Session has expired - then re-authenticate and try once more
320
+ elif response.status_code == 401 and retries == 0:
321
+ logger.debug("Session has expired - try to re-authenticate...")
322
+ self.authenticate()
323
+ retries += 1
324
+ else:
325
+ logger.error(
326
+ "Failed to get PHT business units; error -> %s (%s)",
327
+ response.text,
328
+ response.status_code,
329
+ )
330
+ return None
331
+
332
+ # end method definition
333
+
334
+ def get_product_families(self) -> list | None:
335
+ """Get the list of PHT product families
336
+
337
+ Returns:
338
+ list | None: list of the known product families.
339
+ """
340
+
341
+ request_header = self.request_header()
342
+ request_url = self.config()["productFamilyUrl"]
343
+
344
+ retries = 0
345
+
346
+ while True:
347
+ response = self._session.get(url=request_url, headers=request_header)
348
+ if response.ok:
349
+ return self.parse_request_response(response)
350
+ # Check if Session has expired - then re-authenticate and try once more
351
+ elif response.status_code == 401 and retries == 0:
352
+ logger.debug("Session has expired - try to re-authenticate...")
353
+ self.authenticate()
354
+ retries += 1
355
+ else:
356
+ logger.error(
357
+ "Failed to get PHT product families; error -> %s (%s)",
358
+ response.text,
359
+ response.status_code,
360
+ )
361
+ return None
362
+
363
+ # end method definition
364
+
365
+ def get_products(self) -> list | None:
366
+ """Get the list of PHT products
367
+
368
+ Returns:
369
+ list | None: list of the known products.
370
+ """
371
+
372
+ request_header = self.request_header()
373
+ request_url = self.config()["productUrl"]
374
+
375
+ retries = 0
376
+
377
+ while True:
378
+ response = self._session.get(url=request_url, headers=request_header)
379
+ if response.ok:
380
+ return self.parse_request_response(response)
381
+ # Check if Session has expired - then re-authenticate and try once more
382
+ elif response.status_code == 401 and retries == 0:
383
+ logger.debug("Session has expired - try to re-authenticate...")
384
+ self.authenticate()
385
+ retries += 1
386
+ else:
387
+ logger.error(
388
+ "Failed to get PHT products; error -> %s (%s)",
389
+ response.text,
390
+ response.status_code,
391
+ )
392
+ return None
393
+
394
+ # end method definition
395
+
396
+ def get_master_products(self) -> list | None:
397
+ """Get the list of PHT master products
398
+
399
+ Returns:
400
+ list | None: list of the known master products.
401
+ """
402
+
403
+ request_header = self.request_header()
404
+ request_url = self.config()["masterProductUrl"]
405
+
406
+ retries = 0
407
+
408
+ while True:
409
+ response = self._session.get(url=request_url, headers=request_header)
410
+ if response.ok:
411
+ return self.parse_request_response(response)
412
+ # Check if Session has expired - then re-authenticate and try once more
413
+ elif response.status_code == 401 and retries == 0:
414
+ logger.debug("Session has expired - try to re-authenticate...")
415
+ self.authenticate()
416
+ retries += 1
417
+ else:
418
+ logger.error(
419
+ "Failed to get PHT master products; error -> %s (%s)",
420
+ response.text,
421
+ response.status_code,
422
+ )
423
+ return None
424
+
425
+ # end method definition
426
+
427
+ def filter_products(self, filter_definition: dict | None = None) -> list | None:
428
+ """Get a list of filtered PHT products
429
+
430
+ Args:
431
+ filter_definition (dict): a dictionary of filter conditions.
432
+ Example filters:
433
+ businessUnitName: <String>
434
+ productFamilyName: <String>
435
+ productName: <String>
436
+ productSyncId: <String>
437
+ productStatus: ACTIVE | INACTIVE | MAINTENANCE
438
+ productManager: <String>
439
+ developmentManager: <String>
440
+ attributeOperator: AND | OR
441
+ attributes: {
442
+ "<AttributeName>": {
443
+ "compare": CONTAINS | EXISTS | DOES_NOT_EXISTS,
444
+ "values": List<String>
445
+ },
446
+ ...
447
+ },
448
+ includeAttributes: true | false
449
+ Returns:
450
+ list | None: list of matching products.
451
+ """
452
+
453
+ if not filter_definition:
454
+ return self.get_products()
455
+
456
+ request_header = self.request_header()
457
+ request_url = self.config()["productUrl"] + "/filtered"
458
+ request_data = filter_definition
459
+
460
+ retries = 0
461
+
462
+ while True:
463
+ response = self._session.post(
464
+ url=request_url, headers=request_header, json=request_data
465
+ )
466
+ if response.ok:
467
+ return self.parse_request_response(response)
468
+ # Check if Session has expired - then re-authenticate and try once more
469
+ elif response.status_code == 401 and retries == 0:
470
+ logger.debug("Session has expired - try to re-authenticate...")
471
+ self.authenticate()
472
+ retries += 1
473
+ else:
474
+ logger.error(
475
+ "Failed to get PHT master products; error -> %s (%s)",
476
+ response.text,
477
+ response.status_code,
478
+ )
479
+ return None
480
+
481
+ # end method definition
482
+
483
+ def load_products(self, product_list: list = None) -> bool:
484
+ """Load products into a data frame in the self._data object
485
+
486
+ Args:
487
+ product_list (list, optional): listn of products - if already avaiable. Defaults to None.
488
+
489
+ Returns:
490
+ bool: True if successful, False otherwise.
491
+ """
492
+
493
+ if not product_list:
494
+ product_list = self.get_products()
495
+
496
+ self._data = Data(product_list)
497
+
498
+ if self._data:
499
+ return True
500
+
501
+ return False
502
+
503
+ # end method definition