pyxecm 1.6__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 -4
  2. pyxecm/avts.py +673 -246
  3. pyxecm/coreshare.py +686 -467
  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 +1007 -1130
  16. pyxecm/customizer/exceptions.py +35 -0
  17. pyxecm/customizer/guidewire.py +322 -0
  18. pyxecm/customizer/k8s.py +713 -378
  19. pyxecm/customizer/log.py +107 -0
  20. pyxecm/customizer/m365.py +2867 -909
  21. pyxecm/customizer/nhc.py +1169 -0
  22. pyxecm/customizer/openapi.py +258 -0
  23. pyxecm/customizer/payload.py +16817 -7467
  24. pyxecm/customizer/pht.py +699 -285
  25. pyxecm/customizer/salesforce.py +516 -342
  26. pyxecm/customizer/sap.py +58 -41
  27. pyxecm/customizer/servicenow.py +593 -371
  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 +83 -43
  33. pyxecm/helper/data.py +2406 -870
  34. pyxecm/helper/logadapter.py +27 -0
  35. pyxecm/helper/web.py +229 -101
  36. pyxecm/helper/xml.py +527 -171
  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 +1436 -557
  45. pyxecm/otcs.py +7716 -3161
  46. pyxecm/otds.py +2150 -919
  47. pyxecm/otiv.py +36 -21
  48. pyxecm/otmm.py +1272 -325
  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.6.dist-info → pyxecm-2.0.0.dist-info}/WHEEL +1 -1
  53. pyxecm-1.6.dist-info/METADATA +0 -53
  54. pyxecm-1.6.dist-info/RECORD +0 -32
  55. {pyxecm-1.6.dist-info → pyxecm-2.0.0.dist-info/licenses}/LICENSE +0 -0
  56. {pyxecm-1.6.dist-info → pyxecm-2.0.0.dist-info}/top_level.txt +0 -0
pyxecm/customizer/pht.py CHANGED
@@ -1,48 +1,16 @@
1
- """
2
- PHT stands for Product Hierarchy Tracker and 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
- do_request: Call an PHT REST API in a safe way
14
- parse_request_response: Parse the REST API responses and convert
15
- them to Python dict in a safe way
16
-
17
- authenticate : Authenticates at ServiceNow API
1
+ """PHT stands for Product Hierarchy Tracker.
18
2
 
19
- get_attributes: Get a list of all product attributes (schema) of PHT
20
- get_business_units: Get the list of PHT Business Units
21
- get_product_families: Get the list of PHT product families
22
-
23
- get_products: Get the list of PHT products
24
- get_products_filtered: Get a list of filtered PHT products
25
- get_product: Get a specific product in PHT
26
- search_products: Search products in PHT
27
-
28
- get_master_products: Get the list of PHT master products
29
- get_master_products_filtered: Get a list of filtered PHT master products
30
- get_master_product: Get a specific product in PHT
31
-
32
- get_teams: Get a list of all teams in PHT.
33
- get_team: Get a specific team in PHT.
34
-
35
- get_componets: Get a list of all components in PHT.
36
- get_components_filtered: Get a list of filtered PHT components.
37
- get_component: Get a specific component in PHT.
38
- search_components: Search components in PHT
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.
39
5
 
40
- load_products: Load products into a data frame.
6
+ See: https://pht.opentext.com
41
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
42
10
  """
43
11
 
44
12
  __author__ = "Dr. Marc Diefenbruch"
45
- __copyright__ = "Copyright 2024, OpenText"
13
+ __copyright__ = "Copyright (C) 2024-2025, OpenText"
46
14
  __credits__ = ["Kai-Philip Gatzweiler"]
47
15
  __maintainer__ = "Dr. Marc Diefenbruch"
48
16
  __email__ = "mdiefenb@opentext.com"
@@ -53,9 +21,10 @@ import time
53
21
 
54
22
  import requests
55
23
  from requests.auth import HTTPBasicAuth
56
- from pyxecm.helper.data import Data
57
24
 
58
- logger = logging.getLogger("pyxecm.customizer.pht")
25
+ from pyxecm.helper import Data
26
+
27
+ default_logger = logging.getLogger("pyxecm.customizer.pht")
59
28
 
60
29
  REQUEST_HEADERS = {"Accept": "application/json", "Content-Type": "application/json"}
61
30
 
@@ -63,26 +32,75 @@ REQUEST_TIMEOUT = 60
63
32
  REQUEST_RETRY_DELAY = 20
64
33
  REQUEST_MAX_RETRIES = 2
65
34
 
66
- class PHT(object):
67
- """Used to retrieve data from OpenText PHT."""
35
+
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,)
68
40
 
69
41
  _config: dict
70
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
71
51
 
72
52
  def __init__(
73
53
  self,
74
54
  base_url: str,
75
55
  username: str,
76
56
  password: str,
77
- ):
78
- """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.
79
68
 
