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