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

Potentially problematic release.


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

Files changed (56) hide show
  1. pyxecm/__init__.py +6 -2
  2. pyxecm/avts.py +1492 -0
  3. pyxecm/coreshare.py +1075 -960
  4. pyxecm/customizer/__init__.py +16 -4
  5. pyxecm/customizer/__main__.py +58 -0
  6. pyxecm/customizer/api/__init__.py +5 -0
  7. pyxecm/customizer/api/__main__.py +6 -0
  8. pyxecm/customizer/api/app.py +914 -0
  9. pyxecm/customizer/api/auth.py +154 -0
  10. pyxecm/customizer/api/metrics.py +92 -0
  11. pyxecm/customizer/api/models.py +13 -0
  12. pyxecm/customizer/api/payload_list.py +865 -0
  13. pyxecm/customizer/api/settings.py +103 -0
  14. pyxecm/customizer/browser_automation.py +332 -139
  15. pyxecm/customizer/customizer.py +1075 -1057
  16. pyxecm/customizer/exceptions.py +35 -0
  17. pyxecm/customizer/guidewire.py +322 -0
  18. pyxecm/customizer/k8s.py +787 -338
  19. pyxecm/customizer/log.py +107 -0
  20. pyxecm/customizer/m365.py +3424 -2270
  21. pyxecm/customizer/nhc.py +1169 -0
  22. pyxecm/customizer/openapi.py +258 -0
  23. pyxecm/customizer/payload.py +18201 -7030
  24. pyxecm/customizer/pht.py +1047 -210
  25. pyxecm/customizer/salesforce.py +836 -727
  26. pyxecm/customizer/sap.py +58 -41
  27. pyxecm/customizer/servicenow.py +851 -383
  28. pyxecm/customizer/settings.py +442 -0
  29. pyxecm/customizer/successfactors.py +408 -346
  30. pyxecm/customizer/translate.py +83 -48
  31. pyxecm/helper/__init__.py +5 -2
  32. pyxecm/helper/assoc.py +98 -38
  33. pyxecm/helper/data.py +2482 -742
  34. pyxecm/helper/logadapter.py +27 -0
  35. pyxecm/helper/web.py +229 -101
  36. pyxecm/helper/xml.py +528 -172
  37. pyxecm/maintenance_page/__init__.py +5 -0
  38. pyxecm/maintenance_page/__main__.py +6 -0
  39. pyxecm/maintenance_page/app.py +51 -0
  40. pyxecm/maintenance_page/settings.py +28 -0
  41. pyxecm/maintenance_page/static/favicon.avif +0 -0
  42. pyxecm/maintenance_page/templates/maintenance.html +165 -0
  43. pyxecm/otac.py +234 -140
  44. pyxecm/otawp.py +2689 -0
  45. pyxecm/otcs.py +12344 -7547
  46. pyxecm/otds.py +3166 -2219
  47. pyxecm/otiv.py +36 -21
  48. pyxecm/otmm.py +1363 -296
  49. pyxecm/otpd.py +231 -127
  50. pyxecm-2.0.0.dist-info/METADATA +145 -0
  51. pyxecm-2.0.0.dist-info/RECORD +54 -0
  52. {pyxecm-1.5.dist-info → pyxecm-2.0.0.dist-info}/WHEEL +1 -1
  53. pyxecm-1.5.dist-info/METADATA +0 -51
  54. pyxecm-1.5.dist-info/RECORD +0 -30
  55. {pyxecm-1.5.dist-info → pyxecm-2.0.0.dist-info/licenses}/LICENSE +0 -0
  56. {pyxecm-1.5.dist-info → pyxecm-2.0.0.dist-info}/top_level.txt +0 -0
pyxecm/customizer/pht.py CHANGED
@@ -1,70 +1,106 @@
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
1
+ """PHT stands for Product Hierarchy Tracker.
15
2
 
16
- authenticate : Authenticates at ServiceNow API
3
+ It is an OpenText internal application aiming at creating a common naming reference for Engineering Products and
4
+ track all product-related data. It also provides an approved reporting hierarchy.
17
5
 
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.
6
+ See: https://pht.opentext.com
25
7
 
8
+ Request for User Access Token: https://confluence.opentext.com/display/RDOT/Request+a+User+Access+Token
9
+ PHT API Documentation: https://confluence.opentext.com/display/RDOT/PHT+API+Documentation
26
10
  """
27
11
 
28
12
  __author__ = "Dr. Marc Diefenbruch"
29
- __copyright__ = "Copyright 2024, OpenText"
13
+ __copyright__ = "Copyright (C) 2024-2025, OpenText"
30
14
  __credits__ = ["Kai-Philip Gatzweiler"]
31
15
  __maintainer__ = "Dr. Marc Diefenbruch"
32
16
  __email__ = "mdiefenb@opentext.com"
33
17
 
34
18
  import json
35
19
  import logging
20
+ import time
36
21
 
37
22
  import requests
38
23
  from requests.auth import HTTPBasicAuth
39
- from pyxecm.helper.data import Data
40
24
 
41
- logger = logging.getLogger("pyxecm.customizer.pht")
25
+ from pyxecm.helper import Data
26
+
27
+ default_logger = logging.getLogger("pyxecm.customizer.pht")
42
28
 
43
29
  REQUEST_HEADERS = {"Accept": "application/json", "Content-Type": "application/json"}
44
30
 
45
31
  REQUEST_TIMEOUT = 60
32
+ REQUEST_RETRY_DELAY = 20
33
+ REQUEST_MAX_RETRIES = 2
46
34
 
47
35
 
48
- class PHT(object):
49
- """Used to retrieve data from OpenText PHT."""
36
+ class PHT:
37
+ """Class PHT is used to retrieve data from OpenText PHT. It is a pure read-only access."""
38
+
39
+ logger: logging.Logger = (default_logger,)
50
40
 
51
41
  _config: dict
52
42
  _session = None
43
+ _business_unit_exclusions = None
44
+ _business_unit_inclusions = None
45
+ _product_exclusions = None
46
+ _product_inclusions = None
47
+ _product_category_exclusions = None
48
+ _product_category_inclusions = None
49
+ _product_status_exclusions = None
50
+ _product_status_inclusions = None
53
51
 