80
69
  Args:
81
- base_url (str): base URL of the ServiceNow tenant
82
- username (str): user name in Saleforce
83
- 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
+
84
97
  """
85
98
 
99
+ if logger != default_logger:
100
+ self.logger = logger.getChild("pht")
101
+ for logfilter in logger.filters:
102
+ self.logger.addFilter(logfilter)
103
+
86
104
  pht_config = {}
87
105
 
88
106
  # Store the credentials and parameters in a config dictionary:
@@ -100,9 +118,7 @@ class PHT(object):
100
118
  pht_config["productUsersUrl"] = pht_config["productUrl"] + "/users"
101
119
  pht_config["teamUrl"] = pht_config["restUrl"] + "/team"
102
120
  pht_config["masterProductUrl"] = pht_config["restUrl"] + "/master-product"
103
- pht_config["masterProductFilteredUrl"] = (
104
- pht_config["masterProductUrl"] + "/filtered"
105
- )
121
+ pht_config["masterProductFilteredUrl"] = pht_config["masterProductUrl"] + "/filtered"
106
122
  pht_config["componentUrl"] = pht_config["restUrl"] + "/component"
107
123
  pht_config["componentFilteredUrl"] = pht_config["componentUrl"] + "/filtered"
108
124
  pht_config["componentSearchUrl"] = pht_config["componentUrl"] + "/search"
@@ -112,25 +128,38 @@ class PHT(object):
112
128
 
113
129
  self._session = requests.Session()
114
130
 
115
- 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
116
141
 
117
142
  # end method definition
118
143
 
119
144
  def config(self) -> dict:
120
- """Returns the configuration dictionary
145
+ """Return the configuration dictionary.
121
146
 
122
147
  Returns:
123
- dict: Configuration dictionary
148
+ dict:
149
+ The configuration dictionary.
150
+
124
151
  """
125
152
  return self._config
126
153
 
127
154
  # end method definition
128
155
 
129
156
  def get_data(self) -> Data:
130
- """Get the Data object that holds all processed PHT products
157
+ """Get the Data object that holds all processed PHT products.
131
158
 
132
159
  Returns:
133
- Data: Datastructure with all processed PHT product data.
160
+ Data:
161
+ Datastructure with all processed PHT product data.
162
+
134
163
  """
135
164
 
136
165
  return self._data
@@ -138,13 +167,23 @@ class PHT(object):
138
167
  # end method definition
139
168
 
140
169
  def request_header(self, content_type: str = "") -> dict:
141
- """Returns the request header used for Application calls.
142
- 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
143
173
 
144
174
  Args:
145
- content_type (str, optional): custom content type for the request
146
- Return:
147
- 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
+
148
187
  """
149
188
 
150
189
  request_header = {}
@@ -164,6 +203,7 @@ class PHT(object):
164
203
  method: str = "GET",
165
204
  headers: dict | None = None,
166
205
  data: dict | None = None,
206
+ json_data: dict | None = None,
167
207
  files: dict | None = None,
168
208
  timeout: int | None = REQUEST_TIMEOUT,
169
209
  show_error: bool = True,
@@ -172,24 +212,40 @@ class PHT(object):
172
212
  max_retries: int = REQUEST_MAX_RETRIES,
173
213
  retry_forever: bool = False,
174
214
  ) -> dict | None:
175
- """Call an PHT REST API in a safe way
215
+ """Call an PHT REST API in a safe way.
176
216
 
177
217
  Args:
178
- url (str): URL to send the request to.
179
- method (str, optional): HTTP method (GET, POST, etc.). Defaults to "GET".
180
- headers (dict | None, optional): Request Headers. Defaults to None.
181
- json (dict | None, optional): Request payload. Defaults to None.
182
- files (dict | None, optional): Dictionary of {"name": file-tuple} for multipart encoding upload.
183
- file-tuple can be a 2-tuple ("filename", fileobj) or a 3-tuple ("filename", fileobj, "content_type")
184
- timeout (int | None, optional): Timeout for the request in seconds. Defaults to REQUEST_TIMEOUT.
185
- show_error (bool, optional): Whether or not an error should be logged in case of a failed REST call.
186
- If False, then only a warning is logged. Defaults to True.
187
- failure_message (str, optional): Specific error message. Defaults to "".
188
- max_retries (int, optional): How many retries on Connection errors? Default is REQUEST_MAX_RETRIES.
189
- retry_forever (bool, optional): Eventually wait forever - without timeout. Defaults to False.
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.
190
244
 
191
245
  Returns:
192
- dict | None: Response of PHT REST API or None in case of an error.
246
+ dict | None:
247
+ Response of PHT REST API or None in case of an error.
248
+
193
249
  """
