pyxecm 3.0.0__py3-none-any.whl → 3.1.0__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (53) hide show
  1. pyxecm/avts.py +4 -4
  2. pyxecm/coreshare.py +14 -15
  3. pyxecm/helper/data.py +2 -1
  4. pyxecm/helper/web.py +11 -11
  5. pyxecm/helper/xml.py +41 -10
  6. pyxecm/otac.py +1 -1
  7. pyxecm/otawp.py +19 -19
  8. pyxecm/otca.py +870 -67
  9. pyxecm/otcs.py +1567 -280
  10. pyxecm/otds.py +332 -153
  11. pyxecm/otkd.py +4 -4
  12. pyxecm/otmm.py +1 -1
  13. pyxecm/otpd.py +246 -30
  14. pyxecm-3.1.0.dist-info/METADATA +127 -0
  15. pyxecm-3.1.0.dist-info/RECORD +82 -0
  16. pyxecm_api/app.py +45 -35
  17. pyxecm_api/auth/functions.py +2 -2
  18. pyxecm_api/auth/router.py +2 -3
  19. pyxecm_api/common/functions.py +164 -12
  20. pyxecm_api/settings.py +0 -8
  21. pyxecm_api/terminal/router.py +1 -1
  22. pyxecm_api/v1_csai/router.py +33 -18
  23. pyxecm_customizer/browser_automation.py +98 -48
  24. pyxecm_customizer/customizer.py +43 -25
  25. pyxecm_customizer/guidewire.py +422 -8
  26. pyxecm_customizer/k8s.py +23 -27
  27. pyxecm_customizer/knowledge_graph.py +501 -20
  28. pyxecm_customizer/m365.py +45 -44
  29. pyxecm_customizer/payload.py +1684 -1159
  30. pyxecm_customizer/payload_list.py +3 -0
  31. pyxecm_customizer/salesforce.py +122 -79
  32. pyxecm_customizer/servicenow.py +27 -7
  33. pyxecm_customizer/settings.py +3 -1
  34. pyxecm_customizer/successfactors.py +2 -2
  35. pyxecm_customizer/translate.py +1 -1
  36. pyxecm-3.0.0.dist-info/METADATA +0 -48
  37. pyxecm-3.0.0.dist-info/RECORD +0 -96
  38. pyxecm_api/agents/__init__.py +0 -7
  39. pyxecm_api/agents/app.py +0 -13
  40. pyxecm_api/agents/functions.py +0 -119
  41. pyxecm_api/agents/models.py +0 -10
  42. pyxecm_api/agents/otcm_knowledgegraph/__init__.py +0 -1
  43. pyxecm_api/agents/otcm_knowledgegraph/functions.py +0 -85
  44. pyxecm_api/agents/otcm_knowledgegraph/models.py +0 -61
  45. pyxecm_api/agents/otcm_knowledgegraph/router.py +0 -74
  46. pyxecm_api/agents/otcm_user_agent/__init__.py +0 -1
  47. pyxecm_api/agents/otcm_user_agent/models.py +0 -20
  48. pyxecm_api/agents/otcm_user_agent/router.py +0 -65
  49. pyxecm_api/agents/otcm_workspace_agent/__init__.py +0 -1
  50. pyxecm_api/agents/otcm_workspace_agent/models.py +0 -40
  51. pyxecm_api/agents/otcm_workspace_agent/router.py +0 -200
  52. {pyxecm-3.0.0.dist-info → pyxecm-3.1.0.dist-info}/WHEEL +0 -0
  53. {pyxecm-3.0.0.dist-info → pyxecm-3.1.0.dist-info}/entry_points.txt +0 -0
