pyxecm 1.6__py3-none-any.whl → 2.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (78) hide show
  1. pyxecm/__init__.py +7 -4
  2. pyxecm/avts.py +727 -254
  3. pyxecm/coreshare.py +686 -467
  4. pyxecm/customizer/__init__.py +16 -4
  5. pyxecm/customizer/__main__.py +58 -0
  6. pyxecm/customizer/api/__init__.py +5 -0
  7. pyxecm/customizer/api/__main__.py +6 -0
  8. pyxecm/customizer/api/app.py +163 -0
  9. pyxecm/customizer/api/auth/__init__.py +1 -0
  10. pyxecm/customizer/api/auth/functions.py +92 -0
  11. pyxecm/customizer/api/auth/models.py +13 -0
  12. pyxecm/customizer/api/auth/router.py +78 -0
  13. pyxecm/customizer/api/common/__init__.py +1 -0
  14. pyxecm/customizer/api/common/functions.py +47 -0
  15. pyxecm/customizer/api/common/metrics.py +92 -0
  16. pyxecm/customizer/api/common/models.py +21 -0
  17. pyxecm/customizer/api/common/payload_list.py +870 -0
  18. pyxecm/customizer/api/common/router.py +72 -0
  19. pyxecm/customizer/api/settings.py +128 -0
  20. pyxecm/customizer/api/terminal/__init__.py +1 -0
  21. pyxecm/customizer/api/terminal/router.py +87 -0
  22. pyxecm/customizer/api/v1_csai/__init__.py +1 -0
  23. pyxecm/customizer/api/v1_csai/router.py +87 -0
  24. pyxecm/customizer/api/v1_maintenance/__init__.py +1 -0
  25. pyxecm/customizer/api/v1_maintenance/functions.py +100 -0
  26. pyxecm/customizer/api/v1_maintenance/models.py +12 -0
  27. pyxecm/customizer/api/v1_maintenance/router.py +76 -0
  28. pyxecm/customizer/api/v1_otcs/__init__.py +1 -0
  29. pyxecm/customizer/api/v1_otcs/functions.py +61 -0
  30. pyxecm/customizer/api/v1_otcs/router.py +179 -0
  31. pyxecm/customizer/api/v1_payload/__init__.py +1 -0
  32. pyxecm/customizer/api/v1_payload/functions.py +179 -0
  33. pyxecm/customizer/api/v1_payload/models.py +51 -0
  34. pyxecm/customizer/api/v1_payload/router.py +499 -0
  35. pyxecm/customizer/browser_automation.py +721 -286
  36. pyxecm/customizer/customizer.py +1076 -1425
  37. pyxecm/customizer/exceptions.py +35 -0
  38. pyxecm/customizer/guidewire.py +1186 -0
  39. pyxecm/customizer/k8s.py +901 -379
  40. pyxecm/customizer/log.py +107 -0
  41. pyxecm/customizer/m365.py +2967 -920
  42. pyxecm/customizer/nhc.py +1169 -0
  43. pyxecm/customizer/openapi.py +258 -0
  44. pyxecm/customizer/payload.py +18228 -7820
  45. pyxecm/customizer/pht.py +717 -286
  46. pyxecm/customizer/salesforce.py +516 -342
  47. pyxecm/customizer/sap.py +58 -41
  48. pyxecm/customizer/servicenow.py +611 -372
  49. pyxecm/customizer/settings.py +445 -0
  50. pyxecm/customizer/successfactors.py +408 -346
  51. pyxecm/customizer/translate.py +83 -48
  52. pyxecm/helper/__init__.py +5 -2
  53. pyxecm/helper/assoc.py +83 -43
  54. pyxecm/helper/data.py +2406 -870
  55. pyxecm/helper/logadapter.py +27 -0
  56. pyxecm/helper/web.py +229 -101
  57. pyxecm/helper/xml.py +596 -171
  58. pyxecm/maintenance_page/__init__.py +5 -0
  59. pyxecm/maintenance_page/__main__.py +6 -0
  60. pyxecm/maintenance_page/app.py +51 -0
  61. pyxecm/maintenance_page/settings.py +28 -0
  62. pyxecm/maintenance_page/static/favicon.avif +0 -0
  63. pyxecm/maintenance_page/templates/maintenance.html +165 -0
  64. pyxecm/otac.py +235 -141
  65. pyxecm/otawp.py +2668 -1220
  66. pyxecm/otca.py +569 -0
  67. pyxecm/otcs.py +7956 -3237
  68. pyxecm/otds.py +2178 -925
  69. pyxecm/otiv.py +36 -21
  70. pyxecm/otmm.py +1272 -325
  71. pyxecm/otpd.py +231 -127
  72. pyxecm-2.0.1.dist-info/METADATA +122 -0
  73. pyxecm-2.0.1.dist-info/RECORD +76 -0
  74. {pyxecm-1.6.dist-info → pyxecm-2.0.1.dist-info}/WHEEL +1 -1
  75. pyxecm-1.6.dist-info/METADATA +0 -53
  76. pyxecm-1.6.dist-info/RECORD +0 -32
  77. {pyxecm-1.6.dist-info → pyxecm-2.0.1.dist-info/licenses}/LICENSE +0 -0
  78. {pyxecm-1.6.dist-info → pyxecm-2.0.1.dist-info}/top_level.txt +0 -0
pyxecm/otawp.py CHANGED
@@ -1,42 +1,313 @@
1
- """
2
- Otawp module for synchorinizing the pojects , publsh and create run time instances for that.
3
- loanmanagement is such application.
4
- """
1
+ """Synchronize AppWorks projects, publsh and create run time instances for that."""
2
+
3
+ __author__ = "Dr. Marc Diefenbruch"
4
+ __copyright__ = "Copyright (C) 2024-2025, OpenText"
5
+ __credits__ = ["Kai-Philip Gatzweiler"]
6
+ __maintainer__ = "Dr. Marc Diefenbruch"
7
+ __email__ = "mdiefenb@opentext.com"
5
8
 
6
- import logging
7
- import xml.etree.ElementTree as ET
8
- import uuid
9
9
  import json
10
+ import logging
11
+ import platform
10
12
  import re
13
+ import sys
11
14
  import time
15
+ import uuid
16
+ from http import HTTPStatus
17
+ from importlib.metadata import version
18
+
12
19
  import requests
13
- from .otds import OTDS
14
20
 
15
- logger = logging.getLogger("pyxecm.otawp")
21
+ from pyxecm.helper.xml import XML
22
+ from pyxecm.otds import OTDS
23
+
24
+ APP_NAME = "pyxecm"
25
+ APP_VERSION = version("pyxecm")
26
+ MODULE_NAME = APP_NAME + ".otawp"
27
+
28
+ PYTHON_VERSION = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
29
+ OS_INFO = f"{platform.system()} {platform.release()}"
30
+ ARCH_INFO = platform.machine()
31
+ REQUESTS_VERSION = requests.__version__
16
32
 
17
- REQUEST_HEADERS = {
33
+ USER_AGENT = (
34
+ f"{APP_NAME}/{APP_VERSION} ({MODULE_NAME}/{APP_VERSION}; "
35
+ f"Python/{PYTHON_VERSION}; {OS_INFO}; {ARCH_INFO}; Requests/{REQUESTS_VERSION})"
36
+ )
37
+
38
+ REQUEST_HEADERS_XML = {
39
+ "User-Agent": USER_AGENT,
18
40
  "Content-Type": "text/xml; charset=utf-8",
19
- "accept": "application/xml"
41
+ "accept": "application/xml",
20
42
  }
21
43
 
22
44
  REQUEST_FORM_HEADERS = {
45
+ "User-Agent": USER_AGENT,
23
46
  "accept": "application/xml;charset=utf-8",
24
47
  "Content-Type": "application/x-www-form-urlencoded",
25
48
  }
26
49
 
27
50
  REQUEST_HEADERS_JSON = {
51
+ "User-Agent": USER_AGENT,
28
52
  "Content-Type": "application/json; charset=utf-8",
29
- "accept": "application/json"
53
+ "accept": "application/json",
30
54
  }
31
- REQUEST_TIMEOUT = 60
55
+
56
+ REQUEST_TIMEOUT = 120
57
+ REQUEST_MAX_RETRIES = 10
58
+ REQUEST_RETRY_DELAY = 30
59
+ SYNC_PUBLISH_REQUEST_TIMEOUT = 300
60
+
61
+ default_logger = logging.getLogger(MODULE_NAME)
62
+
63
+ SOAP_FAULT_INDICATOR = "Fault"
64
+
32
65
 
33
66
  class OTAWP:
34
- """Used to automate settings in OpenText AppWorks Platform (OTAWP)."""
67
+ """Class OTAWP is used to automate settings in OpenText AppWorks Platform (OTAWP)."""
68
+
69
+ logger: logging.Logger = default_logger
70
+
35
71
  _config: dict
36
72
  _config = None
37
73
  _cookie = None
38
74
  _otawp_ticket = None
39
75
 
76
+ @classmethod
77
+ def resource_payload(
78
+ cls,
79
+ org_name: str,
80
+ username: str,
81
+ password: str,
82
+ ) -> dict:
83
+ """Create data structure for OTDS resource settings we need for AppWorks.
84
+
85
+ Args:
86
+ org_name (str):
87
+ The name of the organization.
88
+ username (str):
89
+ The user name.
90
+ password (str):
91
+ The password.
92
+
93
+ Returns:
94
+ dict:
95
+ AppWorks specific payload.
96
+
97
+ """
98
+
99
+ additional_payload = {}
100
+ additional_payload["connectorid"] = "rest"
101
+ additional_payload["resourceType"] = "rest"
102
+ user_attribute_mapping = [
103
+ {
104
+ "sourceAttr": ["oTExternalID1"],
105
+ "destAttr": "__NAME__",
106
+ "mappingFormat": "%s",
107
+ },
108
+ {
109
+ "sourceAttr": ["displayname"],
110
+ "destAttr": "DisplayName",
111
+ "mappingFormat": "%s",
112
+ },
113
+ {"sourceAttr": ["mail"], "destAttr": "Email", "mappingFormat": "%s"},
114
+ {
115
+ "sourceAttr": ["oTTelephoneNumber"],
116
+ "destAttr": "Telephone",
117
+ "mappingFormat": "%s",
118
+ },
119
+ {
120
+ "sourceAttr": ["oTMobile"],
121
+ "destAttr": "Mobile",
122
+ "mappingFormat": "%s",
123
+ },
124
+ {
125
+ "sourceAttr": ["oTFacsimileTelephoneNumber"],
126
+ "destAttr": "Fax",
127
+ "mappingFormat": "%s",
128
+ },
129
+ {
130
+ "sourceAttr": ["oTStreetAddress,l,st,postalCode,c"],
131
+ "destAttr": "Address",
132
+ "mappingFormat": "%s%n%s %s %s%n%s",
133
+ },
134
+ {
135
+ "sourceAttr": ["oTCompany"],
136
+ "destAttr": "Company",
137
+ "mappingFormat": "%s",
138
+ },
139
+ {
140
+ "sourceAttr": ["ds-pwp-account-disabled"],
141
+ "destAttr": "AccountDisabled",
142
+ "mappingFormat": "%s",
143
+ },
144
+ {
145
+ "sourceAttr": ["oTExtraAttr9"],
146
+ "destAttr": "IsServiceAccount",
147
+ "mappingFormat": "%s",
148
+ },
149
+ {
150
+ "sourceAttr": ["custom:proxyConfiguration"],
151
+ "destAttr": "ProxyConfiguration",
152
+ "mappingFormat": "%s",
153
+ },
154
+ {
155
+ "sourceAttr": ["c"],
156
+ "destAttr": "Identity-CountryOrRegion",
157
+ "mappingFormat": "%s",
158
+ },
159
+ {
160
+ "sourceAttr": ["gender"],
161
+ "destAttr": "Identity-Gender",
162
+ "mappingFormat": "%s",
163
+ },
164
+ {
165
+ "sourceAttr": ["displayName"],
166
+ "destAttr": "Identity-DisplayName",
167
+ "mappingFormat": "%s",
168
+ },
169
+ {
170
+ "sourceAttr": ["oTStreetAddress"],
171
+ "destAttr": "Identity-Address",
172
+ "mappingFormat": "%s",
173
+ },
174
+ {
175
+ "sourceAttr": ["l"],
176
+ "destAttr": "Identity-City",
177
+ "mappingFormat": "%s",
178
+ },
179
+ {
180
+ "sourceAttr": ["mail"],
181
+ "destAttr": "Identity-Email",
182
+ "mappingFormat": "%s",
183
+ },
184
+ {
185
+ "sourceAttr": ["givenName"],
186
+ "destAttr": "Identity-FirstName",
187
+ "mappingFormat": "%s",
188
+ },
189
+ {
190
+ "sourceAttr": ["sn"],
191
+ "destAttr": "Identity-LastName",
192
+ "mappingFormat": "%s",
193
+ },
194
+ {
195
+ "sourceAttr": ["initials"],
196
+ "destAttr": "Identity-MiddleNames",
197
+ "mappingFormat": "%s",
198
+ },
199
+ {
200
+ "sourceAttr": ["oTMobile"],
201
+ "destAttr": "Identity-Mobile",
202
+ "mappingFormat": "%s",
203
+ },
204
+ {
205
+ "sourceAttr": ["postalCode"],
206
+ "destAttr": "Identity-PostalCode",
207
+ "mappingFormat": "%s",
208
+ },
209
+ {
210
+ "sourceAttr": ["st"],
211
+ "destAttr": "Identity-StateOrProvince",
212
+ "mappingFormat": "%s",
213
+ },
214
+ {
215
+ "sourceAttr": ["title"],
216
+ "destAttr": "Identity-title",
217
+ "mappingFormat": "%s",
218
+ },
219
+ {
220
+ "sourceAttr": ["physicalDeliveryOfficeName"],
221
+ "destAttr": "Identity-physicalDeliveryOfficeName",
222
+ "mappingFormat": "%s",
223
+ },
224
+ {
225
+ "sourceAttr": ["oTFacsimileTelephoneNumber"],
226
+ "destAttr": "Identity-oTFacsimileTelephoneNumber",
227
+ "mappingFormat": "%s",
228
+ },
229
+ {
230
+ "sourceAttr": ["notes"],
231
+ "destAttr": "Identity-notes",
232
+ "mappingFormat": "%s",
233
+ },
234
+ {
235
+ "sourceAttr": ["oTCompany"],
236
+ "destAttr": "Identity-oTCompany",
237
+ "mappingFormat": "%s",
238
+ },
239
+ {
240
+ "sourceAttr": ["oTDepartment"],
241
+ "destAttr": "Identity-oTDepartment",
242
+ "mappingFormat": "%s",
243
+ },
244
+ {
245
+ "sourceAttr": ["birthDate"],
246
+ "destAttr": "Identity-Birthday",
247
+ "mappingFormat": "%s",
248
+ },
249
+ {
250
+ "sourceAttr": ["cn"],
251
+ "destAttr": "Identity-UserName",
252
+ "mappingFormat": "%s",
253
+ },
254
+ {
255
+ "sourceAttr": ["Description"],
256
+ "destAttr": "Identity-UserDescription",
257
+ "mappingFormat": "%s",
258
+ },
259
+ {
260
+ "sourceAttr": ["oTTelephoneNumber"],
261
+ "destAttr": "Identity-Phone",
262
+ "mappingFormat": "%s",
263
+ },
264
+ {
265
+ "sourceAttr": ["displayName"],
266
+ "destAttr": "Identity-IdentityDisplayName",
267
+ "mappingFormat": "%s",
268
+ },
269
+ ]
270
+ additional_payload["userAttributeMapping"] = user_attribute_mapping
271
+ group_attribute_mapping = [
272
+ {
273
+ "sourceAttr": ["cn"],
274
+ "destAttr": "__NAME__",
275
+ "mappingFormat": '%js:function format(name) { return name.replace(/&/g,"-and-"); }',
276
+ },
277
+ {
278
+ "sourceAttr": ["description"],
279
+ "destAttr": "Description",
280
+ "mappingFormat": "%s",
281
+ },
282
+ {
283
+ "sourceAttr": ["description"],
284
+ "destAttr": "Identity-Description",
285
+ "mappingFormat": "%s",
286
+ },
287
+ {
288
+ "sourceAttr": ["displayName"],
289
+ "destAttr": "Identity-DisplayName",
290
+ "mappingFormat": "%s",
291
+ },
292
+ ]
293
+ additional_payload["groupAttributeMapping"] = group_attribute_mapping
294
+ additional_payload["connectorName"] = "REST (Generic)"
295
+ additional_payload["pcCreatePermissionAllowed"] = "true"
296
+ additional_payload["pcModifyPermissionAllowed"] = "true"
297
+ additional_payload["pcDeletePermissionAllowed"] = "false"
298
+ additional_payload["connectionParamInfo"] = [
299
+ {
300
+ "name": "fBaseURL",
301
+ "value": "http://appworks:8080/home/" + org_name + "/app/otdspush",
302
+ },
303
+ {"name": "fUsername", "value": username},
304
+ {"name": "fPassword", "value": password},
305
+ ]
306
+
307
+ return additional_payload
308
+
309
+ # end method definition
310
+
40
311
  def __init__(
41
312
  self,
42
313
  protocol: str,
@@ -44,8 +315,49 @@ class OTAWP:
44
315
  port: int,
45
316
  username: str | None = None,
46
317
  password: str | None = None,
318
+ organization: str | None = None,
47
319
  otawp_ticket: str | None = None,
48
- ):
320
+ config_map_name: str | None = None,
321
+ license_file: str | None = None,
322
+ product_name: str | None = None,
323
+ product_description: str | None = None,
324
+ logger: logging.Logger = default_logger,
325
+ ) -> None:
326
+ """Initialize OTAWP (AppWorks Platform) object.
327
+
328
+ Args:
329
+ protocol (str):
330
+ Either http or https.
331
+ hostname (str):
332
+ The hostname of Extended ECM server to communicate with.
333
+ port (int):
334
+ The port number used to talk to the Extended ECM server.
335
+ username (str, optional):
336
+ The admin user name of OTAWP. Optional if otawp_ticket is provided.
337
+ password (str, optional):
338
+ The admin password of OTAWP. Optional if otawp_ticket is provided.
339
+ organization (str, optional):
340
+ The AppWorks organization. Used in LDAP strings and base URL.
341
+ otawp_ticket (str, optional):
342
+ The authentication ticket of OTAWP.
343
+ config_map_name (str | None, optional):
344
+ The AppWorks Kubernetes Config Map name. Defaults to None.
345
+ license_file (str | None, optional):
346
+ The file name and path to the license file for AppWorks. Defaults to None.
347
+ product_name (str | None, optional):
348
+ The product name for OTAWP used in the OTDS license. Defaults to None.
349
+ product_description (str | None, optional):
350
+ The product description in the OTDS license. Defaults to None.
351
+ logger (logging.Logger, optional):
352
+ The logging object to use for all log messages. Defaults to default_logger.
353
+
354
+ """
355
+
356
+ if logger != default_logger:
357
+ self.logger = logger.getChild("otawp")
358
+ for logfilter in logger.filters:
359
+ self.logger.addFilter(logfilter)
360
+
49
361
  otawp_config = {}
50
362
 
51
363
  otawp_config["hostname"] = hostname if hostname else "appworks"
@@ -53,125 +365,231 @@ class OTAWP:
53
365
  otawp_config["port"] = port if port else 8080
54
366
  otawp_config["username"] = username if username else "sysadmin"
55
367
  otawp_config["password"] = password if password else ""
368
+ otawp_config["organization"] = organization if organization else "system"
369
+ otawp_config["configMapName"] = config_map_name if config_map_name else ""
370
+ otawp_config["licenseFile"] = license_file if license_file else ""
371
+ otawp_config["productName"] = product_name if product_name else "APPWORKS_PLATFORM"
372
+ otawp_config["productDescription"] = (
373
+ product_description if product_description else "OpenText Appworks Platform"
374
+ )
56
375
 
57
376
  if otawp_ticket:
377
+ self._otawp_ticket = otawp_ticket
58
378
  self._cookie = {"defaultinst_SAMLart": otawp_ticket}
59
379
 
60
- otds_base_url = "{}://{}".format(protocol, otawp_config["hostname"])
380
+ server_url = "{}://{}".format(protocol, otawp_config["hostname"])
61
381
  if str(port) not in ["80", "443"]:
62
- otds_base_url += f":{port}"
63
- otds_base_url += "/home/system"
382
+ server_url += ":{}".format(port)
64
383
 
65
- otawp_config["gatewayAuthenticationUrl"] = (
66
- otds_base_url
67
- + "/com.eibus.web.soap.Gateway.wcp?organization=o=system,cn=cordys,cn=defaultInst,o=opentext.net"
68
- )
384
+ otawp_config["serverUrl"] = server_url
69
385
 
70
- otawp_config["soapGatewayUrl"] = (
71
- otds_base_url
72
- + "/com.eibus.web.soap.Gateway.wcp?organization=o=system,cn=cordys,cn=defaultInst,o=opentext.net&defaultinst_ct=abcd"
73
- )
386
+ self._config = otawp_config
74
387
 
75
- otawp_config["createPriority"] = (
76
- otds_base_url
77
- + "/app/entityRestService/api/OpentextCaseManagement/entities/Priority?defaultinst_ct=abcd"
78
- )
79
- otawp_config["getAllPriorities"] = (
80
- otds_base_url
81
- + "/app/entityRestService/api/OpentextCaseManagement/entities/Priority/lists/PriorityList"
82
- )
388
+ self.set_organization(otawp_config["organization"])
83
389
 
84
- otawp_config["createCustomer"] = (
85
- otds_base_url
86
- + "/app/entityRestService/api/OpentextCaseManagement/entities/Customer?defaultinst_ct=abcd"
87
- )
88
- otawp_config["getAllCustomeres"] = (
89
- otds_base_url
90
- + "/app/entityRestService/api/OpentextCaseManagement/entities/Customer/lists/CustomerList"
91
- )
390
+ # end method definition
92
391
 