54
52
  def __init__(
55
53
  self,
56
54
  base_url: str,
57
55
  username: str,
58
56
  password: str,
59
- ):
60
- """Initialize the PHT object
57
+ business_unit_exclusions: list | None = None,
58
+ business_unit_inclusions: list | None = None,
59
+ product_exclusions: list | None = None,
60
+ product_inclusions: list | None = None,
61
+ product_category_exclusions: list | None = None,
62
+ product_category_inclusions: list | None = None,
63
+ product_status_exclusions: list | None = None,
64
+ product_status_inclusions: list | None = None,
65
+ logger: logging.Logger = default_logger,
66
+ ) -> None:
67
+ """Initialize the PHT object.
61
68
 
62
69
  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
70
+ base_url (str):
71
+ The base URL of PHT.
72
+ username (str):
73
+ The user name to access PHT.
74
+ password (str):
75
+ The password of the user.
76
+ business_unit_exclusions (list | None, optional):
77
+ A black list for business units to exclude. Default = None.
78
+ business_unit_inclusions (list | None, optional):
79
+ A white list for business units to include. Default = None.
80
+ product_exclusions (list | None, optional):
81
+ A black list for products to exclude. Default = None.
82
+ product_inclusions (list | None, optional):
83
+ A white list for products to include. Default = None.
84
+ product_category_exclusions (list | None, optional):
85
+ A black list for product categories to exclude. Default = None.
86
+ product_category_inclusions (list | None, optional):
87
+ A white list for product categories to include. Default = None.
88
+ product_status_exclusions (list | None, optional):
89
+ A back list of product status to exclude. Only products with status NOT on
90
+ this list will be included. Default = None.
91
+ product_status_inclusions (list | None, optional):
92
+ A white list of product status to exclude. Only products with status on
93
+ this list will be included. Default = None.
94
+ logger (logging.Logger):
95
+ The logging object used for all log messages. Default = default_logger.
96
+
66
97
  """
67
98
 
99
+ if logger != default_logger:
100
+ self.logger = logger.getChild("pht")
101
+ for logfilter in logger.filters:
102
+ self.logger.addFilter(logfilter)
103
+
68
104
  pht_config = {}
69
105
 
70
106
  # Store the credentials and parameters in a config dictionary:
@@ -77,34 +113,53 @@ class PHT(object):
77
113
  pht_config["businessUnitUrl"] = pht_config["restUrl"] + "/business-unit"
78
114
  pht_config["productFamilyUrl"] = pht_config["restUrl"] + "/product-family"
79
115
  pht_config["productUrl"] = pht_config["restUrl"] + "/product"
80
- pht_config["searchUrl"] = pht_config["productUrl"] + "/product/search"
116
+ pht_config["productFilteredUrl"] = pht_config["productUrl"] + "/filtered"
117
+ pht_config["productSearchUrl"] = pht_config["productUrl"] + "/search"
118
+ pht_config["productUsersUrl"] = pht_config["productUrl"] + "/users"
81
119
  pht_config["teamUrl"] = pht_config["restUrl"] + "/team"
82
- pht_config["componentUrl"] = pht_config["restUrl"] + "/component"
83
120
  pht_config["masterProductUrl"] = pht_config["restUrl"] + "/master-product"
121
+ pht_config["masterProductFilteredUrl"] = pht_config["masterProductUrl"] + "/filtered"
122
+ pht_config["componentUrl"] = pht_config["restUrl"] + "/component"
123
+ pht_config["componentFilteredUrl"] = pht_config["componentUrl"] + "/filtered"
124
+ pht_config["componentSearchUrl"] = pht_config["componentUrl"] + "/search"
125
+ pht_config["componentUsersUrl"] = pht_config["componentUrl"] + "/users"
84
126
 
85
127
  self._config = pht_config
86
128
 
87
129
  self._session = requests.Session()
88
130
 
89
- self._data = Data()
131
+ self._data = Data(logger=self.logger)
132
+
133
+ self._business_unit_exclusions = business_unit_exclusions
134
+ self._business_unit_inclusions = business_unit_inclusions
135
+ self._product_exclusions = product_exclusions
136
+ self._product_inclusions = product_inclusions
137
+ self._product_category_exclusions = product_category_exclusions
138
+ self._product_category_inclusions = product_category_inclusions
139
+ self._product_status_exclusions = product_status_exclusions
140
+ self._product_status_inclusions = product_status_inclusions
90
141
 
91
142
  # end method definition
92
143
 
93
144
  def config(self) -> dict:
94
- """Returns the configuration dictionary
145
+ """Return the configuration dictionary.
95
146
 
96
147
  Returns:
97
- dict: Configuration dictionary
148
+ dict:
149
+ The configuration dictionary.
150
+
98
151
  """
99
152
  return self._config
100
153
 
101
154
  # end method definition
102
155
 
103
156
  def get_data(self) -> Data:
104
- """Get the Data object that holds all processed PHT products
157
+ """Get the Data object that holds all processed PHT products.
105
158
 
106
159
  Returns:
107
- Data: Datastructure with all processed PHT product data.
160
+ Data:
161
+ Datastructure with all processed PHT product data.
162
+
108
163
  """
109
164
 
110
165
  return self._data
@@ -112,13 +167,23 @@ class PHT(object):
112
167
  # end method definition
113
168
 
114
169
  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
170
+ """Return the request header used for Application calls.
171
+
172
+ Consists of Bearer access token and Content Type
117
173
 
118
174
  Args:
119
- content_type (str, optional): custom content type for the request
120
- Return:
121
- dict: request header values
175
+ content_type (str, optional):
176
+ Custom content type for the request.
177
+ Typical values:
178
+ * application/json - Used for sending JSON-encoded data
179
+ * application/x-www-form-urlencoded - The default for HTML forms.
180
+ Data is sent as key-value pairs in the body of the request, similar to query parameters.
181
+ * multipart/form-data - Used for file uploads or when a form includes non-ASCII characters
182
+
183
+ Returns:
184
+ dict:
185
+ The request header values.
186
+
122
187
  """
123
188
 
124
189
  request_header = {}
@@ -132,57 +197,209 @@ class PHT(object):
132
197
 
133
198
  # end method definition
134
199
 
200
+ def do_request(
201
+ self,
202
+ url: str,
203
+ method: str = "GET",
204
+ headers: dict | None = None,
205
+ data: dict | None = None,
206
+ json_data: dict | None = None,
207
+ files: dict | None = None,
208
+ timeout: int | None = REQUEST_TIMEOUT,
209
+ show_error: bool = True,
210
+ failure_message: str = "",
211
+ success_message: str = "",
212
+ max_retries: int = REQUEST_MAX_RETRIES,
213
+ retry_forever: bool = False,
214
+ ) -> dict | None:
215
+ """Call an PHT REST API in a safe way.
216
+
217
+ Args:
218
+ url (str):
219
+ The URL to send the request to.
220
+ method (str, optional):
221
+ HTTP method (GET, POST, etc.). Defaults to "GET".
222
+ headers (dict | None, optional):
223
+ Request Headers. Defaults to None.
224
+ data (dict | None, optional):
225
+ Request payload. Defaults to None.
226
+ json_data (dict | None, optional):
227
+ Request payload. Defaults to None.
228
+ files (dict | None, optional):
229
+ Dictionary of {"name": file-tuple} for multipart encoding upload.
230
+ The file-tuple can be a 2-tuple ("filename", fileobj) or a 3-tuple ("filename", fileobj, "content_type")
231
+ timeout (int | None, optional):
232
+ Timeout for the request in seconds. Defaults to REQUEST_TIMEOUT.
233
+ show_error (bool, optional):
234
+ Whether or not an error should be logged in case of a failed REST call.
235
+ If False, then only a warning is logged. Defaults to True.
236
+ failure_message (str, optional):
237
+ Specific error message. Defaults to "".
238
+ success_message (str, optional):
239
+ Specific success message. Defaults to "".
240
+ max_retries (int, optional):
241
+ How many retries on Connection errors? Default is REQUEST_MAX_RETRIES.
242
+ retry_forever (bool, optional):
243
+ Eventually wait forever - without timeout. Defaults to False.
244
+
245
+ Returns:
246
+ dict | None:
247
+ Response of PHT REST API or None in case of an error.
248
+
249
+ """
250
+
251
+ retries = 0
252
+ while True:
253
+ try:
254
+ response = self._session.request(
255
+ method=method,
256
+ url=url,
257
+ data=data,
258
+ json=json_data,
259
+ files=files,
260
+ headers=headers,
261
+ timeout=timeout,
262
+ )
263
+
264
+ if response.ok:
265
+ if success_message:
266
+ self.logger.debug(success_message)
267
+ return self.parse_request_response(response)
268
+ # Check if Session has expired - then re-authenticate and try once more
269
+ elif response.status_code == 401 and retries == 0:
270
+ self.logger.debug("Session has expired - try to re-authenticate...")
271
+ self.authenticate()
272
+ retries += 1
273
+ else:
274
+ # Handle plain HTML responses to not pollute the logs
275
+ content_type = response.headers.get("content-type", None)
276
+ response_text = "HTML content (see debug log)" if content_type == "text/html" else response.text
277
+
278
+ if show_error:
279
+ self.logger.error(
280
+ "%s; status -> %s; error -> %s",
281
+ failure_message,
282
+ response.status_code,
283
+ response_text,
284
+ )
285
+ else:
286
+ self.logger.warning(
287
+ "%s; status -> %s; warning -> %s",
288
+ failure_message,
289
+ response.status_code,
290
+ response_text,
291
+ )
292
+
293
+ if content_type == "text/html":
294
+ self.logger.debug(
295
+ "%s; status -> %s; warning -> %s",
296
+ failure_message,
297
+ response.status_code,
298
+ response.text,
299
+ )
300
+
301
+ return None
302
+ except requests.exceptions.Timeout:
303
+ if retries <= max_retries:
304
+ self.logger.warning(
305
+ "Request timed out. Retrying in %s seconds...",
306
+ str(REQUEST_RETRY_DELAY),
307
+ )
308
+ retries += 1
309
+ time.sleep(REQUEST_RETRY_DELAY) # Add a delay before retrying
310
+ else:
311
+ self.logger.error(
312
+ "%s; timeout error,",
313
+ failure_message,
314
+ )
315
+ if retry_forever:
316
+ # If it fails after REQUEST_MAX_RETRIES retries we let it wait forever
317
+ self.logger.warning("Turn timeouts off and wait forever...")
318
+ timeout = None
319
+ else:
320
+ return None
321
+ except requests.exceptions.ConnectionError:
322
+ if retries <= max_retries:
323
+ self.logger.warning(
324
+ "Connection error. Retrying in %s seconds...",
325
+ str(REQUEST_RETRY_DELAY),
326
+ )
327
+ retries += 1
328
+ time.sleep(REQUEST_RETRY_DELAY) # Add a delay before retrying
329
+ else:
330
+ self.logger.error(
331
+ "%s; connection error.",
332
+ failure_message,
333
+ )
334
+ if retry_forever:
335
+ # If it fails after REQUEST_MAX_RETRIES retries we let it wait forever
336
+ self.logger.warning("Turn timeouts off and wait forever...")
337
+ timeout = None
338
+ time.sleep(REQUEST_RETRY_DELAY) # Add a delay before retrying
339
+ else:
340
+ return None
341
+
342
+ # end method definition
343
+
135
344
  def parse_request_response(
136
345
  self,
137
346
  response_object: requests.Response,
138
347
  additional_error_message: str = "",
139
348
  show_error: bool = True,
140
349
  ) -> 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.
350
+ """Convert the request response (JSon) to a Python list in a safe way that also handles exceptions.
351
+
352
+ It first tries to load the response.text via json.loads() that produces
353
+ a dict output. Only if response.text is not set or is empty it just converts
354
+ the response_object to a dict using the vars() built-in method.
146
355
 
147
356
  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
357
+ response_object (object):
358
+ This is reponse object delivered by the request call.
359
+ additional_error_message (str, optional):
360
+ Used to provide a more specific error message.
361
+ show_error (bool):
362
+ If True, write an error to the log file.
363
+ If False, write a warning to the log file.
364
+
153
365
  Returns:
154
366
  list: response information or None in case of an error