@@ -48,6 +48,7 @@ class Guidewire:
48
48
  def __init__(
49
49
  self,
50
50
  base_url: str,
51
+ as_url: str,
51
52
  auth_type: str = "",
52
53
  client_id: str = "",
53
54
  client_secret: str = "",
@@ -62,6 +63,8 @@ class Guidewire:
62
63
  Args:
63
64
  base_url (str):
64
65
  The base URL of the Guidewire Cloud API.
66
+ as_url (str):
67
+ The application server endpount the Guidewire system.
65
68
  auth_type (str):
66
69
  The authorization type, either "oauth" or "basic".
67
70
  client_id (str, optional):
@@ -104,21 +107,33 @@ class Guidewire:
104
107
  guidewire_config = {}
105
108
  # Store the credentials and parameters in a config dictionary:
106
109
  guidewire_config["baseUrl"] = base_url.rstrip("/")
110
+ guidewire_config["asUrl"] = as_url.rstrip("/")
107
111
  guidewire_config["authType"] = auth_type
108
112
  guidewire_config["clientId"] = client_id
109
113
  guidewire_config["clientSecret"] = client_secret
110
114
  guidewire_config["username"] = username
111
115
  guidewire_config["password"] = password
112
- guidewire_config["restUrl"] = guidewire_config["baseUrl"] + "/rest" # "/api/v1"
113
- # guidewire_config["tokenUrl"] = guidewire_config["restUrl"] + "/oauth2/token"
116
+ guidewire_config["restUrl"] = (
117
+ guidewire_config["baseUrl"] + "/rest"
118
+ if guidewire_config["baseUrl"]
119
+ else guidewire_config["asUrl"] + "/rest"
120
+ )
114
121
  if token_url:
115
122
  guidewire_config["tokenUrl"] = token_url
116
123
  else:
117
- guidewire_config["tokenUrl"] = guidewire_config["baseUrl"] + "/oauth2/token"
124
+ guidewire_config["tokenUrl"] = (
125
+ guidewire_config["baseUrl"] + "/oauth2/token"
126
+ if guidewire_config["baseUrl"]
127
+ else guidewire_config["asUrl"] + "/oauth2/token"
128
+ )
118
129
 
119
130
  guidewire_config["adminUrl"] = guidewire_config["restUrl"] + "/admin/v1"
120
- guidewire_config["claimUrl"] = guidewire_config["restUrl"] + "/claim/v1"
121
131
  guidewire_config["accountUrl"] = guidewire_config["restUrl"] + "/account/v1"
132
+ guidewire_config["accountSearchUrl"] = guidewire_config["accountUrl"] + "/search/accounts"
133
+ guidewire_config["policyUrl"] = guidewire_config["restUrl"] + "/policy/v1"
134
+ guidewire_config["policySearchUrl"] = guidewire_config["policyUrl"] + "/search/policies"
135
+ guidewire_config["claimUrl"] = guidewire_config["restUrl"] + "/claim/v1"
136
+ guidewire_config["claimSearchUrl"] = guidewire_config["claimUrl"] + "/search/claims-v2"
122
137
 
123
138
  self._config = guidewire_config
124
139
 
@@ -208,7 +223,9 @@ class Guidewire:
208
223
  # Log an error if required credentials are missing
209
224
  # Either username/password AND client credentials (for ROPC)
210
225
  # OR just client credentials (for Client Credentials Grant)
211
- self.logger.error("Authentication requires either client credentials or username/password.")
226
+ self.logger.error(
227
+ "Authentication of type -> '%s' requires either client credentials or username/password.", auth_type
228
+ )
212
229
  return False
213
230
 
214
231
  if self._scope:
@@ -254,7 +271,9 @@ class Guidewire:
254
271
 
255
272
  # end method definition
256
273
 
257
- def do_request(self, method: str, url: str, data: dict | None = None, params: dict | None = None) -> dict:
274
+ def do_request(
275
+ self, method: str, url: str, data: dict | None = None, json_data: dict | None = None, params: dict | None = None
276
+ ) -> dict:
258
277
  """Send a request to the Guidewire REST API.
259
278
 
260
279
  Args:
@@ -264,6 +283,8 @@ class Guidewire:
264
283
  The API endpoint to call.
265
284
  data (dict):
266
285
  The request payload (if applicable).
286
+ json_data (dict | None, optional):
287
+ Request payload for the JSON parameter. Defaults to None.
267
288
  params (dict):
268
289
  The URL parameters (if applicable).
269
290
 
@@ -274,7 +295,7 @@ class Guidewire:
274
295
  """
275
296
 
276
297
  response = self._session.request(
277
- method=method, url=url, headers=self.request_header(), data=data, params=params
298
+ method=method, url=url, headers=self.request_header(), data=data, json=json_data, params=params
278
299
  )
279
300
 
280
301
  return response.json() if response.content else {}
@@ -330,6 +351,92 @@ class Guidewire:
330
351
 
331
352
  # end method definition
332
353
 
354
+ def get_result_value(
355
+ self,
356
+ response: dict,
357
+ key: str,
358
+ index: int = 0,
359
+ show_error: bool = True,
360
+ ) -> str | None:
361
+ """Read an item value from the REST API response.
362
+
363
+ Args:
364
+ response (dict):
365
+ REST API response object.
366
+ key (str):
367
+ Key to find (e.g., "id", "name").
368
+ index (int, optional):
369
+ Index to use if a list of results is delivered (1st element has index 0).
370
+ Defaults to 0.
371
+ show_error (bool, optional):
372
+ Whether an error or just a warning should be logged.
373
+
374
+ Returns:
375
+ str:
376
+ Value of the item with the given key, or None if no value is found.
377
+
378
+ """
379
+
380
+ # First do some sanity checks:
381
+ if not response:
382
+ self.logger.debug("Empty response - no results found!")
383
+ return None
384
+
385
+ # To support also iterators that yield from results,
386
+ # we wrap an attributea element into a data element
387
+ # to make the following code work like for direct REST responses:
388
+ if "attributes" in response:
389
+ response = {"data": response}
390
+
391
+ if "data" not in response:
392
+ if show_error:
393
+ self.logger.error("No 'data' key in REST response - returning None")
394
+ return None
395
+
396
+ results = response["data"]
397
+ if not results:
398
+ self.logger.debug("No results found! Empty data element.")
399
+ return None
400
+
401
+ # check if results is a list or a dict (both is possible - iterator responses will be dict):
402
+ if isinstance(results, dict):
403
+ # result is a dict - we don't need index value
404
+
405
+ attributes = results.get("attributes", {})
406
+ if key in attributes:
407
+ return attributes[key]
408
+ else:
409
+ self.logger.error(
410
+ "Key -> '%s' is not in result attributes!",
411
+ key,
412
+ )
413
+ return None
414
+
415
+ elif isinstance(results, list):
416
+ # result is a list - we need a valid index:
417
+ if index > len(results) - 1:
418
+ self.logger.error(
419
+ "Illegal Index -> %s given. List has only -> %s elements!",
420
+ str(index),
421
+ str(len(results)),
422
+ )
423
+ return None
424
+ data = results[index]
425
+ attributes = data.get("attributes", {})
426
+ if key not in attributes:
427
+ if show_error:
428
+ self.logger.error("Key -> '%s' is not in result attributes -> %s!", key, attributes)
429
+ return None
430
+ return attributes[key]
431
+ else:
432
+ self.logger.error(
433
+ "Result needs to be a list or dict but it is -> %s",
434
+ str(type(results)),
435
+ )
436
+ return None
437
+
438
+ # end method definition
439
+
333
440
  def get_groups(
334
441
  self,
335
442
  fields: list | None = None,
@@ -1020,7 +1127,7 @@ class Guidewire:
1020
1127
  * cn - contains
1021
1128
  - "value": the filue to filter for. Either literal or list of values
1022
1129
  page_size (int, optional):
1023
- The maximum number of groups to return.
1130
+ The maximum number of accounts to return.
1024
1131
 
1025
1132
  Returns:
1026
1133
  iter:
@@ -1075,6 +1182,79 @@ class Guidewire:
1075
1182
 
1076
1183
  # end method definition
1077
1184
 
1185
+ def search_account(self, attributes: dict) -> dict:
1186
+ """Search accounts based on its attributes.
1187
+
1188
+ Args:
1189
+ attributes (dict):
1190
+ The attribute to search the value in. Possible key values:
1191
+ * "accountNumber"
1192
+ * "addressLine1"
1193
+ * "addressLine2"
1194
+ * "city"
1195
+ * "country"
1196
+ * "companyName"
1197
+
1198
+ Returns:
1199
+ dict:
1200
+ JSON response containing account details.
1201
+
1202
+ Example:
1203
+ {
1204
+ 'count': 2,
1205
+ 'data': [
1206
+ {
1207
+ 'attributes': {
1208
+ 'accountHolder': {
1209
+ 'displayName': 'Armstrong and Company',
1210
+ 'id': 'test_pc:1',
1211
+ 'type': 'AccountContact',
1212
+ 'uri': '/account/v1/accounts/pc:ds:1/contacts/test_pc:1'
1213
+ },
1214
+ 'accountNumber': 'C000212105',
1215
+ 'accountStatus': {...},
1216
+ 'businessOperationsDescription': 'business description',
1217
+ 'createdDate': '2025-07-14T03:59:30.055Z',
1218
+ 'frozen': False,
1219
+ 'id': 'pc:ds:1',
1220
+ 'industryCode': {...},
1221
+ 'numberOfContacts': '8',
1222
+ 'organizationType': {...},
1223
+ 'preferredCoverageCurrency': {...},
1224
+ 'preferredSettlementCurrency': {...},
1225
+ 'primaryLanguage': {...},
1226
+ 'primaryLocale': {...},
1227
+ 'primaryLocation': {...},
1228
+ 'producerCodes': [...]
1229
+ },
1230
+ 'checksum': '2',
1231
+ 'links': {
1232
+ 'do-not-destroy': {...},
1233
+ 'freeze': {...},
1234
+ 'merge': {...},
1235
+ 'move-policies': {...},
1236
+ 'move-submissions': {...},
1237
+ 'self': {...}
1238
+ }
1239
+ },
1240
+ {...}
1241
+ ],
1242
+ 'links': {
1243
+ 'first': {...},
1244
+ 'self': {...}
1245
+ }
1246
+ }
1247
+
1248
+ """
1249
+
1250
+ body = {"data": {"attributes": attributes}}
1251
+
1252
+ request_url = self.config()["accountSearchUrl"]
1253
+
1254
+ return self.do_request(method="POST", json_data=body, url=request_url)
1255
+
1256
+ # end method definition
1257
+
1078
1258
  def add_account(self, account_data: dict) -> dict:
1079
1259
  """Create a new account.
1080
1260
 
@@ -1134,6 +1314,218 @@ class Guidewire:
1134
1314
 
1135
1315
  # end method definition
1136
1316
 
1317
+ def get_policies(
1318
+ self,
1319
+ fields: list | None = None,
1320
+ filters: list | None = None,
1321
+ page_size: int = 25,
1322
+ next_page_url: str | None = None,
1323
+ ) -> dict | None:
1324
+ """Retrieve a list of policies.
1325
+
1326
+ Args:
1327
+ fields (list | None, optional):
1328
+ The list of fields in the results. If None, all default
1329
+ fields are returned.
1330
+ Fields for Guidewire accounts:
1331
+ - *all = return all fields
1332
+ - *default = return just the default list of fields
1333
+ - *summary = return the fields defined for giving a summary
1334
+ - *detail = details
1335
+ - displayName
1336
+ - groupType
1337
+ - id
1338
+ - loadFactor
1339
+ - name
1340
+ - organization
1341
+ - parent
1342
+ - securityZone
1343
+ - supervisor
1344
+ filters (list | None, optional):
1345
+ List of dictionaries with three keys each:
1346
+ - "attribute" - name of the attribute to use for the filter (available attributes see above)
1347
+ - "op" - operator:
1348
+ * eq - equal
1349
+ * ne - not equal
1350
+ * lt - less than - also usable for dates (before)
1351
+ * gt - greater than - also usable for dates (after)
1352
+ * le - less or equal
1353
+ * ge - greater or equal
1354
+ * in - is in list
1355
+ * ni - is NOT in list
1356
+ * sw - starts with
1357
+ * cn - contains
1358
+ - "value": the value to filter for. Either literal or list of values
1359
+ page_size (int, optional):
1360
+ The maximum number of groups to return.
1361
+ next_page_url (str, optional):
1362
+ The Guidewire URL to retrieve the next page of Guidewire groups (pagination).
1363
+ This is used for the iterator get_groups_iterator() below.
1364
+
1365
+ Returns:
1366
+ dict | None:
1367
+ JSON response containing claim data.
1368
+
1369
+ """
1370
+
1371
+ if not next_page_url:
1372
+ request_url = self.config()["policyUrl"] + "/policies"
1373
+
1374
+ encoded_query = self.process_parameters(fields=fields, filters=filters, page_size=page_size)
1375
+ if encoded_query:
1376
+ request_url += "?" + encoded_query
1377
+ else:
1378
+ request_url = self.config()["restUrl"] + next_page_url
1379
+
1380
+ return self.do_request(method="GET", url=request_url)
1381
+
1382
+ # end method definition
1383
+
1384
+ def get_policies_iterator(
1385
+ self, fields: list | None = None, filters: list | None = None, page_size: int = 25
1386
+ ) -> iter:
1387
+ """Get an iterator object that can be used to traverse all Guidewire policies.
1388
+
1389
+ Returning a generator avoids loading a large number of nodes into memory at once. Instead you
1390
+ can iterate over the potential large list of groups.
1391
+
1392
+ Example usage:
1393
+ policies = guidewire_object.get_policies_iterator()
1394
+ for policy in policies:
1395
+ logger.info("Traversing Guidewire policy -> '%s'...", policy.get("attributes", {}).get("displayName"))
1396
+
1397
+ Args:
1398
+ fields (list | None, optional):
1399
+ The list of fields in the results. If None, all default
1400
+ fields are returned.
1401
+ Fields for Guidewire accounts:
1402
+ - *all = return all fields
1403
+ - *default = return just the default list of fields
1404
+ - *summary = return the fields defined for giving a summary
1405
+ - *detail = details
1406
+ - displayName
1407
+ - groupType
1408
+ - id
1409
+ - loadFactor
1410
+ - name
1411
+ - organization
1412
+ - parent
1413
+ - securityZone
1414
+ - supervisor
1415
+ filters (list | None, optional):
1416
+ List of dictionaries with three keys each:
1417
+ - "attribute" - name of the attribute to use for the filter (available attributes see above)
1418
+ - "op" - operator:
1419
+ * eq - equal
1420
+ * ne - not equal
1421
+ * lt - less than - also usable for dates (before)
1422
+ * gt - greater than - also usable for dates (after)
1423
+ * le - less or equal
1424
+ * ge - greater or equal
1425
+ * in - is in list
1426
+ * ni - is NOT in list
1427
+ * sw - starts with
1428
+ * cn - contains
1429
+ - "value": the value to filter for. Either literal or list of values
1430
+ page_size (int, optional):
1431
+ The maximum number of policies to return.
1432
+
1433
+ Returns:
1434
+ iter:
1435
+ A generator yielding one Guidewire account per iteration.
1436
+ If the REST API fails, returns no value.
1437
+
1438
+ """
1439
+
1440
+ next_page_url = None
1441
+
1442
+ while True:
1443
+ response = self.get_policies(
1444
+ fields=fields, filters=filters, page_size=page_size, next_page_url=next_page_url
1445
+ )
1446
+ if not response or "data" not in response:
1447
+ # Don't return None! Plain return is what we need for iterators.
1448
+ # Natural Termination: If the generator does not yield, it behaves
1449
+ # like an empty iterable when used in a loop or converted to a list:
1450
+ return
1451
+
1452
+ # Yield users one at a time:
1453
+ yield from response["data"]
1454
+
1455
+ # See if we have an additional result page.
1456
+ # If not terminate the iterator and return
1457
+ # no value.
1458
+ next_page_url = response.get("links", {}).get("next", {}).get("href")
1459
+ if not next_page_url:
1460
+ # Don't return None! Plain return is what we need for iterators.
1461
+ # Natural Termination: If the generator does not yield, it behaves
1462
+ # like an empty iterable when used in a loop or converted to a list:
1463
+ return
1464
+
1465
+ # end method definition
1466
+
1467
+ def search_policy(self, attributes: dict) -> dict:
1468
+ """Search a specific policy based on its attributes.
1469
+
1470
+ See: https://docs.guidewire.com/cloud/pc/202407/apiref/generated/Policy%20API/search-policies--post
1471
+
1472
+ Args:
1473
+ attributes (dict):
1474
+ The attribute to search the value in. Possible key values:
1475
+ * "policyNumber" (str)
1476
+ * "city" (str)
1477
+ * "state" (dict with keys "code", "name"), e.g. {"code": "GA", "name": "Georgia"}
1478
+ * "country" (str)
1479
+ * "postalCode" (str)
1480
+ * "street" (str)
1481
+ * "companyName" (str)
1482
+ * "firstName" (str)
1483
+ * "lastName" (str)
1484
+ * "officialId" (str)
1485
+
1486
+ Returns:
1487
+ dict:
1488
+ JSON response containing account details.
1489
+
1490
+ Example:
1491
+ {
1492
+ 'count': 1,
1493
+ 'data': [
1494
+ {
1495
+ 'attributes': {
1496
+ 'accountNumber': 'C000212105',
1497
+ 'effectiveDate': '2025-07-14T04:01:00.000Z',
1498
+ 'expirationDate': '2026-07-14T04:01:00.000Z',
1499
+ 'insuredName': 'Armstrong and Company',
1500
+ 'policyAddress': '142 Central Ave, Metter, GA 30439',
1501
+ 'policyId': 'pc:Sn09Itxh7Btpc8izhUrtc',
1502
+ 'policyNumber': '5050680845',
1503
+ 'producerOfRecordName': 'Armstrong and Company',
1504
+ 'producerOfServiceName': 'Armstrong and Company',
1505
+ 'product': {
1506
+ 'displayName': 'Manual Products',
1507
+ 'id': 'Manual'
1508
+ }
1509
+ },
1510
+ 'links': {...}
1511
+ }
1512
+ ],
1513
+ 'links': {
1514
+ 'first': {'href': '/policy/v1/search/policies', 'methods': ['post']},
1515
+ 'self': {'href': '/policy/v1/search/policies', 'methods': ['post']}
1516
+ }
1517
+ }
1518
+
1519
+ """
1520
+
1521
+ body = {"data": {"attributes": attributes}}
1522
+
1523
+ request_url = self.config()["policySearchUrl"]
1524
+
1525
+ return self.do_request(method="POST", json_data=body, url=request_url)
1526
+
1527
+ # end method definition
1528
+
1137
1529
  def get_claims(
1138
1530
  self,
1139
1531
  fields: list | None = None,
@@ -1299,6 +1691,28 @@ class Guidewire:
1299
1691
 
1300
1692
  # end method definition
1301
1693
 
1694
+ def search_claim(self, attributes: dict) -> dict:
1695
+ """Search a specific claim based on its attributes.
1696
+
1697
+ Args:
1698
+ attributes (dict):
1699
+ The attribute to search the value in. Possible key values:
1700
+ * TBD
1701
+
1702
+ Returns:
1703
+ dict:
1704
+ JSON response containing account details.
1705
+
1706
+ """
1707
+
1708
+ body = {"data": {"attributes": attributes}}
1709
+
1710
+ request_url = self.config()["claimSearchUrl"]
1711
+
1712
+ return self.do_request(method="POST", json_data=body, url=request_url)
1713
+
1714
+ # end method definition
1715
+
1302
1716
  def add_claim(self, claim_data: dict) -> dict:
1303
1717
  """Create a new claim.
1304
1718
 
pyxecm_customizer/k8s.py CHANGED
@@ -205,7 +205,7 @@ class K8s:
205
205
  self.logger.debug("Pod -> '%s' not found (may be deleted).", pod_name)
206
206
  return None
207
207
  else:
208
- self.logger.error("Failed to get Pod -> '%s'!", pod_name)
208
+ self.logger.error("Failed to get pod -> '%s'!", pod_name)
209
209
  return None # Unexpected error, return None
210
210
  return response
211
211
 
@@ -306,8 +306,9 @@ class K8s:
306
306
 
307
307
  except ApiException:
308
308
  self.logger.error(
309
- "Failed to wait for pod -> '%s'",
309
+ "Failed to wait for pod -> '%s' to reach state -> '%s'.",
310
310
  pod_name,
311
+ condition_name,
311
312
  )
312
313
 
313
314
  # end method definition
@@ -320,7 +321,7 @@ class K8s:
320
321
  time_retry: int = 10,
321
322
  container: str | None = None,
322
323
  timeout: int = 60,
323
- ) -> str:
324
+ ) -> str | None:
324
325
  """Execute a command inside a Kubernetes Pod (similar to kubectl exec on command line).
325
326
 
326
327
  See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/CoreV1Api.md#connect_get_namespaced_pod_exec
@@ -342,14 +343,15 @@ class K8s:
342
343
  Each time a response is found in stdout or stderr we wait another timeout duration [60]
343
344
 
344
345
  Returns:
345
- str:
346
+ str | None:
346
347
  Response of the command or None if the call fails.
347
348
 
348
349
  """