93
- otawp_config["createCaseType"] = (
94
- otds_base_url
95
- + "/app/entityRestService/api/OpentextCaseManagement/entities/CaseType?defaultinst_ct=abcd"
96
- )
97
- otawp_config["getAllCaseTypes"] = (
98
- otds_base_url
99
- + "/app/entityRestService/api/OpentextCaseManagement/entities/CaseType/lists/AllCaseTypes"
100
- )
392
+ def server_url(self) -> str:
393
+ """Return AppWorks server information.
101
394
 
102
- otawp_config["createCategory"] = (
103
- otds_base_url
104
- + "/app/entityRestService/api/OpentextCaseManagement/entities/Category?defaultinst_ct=abcd"
105
- )
106
- otawp_config["getAllCategories"] = (
107
- otds_base_url
108
- + "/app/entityRestService/api/OpentextCaseManagement/entities/Category/lists/CategoryList"
109
- )
395
+ Returns:
396
+ str:
397
+ Server configuration.
110
398
 
111
- otawp_config["createSource"] = (
112
- otds_base_url
113
- + "/app/entityRestService/api/OpentextCaseManagement/entities/Source"
114
- )
399
+ """
115
400
 
116
- otawp_config["getAllSources"] = (
117
- otds_base_url
118
- + "/app/entityRestService/api/OpentextCaseManagement/entities/Source/lists/AllSources"
119
- )
401
+ return self.config()["server"]
120
402
 
121
- otawp_config["getAllSubCategories"] = (
122
- otds_base_url
123
- + "/app/entityRestService/api/OpentextCaseManagement/entities/Category/childEntities/SubCategory/lists/AllSubcategories"
124
- )
403
+ # end method definition
125
404
 