367
+
155
368
  """
156
369
 
157
370
  if not response_object:
158
371
  return None
159
372
 
160
373
  try:
161
- if response_object.text:
162
- list_object = json.loads(response_object.text)
163
- else:
164
- list_object = vars(response_object)
374
+ list_object = json.loads(response_object.text) if response_object.text else vars(response_object)
165
375
  except json.JSONDecodeError as exception:
166
376
  if additional_error_message:
167
377
  message = "Cannot decode response as JSON. {}; error -> {}".format(
168
- additional_error_message, exception
378
+ additional_error_message,
379
+ exception,
169
380
  )
170
381
  else:
171
382
  message = "Cannot decode response as JSON; error -> {}".format(
172
- exception
383
+ exception,
173
384
  )
174
385
  if show_error:
175
- logger.error(message)
386
+ self.logger.error(message)
176
387
  else:
177
- logger.warning(message)
388
+ self.logger.warning(message)
178
389
  return None
179
390
  else:
180
391
  return list_object
181
392
 
182
393
  # end method definition
183
394
 
184
- def authenticate(self) -> str | None:
185
- """Authenticate at PHT with basic authentication."""
395
+ def authenticate(self) -> HTTPBasicAuth | None:
396
+ """Authenticate at PHT with basic authentication.
397
+
398
+ Returns:
399
+ str | None:
400
+ Session authorization string.
401
+
402
+ """
186
403
 
187
404
  self._session.headers.update(self.request_header())
188
405
 
@@ -197,18 +414,18 @@ class PHT(object):
197
414
  # end method definition
198
415
 
199
416
  def get_attributes(self) -> list | None:
200
- """Get a list of all product attributes (schema) of PHT
417
+ """Get a list of all product attributes (schema) of PHT.
201
418
 
202
419
  Returns:
203
420
  list | None: list of product attributes
204
421
 
205
- Example:
422
+ Example:
206
423
  [
207
424
  {
208
425
  'id': 28,
209
426
  'uuid': '43ba5852-eb83-11ed-a752-00505682262c',
210
427
  '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',
428
+ 'description': 'Identifies the Issue to track work for the SCM migration for this project.',
212
429
  'type': 'TEXT',
213
430
  'attributeCategory': {
214
431
  'id': 2,
@@ -224,39 +441,30 @@ class PHT(object):
224
441
  'allowedTeams': []
225
442
  }
226
443
  ]
444
+
227
445
  """
228
446
 
229
447
  request_header = self.request_header()
230
448
  request_url = self.config()["attributeUrl"]
231
449
 
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
450
+ return self.do_request(
451
+ url=request_url,
452
+ method="GET",
453
+ headers=request_header,
454
+ timeout=None,
455
+ failure_message="Failed to get PHT attributes!",
456
+ )
250
457
 
251
458
  # end method definition
252
459
 
253
460
  def get_business_units(self) -> list | None:
254
- """Get the list of PHT Business Units
461
+ """Get the list of PHT Business Units.
255
462
 
256
463
  Returns:
257
- list | None: list of the known business units.
464
+ list | None:
465
+ The list of the known business units.
258
466
 
259
- Example:
467
+ Example:
260
468
  [
261
469
  {
262
470
  'id': 1,
@@ -305,199 +513,828 @@ class PHT(object):
305
513
  'sltOwnerDomain': 'jradko'
306
514
  }
307
515
  ]
516
+
308
517
  """
309
518
 
310
519
  request_header = self.request_header()
311
520
  request_url = self.config()["businessUnitUrl"]
312
521
 
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
522
+ return self.do_request(
523
+ url=request_url,
524
+ method="GET",
525
+ headers=request_header,
526
+ timeout=None,
527
+ failure_message="Failed to get PHT business units!",
528
+ )
331
529
 
332
530
  # end method definition
333
531
 
334
532
  def get_product_families(self) -> list | None:
335
- """Get the list of PHT product families
533
+ """Get the list of PHT product families (LoBs).
336
534
 
337
535
  Returns:
338
- list | None: list of the known product families.
536
+ list | None:
537
+ A list of the known product families.
538
+
339
539
  """
340
540
 
341
541
  request_header = self.request_header()
342
542
  request_url = self.config()["productFamilyUrl"]
343
543
 
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
544
+ return self.do_request(
545
+ url=request_url,
546
+ method="GET",
547
+ headers=request_header,
548
+ timeout=None,
549
+ failure_message="Failed to get PHT product families",
550
+ )
362
551
 
363
552
  # end method definition
364
553
 
365
554
  def get_products(self) -> list | None:
366
- """Get the list of PHT products
555
+ """Get the list of PHT products.
367
556
 
368
557
  Returns:
369
- list | None: list of the known products.
558
+ list | None:
559
+ A list of the known products.
560
+
370
561
  """
371
562
 
372
563
  request_header = self.request_header()
373
564
  request_url = self.config()["productUrl"]
374
565
 
375
- retries = 0
566
+ return self.do_request(
567
+ url=request_url,
568
+ method="GET",
569
+ headers=request_header,
570
+ timeout=None,
571
+ failure_message="Failed to get PHT products",
572
+ )
376
573
 
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
574
+ # end method definition
575
+
576
+ def get_products_filtered(
577
+ self,
578
+ filter_definition: dict | None = None,
579
+ ) -> list | None:
580
+ """Get a list of filtered PHT products.
581
+
582
+ Args:
583
+ filter_definition (dict | None, optional):
584
+ A dictionary of filter conditions. Default is None (no filter).
585
+ Example filters:
586
+ {
587
+ businessUnitName: <String>,
588
+ productFamilyName: <String>,
589
+ productName: <String>,
590
+ productSyncId: <String>,
591
+ productStatus: ACTIVE | INACTIVE | MAINTENANCE,
592
+ productManager: <String>,
593
+ developmentManager: <String>,
594
+ attributeOperator: AND | OR,
595
+ attributes: {
596
+ "<AttributeName>": {
597
+ "compare": CONTAINS | EXISTS | DOES_NOT_EXISTS,
598
+ "values": List<String>
599
+ },
600
+ ...
601
+ },
602
+ includeAttributes: true | false
603
+ statuses: ["ACTIVE"],
604
+ }
605
+
606
+ Returns:
607
+ list | None: list of matching products.
608
+
609
+ """
610
+
611
+ if not filter_definition:
612
+ return self.get_products()
613
+
614
+ request_header = self.request_header()
615
+ request_url = self.config()["productFilteredUrl"]
616
+ request_data = filter_definition
617
+
618
+ return self.do_request(
619
+ url=request_url,
620
+ method="POST",
621
+ headers=request_header,
622
+ json_data=request_data,
623
+ timeout=None,
624
+ failure_message="Failed to get filtered PHT products",
625
+ )
626
+
627
+ # end method definition
628
+
629
+ def get_product(self, sync_id: str) -> dict | None:
630
+ """Get a specific product in PHT.
631
+
632
+ Args:
633
+ sync_id (str): Unique ID of the PHT product.
634
+
635
+ Returns:
636
+ dict | None: product data matching the sync ID
637
+
638
+ """
639
+
640
+ request_header = self.request_header()
641
+ request_url = self.config()["productUrl"] + "/" + str(sync_id)
642
+
643
+ return self.do_request(
644
+ url=request_url,
645
+ method="GET",
646
+ headers=request_header,
647
+ timeout=None,
648
+ failure_message="Failed to retrieve PHT product with sync ID -> {}!".format(
649
+ sync_id,
650
+ ),
651
+ )
652
+
653
+ # end method definition
654
+
655
+ def search_products(
656
+ self,
657
+ query: str,
658
+ business_unit: str | None = None,
659
+ family: str | None = None,
660
+ ) -> list | None:
661
+ """Search for specific product in PHT by the product name, business unit or product family (or a combination).
662
+
663
+ Args:
664
+ query (str):
665
+ Query to search for specific products.
666
+ business_unit (str | None, optional):
667
+ Used to focus the search on a specific Business Unit.
668
+ family (str | None, optional):
669
+ Used to focus the search on a specific product family (Line of Business)
670
+
671
+ Returns:
672
+ list | None:
673
+ Search term matches any part of the component name.
674
+
675
+ """
676
+
677
+ request_header = self.request_header()
678
+ request_url = self.config()["componentSearchUrl"] + "?q=" + query
679
+ if business_unit:
680
+ request_url += "&businessUnit=" + business_unit
681
+ if family:
682
+ request_url += "&family=" + family
683
+
684
+ return self.do_request(
685
+ url=request_url,
686
+ method="GET",
687
+ headers=request_header,
688
+ timeout=None,
689
+ failure_message="Failed to retrieve PHT components matching -> {}!".format(
690
+ query,
691
+ ),
692
+ )
393
693
 
394
694
  # end method definition
395
695
 
396
696
  def get_master_products(self) -> list | None:
397
- """Get the list of PHT master products
697
+ """Get the list of PHT master products.
398
698
 
399
699
  Returns:
400
- list | None: list of the known master products.
700
+ list | None:
701
+ A list of the known master products.
702
+
401
703
  """
402
704
 
403
705
  request_header = self.request_header()
404
706
  request_url = self.config()["masterProductUrl"]
405
707
 
406
- retries = 0
708
+ return self.do_request(
709
+ url=request_url,
710
+ method="GET",
711
+ headers=request_header,
712
+ timeout=None,
713
+ failure_message="Failed to get PHT master products",
714
+ )
407
715
 
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
716
+ # end method definition
717
+
718
+ def get_master_products_filtered(
719
+ self,
720
+ filter_definition: dict | None = None,
721
+ ) -> list | None:
722
+ """Get a list of filtered PHT master products.
723
+
724
+ Args:
725
+ filter_definition (dict | None, optional):
726
+ A dictionary of filter conditions.
727
+ Example filters:
728
+ {
729
+ businessUnitName: <String>,
730
+ productFamilyName: <String>,
731
+ masterproductName: <String>,
732
+ masterproductSyncId: <String>,
733
+ masterproductStatus: ACTIVE | INACTIVE | MAINTENANCE,
734
+ productManagerDomain: <String>,
735
+ attributeOperator: AND | OR,
736
+ attributes: {
737
+ "<AttributeName>": {
738
+ "compare": CONTAINS | EXISTS | DOES_NOT_EXISTS,
739
+ "values": List<String>
740
+ },
741
+ ...
742
+ },
743
+ includeAttributes: true | false
744
+ includeLinkedProducts: true | false
745
+ }
746
+
747
+ Returns:
748
+ list | None: List of matching products.
749
+
750
+ """
751
+
752
+ if not filter_definition:
753
+ return self.get_products()
754
+
755
+ request_header = self.request_header()
756
+ request_url = self.config()["masterProductFilteredUrl"]
757
+ request_data = filter_definition
758
+
759
+ return self.do_request(
760
+ url=request_url,
761
+ method="POST",
762
+ headers=request_header,
763
+ json_data=request_data,
764
+ timeout=None,
765
+ failure_message="Failed to get filtered PHT master products",
766
+ )
424
767
 
425
768
  # end method definition
426
769
 
427
- def filter_products(self, filter_definition: dict | None = None) -> list | None:
428
- """Get a list of filtered PHT products
770
+ def get_master_product(self, sync_id: str) -> dict | None:
771
+ """Get a specific product in PHT.
429
772
 
430
773
  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>