349
350
 
350
351
  pod = self.get_pod(pod_name=pod_name)
351
352
  if not pod:
352
- self.logger.error("Pod -> '%s' does not exist", pod_name)
353
+ self.logger.error("Pod -> '%s' does not exist. Cannot execute command -> %s", pod_name, command)
354
+ return None
353
355
 
354
356
  self.logger.debug("Execute command -> %s in pod -> '%s'", command, pod_name)
355
357
 
@@ -391,10 +393,10 @@ class K8s:
391
393
  return response
392
394
 
393
395
  self.logger.error(
394
- "Failed to execute command with %s retries -> %s in pod -> '%s'; error -> %s",
395
- max_retry,
396
+ "Failed to execute command -> %s in pod -> '%s' after %d retries; error -> %s",
396
397
  command,
397
398
  pod_name,
399
+ max_retry,
398
400
  str(exception),
399
401
  )
400
402
 
@@ -409,7 +411,7 @@ class K8s:
409
411
  commands: list,
410
412
  timeout: int = 30,
411
413
  write_stderr_to_error_log: bool = True,
412
- ) -> str:
414
+ ) -> str | None:
413
415
  """Execute a command inside a Kubernetes pod (similar to kubectl exec on command line).
414
416
 
415
417
  Other than exec_pod_command() method above this is an interactive execution using
@@ -432,19 +434,20 @@ class K8s:
432
434
  Default is write to error log (True).
433
435
 
434
436
  Returns:
435
- str:
437
+ str | None:
436
438
  Response of the command or None if the call fails.
437
439
 
438
440
  """