126
- otawp_config["baseurl"] = (
127
- otds_base_url
128
- + ""
129
- )
130
- otawp_config["createLoan"] = (
131
- otds_base_url
132
- + "/app/entityRestService/api/OpentextCaseManagement/entities/Case?defaultinst_ct=abcd"
133
- )
134
- otawp_config["getAllLoans"] = (
135
- otds_base_url
136
- + "/app/entityRestService/api/OpentextCaseManagement/entities/Case/lists/AllCasesList"
405
+ def set_organization(self, organization: str) -> None:
406
+ """Set the AppWorks organization context.
407
+
408
+ This requires to also update all URLs that are including
409
+ the organization.
410
+
411
+ Args:
412
+ organization (str):
413
+ The AppWorks organization name.
414
+
415
+ """
416
+
417
+ self._config["organization"] = organization
418
+
419
+ otawp_base_url = self._config["serverUrl"] + "/home/{}".format(self._config["organization"])
420
+ self._config["baseUrl"] = otawp_base_url
421
+
422
+ ldap_root = "organization=o={},cn=cordys,cn=defaultInst,o=opentext.net".format(self._config["organization"])
423
+ self._config["gatewayAuthenticationUrl"] = otawp_base_url + "/com.eibus.web.soap.Gateway.wcp?" + ldap_root
424
+
425
+ self._config["soapGatewayUrl"] = self._config["gatewayAuthenticationUrl"] + "&defaultinst_ct=abcd"
426
+
427
+ self._config["entityUrl"] = otawp_base_url + "/app/entityRestService/api/OpentextCaseManagement/entities"
428
+
429
+ self._config["priorityUrl"] = self._config["entityUrl"] + "/Priority"
430
+ self._config["priorityListUrl"] = self._config["priorityUrl"] + "/lists/PriorityList"
431
+
432
+ self._config["customerUrl"] = self._config["entityUrl"] + "/Customer"
433
+ self._config["customerListUrl"] = self._config["customerUrl"] + "/lists/CustomerList"
434
+
435
+ self._config["caseTypeUrl"] = self._config["entityUrl"] + "/CaseType"
436
+ self._config["caseTypeListUrl"] = self._config["caseTypeUrl"] + "/lists/AllCaseTypes"
437
+
438
+ self._config["categoryUrl"] = self._config["entityUrl"] + "/Category"
439
+ self._config["categoryListUrl"] = self._config["categoryUrl"] + "/lists/CategoryList"
440
+
441
+ self._config["subCategoryListUrl"] = (
442
+ self._config["categoryUrl"] + "/childEntities/SubCategory/lists/AllSubcategories"
137
443
  )
138
- self._config = otawp_config
444
+
445
+ self._config["sourceUrl"] = self._config["entityUrl"] + "/Source"
446
+ self._config["sourceListUrl"] = self._config["sourceUrl"] + "/lists/AllSources"
447
+
448
+ self._config["caseUrl"] = self._config["entityUrl"] + "/Case"
449
+ self._config["caseListUrl"] = self._config["caseUrl"] + "/lists/AllCasesList"
450
+
451
+ self.logger.info("AppWorks organization set to -> '%s'.", organization)
139
452
 
140
453
  # end method definition
141
454
 
142
- def baseurl(self) -> dict:
143
- """Returns the configuration dictionary
455
+ def base_url(self) -> str:
456
+ """Return the base URL of AppWorks.
457
+
144
458
  Returns:
145
- dict: Configuration dictionary
459
+ str:
460
+ The base URL of AppWorks Platform.
461
+
462
+ """
463
+
464
+ return self.config()["baseUrl"]
465
+
466
+ # end method definition
467
+
468
+ def license_file(self) -> str:
469
+ """Return the AppWorks license file.
470
+
471
+ Returns:
472
+ str:
473
+ The name (including path) of the AppWorks license file.
474
+
475
+ """
476
+
477
+ return self.config()["licenseFile"]
478
+
479
+ # end method definition
480
+
481
+ def product_name(self) -> str:
482
+ """Return the AppWorks product name as used in the OTDS license.
483
+
484
+ Returns:
485
+ str:
486
+ The AppWorks product name.
487
+
488
+ """
489
+
490
+ return self.config()["productName"]
491
+
492
+ # end method definition
493
+
494
+ def product_description(self) -> str:
495
+ """Return the AppWorks product description as used in the OTDS license.
496
+
497
+ Returns:
498
+ str:
499
+ The AppWorks product description.
500
+
501
+ """
502
+
503
+ return self.config()["productDescription"]
504
+
505
+ # end method definition
506
+
507
+ def hostname(self) -> str:
508
+ """Return the AppWorks hostname.
509
+
510
+ Returns:
511
+ str:
512
+ The AppWorks hostname.
513
+
514
+ """
515
+
516
+ return self.config()["hostname"]
517
+
518
+ def username(self) -> str:
519
+ """Return the AppWorks username.
520
+
521
+ Returns:
522
+ str:
523
+ The AppWorks username
524
+
525
+ """
526
+
527
+ return self.config()["username"]
528
+
529
+ # end method definition
530
+
531
+ def password(self) -> str:
532
+ """Return the AppWorks password.
533
+
534
+ Returns:
535
+ str:
536
+ The AppWorks password.
537
+
538
+ """
539
+
540
+ return self.config()["password"]
541
+
542
+ # end method definition
543
+
544
+ def config_map_name(self) -> str:
545
+ """Return AppWorks Kubernetes config map name.
546
+
547
+ Returns:
548
+ str:
549
+ The Kubernetes config map name of AppWorks.
550
+
146
551
  """
147
- return self.config()["baseurl"]
552
+
553
+ return self.config()["configMapName"]
148
554
 
149
555
  # end method definition
150
556
 
151
557
  def config(self) -> dict:
152
- """Returns the configuration dictionary
558
+ """Return the configuration dictionary.
559
+
153
560
  Returns:
154
561
  dict: Configuration dictionary
562
+
155
563
  """
564
+
156
565
  return self._config
157
566
 
158
567
  # end method definition
159
568
 
160
569
  def cookie(self) -> dict:
161
- """Returns the login cookie of OTAWP.
162
- This is set by the authenticate() method
570
+ """Return the login cookie of OTAWP.
571
+
572
+ This is set by the authenticate() method
573
+
163
574
  Returns:
164
- dict: OTAWP cookie
575
+ dict:
576
+ OTAWP cookie
577
+
165
578
  """
579
+
166
580
  return self._cookie
167
581
 
168
582
  # end method definition
169
583
 
170
584
  def credentials(self) -> str:
171
- """Returns the SOAP payload with credentials (username and password)
585
+ """Return the SOAP payload with credentials (username and password).
586
+
172
587
  Returns:
173
- str: SOAP payload with username and password
588
+ str:
589
+ SOAP payload with username and password.
590
+
174
591
  """
592
+
175
593
  username = self.config()["username"]
176
594
  password = self.config()["password"]
177
595
 
@@ -199,146 +617,203 @@ class OTAWP:
199
617
  </SOAP:Body>
200
618
  </SOAP:Envelope>
201
619
  """
620
+
202
621
  return soap_payload
203
622
 
204
623
  # end method definition
205
624
 
206
625
  def credential_url(self) -> str:
207
- """Returns the Credentials URL of OTAWP
626
+ """Return the credentials URL of OTAWP.
208
627
 
209
628
  Returns:
210
- str: Credentials URL
629
+ str:
630
+ The AppWorks credentials URL.
631
+
211
632
  """
633
+
212
634
  return self.config()["gatewayAuthenticationUrl"]
213
635
 
214
636
  # end method definition
215
637
 
216
638
  def gateway_url(self) -> str:
217
- """Returns soapGatewayUrl URL of OTAWP
639
+ """Return SOAP gateway URL of OTAWP.
218
640
 
219
641
  Returns:
220
- str: soapGatewayUrl URL
642
+ str:
643
+ The AppWorks SOAP gateway URL.
644
+
221
645
  """
646
+
222
647
  return self.config()["soapGatewayUrl"]
223
648
 
224
649
  # end method definition
225
650
 
226
- def create_priority_url(self) -> str:
227
- """Returns createPriority URL of OTAWP
651
+ def get_create_priority_url(self) -> str:
652
+ """Return create priority URL of OTAWP.
228
653
 
229
654
  Returns:
230
- str: createPriority URL
655
+ str:
656
+ The create priority URL.
657
+
231
658
  """
232
- return self.config()["createPriority"]
659
+
660
+ return self.config()["priorityUrl"] + "?defaultinst_ct=abcd"
233
661
 
234
662
  # end method definition
235
663
 
236
- def get_all_priorities_url(self) -> str:
237
- """Returns getAllPriorities URL of OTAWP
664
+ def get_priorities_list_url(self) -> str:
665
+ """Get OTAWP URL to retrieve a list of all priorities.
238
666
 
239
667
  Returns:
240
- str: getAllPriorities URL
668
+ str:
669
+ The AppWorks URL to get a list of all priorities.
670
+
241
671
  """
242
- return self.config()["getAllPriorities"]
672
+
673
+ return self.config()["priorityListUrl"]
243
674
 
244
675
  # end method definition
245
676
 
246
- def create_customer_url(self) -> str:
247
- """Returns createCustomer URL of OTAWP
677
+ def get_create_customer_url(self) -> str:
678
+ """Return create customer URL of OTAWP.
248
679
 
249
680
  Returns:
250
- str: createCustomer url
681
+ str:
682
+ The create customer URL.
683
+
251
684
  """
252
- return self.config()["createCustomer"]
685
+
686
+ return self.config()["customerUrl"] + "?defaultinst_ct=abcd"
253
687
 
254
688
  # end method definition
255
689
 
256
- def get_all_customeres_url(self) -> str:
257
- """Returns getAllCustomeres url of OTAWP
690
+ def get_customers_list_url(self) -> str:
691
+ """Get OTAWP URL to retrieve a list of all customers.
258
692
 
259
693
  Returns:
260
- str: getAllCustomeres url
694
+ str:
695
+ The AppWorks URL to get a list of all customers.
696
+
261
697
  """
262
- return self.config()["getAllCustomeres"]
698
+
699
+ return self.config()["customerListUrl"]
263
700
 
264
701
  # end method definition
265
702
 
266
- def create_casetype_url(self) -> str:
267
- """Returns createCaseType url of OTAWP
703
+ def get_create_casetype_url(self) -> str:
704
+ """Return create case type URL of OTAWP.
268
705
 
269
706
  Returns:
270
- str: createCaseType url
707
+ str:
708
+ The create case type URL.
709
+
271
710
  """
272
- return self.config()["createCaseType"]
711
+
712
+ return self.config()["caseTypeUrl"] + "?defaultinst_ct=abcd"
273
713
 
274
714
  # end method definition
275
715
 
276
- def get_all_case_types_url(self) -> str:
277
- """Returns getAllCaseTypes URL of OTAWP
716
+ def get_casetypes_list_url(self) -> str:
717
+ """Get OTAWP URL to retrieve a list of all case types.
278
718
 
279
719
  Returns:
280
- str: getAllCaseTypes URL
720
+ str:
721
+ The get all case types URL.
722
+
281
723
  """
282
- return self.config()["getAllCaseTypes"]
724
+
725
+ return self.config()["caseTypeListUrl"]
283
726
 
284
727
  # end method definition
285
728
 
286
- def create_category_url(self) -> str:
287
- """Returns createCategory URL of OTAWP
729
+ def get_create_category_url(self) -> str:
730
+ """Get OTAWP URL to create a category.
288
731
 
289
732
  Returns:
290
- str: createCategory URL
733
+ str:
734
+ The create category URL.
735
+
291
736
  """
292
- return self.config()["createCategory"]
737
+
738
+ return self.config()["categoryUrl"] + "?defaultinst_ct=abcd"
293
739
 
294
740
  # end method definition
295
741
 
296
- def get_all_categories_url(self) -> str:
297
- """Returns the getAllCategories URL of OTAWP
742
+ def get_categories_list_url(self) -> str:
743
+ """Get OTAWP URL to retrieve a list of all categories.
298
744
 
299
745
  Returns:
300
- str: getAllCategories URL
746
+ str:
747
+ The get all categories URL.
748
+
301
749
  """
302
- return self.config()["getAllCategories"]
750
+
751
+ return self.config()["categoryListUrl"]
303
752
 
304
753
  # end method definition
305
754
 
306
- def get_all_loans_url(self) -> str:
307
- """Returns getAllLoans URL of OTAWP
755
+ def get_create_case_url(self) -> str:
756
+ """Get OTAWP URL to create a case (e.g. a loan).
308
757
 
309
758
  Returns:
310
- str: getAllLoans URL
759
+ str:
760
+ The create case URL.
761
+
311
762
  """
312
- return self.config()["getAllLoans"]
763
+
764
+ return self.config()["caseUrl"] + "?defaultinst_ct=abcd"
313
765
 
314
766
  # end method definition
315
767
 
316
- def remove_namespace(self, tag):
317
- """Remove namespace from XML tag."""
318
- return tag.split('}', 1)[-1]
768
+ def get_cases_list_url(self) -> str:
769
+ """Return get all loans URL of OTAWP.
770
+
771
+ Returns:
772
+ str:
773
+ The get all loans URL.
774
+
775
+ """
776
+
777
+ return self.config()["caseListUrl"]
319
778
 
320
779
  # end method definition
321
780
 
322
- def parse_xml(self, xml_string):
323
- """Parse XML string and return a dictionary without namespaces."""
324
- def element_to_dict(element):
325
- """Convert XML element to dictionary."""
326
- tag = self.remove_namespace(element.tag)
327
- children = list(element)
328
- if children:
329
- return {tag: {self.remove_namespace(child.tag): element_to_dict(child) for child in children}}
330
- return {tag: element.text.strip() if element.text else None}
331
- root = ET.fromstring(xml_string)
332
- return element_to_dict(root)
781
+ def parse_xml(self, xml_string: str) -> dict:
782
+ """Parse XML string and return a dictionary without namespaces.
783
+
784
+ Args:
785
+ xml_string (str):
786
+ The XML string to process.
787
+
788
+ Returns:
789
+ dict:
790
+ The XML structure converted to a dictionary.
791
+
792
+ """
793
+
794
+ return XML.xml_to_dict(xml_string=xml_string)
333
795
 
334
796
  # end method definition
335
797
 
336
- def find_key(self, data, target_key):
337
- """Recursively search for a key in a nested dictionary and return its value."""
798
+ def find_key(self, data: dict | list, target_key: str) -> str | None:
799
+ """Recursively search for a key in a nested dictionary and return its value.
800
+
801
+ Args:
802
+ data (dict | list):
803
+ The data structure to find a key in.
804
+ target_key (str):
805
+ The key to find.
806
+
807
+ Returns:
808
+ str:
809
+ The value for the key.
810
+
811
+ """
812
+
338
813
  if isinstance(data, dict):
339
814
  if target_key in data:
340
815
  return data[target_key]
341
- for _, value in data.items():
816
+ for value in data.values():
342
817
  result = self.find_key(value, target_key)
343
818
  if result is not None:
344
819
  return result
@@ -347,30 +822,188 @@ class OTAWP:
347
822
  result = self.find_key(item, target_key)
348
823
  if result is not None:
349
824
  return result
825
+
350
826
  return None
351
827
 
352
828
  # end method definition
353
829
 
830
+ def get_soap_element(self, soap_response: str, soap_tag: str) -> str | None:
831
+ """Retrieve an element from the XML SOAP response.
832
+
833
+ Args:
834
+ soap_response (str):
835
+ The unparsed XML string of the SOAP response.
836
+ soap_tag (str):
837
+ The XML tag name (without namespace) of the element
838
+ incuding the text to be returned.
839
+
840
+ Returns:
841
+ str | None:
842
+ SOAP message if found in the SOAP response or NONE otherwise.
843
+
844
+ """
845
+
846
+ soap_data = self.parse_xml(soap_response)
847
+ soap_string = self.find_key(data=soap_data, target_key=soap_tag)
848
+
849
+ return soap_string
850
+
851
+ # end method definition
852
+
853
+ def do_request(
854
+ self,
855
+ url: str,
856
+ method: str = "GET",
857
+ headers: dict | None = None,
858
+ cookies: dict | None = None,
859
+ data: dict | None = None,
860
+ json_data: dict | None = None,
861
+ files: dict | None = None,
862
+ timeout: int | None = REQUEST_TIMEOUT,
863
+ show_error: bool = True,
864
+ show_warning: bool = False,
865
+ warning_message: str = "",
866
+ failure_message: str = "",
867
+ success_message: str = "",
868
+ parse_request_response: bool = True,
869
+ verify: bool = True,
870
+ ) -> dict | None:
871
+ """Call an AppWorks REST API in a safe way.
872
+
873
+ Args:
874
+ url (str):
875
+ The URL to send the request to.
876
+ method (str, optional):
877
+ HTTP method (GET, POST, etc.). Defaults to "GET".
878
+ headers (dict | None, optional):
879
+ Request Headers. Defaults to None.
880
+ cookies (dict | None, optional):
881
+ Request cookies. Defaults to None.
882
+ data (dict | None, optional):
883
+ Request payload. Defaults to None
884
+ json_data (dict | None, optional):
885
+ Request payload for the JSON parameter. Defaults to None.
886
+ files (dict | None, optional):
887
+ Dictionary of {"name": file-tuple} for multipart encoding upload.
888
+ The file-tuple can be a 2-tuple ("filename", fileobj) or a 3-tuple ("filename", fileobj, "content_type")
889
+ timeout (int | None, optional):
890
+ Timeout for the request in seconds. Defaults to REQUEST_TIMEOUT.
891
+ show_error (bool, optional):
892
+ Whether or not an error should be logged in case of a failed REST call.
893
+ If False, then only a warning is logged. Defaults to True.
894
+ show_warning (bool, optional):
895
+ Whether or not an warning should be logged in case of a
896
+ failed REST call.
897
+ If False, then only a warning is logged. Defaults to True.
898
+ warning_message (str, optional):
899
+ Specific warning message. Defaults to "". If not given the error_message will be used.
900
+ failure_message (str, optional):
901
+ Specific error message. Defaults to "".
902
+ success_message (str, optional):
903
+ Specific success message. Defaults to "".
904
+ parse_request_response (bool, optional):
905
+ If True the response.text will be interpreted as json and loaded into a dictionary.
906
+ True is the default.
907
+ user_credentials (bool, optional):
908
+ Defines if admin or user credentials are used for the REST API call.
909
+ Default = False = admin credentials
910
+ verify (bool, optional):
911
+ Specify whether or not SSL certificates should be verified when making an HTTPS request.
912
+ Default = True
913
+
914
+ Returns:
915
+ dict | None:
916
+ Response of OTDS REST API or None in case of an error.
917
+
918
+ """
919
+
920
+ # In case of an expired session we reauthenticate and
921
+ # try 1 more time. Session expiration should not happen
922
+ # twice in a row:
923
+ retries = 0
924
+
925
+ while True:
926
+ try:
927
+ response = requests.request(
928
+ method=method,
929
+ url=url,
930
+ data=data,
931
+ json=json_data,
932
+ files=files,
933
+ headers=headers,
934
+ cookies=cookies,
935
+ timeout=timeout,
936
+ verify=verify,
937
+ )
938
+
939
+ except requests.RequestException as req_exception:
940
+ self.logger.error(
941
+ "%s; error -> %s",
942
+ failure_message if failure_message else "Request to -> %s failed",
943
+ str(req_exception),
944
+ )
945
+ return None
946
+
947
+ if response.ok:
948
+ if success_message:
949
+ self.logger.info(success_message)
950
+ if parse_request_response:
951
+ return self.parse_request_response(response_object=response, show_error=show_error)
952
+ else:
953
+ return response
954
+ # Check if Session has expired - then re-authenticate and try once more
955
+ elif response.status_code == 401 and retries == 0:
956
+ self.logger.warning("Session has expired - try to re-authenticate...")
957
+ self.authenticate(revalidate=True)
958
+ retries += 1
959
+ continue
960
+ elif show_error:
961
+ self.logger.error(
962
+ "%s; status -> %s/%s; error -> %s",
963
+ failure_message,
964
+ response.status_code,
965
+ HTTPStatus(response.status_code).phrase,
966
+ response.text,
967
+ )
968
+ elif show_warning:
969
+ self.logger.warning(
970
+ "%s; status -> %s/%s; warning -> %s",
971
+ warning_message if warning_message else failure_message,
972
+ response.status_code,
973
+ HTTPStatus(response.status_code).phrase,
974
+ response.text,
975
+ )
976
+ return None
977
+ # end while True
978
+
979
+ # end method definition
980
+
354
981
  def parse_request_response(
355
982
  self,
356
983
  response_object: object,
357
984
  additional_error_message: str = "",
358
985
  show_error: bool = True,
359
986
  ) -> dict | None:
360
- """Converts the text property of a request response object to a Python dict in a safe way
361
- that also handles exceptions.
987
+ """Convert the text property of a request response object to a Python dict in a safe way.
362
988
 
363
- Content Server may produce corrupt response when it gets restarted
364
- or hitting resource limits. So we try to avoid a fatal error and bail
365
- out more gracefully.
989
+ Properly handle exceptions.
990
+
991
+ AppWorks may produce corrupt response when it gets restarted
992
+ or hitting resource limits. So we try to avoid a fatal error and bail
993
+ out more gracefully.
366
994
 
367
995
  Args:
368
- response_object (object): this is reponse object delivered by the request call
369
- additional_error_message (str): print a custom error message
370
- show_error (bool): if True log an error, if False log a warning
996
+ response_object (object):
997
+ This is reponse object delivered by the request call.
998
+ additional_error_message (str):
999
+ Print a custom error message.
1000
+ show_error (bool):
1001
+ If True log an error, if False log a warning.
371
1002
 
372
1003
  Returns:
373
- dict: response or None in case of an error
1004
+ dict:
1005
+ Response or None in case of an error.
1006
+
374
1007
  """
375
1008
 
376
1009
  if not response_object:
@@ -381,284 +1014,709 @@ class OTAWP:
381
1014
  except json.JSONDecodeError as exception:
382
1015
  if additional_error_message:
383
1016
  message = "Cannot decode response as JSon. {}; error -> {}".format(
384
- additional_error_message, exception
1017
+ additional_error_message,
1018
+ exception,
385
1019
  )
386
1020
  else:
387
1021
  message = "Cannot decode response as JSon; error -> {}".format(
388
- exception
1022
+ exception,
389
1023
  )
390
1024
  if show_error:
391
- logger.error(message)
1025
+ self.logger.error(message)
392
1026
  else:
393
- logger.warning(message)
1027
+ self.logger.warning(message)
394
1028
  return None
1029
+
395
1030
  return dict_object
396
1031
 
397
1032
  # end method definition
398
1033
 
399
- def authenticate(self, revalidate: bool = False) -> dict | None:
400
- """Authenticate at appworks.
1034
+ def get_entity_value(self, entity: dict, key: str, show_error: bool = True) -> str | int | None:
1035
+ """Read an entity value from the REST API response.
401
1036
 
402
1037
  Args:
403
- revalidate (bool, optional): determine if a re-authentication is enforced
404
- (e.g. if session has timed out with 401 error)
1038
+ entity (dict):
1039
+ An entity - typically consisting of a dictionary with a "_links" and "Properties" keys. Example:
1040
+ {
1041
+ '_links': {
1042
+ 'item': {...}
1043
+ },
1044
+ 'Properties': {
1045
+ 'Name': 'Test 1',
1046
+ 'Description': 'Test 1 Description',
1047
+ 'CasePrefix': 'TEST',
1048
+ 'Status': 1
1049
+ }
1050
+ }
1051
+ key (str):
1052
+ Key to find (e.g., "id", "name"). For key "id" there's a special
1053
+ handling as the ID is only provided in the 'href' in the '_links'
1054
+ sub-dictionary.
1055
+ show_error (bool, optional):
1056
+ Whether an error or just a warning should be logged.
1057
+
405
1058
  Returns:
406
- dict: Cookie information. Also stores cookie information in self._cookie
1059
+ str | None:
1060
+ Value of the entity property with the given key, or None if no value is found.
1061
+
407
1062
  """
408
1063
 
409
- logger.info("SAMLart generation started")
410
- if self._cookie and not revalidate:
411
- logger.info(
412
- "Session still valid - return existing cookie -> %s",
413
- str(self._cookie),
414
- )
415
- return self._cookie
1064
+ if not entity or "Properties" not in entity:
1065
+ return None
416
1066
 
417
- otawp_ticket = "NotSet"
1067
+ properties = entity["Properties"]
418
1068
 
419
- response = None
420
- try:
421
- self.credentials()
422
- response = requests.post(
423
- url=self.credential_url(),
424
- data=self.credentials(),
425
- headers=REQUEST_HEADERS,
426
- timeout=REQUEST_TIMEOUT
427
- )
428
- except requests.exceptions.RequestException as exception:
429
- logger.warning(
430
- "Unable to connect to -> %s; error -> %s",
431
- self.credential_url(),
432
- exception.strerror,
433
- )
434
- logger.warning("OTAWP service may not be ready yet.")
1069
+ if key not in properties and key != "id":
1070
+ if show_error:
1071
+ self.logger.error("Key -> '%s' not found in entity -> '%s'!", key, str(entity))
435
1072
  return None
436
1073
 
437
- if response.ok:
438
- logger.info("SAMLart generated successfully")
439
- authenticate_dict = self.parse_xml(response.text)
440
- if not authenticate_dict:
1074
+ # special handling of IDs which we extract from the self href:
1075
+ if key == "id" and "_links" in entity:
1076
+ links = entity["_links"]
1077
+ if "item" in links:
1078
+ links = links["item"]
1079
+ self_link = links.get("href")
1080
+ match = re.search(r"/(\d+)(?=[^/]*$)", self_link)
1081
+ if not match:
441
1082
  return None
442
- assertion_artifact_dict = self.find_key(
443
- authenticate_dict, "AssertionArtifact"
444
- )
445
- if isinstance(assertion_artifact_dict, dict):
446
- otawp_ticket = assertion_artifact_dict.get("AssertionArtifact")
447
- logger.info("SAML token -> %s", otawp_ticket)
448
- else:
449
- logger.error("Failed to request an OTAWP ticket; error -> %s", response.text)
450
- return None
1083
+ return int(match.group(1))
451
1084
 
452
- self._cookie = {"defaultinst_SAMLart": otawp_ticket, "defaultinst_ct": "abcd"}
453
- self._otawp_ticket = otawp_ticket
454
-
455
- return self._cookie
1085
+ return properties[key]
456
1086
 
457
1087
  # end method definition
458
1088
 
459
- def create_workspace(
1089
+ def get_result_value(
460
1090
  self,
461
- workspace_name: str,
462
- workspace_id: str
463
- ) -> dict | None:
464
- """Creates a workspace in cws
1091
+ response: dict,
1092
+ entity_type: str,
1093
+ key: str,
1094
+ index: int = 0,
1095
+ show_error: bool = True,
1096
+ ) -> str | int | None:
1097
+ """Read an item value from the REST API response.
1098
+
465
1099
  Args:
466
- workspace_name (str): workspace_name
467
- workspace_id (str): workspace_id
1100
+ response (dict):
1101
+ REST API response object.
1102
+ entity_type (str):
1103
+ Name of the sub-dictionary holding the actual values.
1104
+ This typically stands for the type of the AppWorks entity.
1105
+ key (str):
1106
+ Key to find (e.g., "id", "name").
1107
+ index (int, optional):
1108
+ Index to use if a list of results is delivered (1st element has index 0).
1109
+ Defaults to 0.
1110
+ show_error (bool, optional):
1111
+ Whether an error or just a warning should be logged.
1112
+
468
1113
  Returns:
469
- response test or error text
1114
+ str | None:
1115
+ Value of the item with the given key, or None if no value is found.
1116
+
470
1117
  """
471
1118
 
472
- logger.info(
473
- "Create workspace with name -> '%s' and ID -> %s...",
474
- workspace_name,
1119
+ if not response:
1120
+ return None
1121
+
1122
+ if "_embedded" not in response:
1123
+ return None
1124
+
1125
+ embedded_data = response["_embedded"]
1126
+
1127
+ if entity_type not in embedded_data:
1128
+ if show_error:
1129
+ self.logger.error("Entity type -> '%s' is not included in response!", entity_type)
1130
+ return None
1131
+
1132
+ entity_list = embedded_data[entity_type]
1133
+
1134
+ try:
1135
+ entity = entity_list[index]
1136
+ except KeyError:
1137
+ if show_error:
1138
+ self.logger.error("Response does not have an entity at index -> %d", index)
1139
+ return None
1140
+
1141
+ return self.get_entity_value(entity=entity, key=key, show_error=show_error)
1142
+
1143
+ # end method definition
1144
+
1145
+ def get_result_values(
1146
+ self,
1147
+ response: dict,
1148
+ entity_type: str,
1149
+ key: str,
1150
+ show_error: bool = True,
1151
+ ) -> list | None:
1152
+ """Read an values from the REST API response.
1153
+
1154
+ Args:
1155
+ response (dict):
1156
+ REST API response object.
1157
+ entity_type (str):
1158
+ Name of the sub-dictionary holding the actual values.
1159
+ This typically stands for the type of the AppWorks entity.
1160
+ key (str):
1161
+ Key to find (e.g., "id", "name").
1162
+ show_error (bool, optional):
1163
+ Whether an error or just a warning should be logged.
1164
+
1165
+ Returns:
1166
+ list | None:
1167
+ Values of the items with the given key, or [] if the list
1168
+ of values is empty, or None if the response is not in the
1169
+ expected format.
1170
+
1171
+ """
1172
+
1173
+ results = []
1174
+
1175
+ if not response:
1176
+ return None
1177
+
1178
+ if "_embedded" not in response:
1179
+ return None
1180
+
1181
+ embedded_data = response["_embedded"]
1182
+
1183
+ if entity_type not in embedded_data:
1184
+ if show_error:
1185
+ self.logger.error("Entity type -> '%s' is not included in response!", entity_type)
1186
+ return None
1187
+
1188
+ entity_list = embedded_data[entity_type]
1189
+
1190
+ for entity in entity_list or []:
1191
+ entity_value = self.get_entity_value(entity=entity, key=key, show_error=show_error)
1192
+ if entity_value:
1193
+ results.append(entity_value)
1194
+
1195
+ return results
1196
+
1197
+ # end method definition
1198
+
1199
+ def get_result_item(
1200
+ self,
1201
+ response: dict,
1202
+ entity_type: str,
1203
+ key: str,
1204
+ value: str,
1205
+ show_error: bool = True,
1206
+ ) -> dict | None:
1207
+ """Check existence of key / value pair in the response properties of an REST API call.
1208
+
1209
+ Args:
1210
+ response (dict):
1211
+ REST response from an AppWorks REST Call.
1212
+ Name of the sub-dictionary holding the actual values.
1213
+ This typically stands for the type of the AppWorks entity.
1214
+ entity_type (str):
1215
+ Name of the sub-dictionary holding the actual values.
1216
+ This typically stands for the type of the AppWorks entity.
1217
+ key (str):
1218
+ The property name (key).
1219
+ value (str):
1220
+ The value to find in the item with the matching key.
1221
+ show_error (bool, optional):
1222
+ Whether an error or just a warning should be logged.
1223
+
1224
+ Returns:
1225
+ dict | None:
1226
+ Entity data or None in case entity with key/value was not found.
1227
+
1228
+ """
1229
+
1230
+ if not response:
1231
+ return None
1232
+
1233
+ if "_embedded" not in response:
1234
+ return None
1235
+
1236
+ embedded_data = response["_embedded"]
1237
+
1238
+ if entity_type not in embedded_data:
1239
+ if show_error:
1240
+ self.logger.error("Entity type -> '%s' is not included in response!", entity_type)
1241
+ return None
1242
+
1243
+ entity_list = embedded_data[entity_type]
1244
+
1245
+ for entity in entity_list:
1246
+ if "Properties" not in entity:
1247
+ continue
1248
+
1249
+ properties = entity["Properties"]
1250
+
1251
+ if key not in properties:
1252
+ if show_error:
1253
+ self.logger.error("Key -> '%s' is not in properties of entity -> '%s'!", key, str(entity))
1254
+ continue
1255
+ if properties[key] == value:
1256
+ return entity
1257
+
1258
+ return None
1259
+
1260
+ # end method definition
1261
+
1262
+ def authenticate(self, revalidate: bool = False) -> dict | None:
1263
+ """Authenticate at AppWorks.
1264
+
1265
+ Args:
1266
+ revalidate (bool, optional):
1267
+ Determine if a re-authentication is enforced
1268
+ (e.g. if session has timed out with 401 error).
1269
+
1270
+ Returns:
1271
+ dict | None:
1272
+ Cookie information. Also stores cookie information in self._cookie.
1273
+ None in case of an error.
1274
+
1275
+ Example:
1276
+ {
1277
+ 'defaultinst_SAMLart': 'e0pBVkEtQUVTL0...tj5m6w==',
1278
+ 'defaultinst_ct': 'abcd'
1279
+ }
1280
+
1281
+ """
1282
+
1283
+ self.logger.info("Authenticate at AppWorks organization -> '%s'...", self.config()["organization"])
1284
+
1285
+ if self._cookie and not revalidate:
1286
+ self.logger.debug(
1287
+ "Session still valid - return existing cookie -> %s",
1288
+ str(self._cookie),
1289
+ )
1290
+ return self._cookie
1291
+
1292
+ otawp_ticket = "NotSet"
1293
+
1294
+ request_url = self.credential_url()
1295
+
1296
+ retries = 0
1297
+ response = None # seems to be necessary here
1298
+
1299
+ while retries < REQUEST_MAX_RETRIES:
1300
+ try:
1301
+ response = requests.post(
1302
+ url=request_url,
1303
+ data=self.credentials(),
1304
+ headers=REQUEST_HEADERS_XML,
1305
+ timeout=REQUEST_TIMEOUT,
1306
+ )
1307
+ except requests.exceptions.RequestException as exception:
1308
+ self.logger.warning(
1309
+ "Unable to connect to OTAWP authentication endpoint -> %s; error -> %s",
1310
+ self.credential_url(),
1311
+ str(exception),
1312
+ )
1313
+ self.logger.warning("OTAWP service may not be ready yet. Retry in %d seconds...", REQUEST_RETRY_DELAY)
1314
+ time.sleep(REQUEST_RETRY_DELAY)
1315
+ retries += 1
1316
+ continue
1317
+
1318
+ if response.ok:
1319
+ soap_response = self.parse_xml(xml_string=response.text)
1320
+ if not soap_response:
1321
+ self.logger.error("Failed to parse the SOAP response with the authentication data!")
1322
+ self.logger.debug("SOAP message -> %s", response.text)
1323
+ return None
1324
+ otawp_ticket = self.find_key(
1325
+ data=soap_response,
1326
+ target_key="AssertionArtifact",
1327
+ )
1328
+ if otawp_ticket:
1329
+ self.logger.info(
1330
+ "Successfully authenticated at AppWorks organization -> '%s' with URL -> %s and user -> '%s'.",
1331
+ self.config()["organization"],
1332
+ self.credential_url(),
1333
+ self.config()["username"],
1334
+ )
1335
+ self.logger.debug("SAML token -> %s", otawp_ticket)
1336
+ self._cookie = {"defaultinst_SAMLart": otawp_ticket, "defaultinst_ct": "abcd"}
1337
+ self._otawp_ticket = otawp_ticket
1338
+
1339
+ return self._cookie
1340
+ else:
1341
+ self.logger.error(
1342
+ "Cannot retrieve OTAWP ticket! Received corrupt authentication data -> %s",
1343
+ response.text,
1344
+ )
1345
+ return None
1346
+ else:
1347
+ self.logger.error(
1348
+ "Failed to request an OTAWP ticket at authentication URL -> %s with user -> '%s'!%s",
1349
+ self.credential_url(),
1350
+ self.config()["username"],
1351
+ " Reason -> '{}'".format(response.reason) if response.reason else "",
1352
+ )
1353
+ return None
1354
+
1355
+ self.logger.error(
1356
+ "Authentication at AppWorks platform failed after %d retries. %sBailing out.",
1357
+ REQUEST_MAX_RETRIES,
1358
+ "{}. ".format(response.text) if response and response.text else "",
1359
+ )
1360
+ return None
1361
+
1362
+ # end method definition
1363
+
1364
+ def create_workspace(
1365
+ self, workspace_name: str, workspace_id: str, show_error: bool = True
1366
+ ) -> tuple[dict | None, bool]:
1367
+ """Create a workspace in cws.
1368
+
1369
+ Args:
1370
+ workspace_name (str):
1371
+ The name of the workspace.
1372
+ workspace_id (str):
1373
+ The ID of the workspace.
1374
+ show_error (bool, optional):
1375
+ Whether to show an error or a warning instead.
1376
+
1377
+ Returns:
1378
+ dict | None:
1379
+ Response dictionary or error text.
1380
+ bool:
1381
+ True, if a new workspace has been created, False if the workspace did already exist.
1382
+
1383
+ """
1384
+
1385
+ self.logger.info(
1386
+ "Create workspace -> '%s' (%s)...",
1387
+ workspace_name,
475
1388
  workspace_id,
476
1389
  )
1390
+
477
1391
  unique_id = uuid.uuid4()
478
1392
 
479
- license_post_body_json = f"""<SOAP:Envelope xmlns:SOAP="http://schemas.xmlsoap.org/soap/envelope/">
480
- <SOAP:Body>
481
- <createWorkspace xmlns="http://schemas.cordys.com/cws/runtime/types/workspace/creation/DevelopmentWorkspaceCreator/1.0" async="false" workspaceID="__CWS System__" xmlns:c="http://schemas.cordys.com/cws/1.0">
482
- <instance>
483
- <c:Document s="T" path="D43B04C1-CD0B-A1EB-A898-53C71DB5D652">
484
- <c:Header>
485
- <c:System>
486
- <c:TypeID>001A6B1E-0C0C-11DF-F5E9-866B84E5D671</c:TypeID>
487
- <c:ID>D43B04C1-CD0B-A1EB-A898-53C71DB5D652</c:ID>
488
- <c:Name>D43B04C1-CD0B-A1EB-A898-53C71DB5D652</c:Name>
489
- <c:Description>D43B04C1-CD0B-A1EB-A898-53C71DB5D652</c:Description>
490
- </c:System>
491
- </c:Header>
492
- <c:Content>
493
- <DevelopmentWorkspaceCreator type="com.cordys.cws.runtime.types.workspace.creation.DevelopmentWorkspaceCreator" runtimeDocumentID="D43B04C1-CD0B-A1EB-A898-53C71DB61652">
494
- <Workspace>
495
- <uri id="{workspace_id}"/>
496
- </Workspace>
497
- </DevelopmentWorkspaceCreator>
498
- </c:Content>
499
- </c:Document>
500
- </instance>
501
- <__prefetch>
502
- <Document xmlns="http://schemas.cordys.com/cws/1.0" path="{workspace_name}" s="N" isLocal="IN_LOCAL">
503
- <Header>
504
- <System>
505
- <ID>{workspace_id}</ID>
506
- <Name>{workspace_name}</Name>
507
- <TypeID>{{4CE11E00-2D97-45C0-BC6C-FAEC1D871026}}</TypeID>
508
- <ParentID/>
509
- <Description>{workspace_name}</Description>
510
- <CreatedBy>sysadmin</CreatedBy>
511
- <CreationDate/>
512
- <LastModifiedBy>sysadmin</LastModifiedBy>
513
- <LastModifiedDate>2021-04-21T06:52:34.254</LastModifiedDate>
514
- <FQN/>
515
- <Annotation/>
516
- <ParentID/>
517
- <OptimisticLock/>
518
- </System>
519
- </Header>
520
- <Content>
521
- <DevelopmentWorkspace xmlns="http://schemas.cordys.com/cws/runtime/types/workspace/DevelopmentWorkspace/1.0" runtimeDocumentID="D43B04C1-CD0B-A1EB-A898-53C71DB59652" type="com.cordys.cws.runtime.types.workspace.DevelopmentWorkspace">
522
- <ExternalID/>
523
- <OrganizationName/>
524
- <SCMAdapter>
525
- <uri id="{unique_id}"/>
526
- </SCMAdapter>
527
- <UpgradedTo/>
528
- <LastWorkspaceUpgradeStep/>
529
- <Metaspace/>
530
- </DevelopmentWorkspace>
531
- </Content>
532
- </Document>
533
- <Document xmlns="http://schemas.cordys.com/cws/1.0" path="{workspace_name}/Untitled No SCM adapter" s="N" isLocal="IN_LOCAL">
534
- <Header>
535
- <System>
536
- <ID>{unique_id}</ID>
537
- <Name>Untitled No SCM adapter</Name>
538
- <TypeID>{{E89F3F62-8CA3-4F93-95A8-F76642FD5124}}</TypeID>
539
- <ParentID>{workspace_id}</ParentID>
540
- <Description>Untitled No SCM adapter</Description>
541
- <CreatedBy>sysadmin</CreatedBy>
542
- <CreationDate/>
543
- <LastModifiedBy>sysadmin</LastModifiedBy>
544
- <LastModifiedDate>2021-04-21T06:52:34.254</LastModifiedDate>
545
- <FQN/>
546
- <Annotation/>
547
- <OptimisticLock/>
548
- </System>
549
- </Header>
550
- <Content>
551
- <NullAdapter xmlns="http://schemas.cordys.com/cws/runtime/types/teamdevelopment/NullAdapter/1.0" runtimeDocumentID="D43B04C1-CD0B-A1EB-A898-53C71DB51652" type="com.cordys.cws.runtime.types.teamdevelopment.NullAdapter">
552
- <Workspace>
553
- <uri id="{workspace_id}"/>
554
- </Workspace>
555
- </NullAdapter>
556
- </Content>
557
- </Document>
558
- </__prefetch>
559
- </createWorkspace>
560
- </SOAP:Body>
561
- </SOAP:Envelope>"""
1393
+ create_workspace_data = f"""
1394
+ <SOAP:Envelope xmlns:SOAP="http://schemas.xmlsoap.org/soap/envelope/">
1395
+ <SOAP:Body>
1396
+ <createWorkspace xmlns="http://schemas.cordys.com/cws/runtime/types/workspace/creation/DevelopmentWorkspaceCreator/1.0" async="false" workspaceID="__CWS System__" xmlns:c="http://schemas.cordys.com/cws/1.0">
1397
+ <instance>
1398
+ <c:Document s="T" path="D43B04C1-CD0B-A1EB-A898-53C71DB5D652">
1399
+ <c:Header>
1400
+ <c:System>
1401
+ <c:TypeID>001A6B1E-0C0C-11DF-F5E9-866B84E5D671</c:TypeID>
1402
+ <c:ID>D43B04C1-CD0B-A1EB-A898-53C71DB5D652</c:ID>
1403
+ <c:Name>D43B04C1-CD0B-A1EB-A898-53C71DB5D652</c:Name>
1404
+ <c:Description>D43B04C1-CD0B-A1EB-A898-53C71DB5D652</c:Description>
1405
+ </c:System>
1406
+ </c:Header>
1407
+ <c:Content>
1408
+ <DevelopmentWorkspaceCreator type="com.cordys.cws.runtime.types.workspace.creation.DevelopmentWorkspaceCreator" runtimeDocumentID="D43B04C1-CD0B-A1EB-A898-53C71DB61652">
1409
+ <Workspace>
1410
+ <uri id="{workspace_id}"/>
1411
+ </Workspace>
1412
+ </DevelopmentWorkspaceCreator>
1413
+ </c:Content>
1414
+ </c:Document>
1415
+ </instance>
1416
+ <__prefetch>
1417
+ <Document xmlns="http://schemas.cordys.com/cws/1.0" path="{workspace_name}" s="N" isLocal="IN_LOCAL">
1418
+ <Header>
1419
+ <System>
1420
+ <ID>{workspace_id}</ID>
1421
+ <Name>{workspace_name}</Name>
1422
+ <TypeID>{{4CE11E00-2D97-45C0-BC6C-FAEC1D871026}}</TypeID>
1423
+ <ParentID/>
1424
+ <Description>{workspace_name}</Description>
1425
+ <CreatedBy>sysadmin</CreatedBy>
1426
+ <CreationDate/>
1427
+ <LastModifiedBy>sysadmin</LastModifiedBy>
1428
+ <LastModifiedDate>2021-04-21T06:52:34.254</LastModifiedDate>
1429
+ <FQN/>
1430
+ <Annotation/>
1431
+ <ParentID/>
1432
+ <OptimisticLock/>
1433
+ </System>
1434
+ </Header>
1435
+ <Content>
1436
+ <DevelopmentWorkspace xmlns="http://schemas.cordys.com/cws/runtime/types/workspace/DevelopmentWorkspace/1.0" runtimeDocumentID="D43B04C1-CD0B-A1EB-A898-53C71DB59652" type="com.cordys.cws.runtime.types.workspace.DevelopmentWorkspace">
1437
+ <ExternalID/>
1438
+ <OrganizationName/>
1439
+ <SCMAdapter>
1440
+ <uri id="{unique_id}"/>
1441
+ </SCMAdapter>
1442
+ <UpgradedTo/>
1443
+ <LastWorkspaceUpgradeStep/>
1444
+ <Metaspace/>
1445
+ </DevelopmentWorkspace>
1446
+ </Content>
1447
+ </Document>
1448
+ <Document xmlns="http://schemas.cordys.com/cws/1.0" path="{workspace_name}/Untitled No SCM adapter" s="N" isLocal="IN_LOCAL">
1449
+ <Header>
1450
+ <System>
1451
+ <ID>{unique_id}</ID>
1452
+ <Name>Untitled No SCM adapter</Name>
1453
+ <TypeID>{{E89F3F62-8CA3-4F93-95A8-F76642FD5124}}</TypeID>
1454
+ <ParentID>{workspace_id}</ParentID>
1455
+ <Description>Untitled No SCM adapter</Description>
1456
+ <CreatedBy>sysadmin</CreatedBy>
1457
+ <CreationDate/>
1458
+ <LastModifiedBy>sysadmin</LastModifiedBy>
1459
+ <LastModifiedDate>2021-04-21T06:52:34.254</LastModifiedDate>
1460
+ <FQN/>
1461
+ <Annotation/>
1462
+ <OptimisticLock/>
1463
+ </System>
1464
+ </Header>
1465
+ <Content>
1466
+ <NullAdapter xmlns="http://schemas.cordys.com/cws/runtime/types/teamdevelopment/NullAdapter/1.0" runtimeDocumentID="D43B04C1-CD0B-A1EB-A898-53C71DB51652" type="com.cordys.cws.runtime.types.teamdevelopment.NullAdapter">
1467
+ <Workspace>
1468
+ <uri id="{workspace_id}"/>
1469
+ </Workspace>
1470
+ </NullAdapter>
1471
+ </Content>
1472
+ </Document>
1473
+ </__prefetch>
1474
+ </createWorkspace>
1475
+ </SOAP:Body>
1476
+ </SOAP:Envelope>"""
1477
+
1478
+ error_messages = [
1479
+ "Collaborative Workspace Service Container is not able to handle the SOAP request",
1480
+ "Service Group Lookup failure",
1481
+ ]
1482
+
1483
+ exist_messages = [
1484
+ "Object already exists",
1485
+ "createWorkspaceResponse",
1486
+ ]
1487
+
1488
+ request_url = self.gateway_url()
562
1489
 
563
1490
  retries = 0
564
- while True:
565
- response = requests.post(
566
- url=self.gateway_url(),
567
- data=license_post_body_json,
568
- headers=REQUEST_HEADERS,
569
- cookies=self.cookie(),
570
- timeout=None,
571
- )
1491
+
1492
+ while retries < REQUEST_MAX_RETRIES:
1493
+ try:
1494
+ response = requests.post(
1495
+ url=request_url,
1496
+ data=create_workspace_data,
1497
+ headers=REQUEST_HEADERS_XML,
1498
+ cookies=self.cookie(),
1499
+ timeout=REQUEST_TIMEOUT,
1500
+ )
1501
+ except requests.RequestException as req_exception:
1502
+ self.logger.warning(
1503
+ "Request to create workspace -> '%s' failed with error -> %s. Retry in %d seconds...",
1504
+ workspace_name,
1505
+ str(req_exception),
1506
+ REQUEST_RETRY_DELAY,
1507
+ )
1508
+ time.sleep(REQUEST_RETRY_DELAY)
1509
+ retries += 1
1510
+ continue
1511
+
572
1512
  if response.ok:
573
- logger.info(
574
- "Successfully created workspace -> '%s' with ID -> %s",
1513
+ self.logger.info(
1514
+ "Successfully created workspace -> '%s' (%s).",
575
1515
  workspace_name,
576
1516
  workspace_id,
577
1517
  )
578
- return response.text
1518
+ # True indicates that a new workspaces has been created.
1519
+ return (self.parse_xml(response.text), True)
1520
+
579
1521
  # Check if Session has expired - then re-authenticate and try once more
580
1522
  if response.status_code == 401 and retries == 0:
581
- logger.warning("Session has expired - try to re-authenticate...")
1523
+ self.logger.warning("Session expired. Re-authenticating...")
582
1524
  self.authenticate(revalidate=True)
583
1525
  retries += 1
584
- logger.error(response.text)
585
- return response.text
1526
+ continue
1527
+
1528
+ # Check if the workspace does exist already:
1529
+ if any(exist_message in response.text for exist_message in exist_messages):
1530
+ self.logger.info("Workspace -> '%s' with ID -> '%s' already exists!", workspace_name, workspace_id)
1531
+ self.logger.debug("SOAP message -> %s", response.text)
1532
+
1533
+ # False indicates that a new workspaces has NOT been created.
1534
+ return (self.parse_xml(response.text), False)
1535
+
1536
+ # Check if any error message is in the response:
1537
+ if any(error_message in response.text for error_message in error_messages):
1538
+ self.logger.warning(
1539
+ "Workspace service error, waiting %d seconds to retry... (Retry %d of %d)",
1540
+ REQUEST_RETRY_DELAY,
1541
+ retries + 1,
1542
+ REQUEST_MAX_RETRIES,
1543
+ )
1544
+ self.logger.debug("SOAP message -> %s", response.text)
1545
+ time.sleep(REQUEST_RETRY_DELAY)
1546
+ retries += 1
1547
+
1548
+ # end while retries < REQUEST_MAX_RETRIES:
1549
+
1550
+ # After max retries, log and return the response or handle as needed
1551
+ if show_error:
1552
+ self.logger.error(
1553
+ "Max retries reached for workspace -> '%s', unable to create workspace.",
1554
+ workspace_name,
1555
+ )
1556
+ else:
1557
+ self.logger.warning(
1558
+ "Max retries reached for workspace -> '%s', unable to create workspace.",
1559
+ workspace_name,
1560
+ )
1561
+
1562
+ return (None, False)
586
1563
 
587
1564
  # end method definition
588
1565
 
589
- def sync_workspace(
590
- self,
591
- workspace_name: str,
592
- workspace_id: str
593
- ) -> dict | None:
594
- """ sync workspaces
1566
+ def sync_workspace(self, workspace_name: str, workspace_id: str) -> dict | None:
1567
+ """Synchronize workspace.
1568
+
595
1569
  Args:
596
- workspace_name (str): workspace_name
597
- workspace_id (str): workspace_id
1570
+ workspace_name (str):
1571
+ The name of the workspace.
1572
+ workspace_id (str):
1573
+ The ID of the workspace.
1574
+
598
1575
  Returns:
599
- Request response (dictionary) or None if the REST call fails
1576
+ dict | None:
1577
+ Parsed response as a dictionary if successful, None otherwise.
1578
+
600
1579
  """
601
1580
 
602
- logger.info("Start synchronization of workspace -> '%s'...", workspace_name)
1581
+ if not workspace_id:
1582
+ self.logger.error(
1583
+ "Cannot synchronize workspace%s without a workspace ID!",
1584
+ " -> '{}'".format(workspace_name) if workspace_name else "",
1585
+ )
1586
+ return None
603
1587
 
604
- license_post_body_json = f"""<SOAP:Envelope xmlns:SOAP=\"http://schemas.xmlsoap.org/soap/envelope/\">\r\n" +
605
- " <SOAP:Body>\r\n" +
606
- " <Synchronize workspaceID=\"{workspace_id}\" xmlns=\"http://schemas.cordys.com/cws/synchronize/1.0\" >\r\n" +
607
- " <DocumentID/>\r\n" +
608
- " <Asynchronous>false</Asynchronous>\r\n" +
609
- " </Synchronize>\r\n" +
610
- " </SOAP:Body>\r\n" +
611
- "</SOAP:Envelope>"""
612
- # self.authenticate(revalidate=True)
1588
+ self.logger.info("Starting synchronization of workspace -> '%s' (%s)...", workspace_name, workspace_id)
1589
+
1590
+ # SOAP request body
1591
+ sync_workspace_data = f"""
1592
+ <SOAP:Envelope xmlns:SOAP="http://schemas.xmlsoap.org/soap/envelope/">
1593
+ <SOAP:Body>
1594
+ <Synchronize workspaceID="{workspace_id}" xmlns="http://schemas.cordys.com/cws/synchronize/1.0">
1595
+ <DocumentID/>
1596
+ <Asynchronous>false</Asynchronous>
1597
+ </Synchronize>
1598
+ </SOAP:Body>
1599
+ </SOAP:Envelope>
1600
+ """
1601
+
1602
+ request_url = self.gateway_url()
1603
+
1604
+ self.logger.debug(
1605
+ "Synchronize workspace -> '%s' (%s); calling -> '%s'",
1606
+ workspace_name,
1607
+ workspace_id,
1608
+ request_url,
1609
+ )
613
1610
 
614
1611
  retries = 0
615
- while True:
616
- response = requests.post(
617
- url=self.gateway_url(),
618
- data=license_post_body_json,
619
- headers=REQUEST_HEADERS,
620
- cookies=self.cookie(),
621
- timeout=None,
622
- )
1612
+
1613
+ while retries < REQUEST_MAX_RETRIES:
1614
+ try:
1615
+ response = requests.post(
1616
+ url=request_url,
1617
+ data=sync_workspace_data,
1618
+ headers=REQUEST_HEADERS_XML,
1619
+ cookies=self.cookie(),
1620
+ timeout=SYNC_PUBLISH_REQUEST_TIMEOUT,
1621
+ )
1622
+ except requests.RequestException as req_exception:
1623
+ self.logger.warning(
1624
+ "Request to synchronize workspace -> '%s' failed with error -> %s. Retry in %d seconds...",
1625
+ workspace_name,
1626
+ str(req_exception),
1627
+ REQUEST_RETRY_DELAY,
1628
+ )
1629
+ time.sleep(REQUEST_RETRY_DELAY)
1630
+ retries += 1
1631
+ continue
1632
+
623
1633
  if response.ok:
624
- logger.info("Workspace -> '%s' synced successfully", workspace_name)
1634
+ self.logger.info("Successfully synchronized workspace -> '%s' (%s).", workspace_name, workspace_id)
625
1635
  return self.parse_xml(response.text)
1636
+
1637
+ # Check if Session has expired - then re-authenticate and try once more
626
1638
  if response.status_code == 401 and retries == 0:
627
- logger.warning("Session has expired - try to re-authenticate...")
1639
+ self.logger.warning("Session expired. Re-authenticating...")
628
1640
  self.authenticate(revalidate=True)
629
1641
  retries += 1
630
- logger.error(response.text)
631
- return None
1642
+ continue
1643
+
1644
+ if SOAP_FAULT_INDICATOR in response.text:
1645
+ self.logger.error(
1646
+ "Workspace synchronization failed with error -> '%s' when calling -> %s!",
1647
+ self.get_soap_element(soap_response=response.text, soap_tag="faultstring"),
1648
+ self.get_soap_element(soap_response=response.text, soap_tag="faultactor"),
1649
+ )
1650
+ self.logger.debug("SOAP message -> %s", response.text)
1651
+ return None
1652
+
1653
+ self.logger.error("Unexpected error during workspace synchronization -> %s", response.text)
1654
+ time.sleep(REQUEST_RETRY_DELAY)
1655
+ retries += 1
1656
+
1657
+ # end while retries < REQUEST_MAX_RETRIES:
1658
+
1659
+ self.logger.error(
1660
+ "Synchronization failed for workspace -> '%s' after %d retries.",
1661
+ workspace_name,
1662
+ retries,
1663
+ )
1664
+ return None
632
1665
 
633
1666
  # end method definition
634
1667
 
635
1668
  def publish_project(
636
- self,
637
- workspace_name: str,
638
- project_name: str,
639
- workspace_id: str,
640
- project_id: str
641
- ) -> dict | bool:
642
- """
643
- Publish the workspace project.
1669
+ self,
1670
+ workspace_name: str,
1671
+ workspace_id: str,
1672
+ project_name: str,
1673
+ project_id: str,
1674
+ ) -> bool:
1675
+ """Publish the workspace project.
644
1676
 
645
1677
  Args:
646
- workspace_name (str): The name of the workspace.
647
- project_name (str): The name of the project.
648
- workspace_id (str): The workspace ID.
649
- project_id (str): The project ID.
1678
+ workspace_name (str):
1679
+ The name of the workspace.
1680
+ workspace_id (str):
1681
+ The workspace ID.
1682
+ project_name (str):
1683
+ The name of the project.
1684
+ project_id (str):
1685
+ The project ID.
650
1686
 
651
1687
  Returns:
652
- dict | bool: Request response (dictionary) if successful, False if it fails after retries.
1688
+ bool:
1689
+ True if successful, False if it fails after retries.
1690
+
653
1691
  """
654
1692
 
655
- logger.info(
656
- "Publish project -> '%s' in workspace -> '%s'...",
1693
+ self.logger.info(
1694
+ "Publish project -> '%s' (%s) in workspace -> '%s' (%s)...",
657
1695
  project_name,
1696
+ project_id,
658
1697
  workspace_name,
1698
+ workspace_id,
659
1699
  )
660
1700
 
661
- project_publish = f"""<SOAP:Envelope xmlns:SOAP="http://schemas.xmlsoap.org/soap/envelope/">
1701
+ # Validation of parameters:
1702
+ required_fields = {
1703
+ "workspace": workspace_name,
1704
+ "workspace ID": workspace_id,
1705
+ "project": project_name,
1706
+ "project ID": project_id,
1707
+ }
1708
+
1709
+ for name, value in required_fields.items():
1710
+ if not value:
1711
+ self.logger.error(
1712
+ "Cannot publish project%s without a %s!",
1713
+ " -> '{}'".format(project_name) if project_name else "",
1714
+ name,
1715
+ )
1716
+ return None
1717
+
1718
+ project_publish_data = f"""
1719
+ <SOAP:Envelope xmlns:SOAP="http://schemas.xmlsoap.org/soap/envelope/">
662
1720
  <SOAP:Body>
663
1721
  <deployObject xmlns="http://schemas.cordys.com/cws/internal/buildhelper/BuildHelper/1.0" async="false" workspaceID="{workspace_id}" xmlns:c="http://schemas.cordys.com/cws/1.0">
664
1722
  <object>
@@ -666,1055 +1724,1074 @@ class OTAWP:
666
1724
  </object>
667
1725
  </deployObject>
668
1726
  </SOAP:Body>
669
- </SOAP:Envelope>"""
1727
+ </SOAP:Envelope>
1728
+ """
670
1729
 
671
1730
  # Initialize retry parameters
672
- max_retries = 10
673
1731
  retries = 0
674
1732
  success_indicator = "deployObjectResponse"
675
1733
 
676
- while retries < max_retries:
677
- response = requests.post(
678
- url=self.gateway_url(),
679
- data=project_publish,
680
- headers=REQUEST_HEADERS,
681
- cookies=self.cookie(),
682
- timeout=None,
683
- )
1734
+ while retries < REQUEST_MAX_RETRIES:
1735
+ try:
1736
+ response = requests.post(
1737
+ url=self.gateway_url(),
1738
+ data=project_publish_data,
1739
+ headers=REQUEST_HEADERS_XML,
1740
+ cookies=self.cookie(),
1741
+ timeout=SYNC_PUBLISH_REQUEST_TIMEOUT,
1742
+ )
1743
+ except requests.RequestException as req_exception:
1744
+ self.logger.warning(
1745
+ "Request to publish project -> '%s' (%s) failed with error -> %s. Retry in %d seconds...",
1746
+ project_name,
1747
+ project_id,
1748
+ str(req_exception),
1749
+ REQUEST_RETRY_DELAY,
1750
+ )
1751
+ retries += 1
1752
+ time.sleep(REQUEST_RETRY_DELAY)
1753
+ continue
684
1754
 
685
1755
  # Check if the response is successful
686
1756
  if response.ok:
687
- # Check if the response contains the success indicator
688
1757
  if success_indicator in response.text:
689
- logger.info(
690
- "Successfully published project -> '%s' in workspace -> '%s'",
1758
+ self.logger.info(
1759
+ "Successfully published project -> '%s' (%s) in workspace -> '%s' (%s)",
691
1760
  project_name,
1761
+ project_id,
692
1762
  workspace_name,
1763
+ workspace_id,
693
1764
  )
694
1765
  return True
695
-
696
- # If success indicator is not found, retry
697
- logger.warning(
698
- "Expected success indicator -> '%s' but it was not found in response. Retrying in 30 seconds... (Attempt %d of %d)",
699
- success_indicator,
1766
+ else:
1767
+ self.logger.warning(
1768
+ "Expected success indicator -> '%s' but it was not found in response. Retrying in 30 seconds... (Attempt %d of %d)",
1769
+ success_indicator,
1770
+ retries + 1,
1771
+ REQUEST_MAX_RETRIES,
1772
+ )
1773
+ elif response.status_code == 401:
1774
+ # Check for session expiry and retry authentication
1775
+ self.logger.warning("Session has expired - re-authenticating...")
1776
+ self.authenticate(revalidate=True)
1777
+ else:
1778
+ self.logger.error(
1779
+ "Unexpected error (status code -> %d). Retrying in 30 seconds... (Attempt %d of %d)",
1780
+ response.status_code,
700
1781
  retries + 1,
701
- max_retries,
1782
+ REQUEST_MAX_RETRIES,
702
1783
  )
703
- time.sleep(30)
704
- retries += 1
705
- continue
706
-
707
- # Check for session expiry and retry authentication (only once)
708
- if response.status_code == 401 and retries == 0:
709
- logger.warning("Session has expired - re-authenticating...")
710
- self.authenticate(revalidate=True)
711
- retries += 1
712
- continue
1784
+ self.logger.debug(
1785
+ "SOAP message -> %s",
1786
+ response.text,
1787
+ )
1788
+ self.sync_workspace(workspace_name=workspace_name, workspace_id=workspace_id)
1789
+ retries += 1
1790
+ time.sleep(REQUEST_RETRY_DELAY)
713
1791
 
714
- # Log any other error and break the loop
715
- logger.error(
716
- "Error publishing project -> '%s' in workspace -> '%s'; response -> %s",
717
- project_name,
718
- workspace_name,
719
- response.text,
720
- )
721
- break
1792
+ # end while retries < REQUEST_MAX_RETRIES:
722
1793
 
723
1794
  # After reaching the maximum number of retries, log failure and return False
724
- logger.error(
1795
+ self.logger.error(
725
1796
  "Max retries reached. Failed to publish project -> '%s' in workspace -> '%s'.",
726
1797
  project_name,
727
1798
  workspace_name,
728
1799
  )
1800
+
729
1801
  return False
730
1802
 
731
1803
  # end method definition
732
1804
 
733
- def create_priority(
734
- self,
735
- name: str,
736
- description: str,
737
- status: int
738
- ) -> dict | None:
739
- """ Create Priority entity instances.
1805
+ def create_priority(self, name: str, description: str = "", status: int = 1) -> dict | None:
1806
+ """Create Priority entity instance.
740
1807
 
741
1808
  Args:
742
- name (str): name
743
- description (str): description
744
- status (int): status
1809
+ name (str):
1810
+ The name of the priority.
1811
+ description (str, optional):
1812
+ The description of the priority.
1813
+ status (int, optional):
1814
+ The status of the priority. Default is 1.
1815
+
745
1816
  Returns:
746
- dict: Request response (dictionary) or None if the REST call fails
747
- """
748
- create_priority = {
749
- "Properties": {
750
- "Name": name,
751
- "Description": description,
752
- "Status": status
1817
+ dict:
1818
+ Request response (dictionary) or None if the REST call fails
1819
+
1820
+ Example:
1821
+ {
1822
+ 'Identity': {
1823
+ 'Id': '327681'
1824
+ },
1825
+ '_links': {
1826
+ 'self': {
1827
+ 'href': '/OpentextCaseManagement/entities/Priority/items/327681'
1828
+ }
753
1829
  }
754
1830
  }
755
- retries = 0
756
- while True:
757
- response = requests.post(
758
- url=self.create_priority_url(),
759
- json=create_priority,
760
- headers=REQUEST_HEADERS_JSON,
761
- cookies=self.cookie(),
762
- timeout=None,
763
- )
764
- if response.ok:
765
- logger.info("Priority created successfully")
766
- return self.parse_request_response(
767
- response, "This can be normal during restart", False
768
- )
769
- if response.status_code == 401 and retries == 0:
770
- logger.warning("Session has expired - try to re-authenticate...")
771
- self.authenticate(revalidate=True)
772
- retries += 1
773
- logger.error(response.text)
1831
+
1832
+ """
1833
+
1834
+ # Sanity checks as the parameters come directly from payload:
1835
+ if not name:
1836
+ self.logger.error("Cannot create a priority without a name!")
774
1837
  return None
775
1838
 
1839
+ create_priority_data = {
1840
+ "Properties": {"Name": name, "Description": description, "Status": status},
1841
+ }
1842
+
1843
+ request_url = self.get_create_priority_url()
1844
+
1845
+ return self.do_request(
1846
+ url=request_url,
1847
+ method="POST",
1848
+ headers=REQUEST_HEADERS_JSON,
1849
+ cookies=self.cookie(),
1850
+ json_data=create_priority_data,
1851
+ timeout=REQUEST_TIMEOUT,
1852
+ failure_message="Request to create priority -> '{}' failed".format(name),
1853
+ )
1854
+
776
1855
  # end method definition
777
1856
 
778
- def get_all_priorities(
779
- self
780
- ) -> dict | None:
781
- """ Get all priorities from entity
1857
+ def get_priorities(self) -> dict | None:
1858
+ """Get all priorities from entity.
1859
+
782
1860
  Args:
783
1861
  None
1862
+
784
1863
  Returns:
785
- dict: Request response (dictionary) or None if the REST call fails
1864
+ dict:
1865
+ Request response (dictionary with priority values) or None if the REST call fails.
1866
+
1867
+ Example:
1868
+ {
1869
+ 'page': {
1870
+ 'skip': 0,
1871
+ 'top': 0,
1872
+ 'count': 4,
1873
+ 'ftsEnabled': False
1874
+ },
1875
+ '_links': {
1876
+ 'self': {
1877
+ 'href': '/OpentextCaseManagement/entities/Priority/lists/PriorityList'
1878
+ },
1879
+ 'first': {
1880
+ 'href': '/OpentextCaseManagement/entities/Priority/lists/PriorityList'
1881
+ }
1882
+ },
1883
+ '_embedded': {
1884
+ 'PriorityList': {
1885
+ 'PriorityList': [
1886
+ {
1887
+ '_links': {
1888
+ 'href': '/OpentextCaseManagement/entities/Priority/items/1'
1889
+ },
1890
+ 'Properties': {
1891
+ 'Name': 'High',
1892
+ 'Description': 'High',
1893
+ 'Status': 1
1894
+ }
1895
+ },
1896
+ {
1897
+ '_links': {'item': {...}},
1898
+ 'Properties': {'Name': 'Medium', 'Description': 'Medium', 'Status': 1}
1899
+ },
1900
+ {
1901
+ '_links': {'item': {...}},
1902
+ 'Properties': {'Name': 'Low', 'Description': 'Low', 'Status': 1}
1903
+ },
1904
+ {
1905
+ '_links': {'item': {...}},
1906
+ 'Properties': {'Name': 'Marc Test 1', 'Description': 'Marc Test 1 Description', 'Status': 1}
1907
+ }
1908
+ ]
1909
+ }
1910
+ }
1911
+ }
1912
+
786
1913
  """
787
- retries = 0
788
- while True:
789
- response = requests.get(
790
- url=self.get_all_priorities_url(),
791
- headers=REQUEST_HEADERS_JSON,
792
- cookies=self.cookie(),
793
- timeout=None,
794
- )
795
- if response.ok:
796
- authenticate_dict = self.parse_request_response(
797
- response, "This can be normal during restart", False
798
- )
799
- if not authenticate_dict:
800
- return None
801
- return authenticate_dict
802
- if response.status_code == 401 and retries == 0:
803
- logger.warning("Session has expired - try to re-authenticate...")
804
- self.authenticate(revalidate=True)
805
- retries += 1
806
- logger.error(response.text)
807
- return None
1914
+
1915
+ request_url = self.get_priorities_list_url()
1916
+
1917
+ return self.do_request(
1918
+ url=request_url,
1919
+ method="GET",
1920
+ headers=REQUEST_HEADERS_JSON,
1921
+ cookies=self.cookie(),
1922
+ timeout=REQUEST_TIMEOUT,
1923
+ failure_message="Request to get priorities failed",
1924
+ )
808
1925
 
809
1926
  # end method definition
810
1927
 
811
- def create_customer(
812
- self,
813
- customer_name: str,
814
- legal_business_name: str,
815
- trading_name: str
816
- ) -> dict | None:
817
- """ Create customer entity instance
1928
+ def get_priority_by_name(self, name: str) -> dict | None:
1929
+ """Get priority entity instance by its name.
818
1930
 
819
1931
  Args:
820
- customer_name (str): customer_name
821
- legal_business_name (str): legal_business_name
822
- trading_name (str): trading_name
1932
+ name (str):
1933
+ The name of the priority.
1934
+
823
1935
  Returns:
824
- dict: Request response (dictionary) or None if the REST call fails
1936
+ dict | None:
1937
+ Returns the priority item or None if it does not exist.
1938
+
825
1939
  """
826
- create_customer = {
827
- "Properties": {
828
- "CustomerName": customer_name,
829
- "LegalBusinessName": legal_business_name,
830
- "TradingName": trading_name
831
- }
832
- }
833
- retries = 0
834
- while True:
835
- response = requests.post(
836
- url=self.create_customer_url(),
837
- json=create_customer,
838
- headers=REQUEST_HEADERS_JSON,
839
- cookies=self.cookie(),
840
- timeout=None,
841
- )
842
- if response.ok:
843
- logger.info("Customer record created successfully")
844
- return self.parse_request_response(response, "This can be normal during restart", False)
845
- if response.status_code == 401 and retries == 0:
846
- logger.warning("Session has expired - try to re-authenticate...")
847
- self.authenticate(revalidate=True)
848
- retries += 1
849
- logger.error(response.text)
850
- return None
1940
+
1941
+ priorities = self.get_priorities()
1942
+
1943
+ return self.get_result_item(response=priorities, entity_type="PriorityList", key="Name", value=name)
851
1944
 
852
1945
  # end method definition
853
1946
 
854
- def get_all_customers(
855
- self
856
- ) -> dict | None:
857
- """get all customer entity imstances
1947
+ def get_priority_ids(self) -> list:
1948
+ """Get all priority entity instances IDs.
858
1949
 
859
1950
  Args:
860
1951
  None
861
1952
  Returns:
862
- dict: Request response (dictionary) or None if the REST call fails
1953
+ list:
1954
+ A list with all priority IDs.
1955
+
863
1956
  """
864
1957
 
865
- retries = 0
866
- while True:
867
- response = requests.get(
868
- url=self.get_all_customeres_url(),
869
- headers=REQUEST_HEADERS_JSON,
870
- cookies=self.cookie(),
871
- timeout=None,
872
- )
873
- if response.ok:
874
- authenticate_dict = self.parse_request_response(
875
- response, "This can be normal during restart", False
876
- )
877
- if not authenticate_dict:
878
- return None
879
- return authenticate_dict
880
- if response.status_code == 401 and retries == 0:
881
- logger.warning("Session has expired - try to re-authenticate...")
882
- self.authenticate(revalidate=True)
883
- retries += 1
884
- logger.error(response.text)
885
- return None
1958
+ priorities = self.get_priorities()
1959
+
1960
+ return self.get_result_values(response=priorities, entity_type="PriorityList", key="id") or []
886
1961
 
887
1962
  # end method definition
888
1963
 
889
- def create_case_type(
1964
+ def create_customer(
890
1965
  self,
891
- name: str,
892
- description: str,
893
- status: int
1966
+ customer_name: str,
1967
+ legal_business_name: str,
1968
+ trading_name: str,
894
1969
  ) -> dict | None:
895
- """create case_type entity instances
1970
+ """Create customer entity instance.
896
1971
 
897
1972
  Args:
898
- name (str): name
899
- description (str): description
900
- status (str): status
1973
+ customer_name (str):
1974
+ The name of the customer.
1975
+ legal_business_name (str):
1976
+ The legal business name.
1977
+ trading_name (str):
1978
+ The trading name.
1979
+
901
1980
  Returns:
902
- dict: Request response (dictionary) or None if the REST call fails
1981
+ dict:
1982
+ Request response (dictionary) or None if the REST call fails.
1983
+
903
1984
  """
904
- create_case_type = {
1985
+
1986
+ # Sanity checks as the parameters come directly from payload:
1987
+ if not customer_name:
1988
+ self.logger.error("Cannot create a customer without a name!")
1989
+ return None
1990
+
1991
+ create_customer_data = {
905
1992
  "Properties": {
906
- "Name": name,
907
- "Description": description,
908
- "Status": status
909
- }
1993
+ "CustomerName": customer_name,
1994
+ "LegalBusinessName": legal_business_name,
1995
+ "TradingName": trading_name,
1996
+ },
910
1997
  }
911
- retries = 0
912
- while True:
913
- response = requests.post(
914
- url=self.create_casetype_url(),
915
- json=create_case_type,
916
- headers=REQUEST_HEADERS_JSON,
917
- cookies=self.cookie(),
918
- timeout=None,
919
- )
920
- if response.ok:
921
- logger.info("Case type created successfully")
922
- return self.parse_request_response(
923
- response, "This can be normal during restart", False
924
- )
925
- if response.status_code == 401 and retries == 0:
926
- logger.warning("Session has expired - try to re-authenticate...")
927
- self.authenticate(revalidate=True)
928
- retries += 1
929
- logger.error(response.text)
930
- return None
1998
+
1999
+ request_url = self.get_create_customer_url()
2000
+
2001
+ return self.do_request(
2002
+ url=request_url,
2003
+ method="POST",
2004
+ headers=REQUEST_HEADERS_JSON,
2005
+ cookies=self.cookie(),
2006
+ json_data=create_customer_data,
2007
+ timeout=REQUEST_TIMEOUT,
2008
+ failure_message="Request to create customer -> '{}' failed".format(customer_name),
2009
+ )
931
2010
 
932
2011
  # end method definition
933
2012
 
934
- def get_all_case_type(
935
- self
936
- ) -> dict | None:
937
- """get all case type entty instances
2013
+ def get_customers(self) -> dict | None:
2014
+ """Get all customer entity instances.
938
2015
 
939
2016
  Args:
940
2017
  None
2018
+
941
2019
  Returns:
942
- dict: Request response (dictionary) or None if the REST call fails
2020
+ dict | None:
2021
+ Request response (dictionary) or None if the REST call fails.
2022
+
2023
+ Example:
2024
+ {
2025
+ 'page': {
2026
+ 'skip': 0,
2027
+ 'top': 0,
2028
+ 'count': 4,
2029
+ 'ftsEnabled': False
2030
+ },
2031
+ '_links': {
2032
+ 'self': {
2033
+ 'href': '/OpentextCaseManagement/entities/Customer/lists/CustomerList'
2034
+ },
2035
+ 'first': {...}
2036
+ },
2037
+ '_embedded': {
2038
+ 'CustomerList': [
2039
+ {
2040
+ '_links': {
2041
+ 'item': {
2042
+ 'href': '/OpentextCaseManagement/entities/Customer/items/1'
2043
+ }
2044
+ },
2045
+ 'Properties': {
2046
+ 'CustomerName': 'InaPlex Limited',
2047
+ 'LegalBusinessName': 'InaPlex Limited',
2048
+ 'TradingName': 'InaPlex Limited'
2049
+ }
2050
+ },
2051
+ {
2052
+ '_links': {...},
2053
+ 'Properties': {...}
2054
+ },
2055
+ ...
2056
+ ]
2057
+ }
2058
+ }
2059
+
943
2060
  """
944
- retries = 0
945
- while True:
946
- response = requests.get(
947
- url=self.get_all_case_types_url(),
948
- headers=REQUEST_HEADERS_JSON,
949
- cookies=self.cookie(),
950
- timeout=None,
951
- )
952
- if response.ok:
953
- authenticate_dict = self.parse_request_response(
954
- response, "This can be normal during restart", False
955
- )
956
- if not authenticate_dict:
957
- return None
958
- return authenticate_dict
959
- if response.status_code == 401 and retries == 0:
960
- logger.warning("Session has expired - try to re-authenticate...")
961
- self.authenticate(revalidate=True)
962
- retries += 1
963
- logger.error(response.text)
964
- return None
2061
+
2062
+ request_url = self.get_customers_list_url()
2063
+
2064
+ return self.do_request(
2065
+ url=request_url,
2066
+ method="GET",
2067
+ headers=REQUEST_HEADERS_JSON,
2068
+ cookies=self.cookie(),
2069
+ timeout=REQUEST_TIMEOUT,
2070
+ failure_message="Request to get customers failed",
2071
+ )
965
2072
 
966
2073
  # end method definition
967
2074
 
968
- def create_category(
969
- self,
970
- case_prefix: str,
971
- description: str,
972
- name: str,
973
- status: int
974
- ) -> dict | None:
975
- """create category entity instance
2075
+ def get_customer_by_name(self, name: str) -> dict | None:
2076
+ """Get customer entity instance by its name.
976
2077
 
977
2078
  Args:
978
- case_prefix (str): workspace_name
979
- description (str): description
980
- name (str): name
981
- status (str): status
2079
+ name (str):
2080
+ The name of the customer.
2081
+
982
2082
  Returns:
983
- dict: Request response (dictionary) or None if the REST call fails
2083
+ dict | None:
2084
+ Returns the customer data or None if no customer with the given name exists.
2085
+
984
2086
  """
985
- create_categoty = {
986
- "Properties": {
987
- "CasePrefix": case_prefix,
988
- "Description": description,
989
- "Name": name,
990
- "Status": status
991
- }
992
- }
993
- retries = 0
994
- while True:
995
- response = requests.post(
996
- url=self.create_category_url(),
997
- json=create_categoty,
998
- headers=REQUEST_HEADERS_JSON,
999
- cookies=self.cookie(),
1000
- timeout=None,
1001
- )
1002
- if response.ok:
1003
- logger.info("Category created successfully")
1004
- return self.parse_request_response(response, "This can be normal during restart", False)
1005
- if response.status_code == 401 and retries == 0:
1006
- logger.warning("Session has expired - try to re-authenticate...")
1007
- self.authenticate(revalidate=True)
1008
- retries += 1
1009
- logger.error(response.text)
1010
- return None
2087
+
2088
+ customers = self.get_customers()
2089
+
2090
+ return self.get_result_item(response=customers, entity_type="CustomerList", key="CustomerName", value=name)
1011
2091
 
1012
2092
  # end method definition
1013
2093
 
1014
- def get_all_categories(
1015
- self
1016
- ) -> dict | None:
1017
- """Get all categories entity intances
2094
+ def get_customer_ids(self) -> list:
2095
+ """Get all customer entity instances IDs.
1018
2096
 
1019
2097
  Args:
1020
2098
  None
1021
2099
  Returns:
1022
- dict: Request response (dictionary) or None if the REST call fails
2100
+ list:
2101
+ A list of all customer IDs.
2102
+
1023
2103
  """
1024
2104
 
1025
- retries = 0
1026
- while True:
1027
- response = requests.get(
1028
- url=self.get_all_categories_url(),
1029
- headers=REQUEST_HEADERS_JSON,
1030
- cookies=self.cookie(),
1031
- timeout=None,
1032
- )
1033
- if response.ok:
1034
- authenticate_dict = self.parse_request_response(
1035
- response, "This can be normal during restart", False
1036
- )
1037
- if not authenticate_dict:
1038
- return None
1039
- return authenticate_dict
1040
- if response.status_code == 401 and retries == 0:
1041
- logger.warning("Session has expired - try to re-authenticate...")
1042
- self.authenticate(revalidate=True)
1043
- retries += 1
1044
- logger.error(response.text)
1045
- return None
2105
+ customers = self.get_customers()
2106
+
2107
+ return self.get_result_values(response=customers, entity_type="CustomerList", key="id") or []
1046
2108
 
1047
2109
  # end method definition
1048
2110
 
1049
- def create_sub_categoy(
1050
- self,
1051
- name: str,
1052
- description: str,
1053
- status: int,
1054
- parentid: int
1055
- ) -> dict | None:
1056
- """ create sub_categoy entity istances
2111
+ def create_case_type(self, name: str, description: str = "", status: int = 1) -> dict | None:
2112
+ """Create case type entity instances.
1057
2113
 
1058
2114
  Args:
1059
- name (str): name
1060
- description (str): description
1061
- status (int): status
1062
- parentid (int): parentid
2115
+ name (str):
2116
+ The name of the case type.
2117
+ description (str, optional):
2118
+ The description of the case type.
2119
+ status (int, optional): status
2120
+
1063
2121
  Returns:
1064
- dict: Request response (dictionary) or None if the REST call fails
2122
+ dict:
2123
+ Request response (dictionary) or None if the REST call fails.
2124
+
1065
2125
  """
1066
- create_sub_categoty = {
1067
- "Properties": {
1068
- "Name": name,
1069
- "Description": description,
1070
- "Status": status
1071
- }
1072
- }
1073
- retries = 0
1074
- while True:
1075
- base_url = self.baseurl()
1076
- endpoint = "/app/entityRestService/api/OpentextCaseManagement/entities/Category/items/"
1077
- child_path = "/childEntities/SubCategory?defaultinst_ct=abcd"
1078
- response = requests.post(
1079
- url=base_url + endpoint + str(parentid) + child_path,
1080
- json=create_sub_categoty,
1081
- headers=REQUEST_HEADERS_JSON,
1082
- cookies=self.cookie(),
1083
- timeout=None,
1084
- )
1085
- if response.ok:
1086
- logger.info("Sub category created successfully")
1087
- return self.parse_request_response(
1088
- response, "This can be normal during restart", False
1089
- )
1090
- if response.status_code == 401 and retries == 0:
1091
- logger.warning("Session has expired - try to re-authenticate...")
1092
- self.authenticate(revalidate=True)
1093
- retries += 1
1094
- logger.error(response.text)
2126
+
2127
+ # Sanity checks as the parameters come directly from payload:
2128
+ if not name:
2129
+ self.logger.error("Cannot create a case type without a name!")
1095
2130
  return None
1096
2131
 
1097
- # end method definition
2132
+ create_case_type_data = {
2133
+ "Properties": {"Name": name, "Description": description, "Status": status},
2134
+ }
1098
2135
 
1099
- def get_all_sub_categeries(
1100
- self,
1101
- parentid: int
1102
- ) -> dict | None:
1103
- """Get all sub categeries entity instances
2136
+ request_url = self.get_create_casetype_url()
1104
2137
 
1105
- Args:
1106
- parentid (int): parentid
1107
- Returns:
1108
- dict: Request response (dictionary) or None if the REST call fails
1109
- """
1110
- retries = 0
1111
- while True:
1112
- base_url = self.baseurl()
1113
- endpoint = "/app/entityRestService/api/OpentextCaseManagement/entities/Category/items/"
1114
- child_path = "/childEntities/SubCategory"
1115
- response = requests.get(
1116
- url=base_url + endpoint + str(parentid) + child_path,
1117
- headers=REQUEST_HEADERS_JSON,
1118
- cookies=self.cookie(),
1119
- timeout=None,
1120
- )
1121
- if response.ok:
1122
- authenticate_dict = self.parse_request_response(
1123
- response, "This can be normal during restart", False
1124
- )
1125
- if not authenticate_dict:
1126
- return None
1127
- return authenticate_dict
1128
- if response.status_code == 401 and retries == 0:
1129
- logger.warning("Session has expired - try to re-authenticate...")
1130
- self.authenticate(revalidate=True)
1131
- retries += 1
1132
- logger.error(response.text)
1133
- return None
2138
+ return self.do_request(
2139
+ url=request_url,
2140
+ method="POST",
2141
+ headers=REQUEST_HEADERS_JSON,
2142
+ cookies=self.cookie(),
2143
+ json_data=create_case_type_data,
2144
+ timeout=REQUEST_TIMEOUT,
2145
+ failure_message="Request to create case type -> '{}' failed".format(name),
2146
+ )
1134
2147
 
1135
2148
  # end method definition
1136
2149
 
1137
- def create_loan(
1138
- self,
1139
- subject: str,
1140
- description: str,
1141
- loan_amount: str,
1142
- loan_duration_in_months: str,
1143
- category: str,
1144
- subcategory: str,
1145
- piority: str,
1146
- service: str,
1147
- customer: str
1148
-
1149
- ) -> dict | None:
1150
- """create loan entity instance
2150
+ def get_case_types(self) -> dict | None:
2151
+ """Get all case type entity instances.
1151
2152
 
1152
2153
  Args:
1153
- subject (str): subject
1154
- description (str): description
1155
- loan_amount (str): loan_amount
1156
- loan_duration_in_months (str): loan_duration_in_months
1157
- category (str): category
1158
- subcategory (str): subcategory
1159
- piority (str): piority
1160
- service (str): service
1161
- customer (str): customer
1162
- Returns:
1163
- dict: Request response (dictionary) or None if the REST call fails
1164
- """
1165
- create_loan = f"""<SOAP:Envelope xmlns:SOAP=\"http://schemas.xmlsoap.org/soap/envelope/\">\r\n
1166
- <SOAP:Body>\r\n
1167
- <CreateCase xmlns=\"http://schemas/OpentextCaseManagement/Case/operations\">\r\n
1168
- <ns0:Case-create xmlns:ns0=\"http://schemas/OpentextCaseManagement/Case\">\r\n
1169
- <ns0:Subject>{subject}</ns0:Subject>\r\n
1170
- <ns0:Description>{description}</ns0:Description>\r\n
1171
- <ns0:LoanAmount>{loan_amount}</ns0:LoanAmount>\r\n
1172
- <ns0:LoanDurationInMonths>{loan_duration_in_months}</ns0:LoanDurationInMonths>\r\n
1173
- \r\n
1174
- <ns0:CaseType>\r\n
1175
- <ns1:CaseType-id xmlns:ns1=\"http://schemas/OpentextCaseManagement/CaseType\">\r\n
1176
- <ns1:Id>{service}</ns1:Id>\r\n
1177
- </ns1:CaseType-id>\r\n
1178
- </ns0:CaseType>\r\n
1179
- \r\n
1180
- <ns0:Category>\r\n
1181
- <ns2:Category-id xmlns:ns2=\"http://schemas/OpentextCaseManagement/Category\">\r\n
1182
- <ns2:Id>{category}</ns2:Id>\r\n
1183
- </ns2:Category-id>\r\n
1184
- </ns0:Category>\r\n
1185
- \r\n
1186
- <ns0:SubCategory>\r\n
1187
- <ns5:SubCategory-id xmlns:ns5=\"http://schemas/OpentextCaseManagement/Category.SubCategory\">\r\n
1188
- <ns5:Id>{category}</ns5:Id>\r\n
1189
- <ns5:Id1>{subcategory}</ns5:Id1>\r\n
1190
- </ns5:SubCategory-id>\r\n
1191
- </ns0:SubCategory>\r\n
1192
- \r\n
1193
- <ns0:Priority>\r\n
1194
- <ns3:Priority-id xmlns:ns3=\"http://schemas/OpentextCaseManagement/Priority\">\r\n
1195
- <ns3:Id>{piority}</ns3:Id>\r\n
1196
- </ns3:Priority-id>\r\n
1197
- </ns0:Priority>\r\n
1198
- \r\n
1199
- <ns0:ToCustomer>\r\n
1200
- <ns9:Customer-id xmlns:ns9=\"http://schemas/OpentextCaseManagement/Customer\">\r\n
1201
- <ns9:Id>{customer}</ns9:Id>\r\n
1202
- </ns9:Customer-id>\r\n
1203
- </ns0:ToCustomer>\r\n
1204
- \r\n
1205
- </ns0:Case-create>\r\n
1206
- </CreateCase>\r\n
1207
- </SOAP:Body>\r\n
1208
- </SOAP:Envelope>"""
2154
+ None
1209
2155
 
1210
- retries = 0
1211
- while True:
1212
- response = requests.post(
1213
- url=self.gateway_url(),
1214
- data=create_loan,
1215
- headers=REQUEST_HEADERS,
1216
- cookies=self.cookie(),
1217
- timeout=None,
1218
- )
1219
- if response.ok:
1220
- logger.info("Loan created successfully")
1221
- return self.parse_xml(response.text)
1222
- if response.status_code == 401 and retries == 0:
1223
- logger.warning("Session has expired - try to re-authenticate...")
1224
- self.authenticate(revalidate=True)
1225
- retries += 1
1226
- logger.error(response.text)
1227
- return None
2156
+ Returns:
2157
+ dict:
2158
+ Request response (dictionary) or None if the REST call fails.
2159
+
2160
+ Example:
2161
+ {
2162
+ 'page': {
2163
+ 'skip': 0,
2164
+ 'top': 0,
2165
+ 'count': 5,
2166
+ 'ftsEnabled': False
2167
+ },
2168
+ '_links': {
2169
+ 'self': {
2170
+ 'href': '/OpentextCaseManagement/entities/CaseType/lists/AllCaseTypes'
2171
+ },
2172
+ 'first': {...}
2173
+ },
2174
+ '_embedded': {
2175
+ 'AllCaseTypes': [
2176
+ {
2177
+ '_links': {
2178
+ 'item': {
2179
+ 'href': '/OpentextCaseManagement/entities/CaseType/items/1'
2180
+ }
2181
+ },
2182
+ 'Properties': {
2183
+ 'Name': 'Query',
2184
+ 'Description': 'Query',
2185
+ 'Status': 1
2186
+ }
2187
+ },
2188
+ {
2189
+ '_links': {...},
2190
+ 'Properties': {...}
2191
+ },
2192
+ ...
2193
+ ]
2194
+ }
2195
+ }
2196
+
2197
+ """
2198
+
2199
+ request_url = self.get_casetypes_list_url()
2200
+
2201
+ return self.do_request(
2202
+ url=request_url,
2203
+ method="GET",
2204
+ headers=REQUEST_HEADERS_JSON,
2205
+ cookies=self.cookie(),
2206
+ timeout=REQUEST_TIMEOUT,
2207
+ failure_message="Request to get case types failed",
2208
+ )
1228
2209
 
1229
2210
  # end method definition
1230
2211
 
1231
- def get_all_loan(
1232
- self
1233
- ) -> dict | None:
1234
- """get all loan entity instances
2212
+ def get_case_type_by_name(self, name: str) -> dict | None:
2213
+ """Get case type entity instance by its name.
1235
2214
 
1236
2215
  Args:
1237
- None
2216
+ name (str):
2217
+ The name of the case type.
2218
+
1238
2219
  Returns:
1239
- dict: Request response (dictionary) or None if the REST call fails
2220
+ dict | None:
2221
+ Returns the case type data or None if no case type with the given name exists.
2222
+
1240
2223
  """
1241
2224
 
1242
- retries = 0
1243
- while True:
1244
- response = requests.get(
1245
- url=self.get_all_loans_url(),
1246
- headers=REQUEST_HEADERS_JSON,
1247
- cookies=self.cookie(),
1248
- timeout=None,
1249
- )
1250
- if response.ok:
1251
- authenticate_dict = self.parse_request_response(
1252
- response, "This can be normal during restart", False
1253
- )
1254
- if not authenticate_dict:
1255
- return None
1256
- return authenticate_dict
1257
- if response.status_code == 401 and retries == 0:
1258
- logger.warning("Session has expired - try to re-authenticate...")
1259
- self.authenticate(revalidate=True)
1260
- retries += 1
1261
- else:
1262
- logger.error(response.text)
1263
- return None
2225
+ case_types = self.get_case_types()
2226
+
2227
+ return self.get_result_item(response=case_types, entity_type="AllCaseTypes", key="Name", value=name)
1264
2228
 
1265
2229
  # end method definition
1266
2230
 
1267
- def validate_workspace_response(
1268
- self,
1269
- response: str,
1270
- workspace_name: str
1271
- ) -> bool:
1272
- """
1273
- Verify if the workspace exists or was created successfully.
2231
+ def get_case_type_ids(self) -> list:
2232
+ """Get All CaseType entity instances IDs.
1274
2233
 
1275
2234
  Args:
1276
- response (str): response to validate
1277
- workspace_name (str): The name of the workspace.
2235
+ None
1278
2236
 
1279
2237
  Returns:
1280
- bool: True if the workspace exists or was created successfully, else False.
2238
+ list:
2239
+ List of all case type IDs.
2240
+
1281
2241
  """
1282
2242
 
1283
- if "Object already exists" in response or "createWorkspaceResponse" in response:
1284
- logger.info(
1285
- "The workspace already exists or was created with the name -> '%s'",
1286
- workspace_name,
1287
- )
1288
- return True
2243
+ case_types = self.get_case_types()
1289
2244
 
1290
- logger.info(
1291
- "The workspace -> '%s' does not exist or was not created. Please verify configurtion!",
1292
- workspace_name,
1293
- )
1294
- return False
2245
+ return self.get_result_values(response=case_types, entity_type="AllCaseTypes", key="id") or []
1295
2246
 
1296
2247
  # end method definition
1297
2248
 
1298
- def is_workspace_already_exists(
2249
+ def create_category(
1299
2250
  self,
1300
- response: str,
1301
- workspace_name: str
1302
- ) -> bool:
1303
- """verify is workspace exists
1304
- Args:
1305
- workspace_name (str): workspace_name
1306
- Returns:
1307
- bool: return true if workspace exist else return false
1308
- """
2251
+ case_prefix: str,
2252
+ name: str,
2253
+ description: str,
2254
+ status: int = 1,
2255
+ ) -> dict | None:
2256
+ """Create category entity instance.
1309
2257
 
1310
- if "Object already exists" in response:
1311
- logger.info(
1312
- "The workspace already exists with the name -> '%s'", workspace_name
1313
- )
1314
- return True
1315
- logger.info(
1316
- "The Workspace has been created with the name -> '%s'", workspace_name
1317
- )
1318
- return False
2258
+ Args:
2259
+ case_prefix (str):
2260
+ The prefix for the case.
2261
+ description (str):
2262
+ The description for the category.
2263
+ name (str):
2264
+ The name of the category.
2265
+ status (int):
2266
+ The status code.
1319
2267
 
1320
- # end method definition
2268
+ Returns:
2269
+ dict:
2270
+ Request response (dictionary) or None if the REST call fails.
2271
+
2272
+ Example:
2273
+ {
2274
+ 'Identity': {
2275
+ 'Id': '327681'
2276
+ },
2277
+ '_links': {
2278
+ 'self': {
2279
+ href': '/OpentextCaseManagement/entities/Category/items/327681'
2280
+ }
2281
+ }
2282
+ }
1321
2283
 
1322
- def create_workspace_with_retry(self, workspace_name: str, workspace_gui_id: str) -> dict | None:
1323
- """
1324
- Calls create_workspace and retries if the response contains specific error messages.
1325
- Retries until the response does not contain the errors or a max retry limit is reached.
1326
2284
  """
1327
2285
 
1328
- max_retries = 20 # Define the maximum number of retries
1329
- retries = 0
1330
- error_messages = [
1331
- "Collaborative Workspace Service Container is not able to handle the SOAP request",
1332
- "Service Group Lookup failure"
1333
- ]
2286
+ # Sanity checks as the parameters come directly from payload:
2287
+ if not name:
2288
+ self.logger.error("Cannot create a category without a name!")
2289
+ return None
1334
2290
 
1335
- while retries < max_retries:
1336
- response = self.create_workspace(workspace_name, workspace_gui_id)
2291
+ create_category_data = {
2292
+ "Properties": {
2293
+ "CasePrefix": case_prefix,
2294
+ "Description": description,
2295
+ "Name": name,
2296
+ "Status": status,
2297
+ },
2298
+ }
1337
2299
 
1338
- # Check if any error message is in the response
1339
- if any(error_message in response for error_message in error_messages):
1340
- logger.info("Workspace service error, waiting 60 seconds to retry... (Retry %d of %d)", retries + 1, max_retries)
1341
- time.sleep(60)
1342
- retries += 1
1343
- else:
1344
- logger.info("Collaborative Workspace Service Container is ready")
1345
- return response
2300
+ request_url = self.get_create_category_url()
1346
2301
 
1347
- # After max retries, log and return the response or handle as needed
1348
- logger.error(
1349
- "Max retries reached for workspace -> '%s', unable to create successfully.",
1350
- workspace_name,
2302
+ return self.do_request(
2303
+ url=request_url,
2304
+ method="POST",
2305
+ headers=REQUEST_HEADERS_JSON,
2306
+ cookies=self.cookie(),
2307
+ json_data=create_category_data,
2308
+ timeout=REQUEST_TIMEOUT,
2309
+ failure_message="Request to create category -> '{}' failed".format(name),
1351
2310
  )
1352
- return response
1353
2311
 
1354
2312
  # end method definition
1355
2313
 
1356
- def loan_management_runtime(self) -> dict | None:
1357
- """it will create all runtime objects for loan management application
2314
+ def get_categories(self) -> dict | None:
2315
+ """Get all categories entity instances.
2316
+
1358
2317
  Args:
1359
2318
  None
1360
2319
  Returns:
1361
- None
2320
+ dict | None:
2321
+ Request response (dictionary) or None if the REST call fails.
2322
+
2323
+ Example:
2324
+ {
2325
+ 'page': {
2326
+ 'skip': 0,
2327
+ 'top': 0,
2328
+ 'count': 3,
2329
+ 'ftsEnabled': False
2330
+ },
2331
+ '_links': {
2332
+ 'self': {
2333
+ 'href': '/OpentextCaseManagement/entities/Category/lists/CategoryList'
2334
+ },
2335
+ 'first': {...}
2336
+ },
2337
+ '_embedded': {
2338
+ 'CategoryList': [
2339
+ {
2340
+ '_links': {
2341
+ 'item': {
2342
+ 'href': '/OpentextCaseManagement/entities/Category/items/1'
2343
+ }
2344
+ },
2345
+ 'Properties': {
2346
+ 'Name': 'Short Term Loan',
2347
+ 'Description': 'Short Term Loan',
2348
+ 'CasePrefix': 'LOAN',
2349
+ 'Status': 1
2350
+ }
2351
+ },
2352
+ {
2353
+ '_links': {...},
2354
+ 'Properties': {...}
2355
+ },
2356
+ {
2357
+ '_links': {...},
2358
+ 'Properties': {...}
2359
+ }
2360
+ ]
2361
+ }
2362
+ }
2363
+
1362
2364
  """
1363
2365
 
1364
- logger.debug(" RUNTIME -->> Category instance creation started ........ ")
1365
- category_resp_dict = []
1366
- if not self.verify_category_exists("Short Term Loan"):
1367
- self.create_category("LOAN","Short Term Loan","Short Term Loan",1)
1368
- if not self.verify_category_exists("Long Term Loan"):
1369
- self.create_category("LOAN","Long Term Loan","Long Term Loan",1)
1370
- if not self.verify_category_exists("Flexi Loan"):
1371
- self.create_category("LOAN","Flexi Loan","Flexi Loan",1)
1372
- category_resp_dict = self.get_category_lists()
1373
- logger.debug(" RUNTIME -->> Category instance creation ended")
1374
-
1375
- ############################# Sub category
1376
- logger.debug(" RUNTIME -->> Sub Category instance creation started ........")
1377
- stl = 0
1378
- ltl = 0
1379
- fl = 0
1380
- if not self.verify_sub_category_exists("Business",0,category_resp_dict):
1381
- response_dict = self.create_sub_categoy("Business","Business",1,category_resp_dict[0])
1382
- stl = response_dict["Identity"]["Id"]
1383
- logger.info("Sub category id stl: %s ", stl)
1384
- else:
1385
- stl = self.return_sub_category_exists_id("Business",0,category_resp_dict)
1386
- logger.info("Sub category id stl -> %s ", stl)
2366
+ request_url = self.get_categories_list_url()
1387
2367
 
1388
- if not self.verify_sub_category_exists("Business",1,category_resp_dict):
1389
- response_dict=self.create_sub_categoy("Business","Business",1,category_resp_dict[1])
1390
- ltl = response_dict["Identity"]["Id"]
1391
- logger.info("Sub category id ltl -> %s ", ltl)
1392
- else:
1393
- ltl = self.return_sub_category_exists_id("Business",1,category_resp_dict)
1394
- logger.info("Sub category id ltl -> %s ", ltl)
1395
- if not self.verify_sub_category_exists("Business",2,category_resp_dict):
1396
- response_dict= self.create_sub_categoy("Business","Business",1,category_resp_dict[2])
1397
- fl = response_dict["Identity"]["Id"]
1398
- logger.info("Sub category id fl -> %s ", fl)
1399
- else:
1400
- fl = self.return_sub_category_exists_id("Business",2,category_resp_dict)
1401
- logger.info("Sub category id fl -> %s ", fl)
1402
- logger.debug(" RUNTIME -->> Sub Category instance creation ended")
1403
-
1404
- ############################# Case Types
1405
- logger.debug(" RUNTIME -->> Case Types instance creation started ........")
1406
- case_type_list = []
1407
-
1408
- if not self.vverify_case_type_exists("Query"):
1409
- self.create_case_type("Query","Query",1)
1410
- if not self.vverify_case_type_exists("Help"):
1411
- self.create_case_type("Help","Help",1)
1412
- if not self.vverify_case_type_exists("Update Contact Details"):
1413
- self.create_case_type("Update Contact Details","Update Contact Details",1)
1414
- if not self.vverify_case_type_exists("New Loan Request"):
1415
- self.create_case_type("New Loan Request","New Loan Request",1)
1416
- if not self.vverify_case_type_exists("Loan Closure"):
1417
- self.create_case_type("Loan Closure","Loan Closure",1)
1418
- case_type_list = self.get_case_type_lists()
1419
- logger.debug(" RUNTIME -->> Case Types instance creation ended")
1420
-
1421
- ############################# CUSTMOR
1422
- logger.debug(" RUNTIME -->> Customer instance creation stated ........")
1423
- customer_list = []
1424
- if not self.verify_customer_exists("InaPlex Limited"):
1425
- self.create_customer("InaPlex Limited","InaPlex Limited","InaPlex Limited")
1426
-
1427
- if not self.verify_customer_exists("Interwoven, Inc"):
1428
- self.create_customer("Interwoven, Inc","Interwoven, Inc","Interwoven, Inc")
1429
-
1430
- if not self.verify_customer_exists("Jones Lang LaSalle"):
1431
- self.create_customer("Jones Lang LaSalle","Jones Lang LaSalle","Jones Lang LaSalle")
1432
-
1433
- if not self.verify_customer_exists("Key Point Consulting"):
1434
- self.create_customer("Key Point Consulting","Key Point Consulting","Key Point Consulting")
1435
-
1436
- customer_list = self.get_customer_lists()
1437
- logger.debug(" RUNTIME -->> Customer instance creation ended")
1438
-
1439
- ######################################## PRIORITY
1440
- logger.debug(" RUNTIME -->> priority instance creation started ........")
1441
- prioity_list = []
1442
- if not self.verify_priority_exists("High"):
1443
- self.create_priority("High","High",1)
1444
- if not self.verify_priority_exists("Medium"):
1445
- self.create_priority("Medium","Medium",1)
1446
- if not self.verify_priority_exists("Low"):
1447
- self.create_priority("Low","Low",1)
1448
- prioity_list = self.get_priority_lists()
1449
- logger.debug(" RUNTIME -->> priority instance creation ended")
1450
-
1451
- ############################# LOAN
1452
- loan_for_business = "Loan for Business1"
1453
- loan_for_corporate_business = "Loan for Corporate Business1"
1454
- loan_for_business_loan_request = "Loan for Business Loan Request1"
1455
-
1456
- logger.debug(" RUNTIME -->> loan instance creation started ........")
1457
- loan_resp_dict = self.get_all_loan()
1458
- names = [item["Properties"]["Subject"] for item in loan_resp_dict["_embedded"]["AllCasesList"]]
1459
- if loan_for_business in names:
1460
- logger.info("Customer record Loan_for_business exists")
1461
- else:
1462
- logger.info("Creating customer Record with Loan_for_business ")
1463
- response_dict = self.create_loan(
1464
- loan_for_business,
1465
- loan_for_business,
1466
- 1,
1467
- 2,
1468
- category_resp_dict[0],
1469
- stl,
1470
- prioity_list[0],
1471
- case_type_list[0],
1472
- customer_list[0],
1473
- )
2368
+ return self.do_request(
2369
+ url=request_url,
2370
+ method="GET",
2371
+ headers=REQUEST_HEADERS_JSON,
2372
+ cookies=self.cookie(),
2373
+ timeout=REQUEST_TIMEOUT,
2374
+ failure_message="Request to get categories failed",
2375
+ )
1474
2376
 
1475
- if loan_for_corporate_business in names:
1476
- logger.info("Customer record Loan_for_Corporate_Business exists")
1477
- else:
1478
- logger.info("Creating customer Record with Loan_for_Corporate_Business ")
1479
- response_dict = self.create_loan(
1480
- loan_for_corporate_business,
1481
- loan_for_corporate_business,
1482
- 1,
1483
- 2,
1484
- category_resp_dict[1],
1485
- ltl,
1486
- prioity_list[1],
1487
- case_type_list[1],
1488
- customer_list[1],
1489
- )
2377
+ # end method definition
1490
2378
 
1491
- if loan_for_business_loan_request in names:
1492
- logger.info("Customer record Loan_for_business_Loan_Request exists")
1493
- else:
1494
- logger.info("Creating customer Record with loan_for_business_loan_request")
1495
- response_dict = self.create_loan(
1496
- loan_for_business_loan_request,
1497
- loan_for_business_loan_request,
1498
- 1,
1499
- 2,
1500
- category_resp_dict[2],
1501
- fl,
1502
- prioity_list[2],
1503
- case_type_list[2],
1504
- customer_list[2],
1505
- )
1506
- logger.debug(" RUNTIME -->> loan instance creation ended")
2379
+ def get_category_by_name(self, name: str) -> dict | None:
2380
+ """Get category entity instance by its name.
1507
2381
 
1508
- # end method definition
2382
+ The category ID is only provided by the 'href' in '_links' / 'item'.
1509
2383
 
1510
- def get_category_lists(self) -> list:
1511
- """get All category entty instances id's
1512
2384
  Args:
1513
- None
2385
+ name (str):
2386
+ The name of the category.
2387
+
1514
2388
  Returns:
1515
- list: list of category IDs
2389
+ dict | None:
2390
+ Returns the category item or None if a category with the given name does not exist.
2391
+
2392
+ Example:
2393
+ {
2394
+ '_links': {
2395
+ 'item': {
2396
+ 'href': '/OpentextCaseManagement/entities/Category/items/327681'
2397
+ }
2398
+ },
2399
+ 'Properties': {
2400
+ 'Name': 'Test 1',
2401
+ 'Description': 'Test 1 Description',
2402
+ 'CasePrefix': 'TEST',
2403
+ 'Status': 1
2404
+ }
2405
+ }
2406
+
1516
2407
  """
1517
2408
 
1518
- category_resp_dict = []
1519
- categoy_resp_dict = self.get_all_categories()
1520
- for item in categoy_resp_dict["_embedded"]["CategoryList"]:
1521
- first_item_href = item["_links"]["item"]["href"]
1522
- integer_value = int(re.search(r'\d+', first_item_href).group())
1523
- logger.info("Category created with ID -> %d", integer_value)
1524
- category_resp_dict.append(integer_value)
1525
- logger.info("All extracted category IDs -> %s", category_resp_dict)
2409
+ categories = self.get_categories()
1526
2410
 
1527
- return category_resp_dict
2411
+ return self.get_result_item(response=categories, entity_type="CategoryList", key="Name", value=name)
1528
2412
 
1529
2413
  # end method definition
1530
2414
 
1531
- def get_case_type_lists(self) -> list:
1532
- """Get All CaseType entity instances IDs
2415
+ def get_category_ids(self) -> list:
2416
+ """Get All category entity instances IDs.
2417
+
1533
2418
  Args:
1534
2419
  None
1535
2420
  Returns:
1536
- list: list contains CaseType IDs
2421
+ list: list of category IDs
2422
+
1537
2423
  """
1538
2424
 
1539
- case_type_list = []
1540
- casetype_resp_dict = self.get_all_case_type()
1541
- for item in casetype_resp_dict["_embedded"]["AllCaseTypes"]:
1542
- first_item_href = item["_links"]["item"]["href"]
1543
- integer_value = int(re.search(r'\d+', first_item_href).group())
1544
- logger.info("Case type created with ID -> %d", integer_value)
1545
- case_type_list.append(integer_value)
1546
- logger.info("All extracted case type IDs -> %s", case_type_list)
2425
+ categories = self.get_categories()
1547
2426
 
1548
- return case_type_list
2427
+ return self.get_result_values(response=categories, entity_type="CategoryList", key="id") or []
1549
2428
 
1550
2429
  # end method definition
1551
2430
 
1552
- def get_customer_lists(self) -> list:
1553
- """Get all customer entity instances id's
2431
+ def create_sub_category(
2432
+ self,
2433
+ parent_id: int,
2434
+ name: str,
2435
+ description: str = "",
2436
+ status: int = 1,
2437
+ ) -> dict | None:
2438
+ """Create sub categoy entity instances.
2439
+
1554
2440
  Args:
1555
- None
2441
+ parent_id (int):
2442
+ The parent ID of the category.
2443
+ name (str):
2444
+ The name of the sub-category.
2445
+ description (str, optional):
2446
+ The description for the sub-category.
2447
+ status (int, optional):
2448
+ The status ID. Default is 1.
2449
+
1556
2450
  Returns:
1557
- list: list of customer IDs
2451
+ dict:
2452
+ Request response (dictionary) or None if the REST call fails.
2453
+
1558
2454
  """
1559
2455
 
1560
- customer_list = []
1561
- customer_resp_dict = self.get_all_customers()
1562
- for item in customer_resp_dict["_embedded"]["CustomerList"]:
1563
- first_item_href = item["_links"]["item"]["href"]
1564
- integer_value = int(re.search(r'\d+', first_item_href).group())
1565
- logger.info("Customer created with ID -> %d", integer_value)
1566
- customer_list.append(integer_value)
1567
- logger.info("All extracted Customer IDs -> %s ", customer_list)
1568
- return customer_list
2456
+ # Sanity checks as the parameters come directly from payload:
2457
+ if not name:
2458
+ self.logger.error("Cannot create a sub-category without a name!")
2459
+ return None
2460
+ if not parent_id:
2461
+ self.logger.error("Cannot create a sub-category -> '%s' without a parent category ID!", name)
2462
+ return None
2463
+
2464
+ create_sub_category_data = {
2465
+ "Properties": {"Name": name, "Description": description, "Status": status},
2466
+ }
2467
+
2468
+ request_url = (
2469
+ self.config()["categoryUrl"] + "/items/" + str(parent_id) + "/childEntities/SubCategory?defaultinst_ct=abcd"
2470
+ )
2471
+
2472
+ return self.do_request(
2473
+ url=request_url,
2474
+ method="POST",
2475
+ headers=REQUEST_HEADERS_JSON,
2476
+ cookies=self.cookie(),
2477
+ json_data=create_sub_category_data,
2478
+ timeout=REQUEST_TIMEOUT,
2479
+ failure_message="Request to create sub-category -> '{}' with parent category ID -> {} failed".format(
2480
+ name, parent_id
2481
+ ),
2482
+ )
1569
2483
 
1570
2484
  # end method definition
1571
2485
 
1572
- def get_priority_lists(self) -> list:
1573
- """get all priority entity instances IDs
2486
+ def get_sub_categories(self, parent_id: int) -> dict | None:
2487
+ """Get all sub categeries entity instances.
2488
+
1574
2489
  Args:
1575
- None
2490
+ parent_id (int):
2491
+ The parent ID of the sub categories.
2492
+
1576
2493
  Returns:
1577
- list: list contains priority IDs
2494
+ dict | None:
2495
+ Request response (dictionary) or None if the REST call fails.
2496
+
2497
+ Example:
2498
+ {
2499
+ 'page': {
2500
+ 'skip': 0,
2501
+ 'top': 10,
2502
+ 'count': 1
2503
+ },
2504
+ '_links': {
2505
+ 'self': {...},
2506
+ 'first': {...}
2507
+ },
2508
+ '_embedded': {
2509
+ 'SubCategory': [
2510
+ {
2511
+ '_links': {...},
2512
+ 'Identity': {'Id': '1'},
2513
+ 'Properties': {'Name': 'Business', 'Description': 'Business', 'Status': 1},
2514
+ 'ParentCategory': {
2515
+ '_links': {
2516
+ 'self': {'href': '/OpentextCaseManagement/entities/Category/items/1/childEntities/SubCategory/items/1'}
2517
+ },
2518
+ 'Properties': {
2519
+ 'CasePrefix': 'LOAN',
2520
+ 'Description': 'Short Term Loan',
2521
+ 'Name': 'Short Term Loan',
2522
+ 'Status': 1
2523
+ }
2524
+ }
2525
+ }
2526
+ ]
2527
+ }
2528
+ }
2529
+
1578
2530
  """
1579
2531
 
1580
- prioity_list = []
1581
- authenticate_dict = self.get_all_priorities()
1582
- for item in authenticate_dict["_embedded"]["PriorityList"]:
1583
- first_item_href = item["_links"]["item"]["href"]
1584
- integer_value = int(re.search(r'\d+', first_item_href).group())
1585
- logger.info("Priority created with ID -> %d", integer_value)
1586
- prioity_list.append(integer_value)
1587
- logger.info("All extracted priority IDs -> %s ", prioity_list)
2532
+ request_url = self.config()["categoryUrl"] + "/items/" + str(parent_id) + "/childEntities/SubCategory"
1588
2533
 
1589
- return prioity_list
2534
+ return self.do_request(
2535
+ url=request_url,
2536
+ method="GET",
2537
+ headers=REQUEST_HEADERS_JSON,
2538
+ cookies=self.cookie(),
2539
+ timeout=REQUEST_TIMEOUT,
2540
+ failure_message="Request to get sub-categories for parent category with ID -> {} failed".format(parent_id),
2541
+ )
1590
2542
 
1591
2543
  # end method definition
1592
2544
 
1593
- def verify_category_exists(self, name: str) -> bool:
1594
- """verify category entity instance already exists
2545
+ def get_sub_category_by_parent_and_name(self, parent_id: int, name: str) -> dict | None:
2546
+ """Get sub category entity instance by its name.
2547
+
1595
2548
  Args:
1596
- name (str): name of the category
2549
+ parent_id (int):
2550
+ The ID of the parent category.
2551
+ name (str):
2552
+ The name of the sub category.
2553
+
1597
2554
  Returns:
1598
- bool: returns True if already record exists with same name, else returns False
2555
+ dict | None:
2556
+ Returns the sub-category item or None if the sub-category with this name
2557
+ does not exist in the parent category with the given ID.
2558
+
1599
2559
  """
1600
2560
 
1601
- categoy_resp_dict = self.get_all_categories()
1602
- names = [item["Properties"]["Name"] for item in categoy_resp_dict["_embedded"]["CategoryList"]]
1603
- if name in names:
1604
- logger.info("Category record -> '%s' already exists", name)
1605
- return True
1606
- logger.info("Creating category record -> '%s'", name)
2561
+ # Get all sub-categories under a given category provided by the parent ID:
2562
+ sub_categories = self.get_sub_categories(parent_id=parent_id)
1607
2563
 
1608
- return False
2564
+ return self.get_result_item(response=sub_categories, entity_type="SubCategory", key="Name", value=name)
1609
2565
 
1610
2566
  # end method definition
1611
2567
 
1612
- def vverify_case_type_exists(self, name: str) -> bool:
1613
- """verify case type entity instance already exists
2568
+ def get_sub_category_id(self, parent_id: int, name: str) -> int | None:
2569
+ """Get the sub category entity instance ID.
2570
+
1614
2571
  Args:
1615
- name (str): name of the case type
2572
+ parent_id (int):
2573
+ ID of the parent category.
2574
+ name (str):
2575
+ The name of the sub-category.
2576
+
1616
2577
  Returns:
1617
- bool: returns True if already record exists with same name, else returns False
2578
+ int | None:
2579
+ Returns the sub-category ID if it exists with the given name in a given parent category.
2580
+ Else returns None.
2581
+
1618
2582
  """
1619
2583
 
1620
- casetype_resp_dict = self.get_all_case_type()
1621
- names = [item["Properties"]["Name"] for item in casetype_resp_dict["_embedded"]["AllCaseTypes"]]
1622
- if name in names:
1623
- logger.info("Case type record -> '%s' already exists", name)
1624
- return True
1625
- logger.info("Creating case type record -> '%s'", name)
2584
+ sub_cat = self.get_sub_category_by_parent_and_name(parent_id=parent_id, name=name)
2585
+ if not sub_cat or "Identity" not in sub_cat:
2586
+ return None
1626
2587
 
1627
- return False
2588
+ return sub_cat["Identity"].get("Id")
1628
2589
 
1629
2590
  # end method definition
1630
2591
 
1631
- def verify_customer_exists(self, name: str) -> bool:
1632
- """verify cusomer entty instance already exists
2592
+ def create_case(
2593
+ self,
2594
+ subject: str,
2595
+ description: str,
2596
+ loan_amount: str,
2597
+ loan_duration_in_months: str,
2598
+ category_id: str,
2599
+ sub_category_id: str,
2600
+ priority_id: str,
2601
+ case_type_id: str,
2602
+ customer_id: str,
2603
+ ) -> dict | None:
2604
+ """Create a case entity instance.
2605
+
2606
+ The category, priority, case type and customer entities are
2607
+ referred to with their IDs. These entities need to be created
2608
+ beforehand.
2609
+
2610
+ TODO: This is currently hard-coded for loan cases. Need to be more generic.
2611
+
1633
2612
  Args:
1634
- name (str): name of the customer
2613
+ subject (str):
2614
+ The subject of the case.
2615
+ description (str):
2616
+ The description of the case.
2617
+ loan_amount (str):
2618
+ The loan amount of the case.
2619
+ loan_duration_in_months (str):
2620
+ The loan duration of the case (in number of months).
2621
+ category_id (str):
2622
+ The category ID of the case.
2623
+ sub_category_id (str):
2624
+ The sub-category ID of the case.
2625
+ priority_id (str):
2626
+ The priority ID of the case.
2627
+ case_type_id (str):
2628
+ The case type (service) ID of the case.
2629
+ customer_id (str):
2630
+ The ID of the customer for the case.
2631
+
1635
2632
  Returns:
1636
- bool: returns True if already record exists with same name, else returns False
2633
+ dict | None:
2634
+ Request response (dictionary) or None if the REST call fails
2635
+
1637
2636
  """
1638
- customer_resp_dict = self.get_all_customers()
1639
- names = [item["Properties"]["CustomerName"] for item in customer_resp_dict["_embedded"]["CustomerList"]]
1640
- if name in names:
1641
- logger.info("Customer -> '%s' already exists", name)
1642
- return True
1643
- logger.info("Creating customer -> '%s'", name)
1644
- return False
1645
2637
 
1646
- # end method definition
2638
+ # Validation of parameters:
2639
+ required_fields = {
2640
+ "subject": subject,
2641
+ "category ID": category_id,
2642
+ "sub-category ID": sub_category_id,
2643
+ "priority ID": priority_id,
2644
+ "case type ID": case_type_id,
2645
+ "customer ID": customer_id,
2646
+ }
1647
2647
 
1648
- def verify_priority_exists(self, name: str) -> bool:
1649
- """verify piority entity instance already exists
1650
- Args:
1651
- name (str): name of the priority
1652
- Returns:
1653
- bool: returns True if already record exists with same name, else returns False
2648
+ for name, value in required_fields.items():
2649
+ if not value:
2650
+ self.logger.error("Cannot create a case without a %s!", name)
2651
+ return None
2652
+
2653
+ create_case_data = f"""
2654
+ <SOAP:Envelope xmlns:SOAP=\"http://schemas.xmlsoap.org/soap/envelope/\">
2655
+ <SOAP:Body>
2656
+ <CreateCase xmlns=\"http://schemas/OpentextCaseManagement/Case/operations\">
2657
+ <ns0:Case-create xmlns:ns0=\"http://schemas/OpentextCaseManagement/Case\">
2658
+ <ns0:Subject>{subject}</ns0:Subject>
2659
+ <ns0:Description>{description}</ns0:Description>
2660
+ <ns0:LoanAmount>{loan_amount}</ns0:LoanAmount>
2661
+ <ns0:LoanDurationInMonths>{loan_duration_in_months}</ns0:LoanDurationInMonths>
2662
+ <ns0:CaseType>
2663
+ <ns1:CaseType-id xmlns:ns1=\"http://schemas/OpentextCaseManagement/CaseType\">
2664
+ <ns1:Id>{case_type_id}</ns1:Id>
2665
+ </ns1:CaseType-id>
2666
+ </ns0:CaseType>
2667
+ <ns0:Category>
2668
+ <ns2:Category-id xmlns:ns2=\"http://schemas/OpentextCaseManagement/Category\">
2669
+ <ns2:Id>{category_id}</ns2:Id>
2670
+ </ns2:Category-id>
2671
+ </ns0:Category>
2672
+ <ns0:SubCategory>
2673
+ <ns5:SubCategory-id xmlns:ns5=\"http://schemas/OpentextCaseManagement/Category.SubCategory\">
2674
+ <ns5:Id>{category_id}</ns5:Id>
2675
+ <ns5:Id1>{sub_category_id}</ns5:Id1>
2676
+ </ns5:SubCategory-id>
2677
+ </ns0:SubCategory>
2678
+ <ns0:Priority>
2679
+ <ns3:Priority-id xmlns:ns3=\"http://schemas/OpentextCaseManagement/Priority\">
2680
+ <ns3:Id>{priority_id}</ns3:Id>
2681
+ </ns3:Priority-id>
2682
+ </ns0:Priority>
2683
+ <ns0:ToCustomer>
2684
+ <ns9:Customer-id xmlns:ns9=\"http://schemas/OpentextCaseManagement/Customer\">
2685
+ <ns9:Id>{customer_id}</ns9:Id>
2686
+ </ns9:Customer-id>
2687
+ </ns0:ToCustomer>
2688
+ </ns0:Case-create>
2689
+ </CreateCase>
2690
+ </SOAP:Body>
2691
+ </SOAP:Envelope>
1654
2692
  """
1655
2693
 
1656
- authenticate_dict = self.get_all_priorities()
1657
- names = [item["Properties"]["Name"] for item in authenticate_dict["_embedded"]["PriorityList"]]
1658
- if name in names:
1659
- logger.info("Priority -> '%s' already exists", name)
1660
- return True
1661
- logger.info("Creating priority -> '%s'", name)
2694
+ request_url = self.gateway_url()
1662
2695
 
1663
- return False
2696
+ self.logger.debug(
2697
+ "Create case with subject -> '%s'; calling -> '%s'",
2698
+ subject,
2699
+ request_url,
2700
+ )
2701
+
2702
+ retries = 0
2703
+ while True:
2704
+ try:
2705
+ response = requests.post(
2706
+ url=request_url,
2707
+ data=create_case_data,
2708
+ headers=REQUEST_HEADERS_XML,
2709
+ cookies=self.cookie(),
2710
+ timeout=REQUEST_TIMEOUT,
2711
+ )
2712
+ except requests.RequestException as req_exception:
2713
+ self.logger.error(
2714
+ "Request to create case with subject -> '%s' failed with error -> %s",
2715
+ subject,
2716
+ str(req_exception),
2717
+ )
2718
+ return None
2719
+
2720
+ if response.ok:
2721
+ return self.parse_xml(response.text)
2722
+ elif response.status_code == 401 and retries == 0:
2723
+ self.logger.warning("Session has expired - try to re-authenticate...")
2724
+ self.authenticate(revalidate=True)
2725
+ retries += 1
2726
+ else:
2727
+ self.logger.error(
2728
+ "Failed to create case with subject -> '%s' for customer with ID -> '%s' with error -> '%s' when calling -> %s!",
2729
+ subject,
2730
+ customer_id,
2731
+ self.get_soap_element(soap_response=response.text, soap_tag="faultstring"),
2732
+ self.get_soap_element(soap_response=response.text, soap_tag="faultactor"),
2733
+ )
2734
+ self.logger.debug("SOAP message -> %s", response.text)
2735
+ return None
1664
2736
 
1665
2737
  # end method definition
1666
2738
 
1667
- def verify_sub_category_exists(self, name: str, index: int, category_resp_dict: list) -> bool:
1668
- """verify sub category entity instance already exists
2739
+ def get_cases(self) -> dict | None:
2740
+ """Get all case entity instances.
2741
+
1669
2742
  Args:
1670
- name (str): name of the sub category
2743
+ None
2744
+
1671
2745
  Returns:
1672
- bool: returns true if record already exists with same name, else returns false
2746
+ dict:
2747
+ Request response (dictionary) or None if the REST call fails.
2748
+
1673
2749
  """
1674
2750
 
1675
- subcategoy_resp_dict = self.get_all_sub_categeries(category_resp_dict[index])
1676
- names = [item["Properties"]["Name"] for item in subcategoy_resp_dict["_embedded"]["SubCategory"]]
1677
- stl=0
1678
- if name in names:
1679
- logger.info("Sub category -> '%s' already exists", name)
1680
- for item in subcategoy_resp_dict["_embedded"]["SubCategory"]:
1681
- stl = item["Identity"]["Id"]
1682
- logger.info("Sub category created with ID -> %s", stl)
1683
- return True
1684
- logger.info("Creating sub category -> '%s'", name)
2751
+ request_url = self.get_cases_list_url()
1685
2752
 
1686
- return False
2753
+ return self.do_request(
2754
+ url=request_url,
2755
+ method="GET",
2756
+ headers=REQUEST_HEADERS_JSON,
2757
+ cookies=self.cookie(),
2758
+ timeout=REQUEST_TIMEOUT,
2759
+ failure_message="Request to get cases failed",
2760
+ )
1687
2761
 
1688
2762
  # end method definition
1689
2763
 
1690
- def return_sub_category_exists_id(self, name: str, index: int, category_resp_dict: list) -> int:
1691
- """verify sub category entity instance id already exists
2764
+ def get_case_by_name(self, name: str) -> dict | None:
2765
+ """Get case instance by its name.
2766
+
1692
2767
  Args:
1693
- name (str): name of the sub-category
2768
+ name (str):
2769
+ The name of the case.
2770
+
1694
2771
  Returns:
1695
- bool: returns true if record already exists with same name, else returns false
2772
+ dict | None:
2773
+ Returns the category item or None if a category with the given name does not exist.
2774
+
1696
2775
  """
1697
2776
 
1698
- subcategoy_resp_dict = self.get_all_sub_categeries(category_resp_dict[index])
1699
- names = [item["Properties"]["Name"] for item in subcategoy_resp_dict["_embedded"]["SubCategory"]]
1700
- stl=0
1701
- if name in names:
1702
- logger.info("Sub category record -> '%s' already exists", name)
1703
- for item in subcategoy_resp_dict["_embedded"]["SubCategory"]:
1704
- stl = item["Identity"]["Id"]
1705
- logger.info("Sub category created with ID -> %s", stl)
1706
- return stl
2777
+ categories = self.get_cases()
1707
2778
 
1708
- return None
2779
+ return self.get_result_item(response=categories, entity_type="AllCasesList", key="Name", value=name)
1709
2780
 
1710
2781
  # end method definition
1711
2782
 
1712
- def create_users_from_config_file(self, otawpsection: str, _otds: OTDS):
1713
- """read user information from customizer file and call create user method
2783
+ def create_users_from_config_file(self, otawpsection: str, otds_object: OTDS) -> None:
2784
+ """Read user information from customizer file and call create user method.
2785
+
1714
2786
  Args:
1715
- otawpsection (str): yaml bock related to appworks
2787
+ otawpsection (str):
2788
+ Payload section for AppWorks.
2789
+ otds_object (OTDS):
2790
+ The OTDS object.
2791
+
1716
2792
  Returns:
1717
2793
  None
2794
+
1718
2795
  """
1719
2796
 
1720
2797
  otds = otawpsection.get("otds", {})
@@ -1722,7 +2799,7 @@ class OTAWP:
1722
2799
  users = otds.get("users", [])
1723
2800
  if users is not None:
1724
2801
  for user in users:
1725
- _otds.add_user(
2802
+ otds_object.add_user(
1726
2803
  user.get("partition"),
1727
2804
  user.get("name"),
1728
2805
  user.get("description"),
@@ -1733,31 +2810,37 @@ class OTAWP:
1733
2810
  roles = otds.get("roles", [])
1734
2811
  if roles is not None:
1735
2812
  for role in roles:
1736
- _otds.add_user_to_group(
2813
+ otds_object.add_user_to_group(
1737
2814
  user.get("name") + "@" + user.get("partition"),
1738
- # user.get('name'),
1739
2815
  role.get("name"),
1740
2816
  )
1741
2817
  else:
1742
- logger.error(
1743
- "Verifying Users section: roles section not presented in yaml for otds users"
2818
+ self.logger.warning(
2819
+ "Roles section not in payload for AppWorks users.",
1744
2820
  )
1745
2821
  else:
1746
- logger.error(
1747
- "Verifying Users section: user section not presented in yaml"
2822
+ self.logger.error(
2823
+ "User section not in payload for AppWorks users.",
1748
2824
  )
1749
2825
  else:
1750
- logger.error("Verifying Users section: otds section not presented in yaml")
2826
+ self.logger.error(
2827
+ "OTDS section not in payload for AppWorks users.",
2828
+ )
1751
2829
 
1752
2830
  # end method definition
1753
2831
 
1754
- def create_roles_from_config_file(self, otawpsection: str, _otds: OTDS):
1755
- """read grop information from customizer file and call create grop method
2832
+ def create_roles_from_config_file(self, otawpsection: str, otds_object: OTDS) -> None:
2833
+ """Read grop information from customizer file and call create grop method.
2834
+
1756
2835
  Args:
1757
- otawpsection (str): yaml bock related to appworks
1758
- _otds (object): the OTDS object used to access the OTDS REST API
2836
+ otawpsection (str):
2837
+ Payload section for AppWorks.
2838
+ otds_object (OTDS):
2839
+ The OTDS object used to access the OTDS REST API.
2840
+
1759
2841
  Returns:
1760
2842
  None
2843
+
1761
2844
  """
1762
2845
 
1763
2846
  otds = otawpsection.get("otds", {})
@@ -1766,45 +2849,410 @@ class OTAWP:
1766
2849
  if roles is not None:
1767
2850
  for role in roles:
1768
2851
  # Add new group if it does not yet exist:
1769
- if not _otds.get_group(group=role.get("name"), show_error=False):
1770
- _otds.add_group(
2852
+ if not otds_object.get_group(group=role.get("name"), show_error=False):
2853
+ otds_object.add_group(
1771
2854
  role.get("partition"),
1772
2855
  role.get("name"),
1773
2856
  role.get("description"),
1774
2857
  )
1775
2858
  else:
1776
- logger.error(
1777
- "Verifying roles section: roles section not presented in yaml"
2859
+ self.logger.error(
2860
+ "Roles section not in payload for AppWorks roles/groups.",
1778
2861
  )
1779
2862
  else:
1780
- logger.error("Verifying roles section: otds section not presented in yaml")
2863
+ self.logger.error(
2864
+ "OTDS section not in payload for AppWorks roles/groups.",
2865
+ )
1781
2866
 
1782
2867
  # end method definition
1783
2868
 
1784
- def create_loanruntime_from_config_file(self, platform: str):
1785
- """verify flag and call loan_management_runtime()
2869
+ def create_cws_config(
2870
+ self,
2871
+ partition: str,
2872
+ resource_name: str,
2873
+ otcs_url: str,
2874
+ ) -> dict | None:
2875
+ """Create a workspace configuration in CWS.
2876
+
1786
2877
  Args:
1787
- platform (str): yaml bock related to platform
2878
+ partition (str):
2879
+ The partition name for the workspace.
2880
+ resource_name (str):
2881
+ The resource name.
2882
+ otcs_url (str):
2883
+ The OTCS endpoint URL.
2884
+
1788
2885
  Returns:
1789
- None
2886
+ dict | None:
2887
+ Response dictionary if successful, or None if the request fails.
2888
+
1790
2889
  """
1791
2890
 
1792
- runtime = platform.get("runtime", {})
1793
- if runtime is not None:
1794
- app_names = runtime.get("appNames", [])
1795
- if app_names is not None:
1796
- for app_name in app_names:
1797
- if app_name == "loanManagement":
1798
- self.loan_management_runtime()
1799
- else:
1800
- logger.error(
1801
- "Verifying runtime section: loanManagement not exits in yaml entry"
1802
- )
1803
- else:
1804
- logger.error(
1805
- "Verifying runtime section: App name section is empty in yaml"
2891
+ # Construct the SOAP request body
2892
+ cws_config_data = f"""
2893
+ <SOAP:Envelope xmlns:SOAP="http://schemas.xmlsoap.org/soap/envelope/">
2894
+ <SOAP:Header>
2895
+ <header xmlns="http://schemas.cordys.com/General/1.0/">
2896
+ <Logger/>
2897
+ </header>
2898
+ <i18n:international xmlns:i18n="http://www.w3.org/2005/09/ws-i18n">
2899
+ <i18n:locale>en-US</i18n:locale>
2900
+ </i18n:international>
2901
+ </SOAP:Header>
2902
+ <SOAP:Body>
2903
+ <UpdateXMLObject xmlns="http://schemas.cordys.com/1.0/xmlstore">
2904
+ <tuple lastModified="{int(time.time() * 1000)}"
2905
+ key="/com/ot-ps/csws/otcs_ws_config.xml"
2906
+ level="organization"
2907
+ name="otcs_ws_config.xml"
2908
+ original="/com/ot-ps/csws/otcs_ws_config.xml"
2909
+ version="organization">
2910
+ <new>
2911
+ <CSWSConfig>
2912
+ <Partition>{partition}</Partition>
2913
+ <EndPointUrl>{otcs_url}/cws/services/Authentication</EndPointUrl>
2914
+ <Resources>
2915
+ <Resource type="Cordys">
2916
+ <Name>__OTDS#Shared#Platform#Resource__</Name>
2917
+ <Space>shared</Space>
2918
+ </Resource>
2919
+ <Resource type="OTCS">
2920
+ <Name>{resource_name}</Name>
2921
+ <Space>shared</Space>
2922
+ </Resource>
2923
+ </Resources>
2924
+ </CSWSConfig>
2925
+ </new>
2926
+ </tuple>
2927
+ </UpdateXMLObject>
2928
+ </SOAP:Body>
2929
+ </SOAP:Envelope>
2930
+ """
2931
+
2932
+ error_messages = [
2933
+ "Collaborative Workspace Service Container is not able to handle the SOAP request",
2934
+ "Service Group Lookup failure",
2935
+ ]
2936
+
2937
+ request_url = self.gateway_url()
2938
+
2939
+ self.logger.debug(
2940
+ "Create CWS configuration with partition -> '%s', user -> '%s', and OTCS URL -> '%s'; calling -> '%s'",
2941
+ partition,
2942
+ resource_name,
2943
+ otcs_url,
2944
+ request_url,
2945
+ )
2946
+
2947
+ retries = 0
2948
+
2949
+ while retries < REQUEST_MAX_RETRIES:
2950
+ try:
2951
+ response = requests.post(
2952
+ url=request_url,
2953
+ data=cws_config_data,
2954
+ headers=REQUEST_HEADERS_XML,
2955
+ cookies=self.cookie(),
2956
+ timeout=None,
2957
+ )
2958
+
2959
+ except requests.RequestException as req_exception:
2960
+ self.logger.error(
2961
+ "Request to create CWS config for partition -> '%s' failed with error -> %s. Retry in %d seconds...",
2962
+ partition,
2963
+ str(req_exception),
2964
+ REQUEST_RETRY_DELAY,
2965
+ )
2966
+ retries += 1
2967
+ time.sleep(REQUEST_RETRY_DELAY)
2968
+ self.logger.info("Retrying... Attempt %d/%d", retries, REQUEST_MAX_RETRIES)
2969
+ continue
2970
+
2971
+ # Handle successful response
2972
+ if response.ok:
2973
+ if any(error_message in response.text for error_message in error_messages):
2974
+ self.logger.warning(
2975
+ "Service error detected, retrying in %d seconds... (Retry %d of %d)",
2976
+ REQUEST_RETRY_DELAY,
2977
+ retries + 1,
2978
+ REQUEST_MAX_RETRIES,
2979
+ )
2980
+ time.sleep(REQUEST_RETRY_DELAY)
2981
+ retries += 1
2982
+ else:
2983
+ self.logger.info("Successfully created CWS configuration.")
2984
+ return self.parse_xml(response.text)
2985
+
2986
+ # Handle session expiration
2987
+ if response.status_code == 401 and retries == 0:
2988
+ self.logger.warning("Session expired. Re-authenticating...")
2989
+ self.authenticate(revalidate=True)
2990
+ retries += 1
2991
+ continue
2992
+
2993
+ # Handle case where object has been changed by another user:
2994
+ if "Object has been changed by other user" in response.text:
2995
+ self.logger.info("CWS config already exists")
2996
+ self.logger.debug("SOAP message -> %s", response.text)
2997
+ return self.parse_xml(response.text)
2998
+
2999
+ # Log errors for failed requests
3000
+ self.logger.error("Failed to create CWS config; error -> %s", response.text)
3001
+ time.sleep(REQUEST_RETRY_DELAY)
3002
+ retries += 1
3003
+ # end while retries < REQUEST_MAX_RETRIES:
3004
+
3005
+ # Log when retries are exhausted
3006
+ self.logger.error("Retry limit exceeded. CWS config creation failed.")
3007
+ return None
3008
+
3009
+ # end method definition
3010
+
3011
+ def verify_user_having_role(self, organization: str, user_name: str, role_name: str) -> bool:
3012
+ """Verify that the user has the specified role.
3013
+
3014
+ Args:
3015
+ organization (str):
3016
+ The organization name.
3017
+ user_name (str):
3018
+ The username to verify.
3019
+ role_name (str):
3020
+ The role to check for the user.
3021
+
3022
+ Returns:
3023
+ bool:
3024
+ True if the user has the role, False if not, or None if request fails.
3025
+
3026
+ """
3027
+
3028
+ self.logger.info(
3029
+ "Verify user -> '%s' has role -> '%s' in organization -> '%s'...", user_name, role_name, organization
3030
+ )
3031
+
3032
+ # Construct the SOAP request body
3033
+ user_role_data = f"""
3034
+ <SOAP:Envelope xmlns:SOAP="http://schemas.xmlsoap.org/soap/envelope/">
3035
+ <SOAP:Header xmlns:SOAP="http://schemas.xmlsoap.org/soap/envelope/">
3036
+ <header xmlns="http://schemas.cordys.com/General/1.0/">
3037
+ <Logger xmlns="http://schemas.cordys.com/General/1.0/"/>
3038
+ </header>
3039
+ <i18n:international xmlns:i18n="http://www.w3.org/2005/09/ws-i18n">
3040
+ <i18n:locale>en-US</i18n:locale>
3041
+ </i18n:international>
3042
+ </SOAP:Header>
3043
+ <SOAP:Body>
3044
+ <SearchLDAP xmlns:xfr="http://schemas.cordys.com/1.0/xforms/runtime" xmlns="http://schemas.cordys.com/1.0/ldap">
3045
+ <dn xmlns="http://schemas.cordys.com/1.0/ldap">cn=organizational users,o={organization},cn=cordys,cn=defaultInst,o=opentext.net</dn>
3046
+ <scope xmlns="http://schemas.cordys.com/1.0/ldap">1</scope>
3047
+ <filter xmlns="http://schemas.cordys.com/1.0/ldap">&amp;(objectclass=busorganizationaluser)(&amp;(!(cn=SYSTEM))(!(cn=anonymous))(!(cn=wcpLicUser)))(|(description=*{user_name}*)(&amp;(!(description=*))(cn=*{user_name}*)))</filter>
3048
+ <sort xmlns="http://schemas.cordys.com/1.0/ldap">ascending</sort>
3049
+ <sortBy xmlns="http://schemas.cordys.com/1.0/ldap"/>
3050
+ <returnValues xmlns="http://schemas.cordys.com/1.0/ldap">false</returnValues>
3051
+ <return xmlns="http://schemas.cordys.com/1.0/ldap"/>
3052
+ </SearchLDAP>
3053
+ </SOAP:Body>
3054
+ </SOAP:Envelope>
3055
+ """
3056
+
3057
+ retries = 0
3058
+
3059
+ while retries < REQUEST_MAX_RETRIES:
3060
+ try:
3061
+ response = requests.post(
3062
+ url=self.gateway_url(),
3063
+ data=user_role_data,
3064
+ headers=REQUEST_HEADERS_XML,
3065
+ cookies=self.cookie(),
3066
+ timeout=None,
1806
3067
  )
1807
- else:
1808
- logger.error("Verifying runtime section: Runtime section is empty in yaml")
3068
+
3069
+ except requests.RequestException:
3070
+ self.logger.error(
3071
+ "Request failed during verification of user -> '%s' for role -> '%s'. Retry in %d seconds...",
3072
+ user_name,
3073
+ role_name,
3074
+ REQUEST_RETRY_DELAY,
3075
+ )
3076
+ retries += 1
3077
+ time.sleep(REQUEST_RETRY_DELAY)
3078
+ self.logger.info("Retrying... Attempt %d/%d", retries, REQUEST_MAX_RETRIES)
3079
+ continue
3080
+
3081
+ # Handle successful response
3082
+ if response.ok:
3083
+ if role_name in response.text: # Corrected syntax for checking if 'Developer' is in the response text
3084
+ self.logger.info("Verified user -> '%s' already has the role -> '%s'.", user_name, role_name)
3085
+ return True # Assuming the user has the role if the response contains 'Developer'
3086
+ else:
3087
+ self.logger.info("Verified user -> '%s' does not yet have role -> '%s'.", user_name, role_name)
3088
+ return False
3089
+
3090
+ # Handle session expiration
3091
+ if response.status_code == 401 and retries == 0:
3092
+ self.logger.warning("Session expired. Re-authenticating...")
3093
+ self.authenticate(revalidate=True)
3094
+ retries += 1
3095
+ continue
3096
+
3097
+ # Log errors for failed requests
3098
+ self.logger.error(
3099
+ "Failed to verify that user -> '%s' has role -> '%s'; error -> %s",
3100
+ user_name,
3101
+ role_name,
3102
+ response.text,
3103
+ )
3104
+ time.sleep(REQUEST_RETRY_DELAY)
3105
+ retries += 1
3106
+ self.logger.info("Retrying... Attempt %d/%d", retries, REQUEST_MAX_RETRIES)
3107
+
3108
+ # Log when retries are exhausted
3109
+ self.logger.error("Retry limit exceeded. User role verification failed.")
3110
+
3111
+ return False # Return False if the retries limit is exceeded
1809
3112
 
1810
3113
  # end method definition
3114
+
3115
+ def assign_role_to_user(self, organization: str, user_name: str, role_name: str) -> bool:
3116
+ """Assign a role to a user and verify the role assignment.
3117
+
3118
+ Args:
3119
+ organization (str):
3120
+ The organization name.
3121
+ user_name (str):
3122
+ The username to get the role.
3123
+ role_name (str):
3124
+ The role to be assigned.
3125
+
3126
+ Returns:
3127
+ bool:
3128
+ True if the user received the role, False otherwise.
3129
+
3130
+ """
3131
+ self.logger.info(
3132
+ "Assign role -> '%s' to user -> '%s' in organization -> '%s'...", role_name, user_name, organization
3133
+ )
3134
+
3135
+ # Check if user already has the role before making the request
3136
+ if self.verify_user_having_role(organization, user_name, role_name):
3137
+ return True
3138
+
3139
+ # Construct the SOAP request body
3140
+ developer_role_data = f"""\
3141
+ <SOAP:Envelope xmlns:SOAP="http://schemas.xmlsoap.org/soap/envelope/">
3142
+ <SOAP:Header xmlns:SOAP="http://schemas.xmlsoap.org/soap/envelope/">
3143
+ <header xmlns="http://schemas.cordys.com/General/1.0/">
3144
+ <Logger xmlns="http://schemas.cordys.com/General/1.0/"/>
3145
+ </header>
3146
+ <i18n:international xmlns:i18n="http://www.w3.org/2005/09/ws-i18n">
3147
+ <i18n:locale>en-US</i18n:locale>
3148
+ </i18n:international>
3149
+ </SOAP:Header>
3150
+ <SOAP:Body>
3151
+ <Update xmlns="http://schemas.cordys.com/1.0/ldap">
3152
+ <tuple>
3153
+ <old>
3154
+ <entry dn="cn=sysadmin,cn=organizational users,o={organization},cn=cordys,cn=defaultInst,o=opentext.net">
3155
+ <role>
3156
+ <string>cn=everyoneIn{organization},cn=organizational roles,o={organization},cn=cordys,cn=defaultInst,o=opentext.net</string>
3157
+ <string>cn=Administrator,cn=Cordys@Work,cn=cordys,cn=defaultInst,o=opentext.net</string>
3158
+ <string>cn=OTDS Push Service,cn=OpenText OTDS Platform Push Connector,cn=cordys,cn=defaultInst,o=opentext.net</string>
3159
+ </role>
3160
+ <description>
3161
+ <string>{user_name}</string>
3162
+ </description>
3163
+ <cn>
3164
+ <string>{user_name}</string>
3165
+ </cn>
3166
+ <objectclass>
3167
+ <string>top</string>
3168
+ <string>busorganizationalobject</string>
3169
+ <string>busorganizationaluser</string>
3170
+ </objectclass>
3171
+ <authenticationuser>
3172
+ <string>cn={user_name},cn=authenticated users,cn=cordys,cn=defaultInst,o=opentext.net</string>
3173
+ </authenticationuser>
3174
+ </entry>
3175
+ </old>
3176
+ <new>
3177
+ <entry dn="cn={user_name},cn=organizational users,o={organization},cn=cordys,cn=defaultInst,o=opentext.net">
3178
+ <role>
3179
+ <string>cn=everyoneIn{organization},cn=organizational roles,o={organization},cn=cordys,cn=defaultInst,o=opentext.net</string>
3180
+ <string>cn=Administrator,cn=Cordys@Work,cn=cordys,cn=defaultInst,o=opentext.net</string>
3181
+ <string>cn=OTDS Push Service,cn=OpenText OTDS Platform Push Connector,cn=cordys,cn=defaultInst,o=opentext.net</string>
3182
+ <string>cn={role_name},cn=Cordys@Work,cn=cordys,cn=defaultInst,o=opentext.net</string>
3183
+ </role>
3184
+ <description>
3185
+ <string>{user_name}</string>
3186
+ </description>
3187
+ <cn>
3188
+ <string>{user_name}</string>
3189
+ </cn>
3190
+ <objectclass>
3191
+ <string>top</string>
3192
+ <string>busorganizationalobject</string>
3193
+ <string>busorganizationaluser</string>
3194
+ </objectclass>
3195
+ <authenticationuser>
3196
+ <string>cn={user_name},cn=authenticated users,cn=cordys,cn=defaultInst,o=opentext.net</string>
3197
+ </authenticationuser>
3198
+ </entry>
3199
+ </new>
3200
+ </tuple>
3201
+ </Update>
3202
+ </SOAP:Body>
3203
+ </SOAP:Envelope>
3204
+ """
3205
+
3206
+ request_url = self.gateway_url()
3207
+
3208
+ self.logger.debug(
3209
+ "Assign role -> '%s' to user -> '%s' in organization -> '%s'; calling -> '%s'",
3210
+ role_name,
3211
+ user_name,
3212
+ organization,
3213
+ request_url,
3214
+ )
3215
+
3216
+ retries = 0
3217
+
3218
+ while retries < REQUEST_MAX_RETRIES:
3219
+ try:
3220
+ response = requests.post(
3221
+ url=request_url,
3222
+ data=developer_role_data,
3223
+ headers=REQUEST_HEADERS_XML,
3224
+ cookies=self.cookie(),
3225
+ timeout=REQUEST_TIMEOUT,
3226
+ )
3227
+
3228
+ if response.ok and role_name in response.text:
3229
+ self.logger.info("Successfully assigned the role -> '%s' to user -> '%s'.", role_name, user_name)
3230
+ return True
3231
+
3232
+ # Handle session expiration
3233
+ if response.status_code == 401 and retries == 0:
3234
+ self.logger.warning("Session expired. Re-authenticating...")
3235
+ self.authenticate(revalidate=True)
3236
+ retries += 1
3237
+ continue # Retry immediately after re-authentication
3238
+
3239
+ # Log failure response
3240
+ self.logger.error(
3241
+ "Failed to assign role -> '%s' to user -> '%s'; error -> %s (%s)",
3242
+ role_name,
3243
+ user_name,
3244
+ response.status_code,
3245
+ response.text,
3246
+ )
3247
+
3248
+ except requests.RequestException as req_exception:
3249
+ self.logger.error("Request failed; error -> %s", str(req_exception))
3250
+
3251
+ retries += 1
3252
+ self.logger.info("Retrying... Attempt %d/%d", retries, REQUEST_MAX_RETRIES)
3253
+ time.sleep(REQUEST_RETRY_DELAY)
3254
+
3255
+ self.logger.error("Retry limit exceeded. Role assignment failed for user '%s'.", user_name)
3256
+ return False
3257
+
3258
+ # end method definition