774
+ sync_id (str): Unique PHT ID of the master product.
775
+
776
+ Returns:
777
+ dict | None: product data matching the sync ID
778
+
779
+ """
780
+
781
+ request_header = self.request_header()
782
+ request_url = self.config()["productUrl"] + "/" + str(sync_id)
783
+
784
+ return self.do_request(
785
+ url=request_url,
786
+ method="GET",
787
+ headers=request_header,
788
+ timeout=None,
789
+ failure_message="Failed to retrieve PHT product with sync ID -> {}".format(
790
+ sync_id,
791
+ ),
792
+ )
793
+
794
+ # end method definition
795
+
796
+ def get_teams(self) -> list | None:
797
+ """Get a list of all teams in PHT.
798
+
799
+ Returns:
800
+ list | None: list of PHT teams
801
+
802
+ """
803
+
804
+ request_header = self.request_header()
805
+ request_url = self.config()["teamUrl"]
806
+
807
+ return self.do_request(
808
+ url=request_url,
809
+ method="GET",
810
+ headers=request_header,
811
+ timeout=None,
812
+ failure_message="Failed to retrieve PHT teams",
813
+ )
814
+
815
+ # end method definition
816
+
817
+ def get_team(self, team_id: str) -> dict | None:
818
+ """Get a specific team in PHT.
819
+
820
+ Args:
821
+ team_id (str): Unique PHT ID of the team.
822
+
823
+ Returns:
824
+ dict | None: Details of the PHT team.
825
+
826
+ """
827
+
828
+ request_header = self.request_header()
829
+ request_url = self.config()["teamUrl"] + "/" + str(team_id)
830
+
831
+ return self.do_request(
832
+ url=request_url,
833
+ method="GET",
834
+ headers=request_header,
835
+ timeout=None,
836
+ failure_message="Failed to retrieve PHT team with ID -> {}".format(team_id),
837
+ )
838
+
839
+ # end method definition
840
+
841
+ def get_components(self) -> list:
842
+ """Get a list of all components in PHT.
843
+
844
+ Returns:
845
+ list: list of PHT components
846
+
847
+ Example:
848
+ [
849
+ {
850
+ 'id': 468,
851
+ 'syncId': '6380c3da-8ded-40cd-8071-61f6721956f5',
852
+ 'name': 'XOTE',
853
+ 'developmentManager': {
854
+ 'id': 237,
855
+ 'domain': 'kurt',
856
+ 'email': 'kurt.junker@opentext.com',
857
+ 'name': 'Kurt Junker',
858
+ 'role': None,
859
+ 'status': 'ACTIVE',
860
+ 'location': 'Grasbrunn, DEU',
861
+ 'title': 'Sr. Manager, Software Engineering',
862
+ 'type': 'OTHERS'
445
863
  },
446
- ...
447
- },
448
- includeAttributes: true | false
864
+ 'componentCategory': {
865
+ 'id': 2,
866
+ 'name': 'Testing scripts',
867
+ 'shortName': 'Testing scripts'
868
+ },
869
+ 'comment': 'Test Framework maintained and used by Core Archive Team',
870
+ 'status': 'MAINTENANCE',
871
+ 'attributes': [
872
+ {
873
+ 'id': 409,
874
+ 'attribute': {
875
+ 'id': 4,
876
+ 'uuid': '03e228b5-9eae-11ea-96ab-00505682bce9',
877
+ 'name': 'Build Advocate',
878
+ 'description': 'Primary contact for build items.',
879
+ 'type': 'USER'
880
+ },
881
+ 'value': 'burkhard',
882
+ 'textAttributeValue': None,
883
+ 'userAttributeValue': {
884
+ 'id': 414,
885
+ 'domain': 'burkhard',
886
+ 'email': 'burkhard.meier@opentext.com',
887
+ 'name': 'Burkhard Meier',
888
+ 'role': None,
889
+ 'status': 'ACTIVE',
890
+ 'location': 'Virtual, DEU',
891
+ 'title': 'Principal Software Engineer',
892
+ 'type': 'DEV'
893
+ },
894
+ 'listAttributeValue': None
895
+ },
896
+ ...
897
+ ],
898
+ 'sourceRepos': [],
899
+ 'artifacts': [],
900
+ 'products': [],
901
+ 'teams': [],
902
+ 'users': [],
903
+ 'guestTeams': [],
904
+ 'guestUsers': [],
905
+ 'relatedLOBS': []
906
+ }
907
+ ]
908
+
909
+ """
910
+
911
+ request_header = self.request_header()
912
+ request_url = self.config()["componentUrl"]
913
+
914
+ return self.do_request(
915
+ url=request_url,
916
+ method="GET",
917
+ headers=request_header,
918
+ timeout=None,
919
+ failure_message="Failed to retrieve PHT components",
920
+ )
921
+
922
+ # end method definition
923
+
924
+ def get_components_filtered(
925
+ self,
926
+ filter_definition: dict | None = None,
927
+ ) -> list | None:
928
+ """Get a list of filtered PHT components.
929
+
930
+ Args:
931
+ filter_definition (dict | None, optional):
932
+ A dictionary of filter conditions.
933
+ Example filters:
934
+ {
935
+ componentName: <String>,
936
+ componentSyncId: <String>,
937
+ componentStatus: ACTIVE | INACTIVE | MAINTENANCE,
938
+ developmentManager: <String>,
939
+ attributeOperator: AND | OR,
940
+ attributes: {
941
+ "<AttributeName>": {
942
+ "compare": CONTAINS | EXISTS | DOES_NOT_EXISTS,
943
+ "values": List<String>
944
+ },
945
+ ...
946
+ },
947
+ includeAttributes: true | false
948
+ }
949
+
449
950
  Returns:
450
- list | None: list of matching products.
951
+ list | None: list of matching components.
952
+
451
953
  """
452
954
 
453
955
  if not filter_definition:
454
956
  return self.get_products()
455
957
 
456
958
  request_header = self.request_header()
457
- request_url = self.config()["productUrl"] + "/filtered"
959
+ request_url = self.config()["masterProductFilteredUrl"]
458
960
  request_data = filter_definition
459
961
 
460
- retries = 0
962
+ return self.do_request(
963
+ url=request_url,
964
+ method="POST",
965
+ headers=request_header,
966
+ json_data=request_data,
967
+ timeout=None,
968
+ failure_message="Failed to get filtered PHT components",
969
+ )
461
970
 
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
971
+ # end method definition
972
+
973
+ def get_component(self, sync_id: str) -> dict | None:
974
+ """Get a specific component in PHT.
975
+
976
+ Args:
977
+ sync_id (str):
978
+ Unique PHT ID of the component.
979
+
980
+ Returns:
981
+ dict | None:
982
+ Details of the PHT component. None in case of an error.
983
+
984
+ """
985
+
986
+ request_header = self.request_header()
987
+ request_url = self.config()["componentUrl"] + "/" + str(sync_id)
988
+
989
+ return self.do_request(
990
+ url=request_url,
991
+ method="GET",
992
+ headers=request_header,
993
+ timeout=None,
994
+ failure_message="Failed to retrieve PHT component with sync ID -> {}!".format(
995
+ sync_id,
996
+ ),
997
+ )
998
+
999
+ # end method definition
1000
+
1001
+ def search_components(self, query: str) -> list | None:
1002
+ """Search for specific components in PHT by the component name.
1003
+
1004
+ Args:
1005
+ query (str): Search term to match any part of the component name.
1006
+
1007
+ Returns:
1008
+ list | None: List of matching components.
1009
+
1010
+ """
1011
+
1012
+ request_header = self.request_header()
1013
+ request_url = self.config()["componentSearchUrl"] + "?q=" + query
1014
+
1015
+ return self.do_request(
1016
+ url=request_url,
1017
+ method="GET",
1018
+ headers=request_header,
1019
+ timeout=None,
1020
+ failure_message="Failed to retrieve PHT components matching -> {}".format(
1021
+ query,
1022
+ ),
1023
+ )
1024
+
1025
+ # end method definition
1026
+
1027
+ def load_business_units(self, business_unit_list: list | None = None) -> bool:
1028
+ """Load business units into a data frame in the self._data object.
1029
+
1030
+ Args:
1031
+ business_unit_list (list, optional):
1032
+ List of business units - if already avaiable. Defaults to None.
1033
+ If None, then the list of all business units is created on-the-fly.
1034
+
1035
+ Returns:
1036
+ bool:
1037
+ True if successful, False otherwise.
1038
+
1039
+ """
1040
+
1041
+ if not business_unit_list:
1042
+ self.logger.info("Load PHT business unit list...")
1043
+ # First, get the list of all products:
1044
+ business_unit_list = self.get_business_units()
1045
+ if business_unit_list:
1046
+ self.logger.info(
1047
+ "Completed loading of -> %s PHT business units",
1048
+ str(len(business_unit_list)),
1049
+ )
473
1050
  else:
474
- logger.error(
475
- "Failed to get PHT master products; error -> %s (%s)",
476
- response.text,
477
- response.status_code,
1051
+ self.logger.error("Failed to load PHT business units!")
1052
+ return False
1053
+
1054
+ # Put the business unit list in an initial data frame.
1055
+ # This makes it easy to filter with powerful Pandas capabilities:
1056
+ self._data = Data(business_unit_list, logger=self.logger)
1057
+
1058
+ # Filter based on black list for business units:
1059
+ if self._business_unit_exclusions:
1060
+ self.logger.info("Found PHT business unit exclusions...")
1061
+ condition = [
1062
+ {
1063
+ "field": "name",
1064
+ "value": self._business_unit_exclusions,
1065
+ "equal": False,
1066
+ },
1067
+ ]
1068
+ self._data.filter(conditions=condition)
1069
+
1070
+ # Filter based on white list for business units:
1071
+ if self._business_unit_inclusions:
1072
+ self.logger.info("Found PHT business unit inclusions...")
1073
+ condition = [
1074
+ {"field": "name", "value": self._business_unit_inclusions},
1075
+ ]
1076
+ self._data.filter(conditions=condition)
1077
+
1078
+ return bool(self._data)
1079
+
1080
+ # end method definition
1081
+
1082
+ def load_product_families(self, product_family_list: list | None = None, append: bool = False) -> bool:
1083
+ """Load product families (LoBs) into a data frame in the self._data object.
1084
+
1085
+ Args:
1086
+ product_family_list (list, optional):
1087
+ List of product families (LoBs) - if already avaiable. Defaults to None.
1088
+ If None, then the list of all product families is created on-the-fly.
1089
+ append (bool):
1090
+ Whether or not the product families should be added to an existing data frame
1091
+ or if the data frame should be reset with the product family data only.
1092
+ Default is False (drop existing data rows).
1093
+
1094
+ Returns:
1095
+ bool:
1096
+ True if successful, False otherwise.
1097
+
1098
+ """
1099
+
1100
+ if not product_family_list:
1101
+ self.logger.info("Load PHT product family (LoB) list...")
1102
+ # First, get the list of all products:
1103
+ product_family_list = self.get_product_families()
1104
+ if product_family_list:
1105
+ self.logger.info(
1106
+ "Completed loading of -> %s PHT product families (LoBs)",
1107
+ str(len(product_family_list)),
478
1108
  )
479
- return None
1109
+ else:
1110
+ self.logger.error("Failed to load PHT product families!")
1111
+ return False
1112
+
1113
+ # Put the product family (LoB) list in an initial data frame.
1114
+ # This makes it easy to filter with powerful Pandas capabilities:
1115
+ data = Data(product_family_list, logger=self.logger)
1116
+
1117
+ # Filter based on black list for business units:
1118
+ if self._business_unit_exclusions:
1119
+ self.logger.info("Found PHT business unit exclusions...")
1120
+ condition = [
1121
+ {
1122
+ "field": "businessUnit.name",
1123
+ "value": self._business_unit_exclusions,
1124
+ "equal": False,
1125
+ },
1126
+ ]
1127
+ data.filter(conditions=condition)
1128
+
1129
+ # Filter based on white list for business units:
1130
+ if self._business_unit_inclusions:
1131
+ self.logger.info("Found PHT business unit inclusions...")
1132
+ condition = [
1133
+ {"field": "businessUnit.name", "value": self._business_unit_inclusions},
1134
+ ]
1135
+ data.filter(conditions=condition)
1136
+
1137
+ if self.get_data() and not data.get_data_frame().empty and append:
1138
+ self.get_data().append(add_data=data)
1139
+ else:
1140
+ self._data = data
1141
+
1142
+ return bool(self._data)
480
1143
 
481
1144
  # end method definition
482
1145
 
483
- def load_products(self, product_list: list = None) -> bool:
484
- """Load products into a data frame in the self._data object
1146
+ def load_products(
1147
+ self,
1148
+ product_list: list | None = None,
1149
+ append: bool = False,
1150
+ attributes_to_extract: list | None = None,
1151
+ ) -> bool:
1152
+ """Load products into a data frame in the self._data object.
1153
+
1154
+ The data frame has these columns:
1155
+ "syncId"
1156
+ "id"
1157
+ "name"
1158
+ "shortCode"
1159
+ "family"
1160
+ "businessUnit"
1161
+ "familySyncId"
1162
+ "businessUnitSyncId"
1163
+ "manager"
1164
+ "developmentManager"
1165
+ "status"
1166
+ "category"
1167
+ "attributes"
485
1168
 
486
1169
  Args:
487
- product_list (list, optional): listn of products - if already avaiable. Defaults to None.
1170
+ product_list (list, optional):
1171
+ List of products - if already avaiable. Defaults to None.
1172
+ If None, then the list of all products is created on-the-fly.
1173
+ append (bool):
1174
+ Whether or not the products should be added to an existing data frame
1175
+ or if the data frame should be reset with the product data only.
1176
+ Default is False (drop existing data rows).
1177
+ attributes_to_extract (list):
1178
+ A list of attributes names that should be extracted for the PHT
1179
+ "attributes" data structure inside product.
488
1180
 
489
1181
  Returns:
490
- bool: True if successful, False otherwise.
1182
+ bool:
1183
+ True if successful, False otherwise.
1184
+
491
1185
  """
