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.
- pyxecm/__init__.py +7 -4
- pyxecm/avts.py +727 -254
- pyxecm/coreshare.py +686 -467
- 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 +163 -0
- pyxecm/customizer/api/auth/__init__.py +1 -0
- pyxecm/customizer/api/auth/functions.py +92 -0
- pyxecm/customizer/api/auth/models.py +13 -0
- pyxecm/customizer/api/auth/router.py +78 -0
- pyxecm/customizer/api/common/__init__.py +1 -0
- pyxecm/customizer/api/common/functions.py +47 -0
- pyxecm/customizer/api/common/metrics.py +92 -0
- pyxecm/customizer/api/common/models.py +21 -0
- pyxecm/customizer/api/common/payload_list.py +870 -0
- pyxecm/customizer/api/common/router.py +72 -0
- pyxecm/customizer/api/settings.py +128 -0
- pyxecm/customizer/api/terminal/__init__.py +1 -0
- pyxecm/customizer/api/terminal/router.py +87 -0
- pyxecm/customizer/api/v1_csai/__init__.py +1 -0
- pyxecm/customizer/api/v1_csai/router.py +87 -0
- pyxecm/customizer/api/v1_maintenance/__init__.py +1 -0
- pyxecm/customizer/api/v1_maintenance/functions.py +100 -0
- pyxecm/customizer/api/v1_maintenance/models.py +12 -0
- pyxecm/customizer/api/v1_maintenance/router.py +76 -0
- pyxecm/customizer/api/v1_otcs/__init__.py +1 -0
- pyxecm/customizer/api/v1_otcs/functions.py +61 -0
- pyxecm/customizer/api/v1_otcs/router.py +179 -0
- pyxecm/customizer/api/v1_payload/__init__.py +1 -0
- pyxecm/customizer/api/v1_payload/functions.py +179 -0
- pyxecm/customizer/api/v1_payload/models.py +51 -0
- pyxecm/customizer/api/v1_payload/router.py +499 -0
- pyxecm/customizer/browser_automation.py +721 -286
- pyxecm/customizer/customizer.py +1076 -1425
- pyxecm/customizer/exceptions.py +35 -0
- pyxecm/customizer/guidewire.py +1186 -0
- pyxecm/customizer/k8s.py +901 -379
- pyxecm/customizer/log.py +107 -0
- pyxecm/customizer/m365.py +2967 -920
- pyxecm/customizer/nhc.py +1169 -0
- pyxecm/customizer/openapi.py +258 -0
- pyxecm/customizer/payload.py +18228 -7820
- pyxecm/customizer/pht.py +717 -286
- pyxecm/customizer/salesforce.py +516 -342
- pyxecm/customizer/sap.py +58 -41
- pyxecm/customizer/servicenow.py +611 -372
- pyxecm/customizer/settings.py +445 -0
- pyxecm/customizer/successfactors.py +408 -346
- pyxecm/customizer/translate.py +83 -48
- pyxecm/helper/__init__.py +5 -2
- pyxecm/helper/assoc.py +83 -43
- pyxecm/helper/data.py +2406 -870
- pyxecm/helper/logadapter.py +27 -0
- pyxecm/helper/web.py +229 -101
- pyxecm/helper/xml.py +596 -171
- 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 +235 -141
- pyxecm/otawp.py +2668 -1220
- pyxecm/otca.py +569 -0
- pyxecm/otcs.py +7956 -3237
- pyxecm/otds.py +2178 -925
- pyxecm/otiv.py +36 -21
- pyxecm/otmm.py +1272 -325
- pyxecm/otpd.py +231 -127
- pyxecm-2.0.1.dist-info/METADATA +122 -0
- pyxecm-2.0.1.dist-info/RECORD +76 -0
- {pyxecm-1.6.dist-info → pyxecm-2.0.1.dist-info}/WHEEL +1 -1
- pyxecm-1.6.dist-info/METADATA +0 -53
- pyxecm-1.6.dist-info/RECORD +0 -32
- {pyxecm-1.6.dist-info → pyxecm-2.0.1.dist-info/licenses}/LICENSE +0 -0
- {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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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):
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
"""
|
|
162
|
+
"""Return the configuration dictionary.
|
|
121
163
|
|
|
122
164
|
Returns:
|
|
123
|
-
dict:
|
|
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:
|
|
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
|
-
"""
|
|
142
|
-
|
|
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):
|
|
146
|
-
|
|
147
|
-
|
|
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):
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
"""
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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):
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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,
|
|
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) ->
|
|
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
|
-
|
|
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
|
|
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:
|
|
481
|
+
list | None:
|
|
482
|
+
The list of the known business units.
|
|
402
483
|
|
|
403
|
-
|
|
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:
|
|
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:
|
|
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,
|
|
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
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
"
|
|
527
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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,
|
|
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
|
-
|
|
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:
|
|
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,
|
|
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
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
"
|
|
644
|
-
|
|
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
|
-
|
|
649
|
-
|
|
650
|
-
}
|
|
760
|
+
includeAttributes: true | false
|
|
761
|
+
includeLinkedProducts: true | false
|
|
762
|
+
}
|
|
763
|
+
|
|
651
764
|
Returns:
|
|
652
|
-
list | None:
|
|
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
|
-
|
|
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:
|
|
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 -> {}
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
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,
|
|
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
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
"
|
|
834
|
-
|
|
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
|
-
|
|
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
|
-
|
|
983
|
+
json_data=request_data,
|
|
856
984
|
timeout=None,
|
|
857
|
-
failure_message="Failed to get filtered PHT
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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 -> {}
|
|
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
|
|
907
|
-
"""Load
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
922
|
-
|
|
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
|
|
1355
|
+
return bool(self._data)
|
|
925
1356
|
|
|
926
1357
|
# end method definition
|