439
441
 
440
- pod = self.get_pod(pod_name=pod_name)
441
- if not pod:
442
- self.logger.error("Pod -> '%s' does not exist", pod_name)
443
-
444
442
  if not commands:
445
443
  self.logger.error("No commands to execute on pod ->'%s'!", pod_name)
446
444
  return None
447
445
 
446
+ pod = self.get_pod(pod_name=pod_name)
447
+ if not pod:
448
+ self.logger.error("Pod -> '%s' does not exist. Cannot execute commands -> %s", pod_name, commands)
449
+ return None
450
+
448
451
  # Get first command - this should be the shell:
449
452
  command = commands.pop(0)
450
453
 
@@ -499,7 +502,7 @@ class K8s:
499
502
 
500
503
  # end method definition
501
504
 
502
- def delete_pod(self, pod_name: str) -> None:
505
+ def delete_pod(self, pod_name: str) -> V1Pod | None:
503
506
  """Delete a pod in the configured namespace (the namespace is defined in the class constructor).
504
507
 
505
508
  See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/CoreV1Api.md#delete_namespaced_pod
@@ -509,21 +512,14 @@ class K8s:
509
512
  The name of the Kubernetes pod in the current namespace.
510
513
 
511
514
  Returns:
512
- V1Status (object) or None if the call fails.
513
- - api_version: The Kubernetes API version.
514
- - kind: The Kubernetes object kind, which is always "Status".
515
- - metadata: Additional metadata about the status object, such as the resource version.
516
- - status: The status of the operation, which is either "Success" or an error status.
517
- - message: A human-readable message explaining the status.
518
- - reason: A short string that describes the reason for the status.
519
- - code: An HTTP status code that corresponds to the status.
520
- See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1Status.md
515
+ V1Pod (object) or None if the call fails.
521
516
 
522
517
  """