194
250
 
195
251
  retries = 0
@@ -198,7 +254,8 @@ class PHT(object):
198
254
  response = self._session.request(
199
255
  method=method,
200
256
  url=url,
201
- json=data,
257
+ data=data,
258
+ json=json_data,
202
259
  files=files,
203
260
  headers=headers,
204
261
  timeout=timeout,
@@ -206,30 +263,27 @@ class PHT(object):
206
263
 
207
264
  if response.ok:
208
265
  if success_message:
209
- logger.debug(success_message)
266
+ self.logger.debug(success_message)
210
267
  return self.parse_request_response(response)
211
268
  # Check if Session has expired - then re-authenticate and try once more
212
269
  elif response.status_code == 401 and retries == 0:
213
- logger.debug("Session has expired - try to re-authenticate...")
270
+ self.logger.debug("Session has expired - try to re-authenticate...")
214
271
  self.authenticate()
215
272
  retries += 1
216
273
  else:
217
274
  # Handle plain HTML responses to not pollute the logs
218
275
  content_type = response.headers.get("content-type", None)
219
- if content_type == "text/html":
220
- response_text = "HTML content (see debug log)"
221
- else:
222
- response_text = response.text
276
+ response_text = "HTML content (see debug log)" if content_type == "text/html" else response.text
223
277
 
224
278
  if show_error:
225
- logger.error(
279
+ self.logger.error(
226
280
  "%s; status -> %s; error -> %s",
227
281
  failure_message,
228
282
  response.status_code,
229
283
  response_text,
230
284
  )
231
285
  else:
232
- logger.warning(
286
+ self.logger.warning(
233
287
  "%s; status -> %s; warning -> %s",
234
288
  failure_message,
235
289
  response.status_code,
@@ -237,7 +291,7 @@ class PHT(object):
237
291
  )
238
292
 
239
293
  if content_type == "text/html":
240
- logger.debug(
294
+ self.logger.debug(
241
295
  "%s; status -> %s; warning -> %s",
242
296
  failure_message,
243
297
  response.status_code,
@@ -247,39 +301,39 @@ class PHT(object):
247
301
  return None
248
302
  except requests.exceptions.Timeout:
249
303
  if retries <= max_retries:
250
- logger.warning(
304
+ self.logger.warning(
251
305
  "Request timed out. Retrying in %s seconds...",
252
306
  str(REQUEST_RETRY_DELAY),
253
307
  )
254
308
  retries += 1
255
309
  time.sleep(REQUEST_RETRY_DELAY) # Add a delay before retrying
256
310
  else:
257
- logger.error(
258
- "%s; timeout error",
311
+ self.logger.error(
312
+ "%s; timeout error,",
259
313
  failure_message,
260
314
  )
261
315
  if retry_forever:
262
316
  # If it fails after REQUEST_MAX_RETRIES retries we let it wait forever
263
- logger.warning("Turn timeouts off and wait forever...")
317
+ self.logger.warning("Turn timeouts off and wait forever...")
264
318
  timeout = None
265
319
  else:
266
320
  return None
267
321
  except requests.exceptions.ConnectionError:
268
322
  if retries <= max_retries:
269
- logger.warning(
323
+ self.logger.warning(
270
324
  "Connection error. Retrying in %s seconds...",
271
325
  str(REQUEST_RETRY_DELAY),
272
326
  )
273
327
  retries += 1
274
328
  time.sleep(REQUEST_RETRY_DELAY) # Add a delay before retrying
275
329
  else:
276
- logger.error(
277
- "%s; connection error",
330
+ self.logger.error(
331
+ "%s; connection error.",
278
332
  failure_message,
279
333
  )
280
334
  if retry_forever:
281
335
  # If it fails after REQUEST_MAX_RETRIES retries we let it wait forever
282
- logger.warning("Turn timeouts off and wait forever...")
336
+ self.logger.warning("Turn timeouts off and wait forever...")
283
337
  timeout = None
284
338
  time.sleep(REQUEST_RETRY_DELAY) # Add a delay before retrying
285
339
  else:
@@ -293,51 +347,59 @@ class PHT(object):
293
347
  additional_error_message: str = "",
294
348
  show_error: bool = True,
295
349
  ) -> list | None:
296
- """Converts the request response (JSon) to a Python list in a safe way
297
- that also handles exceptions. It first tries to load the response.text
298
- via json.loads() that produces a dict output. Only if response.text is
299
- not set or is empty it just converts the response_object to a dict using
300
- 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.
301
355
 
302
356
  Args:
303
- response_object (object): this is reponse object delivered by the request call
304
- additional_error_message (str, optional): use a more specific error message
305
- in case of an error
306
- show_error (bool): True: write an error to the log file
307
- 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
+
308
365
  Returns:
309
366
  list: response information or None in case of an error
367
+
310
368
  """
311
369
 
312
370
  if not response_object:
313
371
  return None
314
372
 
315
373
  try:
316
- if response_object.text:
317
- list_object = json.loads(response_object.text)
318
- else:
319
- list_object = vars(response_object)
374
+ list_object = json.loads(response_object.text) if response_object.text else vars(response_object)
320
375
  except json.JSONDecodeError as exception:
321
376
  if additional_error_message:
322
377
  message = "Cannot decode response as JSON. {}; error -> {}".format(
323
- additional_error_message, exception
378
+ additional_error_message,
379
+ exception,
324
380
  )
325
381
  else:
326
382
  message = "Cannot decode response as JSON; error -> {}".format(
327
- exception
383
+ exception,
328
384
  )
329
385
  if show_error:
330
- logger.error(message)
386
+ self.logger.error(message)
331
387
  else:
332
- logger.warning(message)
388
+ self.logger.warning(message)
333
389
  return None
334
390
  else:
335
391
  return list_object
336
392
 
337
393
  # end method definition
338
394
 
339
- def authenticate(self) -> str | None:
340
- """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
+ """
341
403
 
342
404
  self._session.headers.update(self.request_header())
343
405
 
@@ -352,18 +414,18 @@ class PHT(object):
352
414
  # end method definition
353
415
 
354
416
  def get_attributes(self) -> list | None:
355
- """Get a list of all product attributes (schema) of PHT
417
+ """Get a list of all product attributes (schema) of PHT.
356
418
 
357
419
  Returns:
358
420
  list | None: list of product attributes
359
421
 
360
- Example:
422
+ Example:
361
423
  [
362
424
  {
363
425
  'id': 28,
364
426
  'uuid': '43ba5852-eb83-11ed-a752-00505682262c',
365
427
  'name': 'UBM SCM Migration JIRA/ValueEdge',
366
- '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.',
367
429
  'type': 'TEXT',
368
430
  'attributeCategory': {
369
431
  'id': 2,
@@ -379,6 +441,7 @@ class PHT(object):
379
441
  'allowedTeams': []
380
442
  }
381
443
  ]
444
+
382
445
  """
383
446
 
384
447
  request_header = self.request_header()
@@ -395,12 +458,13 @@ class PHT(object):
395
458
  # end method definition
396
459
 
397
460
  def get_business_units(self) -> list | None:
398
- """Get the list of PHT Business Units
461
+ """Get the list of PHT Business Units.
399
462
 
400
463
  Returns:
401
- list | None: list of the known business units.
464
+ list | None:
465
+ The list of the known business units.
402
466
 
403
- Example:
467
+ Example:
404
468
  [
405
469
  {
406
470
  'id': 1,
@@ -449,6 +513,7 @@ class PHT(object):
449
513
  'sltOwnerDomain': 'jradko'
450
514
  }
451
515
  ]
516
+
452
517
  """
453
518
 
454
519
  request_header = self.request_header()
@@ -465,10 +530,12 @@ class PHT(object):
465
530
  # end method definition
466
531
 
467
532
  def get_product_families(self) -> list | None:
468
- """Get the list of PHT product families
533
+ """Get the list of PHT product families (LoBs).
469
534
 
470
535
  Returns:
471
- list | None: list of the known product families.
536
+ list | None:
537
+ A list of the known product families.
538
+
472
539
  """
473
540
 
474
541
  request_header = self.request_header()
@@ -479,16 +546,18 @@ class PHT(object):
479
546
  method="GET",
480
547
  headers=request_header,
481
548
  timeout=None,
482
- failure_message="Failed to get PHT product families!",
549
+ failure_message="Failed to get PHT product families",
483
550
  )
484
551
 
485
552
  # end method definition
486
553
 
487
554
  def get_products(self) -> list | None:
488
- """Get the list of PHT products
555
+ """Get the list of PHT products.
489
556
 
490
557
  Returns:
491
- list | None: list of the known products.
558
+ list | None:
559
+ A list of the known products.
560
+
492
561
  """
493
562
 
494
563
  request_header = self.request_header()
@@ -499,39 +568,44 @@ class PHT(object):
499
568
  method="GET",
500
569
  headers=request_header,
501
570
  timeout=None,
502
- failure_message="Failed to get PHT products!",
571
+ failure_message="Failed to get PHT products",
503
572
  )
504
573
 
505
574
  # end method definition
506
575
 
507
576
  def get_products_filtered(
508
- self, filter_definition: dict | None = None
577
+ self,
578
+ filter_definition: dict | None = None,
509
579
  ) -> list | None:
510
- """Get a list of filtered PHT products
580
+ """Get a list of filtered PHT products.
511
581
 
512
582
  Args:
513
- filter_definition (dict): a dictionary of filter conditions.
514
- Example filters:
515
- {
516
- businessUnitName: <String>,
517
- productFamilyName: <String>,
518
- productName: <String>,
519
- productSyncId: <String>,
520
- productStatus: ACTIVE | INACTIVE | MAINTENANCE,
521
- productManager: <String>,
522
- developmentManager: <String>,
523
- attributeOperator: AND | OR,
524
- attributes: {
525
- "<AttributeName>": {
526
- "compare": CONTAINS | EXISTS | DOES_NOT_EXISTS,
527
- "values": List<String>
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
+ ...
528
601
  },
529
- ...
530
- },
531
- includeAttributes: true | false
532
- }
602
+ includeAttributes: true | false
603
+ statuses: ["ACTIVE"],
604
+ }
605
+
533
606
  Returns:
534
607
  list | None: list of matching products.
608
+
535
609
  """
536
610
 
537
611
  if not filter_definition:
@@ -545,18 +619,22 @@ class PHT(object):
545
619
  url=request_url,
546
620
  method="POST",
547
621
  headers=request_header,
548
- data=request_data,
622
+ json_data=request_data,
549
623
  timeout=None,
550
- failure_message="Failed to get filtered PHT products!",
624
+ failure_message="Failed to get filtered PHT products",
551
625
  )
552
626
 
553
627
  # end method definition
554
628
 
555
- def get_product(self, sync_id: int) -> dict | None:
629
+ def get_product(self, sync_id: str) -> dict | None:
556
630
  """Get a specific product in PHT.
557
631
 
632
+ Args:
633
+ sync_id (str): Unique ID of the PHT product.
634
+
558
635
  Returns:
559
636
  dict | None: product data matching the sync ID
637
+
560
638
  """
561
639
 
562
640
  request_header = self.request_header()
@@ -568,19 +646,32 @@ class PHT(object):
568
646
  headers=request_header,
569
647
  timeout=None,
570
648
  failure_message="Failed to retrieve PHT product with sync ID -> {}!".format(
571
- sync_id
649
+ sync_id,
572
650
  ),
573
651
  )
574
652
 
575
653
  # end method definition
576
654
 
577
655
  def search_products(
578
- self, query: str, business_unit: str | None = None, family: str | None = None
656
+ self,
657
+ query: str,
658
+ business_unit: str | None = None,
659
+ family: str | None = None,
579
660
  ) -> list | None:
580
- """Search for specific product in PHT by the product name.
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)
581
670
 
582
671
  Returns:
583
- str: search term matches any part of the component name
672
+ list | None:
673
+ Search term matches any part of the component name.
674
+
584
675
  """
585
676
 
586
677
  request_header = self.request_header()
@@ -596,17 +687,19 @@ class PHT(object):
596
687
  headers=request_header,
597
688
  timeout=None,
598
689
  failure_message="Failed to retrieve PHT components matching -> {}!".format(
599
- query
690
+ query,
600
691
  ),
601
692
  )
602
693
 
603
694
  # end method definition
604
695
 
605
696
  def get_master_products(self) -> list | None:
606
- """Get the list of PHT master products
697
+ """Get the list of PHT master products.
607
698
 
608
699
  Returns:
609
- list | None: list of the known master products.
700
+ list | None:
701
+ A list of the known master products.
702
+
610
703
  """
611
704
 
612
705
  request_header = self.request_header()
@@ -617,39 +710,43 @@ class PHT(object):
617
710
  method="GET",
618
711
  headers=request_header,
619
712
  timeout=None,
620
- failure_message="Failed to get PHT master products!",
713
+ failure_message="Failed to get PHT master products",
621
714
  )
622
715
 
623
716
  # end method definition
624
717
 
625
718
  def get_master_products_filtered(
626
- self, filter_definition: dict | None = None
719
+ self,
720
+ filter_definition: dict | None = None,
627
721
  ) -> list | None:
628
- """Get a list of filtered PHT master products
722
+ """Get a list of filtered PHT master products.
629
723
 
630
724
  Args:
631
- filter_definition (dict): a dictionary of filter conditions.
632
- Example filters:
633
- {
634
- businessUnitName: <String>,
635
- productFamilyName: <String>,
636
- masterproductName: <String>,
637
- masterproductSyncId: <String>,
638
- masterproductStatus: ACTIVE | INACTIVE | MAINTENANCE,
639
- productManagerDomain: <String>,
640
- attributeOperator: AND | OR,
641
- attributes: {
642
- "<AttributeName>": {
643
- "compare": CONTAINS | EXISTS | DOES_NOT_EXISTS,
644
- "values": List<String>
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
+ ...
645
742
  },
646
- ...
647
- },
648
- includeAttributes: true | false
649
- includeLinkedProducts: true | false
650
- }
743
+ includeAttributes: true | false
744
+ includeLinkedProducts: true | false
745
+ }
746
+
651
747
  Returns:
652
- list | None: list of matching products.
748
+ list | None: List of matching products.
749
+
653
750
  """
654
751
 
655
752
  if not filter_definition:
@@ -663,18 +760,22 @@ class PHT(object):
663
760
  url=request_url,
664
761
  method="POST",
665
762
  headers=request_header,
666
- data=request_data,
763
+ json_data=request_data,
667
764
  timeout=None,
668
- failure_message="Failed to get filtered PHT master products!",
765
+ failure_message="Failed to get filtered PHT master products",
669
766
  )
670
767
 
671
768
  # end method definition
672
769
 
673
- def get_master_product(self, sync_id: int) -> dict | None:
770
+ def get_master_product(self, sync_id: str) -> dict | None:
674
771
  """Get a specific product in PHT.
675
772
 
773
+ Args:
774
+ sync_id (str): Unique PHT ID of the master product.
775
+
676
776
  Returns:
677
777
  dict | None: product data matching the sync ID
778
+
678
779
  """
679
780
 
680
781
  request_header = self.request_header()
@@ -685,18 +786,19 @@ class PHT(object):
685
786
  method="GET",
686
787
  headers=request_header,
687
788
  timeout=None,
688
- failure_message="Failed to retrieve PHT product with sync ID -> {}!".format(
689
- sync_id
789
+ failure_message="Failed to retrieve PHT product with sync ID -> {}".format(
790
+ sync_id,
690
791
  ),
691
792
  )
692
793
 
693
794
  # end method definition
694
795
 
695
- def get_teams(self) -> list:
796
+ def get_teams(self) -> list | None:
696
797
  """Get a list of all teams in PHT.
697
798
 
698
799
  Returns:
699
- list: list of PHT teams
800
+ list | None: list of PHT teams
801
+
700
802
  """
701
803
 
702
804
  request_header = self.request_header()
@@ -707,16 +809,20 @@ class PHT(object):
707
809
  method="GET",
708
810
  headers=request_header,
709
811
  timeout=None,
710
- failure_message="Failed to retrieve PHT teams!",
812
+ failure_message="Failed to retrieve PHT teams",
711
813
  )
712
814
 
713
815
  # end method definition
714
816
 
715
- def get_team(self, team_id: int) -> dict | None:
817
+ def get_team(self, team_id: str) -> dict | None:
716
818
  """Get a specific team in PHT.
717
819
 
820
+ Args:
821
+ team_id (str): Unique PHT ID of the team.
822
+
718
823
  Returns:
719
- dict | None: dict of the PHT team
824
+ dict | None: Details of the PHT team.
825
+
720
826
  """
721
827
 
722
828
  request_header = self.request_header()
@@ -727,7 +833,7 @@ class PHT(object):
727
833
  method="GET",
728
834
  headers=request_header,
729
835
  timeout=None,
730
- failure_message="Failed to retrieve PHT teams!",
836
+ failure_message="Failed to retrieve PHT team with ID -> {}".format(team_id),
731
837
  )
732
838
 
733
839
  # end method definition
@@ -738,67 +844,68 @@ class PHT(object):
738
844
  Returns:
739
845
  list: list of PHT components
740
846
 
741
- Example result:
742
- [
743
- {
744
- 'id': 468,
745
- 'syncId': '6380c3da-8ded-40cd-8071-61f6721956f5',
746
- 'name': 'XOTE',
747
- 'developmentManager': {
748
- 'id': 237,
749
- 'domain': 'kurt',
750
- 'email': 'kurt.junker@opentext.com',
751
- 'name': 'Kurt Junker',
752
- 'role': None,
753
- 'status': 'ACTIVE',
754
- 'location': 'Grasbrunn, DEU',
755
- 'title': 'Sr. Manager, Software Engineering',
756
- 'type': 'OTHERS'
757
- },
758
- 'componentCategory': {
759
- 'id': 2,
760
- 'name': 'Testing scripts',
761
- 'shortName': 'Testing scripts'
762
- },
763
- 'comment': 'Test Framework maintained and used by Core Archive Team',
764
- 'status': 'MAINTENANCE',
765
- 'attributes': [
766
- {
767
- 'id': 409,
768
- 'attribute': {
769
- 'id': 4,
770
- 'uuid': '03e228b5-9eae-11ea-96ab-00505682bce9',
771
- 'name': 'Build Advocate',
772
- 'description': 'Primary contact for build items.',
773
- 'type': 'USER'
774
- },
775
- 'value': 'burkhard',
776
- 'textAttributeValue': None,
777
- 'userAttributeValue': {
778
- 'id': 414,
779
- 'domain': 'burkhard',
780
- 'email': 'burkhard.meier@opentext.com',
781
- 'name': 'Burkhard Meier',
782
- 'role': None,
783
- 'status': 'ACTIVE',
784
- 'location': 'Virtual, DEU',
785
- 'title': 'Principal Software Engineer',
786
- 'type': 'DEV'
787
- },
788
- 'listAttributeValue': None
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'
789
863
  },
790
- ...
791
- ],
792
- 'sourceRepos': [],
793
- 'artifacts': [],
794
- 'products': [],
795
- 'teams': [],
796
- 'users': [],
797
- 'guestTeams': [],
798
- 'guestUsers': [],
799
- 'relatedLOBS': []
800
- }
801
- ]
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
+
802
909
  """
803
910
 
804
911
  request_header = self.request_header()
@@ -809,36 +916,40 @@ class PHT(object):
809
916
  method="GET",
810
917
  headers=request_header,
811
918
  timeout=None,
812
- failure_message="Failed to retrieve PHT components!",
919
+ failure_message="Failed to retrieve PHT components",
813
920
  )
814
921
 
815
922
  # end method definition
816
923
 
817
924
  def get_components_filtered(
818
- self, filter_definition: dict | None = None
925
+ self,
926
+ filter_definition: dict | None = None,
819
927
  ) -> list | None:
820
- """Get a list of filtered PHT components
928
+ """Get a list of filtered PHT components.
821
929
 
822
930
  Args:
823
- filter_definition (dict): a dictionary of filter conditions.
824
- Example filters:
825
- {
826
- componentName: <String>,
827
- componentSyncId: <String>,
828
- componentStatus: ACTIVE | INACTIVE | MAINTENANCE,
829
- developmentManager: <String>,
830
- attributeOperator: AND | OR,
831
- attributes: {
832
- "<AttributeName>": {
833
- "compare": CONTAINS | EXISTS | DOES_NOT_EXISTS,
834
- "values": List<String>
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
+ ...
835
946
  },
836
- ...
837
- },
838
- includeAttributes: true | false
839
- }
947
+ includeAttributes: true | false
948
+ }
949
+
840
950
  Returns:
841
951
  list | None: list of matching components.
952
+
842
953
  """
843
954
 
844
955
  if not filter_definition:
@@ -852,18 +963,24 @@ class PHT(object):
852
963
  url=request_url,
853
964
  method="POST",
854
965
  headers=request_header,
855
- data=request_data,
966
+ json_data=request_data,
856
967
  timeout=None,
857
- failure_message="Failed to get filtered PHT master products!",
968
+ failure_message="Failed to get filtered PHT components",
858
969
  )
859
970
 
860
971
  # end method definition
861
972
 
862
- def get_component(self, sync_id: int) -> dict | None:
973
+ def get_component(self, sync_id: str) -> dict | None:
863
974
  """Get a specific component in PHT.
864
975
 
976
+ Args:
977
+ sync_id (str):
978
+ Unique PHT ID of the component.
979
+
865
980
  Returns:
866
- dict | None: PHT component
981
+ dict | None:
982
+ Details of the PHT component. None in case of an error.
983
+
867
984
  """
868
985
 
869
986
  request_header = self.request_header()
@@ -875,7 +992,7 @@ class PHT(object):
875
992
  headers=request_header,
876
993
  timeout=None,
877
994
  failure_message="Failed to retrieve PHT component with sync ID -> {}!".format(
878
- sync_id
995
+ sync_id,
879
996
  ),
880
997
  )
881
998
 
@@ -884,8 +1001,12 @@ class PHT(object):
884
1001
  def search_components(self, query: str) -> list | None:
885
1002
  """Search for specific components in PHT by the component name.
886
1003
 
1004
+ Args:
1005
+ query (str): Search term to match any part of the component name.
1006
+
887
1007
  Returns:
888
- str: search term matches any part of the component name
1008
+ list | None: List of matching components.
1009
+
889
1010
  """
890
1011
 
891
1012
  request_header = self.request_header()
@@ -896,31 +1017,324 @@ class PHT(object):
896
1017
  method="GET",
897
1018
  headers=request_header,
898
1019
  timeout=None,
899
- failure_message="Failed to retrieve PHT components matching -> {}!".format(
900
- query
1020
+ failure_message="Failed to retrieve PHT components matching -> {}".format(
1021
+ query,
901
1022
  ),
902
1023
  )
903
1024
 
904
1025
  # end method definition
905
1026
 
906
- def load_products(self, product_list: list = None) -> bool:
907
- """Load products into a data frame in the self._data object
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.
908
1029
 
909
1030
  Args:
910
- product_list (list, optional): listn of products - if already avaiable. Defaults to None.
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.
911
1034
 
912
1035
  Returns:
913
- bool: True if successful, False otherwise.
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
+ )
1050
+ else:
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)),
1108
+ )
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)
1143
+
1144
+ # end method definition
1145
+
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"
1168
+
1169
+ Args:
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.
1180
+
1181
+ Returns:
1182
+ bool:
1183
+ True if successful, False otherwise.
1184
+
914
1185
  """
915
1186
 
916
1187
  if not product_list:
1188
+ self.logger.info("Load PHT product list...")
1189
+ # First, get the list of all products:
917
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)
918
1263
 
919
- self._data = Data(product_list)
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)
920
1271
 
921
- if self._data:
922
- return True
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)
1308
+
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)
1320
+
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
923
1337
 
924
- return False
1338
+ return bool(self._data)
925
1339
 
926
1340
  # end method definition