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