492
1186
 
493
1187
  if not product_list:
1188
+ self.logger.info("Load PHT product list...")
1189
+ # First, get the list of all products:
494
1190
  product_list = self.get_products()
1191
+ if product_list:
1192
+ self.logger.info(
1193
+ "Completed loading of -> %s PHT products",
1194
+ str(len(product_list)),
1195
+ )
1196
+ else:
1197
+ self.logger.error("Failed to load PHT products!")
1198
+ return False
1199
+
1200
+ attribute_columns = []
1201
+
1202
+ for product in product_list:
1203
+ product_family = product["productFamily"]
1204
+ business_unit = product_family["businessUnit"]
1205
+ category = product.get("productCategory")
1206
+
1207
+ product["businessUnitSyncId"] = business_unit["syncId"]
1208
+ product["familySyncId"] = product_family["syncId"]
1209
+ if category:
1210
+ product["category"] = category["name"]
1211
+
1212
+ attributes = product.get("attributes")
1213
+ # Does this product have attributes and do we want to extract any?
1214
+ if attributes and attributes_to_extract:
1215
+ for attribute in attributes:
1216
+ if attribute.get("name") in attributes_to_extract:
1217
+ # We fist check if there's a text value
1218
+ value = None
1219
+ value = attribute.get("textAttributeValue")
1220
+ # If we don't have a text value we try to get a list value:
1221
+ if not value and attribute.get("listAttributeValue"):
1222
+ value = attribute.get("listAttributeValue")["name"]
1223
+ # Create a new key / value pait with the extracted attribute and its value:
1224
+ product[attribute.get("name")] = value
1225
+ # We keep the attribute name as a column below:
1226
+ if attribute.get("name") not in attribute_columns:
1227
+ attribute_columns.append(attribute.get("name"))
1228
+
1229
+ # Put the product list in an initial data frame.
1230
+ # This makes it easy to filter with powerful Pandas capabilities:
1231
+ data = Data(product_list, logger=self.logger)
1232
+
1233
+ data.keep_columns(
1234
+ column_names=[
1235
+ "syncId",
1236
+ "id",
1237
+ "name",
1238
+ "shortCode",
1239
+ "family",
1240
+ "businessUnit",
1241
+ "familySyncId",
1242
+ "businessUnitSyncId",
1243
+ "manager",
1244
+ "developmentManager",
1245
+ "status",
1246
+ "category",
1247
+ "attributes",
1248
+ "comment",
1249
+ ]
1250
+ + attribute_columns,
1251
+ )
1252
+ # Filter based on black list for Business Units:
1253
+ if self._business_unit_exclusions:
1254
+ self.logger.info("Found PHT business unit exclusions...")
1255
+ condition = [
1256
+ {
1257
+ "field": "businessUnit",
1258
+ "value": self._business_unit_exclusions,
1259
+ "equal": False,
1260
+ },
1261
+ ]
1262
+ data.filter(conditions=condition)
1263
+
1264
+ # Filter based on white list for Business Units:
1265
+ if self._business_unit_inclusions:
1266
+ self.logger.info("Found PHT business unit inclusions...")
1267
+ condition = [
1268
+ {"field": "businessUnit", "value": self._business_unit_inclusions},
1269
+ ]
1270
+ data.filter(conditions=condition)
1271
+
1272
+ # Filter based on black list for products:
1273
+ if self._product_exclusions:
1274
+ self.logger.info("Found PHT product exclusions...")
1275
+ condition = [
1276
+ {"field": "name", "value": self._product_exclusions, "equal": False},
1277
+ ]
1278
+ data.filter(conditions=condition)
1279
+
1280
+ # Filter based on white list for products:
1281
+ if self._product_inclusions:
1282
+ self.logger.info("Found PHT product inclusions...")
1283
+ condition = [{"field": "name", "value": self._product_inclusions}]
1284
+ data.filter(conditions=condition)
1285
+
1286
+ # Filter based on black list for product categories:
1287
+ if self._product_category_exclusions:
1288
+ self.logger.info("Found PHT product category exclusions...")
1289
+ condition = [
1290
+ {
1291
+ "field": "category",
1292
+ "value": self._product_category_exclusions,
1293
+ "equal": False,
1294
+ },
1295
+ ]
1296
+ data.filter(conditions=condition)
1297
+
1298
+ # Filter based on white list for product categories:
1299
+ if self._product_category_inclusions:
1300
+ self.logger.info("Found PHT product category inclusions...")
1301
+ condition = [
1302
+ {
1303
+ "field": "category",
1304
+ "value": self._product_category_inclusions,
1305
+ },
1306
+ ]
1307
+ data.filter(conditions=condition)
495
1308
 
496
- self._data = Data(product_list)
1309
+ # Filter based on product status exclusions:
1310
+ if self._product_status_exclusions:
1311
+ self.logger.info("Found PHT product status exclusions...")
1312
+ condition = [
1313
+ {
1314
+ "field": "status",
1315
+ "value": self._product_status_exclusions,
1316
+ "equal": False,
1317
+ },
1318
+ ]
1319
+ data.filter(conditions=condition)
497
1320
 
498
- if self._data:
499
- return True
1321
+ # Filter based on product status inclusions:
1322
+ if self._product_status_inclusions:
1323
+ self.logger.info("Found PHT product status inclusions...")
1324
+ condition = [
1325
+ {
1326
+ "field": "status",
1327
+ "value": self._product_status_inclusions,
1328
+ "equal": True,
1329
+ },
1330
+ ]
1331
+ data.filter(conditions=condition)
1332
+
1333
+ if self.get_data() and not data.get_data_frame().empty and append:
1334
+ self.get_data().append(data)
1335
+ else:
1336
+ self._data = data
500
1337
 
501
- return False
1338
+ return bool(self._data)
502
1339
 
503
1340
  # end method definition