523
518
 
524
519
  pod = self.get_pod(pod_name=pod_name)
525
520
  if not pod:
526
- self.logger.error("Pod -> '%s' does not exist!", pod_name)
521
+ self.logger.error("Pod -> '%s' does not exist! Cannot delete it.", pod_name)
522
+ return None
527
523
 
528
524
  try:
529
525
  response = self.get_core_v1_api().delete_namespaced_pod(
@@ -1105,7 +1101,7 @@ class K8s:
1105
1101
  rule_index += 1
1106
1102
 
1107
1103
  if not host:
1108
- self.logger.error("Cannot find host.")
1104
+ self.logger.error("Cannot find host to upgrade the Kubernetes Ingress -> '%s'", ingress_name)
1109
1105
  return None
1110
1106
 
1111
1107
  body = [
@@ -1351,7 +1347,7 @@ class K8s:
1351
1347
  body,
1352
1348
  pretty="true",
1353
1349
  )
1354
- self.logger.info("Triggered restart of deployment -> '%s'.", deployment_name)
1350
+ self.logger.debug("Triggered restart of deployment -> '%s'.", deployment_name)
1355
1351
 
1356
1352
  except ApiException as api_exception:
1357
1353
  self.logger.error(
@@ -1484,7 +1480,7 @@ class K8s:
1484
1480
 
1485
1481
  try:
1486
1482
  self.get_apps_v1_api().patch_namespaced_stateful_set(sts_name, self.get_namespace(), body, pretty="true")
1487
- self.logger.info("Triggered restart of stateful set -> '%s'.", sts_name)
1483
+ self.logger.debug("Triggered restart of stateful set -> '%s'.", sts_name)
1488
1484
 
1489
1485
  except ApiException as api_exception:
1490
1486
  self.logger.error("Failed to restart stateful set -> '%s'; error -> %s!", sts_name, str(api_exception))