pyxecm 3.1.0__py3-none-any.whl → 3.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pyxecm might be problematic. Click here for more details.
- pyxecm/otca.py +8 -3
- pyxecm/otcs.py +166 -86
- {pyxecm-3.1.0.dist-info → pyxecm-3.1.1.dist-info}/METADATA +1 -1
- {pyxecm-3.1.0.dist-info → pyxecm-3.1.1.dist-info}/RECORD +11 -11
- pyxecm_api/common/functions.py +0 -97
- pyxecm_customizer/browser_automation.py +65 -33
- pyxecm_customizer/guidewire.py +8 -8
- pyxecm_customizer/knowledge_graph.py +14 -17
- pyxecm_customizer/payload.py +33 -23
- {pyxecm-3.1.0.dist-info → pyxecm-3.1.1.dist-info}/WHEEL +0 -0
- {pyxecm-3.1.0.dist-info → pyxecm-3.1.1.dist-info}/entry_points.txt +0 -0
pyxecm/otca.py
CHANGED
|
@@ -2240,9 +2240,14 @@ class OTCA:
|
|
|
2240
2240
|
}
|
|
2241
2241
|
},
|
|
2242
2242
|
},
|
|
2243
|
-
"responseTemplate": {
|
|
2244
|
-
|
|
2245
|
-
|
|
2243
|
+
"responseTemplate": {
|
|
2244
|
+
'scratchpad': {
|
|
2245
|
+
'item': {
|
|
2246
|
+
'input': {'where': 'response.context_update.where'}
|
|
2247
|
+
}
|
|
2248
|
+
},
|
|
2249
|
+
"agents": ["retrieverAgent"],
|
|
2250
|
+
}
|
|
2246
2251
|
|
|
2247
2252
|
Returns:
|
|
2248
2253
|
dict: Tool details or None in case of an error.
|
pyxecm/otcs.py
CHANGED
|
@@ -500,6 +500,7 @@ class OTCS:
|
|
|
500
500
|
self._use_numeric_category_identifier = use_numeric_category_identifier
|
|
501
501
|
self._executor = ThreadPoolExecutor(max_workers=thread_number)
|
|
502
502
|
self._workspace_type_lookup = {}
|
|
503
|
+
self._workspace_type_names = []
|
|
503
504
|
|
|
504
505
|
# end method definition
|
|
505
506
|
|
|
@@ -1018,7 +1019,12 @@ class OTCS:
|
|
|
1018
1019
|
if success_message:
|
|
1019
1020
|
self.logger.info(success_message)
|
|
1020
1021
|
if parse_request_response and not stream:
|
|
1021
|
-
|
|
1022
|
+
# There are cases where OTCS returns response.ok (200) but
|
|
1023
|
+
# because of restart or scaling of pods the response text is not
|
|
1024
|
+
# valid JSON. So parse_request_response() may raise an ConnectionError exception that
|
|
1025
|
+
# is handled in the exception block below (with waiting for readiness and retry logic)
|
|
1026
|
+
parsed_response = self.parse_request_response(response_object=response)
|
|
1027
|
+
return parsed_response
|
|
1022
1028
|
else:
|
|
1023
1029
|
return response
|
|
1024
1030
|
# Check if Session has expired - then re-authenticate and try once more
|
|
@@ -1123,17 +1129,20 @@ class OTCS:
|
|
|
1123
1129
|
else:
|
|
1124
1130
|
return None
|
|
1125
1131
|
# end except Timeout
|
|
1126
|
-
except requests.exceptions.ConnectionError:
|
|
1132
|
+
except requests.exceptions.ConnectionError as connection_error:
|
|
1127
1133
|
if retries <= max_retries:
|
|
1128
1134
|
self.logger.warning(
|
|
1129
|
-
"
|
|
1135
|
+
"Cannot connect to OTCS at -> %s; error -> %s! Retrying in %d seconds... %d/%d",
|
|
1130
1136
|
url,
|
|
1137
|
+
str(connection_error),
|
|
1131
1138
|
REQUEST_RETRY_DELAY,
|
|
1132
1139
|
retries,
|
|
1133
1140
|
max_retries,
|
|
1134
1141
|
)
|
|
1135
1142
|
retries += 1
|
|
1136
1143
|
|
|
1144
|
+
# The connection error could have been caused by a restart of the OTCS pod or services.
|
|
1145
|
+
# So we better check if OTCS is ready to receive requests again before retrying:
|
|
1137
1146
|
while not self.is_ready():
|
|
1138
1147
|
self.logger.warning(
|
|
1139
1148
|
"Content Server is not ready to receive requests. Waiting for state change in %d seconds...",
|
|
@@ -1143,8 +1152,9 @@ class OTCS:
|
|
|
1143
1152
|
|
|
1144
1153
|
else:
|
|
1145
1154
|
self.logger.error(
|
|
1146
|
-
"%s; connection error",
|
|
1155
|
+
"%s; connection error -> %s",
|
|
1147
1156
|
failure_message,
|
|
1157
|
+
str(connection_error),
|
|
1148
1158
|
)
|
|
1149
1159
|
if retry_forever:
|
|
1150
1160
|
# If it fails after REQUEST_MAX_RETRIES retries
|
|
@@ -1183,13 +1193,17 @@ class OTCS:
|
|
|
1183
1193
|
The response object delivered by the request call.
|
|
1184
1194
|
additional_error_message (str):
|
|
1185
1195
|
Custom error message to include in logs.
|
|
1186
|
-
show_error (bool):
|
|
1187
|
-
If True, logs an error. If False, logs a warning.
|
|
1196
|
+
show_error (bool, optional):
|
|
1197
|
+
If True, logs an error / raises an exception. If False, logs a warning.
|
|
1188
1198
|
|
|
1189
1199
|
Returns:
|
|
1190
1200
|
dict | None:
|
|
1191
1201
|
Parsed response as a dictionary, or None in case of an error.
|
|
1192
1202
|
|
|
1203
|
+
Raises:
|
|
1204
|
+
requests.exceptions.ConnectionError:
|
|
1205
|
+
If the response cannot be decoded as JSON.
|
|
1206
|
+
|
|
1193
1207
|
"""
|
|
1194
1208
|
|
|
1195
1209
|
if not response_object:
|
|
@@ -1214,12 +1228,13 @@ class OTCS:
|
|
|
1214
1228
|
exception,
|
|
1215
1229
|
)
|
|
1216
1230
|
if show_error:
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1231
|
+
# Raise ConnectionError instead of returning None
|
|
1232
|
+
raise requests.exceptions.ConnectionError(message) from exception
|
|
1233
|
+
self.logger.warning(message)
|
|
1220
1234
|
return None
|
|
1221
|
-
|
|
1222
|
-
|
|
1235
|
+
# end try-except block
|
|
1236
|
+
|
|
1237
|
+
return dict_object
|
|
1223
1238
|
|
|
1224
1239
|
# end method definition
|
|
1225
1240
|
|
|
@@ -1710,9 +1725,9 @@ class OTCS:
|
|
|
1710
1725
|
# than creating a list with all values at once.
|
|
1711
1726
|
# This is especially important for large result sets.
|
|
1712
1727
|
yield from (
|
|
1713
|
-
item[data_name][property_name]
|
|
1728
|
+
item[data_name][property_name] if property_name else item[data_name]
|
|
1714
1729
|
for item in response["results"]
|
|
1715
|
-
if isinstance(item.get(data_name), dict) and property_name in item[data_name]
|
|
1730
|
+
if isinstance(item.get(data_name), dict) and (not property_name or property_name in item[data_name])
|
|
1716
1731
|
)
|
|
1717
1732
|
|
|
1718
1733
|
# end method definition
|
|
@@ -5054,9 +5069,9 @@ class OTCS:
|
|
|
5054
5069
|
show_hidden (bool, optional):
|
|
5055
5070
|
Whether to list hidden items. Defaults to False.
|
|
5056
5071
|
limit (int, optional):
|
|
5057
|
-
The maximum number of results to return. Defaults to 100.
|
|
5072
|
+
The maximum number of results to return (page size). Defaults to 100.
|
|
5058
5073
|
page (int, optional):
|
|
5059
|
-
The page of results to retrieve. Defaults to 1 (first page).
|
|
5074
|
+
The page of results to retrieve (page number). Defaults to 1 (first page).
|
|
5060
5075
|
fields (str | list, optional):
|
|
5061
5076
|
Which fields to retrieve.
|
|
5062
5077
|
This can have a significant impact on performance.
|
|
@@ -5408,11 +5423,10 @@ class OTCS:
|
|
|
5408
5423
|
continue
|
|
5409
5424
|
category_key = next(iter(category_schema))
|
|
5410
5425
|
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
if not attribute_schema:
|
|
5426
|
+
# There can be multiple attributes with the same name in a category
|
|
5427
|
+
# if the category has sets:
|
|
5428
|
+
attribute_schemas = [cat_elem for cat_elem in category_schema.values() if cat_elem.get("name") == attribute]
|
|
5429
|
+
if not attribute_schemas:
|
|
5416
5430
|
self.logger.debug(
|
|
5417
5431
|
"Node -> '%s' (%s) does not have attribute -> '%s'. Skipping...",
|
|
5418
5432
|
node_name,
|
|
@@ -5420,73 +5434,81 @@ class OTCS:
|
|
|
5420
5434
|
attribute,
|
|
5421
5435
|
)
|
|
5422
5436
|
continue
|
|
5423
|
-
attribute_key = attribute_schema["key"]
|
|
5424
|
-
# Split the attribute key once (1) at the first underscore from the right.
|
|
5425
|
-
# rsplit delivers a list and [-1] delivers the last list item:
|
|
5426
|
-
attribute_id = attribute_key.rsplit("_", 1)[-1]
|
|
5427
5437
|
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
),
|
|
5435
|
-
None,
|
|
5436
|
-
)
|
|
5437
|
-
if not set_schema:
|
|
5438
|
-
self.logger.debug(
|
|
5439
|
-
"Node -> '%s' (%s) does not have attribute set -> '%s'. Skipping...",
|
|
5440
|
-
node_name,
|
|
5441
|
-
node_id,
|
|
5442
|
-
attribute_set,
|
|
5443
|
-
)
|
|
5444
|
-
continue
|
|
5445
|
-
set_key = set_schema["key"]
|
|
5446
|
-
else:
|
|
5447
|
-
set_schema = None
|
|
5448
|
-
set_key = None
|
|
5438
|
+
# Traverse the attribute schemas with the matching attribute name:
|
|
5439
|
+
for attribute_schema in attribute_schemas:
|
|
5440
|
+
attribute_key = attribute_schema["key"]
|
|
5441
|
+
# Split the attribute key once (1) at the first underscore from the right.
|
|
5442
|
+
# rsplit delivers a list and [-1] delivers the last list item:
|
|
5443
|
+
attribute_id = attribute_key.rsplit("_", 1)[-1]
|
|
5449
5444
|
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5445
|
+
if attribute_set: # is the attribute_set parameter provided?
|
|
5446
|
+
set_schema = next(
|
|
5447
|
+
(
|
|
5448
|
+
cat_elem
|
|
5449
|
+
for cat_elem in category_schema.values()
|
|
5450
|
+
if cat_elem.get("name") == attribute_set and cat_elem.get("persona") == "set"
|
|
5451
|
+
),
|
|
5452
|
+
None,
|
|
5453
|
+
)
|
|
5454
|
+
if not set_schema:
|
|
5455
|
+
self.logger.debug(
|
|
5456
|
+
"Node -> '%s' (%s) does not have attribute set -> '%s'. Skipping...",
|
|
5457
|
+
node_name,
|
|
5458
|
+
node_id,
|
|
5459
|
+
attribute_set,
|
|
5460
|
+
)
|
|
5461
|
+
continue
|
|
5462
|
+
set_key = set_schema["key"]
|
|
5463
|
+
else: # no attribute set value provided via the attribute_set parameter:
|
|
5464
|
+
if "_x_" in attribute_key:
|
|
5465
|
+
# The lookup does not include a set name but this attribute key
|
|
5466
|
+
# belongs to a set attribute - so we can skip it:
|
|
5467
|
+
continue
|
|
5468
|
+
set_schema = None
|
|
5469
|
+
set_key = None
|
|
5470
|
+
|
|
5471
|
+
prefix = set_key + "_" if set_key else category_key + "_"
|
|
5472
|
+
|
|
5473
|
+
data = node["data"]["categories"]
|
|
5474
|
+
for cat_data in data:
|
|
5475
|
+
if set_key:
|
|
5476
|
+
for i in range(1, int(set_schema["multi_value_length_max"])):
|
|
5477
|
+
key = prefix + str(i) + "_" + attribute_id
|
|
5478
|
+
attribute_value = cat_data.get(key)
|
|
5479
|
+
if not attribute_value:
|
|
5480
|
+
break
|
|
5481
|
+
# Is it a multi-value attribute (i.e. a list of values)?
|
|
5482
|
+
if isinstance(attribute_value, list):
|
|
5483
|
+
if value in attribute_value:
|
|
5484
|
+
# Create a "results" dict that is compatible with normal REST calls
|
|
5485
|
+
# to not break get_result_value() method that may be called on the result:
|
|
5486
|
+
results["results"].append(node)
|
|
5487
|
+
elif value == attribute_value:
|
|
5488
|
+
# Create a results dict that is compatible with normal REST calls
|
|
5489
|
+
# to not break get_result_value() method that may be called on the result:
|
|
5490
|
+
results["results"].append(node)
|
|
5491
|
+
# end if set_key
|
|
5492
|
+
else:
|
|
5493
|
+
key = prefix + attribute_id
|
|
5457
5494
|
attribute_value = cat_data.get(key)
|
|
5458
5495
|
if not attribute_value:
|
|
5459
|
-
|
|
5496
|
+
continue
|
|
5460
5497
|
# Is it a multi-value attribute (i.e. a list of values)?
|
|
5461
5498
|
if isinstance(attribute_value, list):
|
|
5462
5499
|
if value in attribute_value:
|
|
5463
5500
|
# Create a "results" dict that is compatible with normal REST calls
|
|
5464
5501
|
# to not break get_result_value() method that may be called on the result:
|
|
5465
5502
|
results["results"].append(node)
|
|
5503
|
+
# If not a multi-value attribute, check for equality:
|
|
5466
5504
|
elif value == attribute_value:
|
|
5467
5505
|
# Create a results dict that is compatible with normal REST calls
|
|
5468
5506
|
# to not break get_result_value() method that may be called on the result:
|
|
5469
5507
|
results["results"].append(node)
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
if not attribute_value:
|
|
5475
|
-
continue
|
|
5476
|
-
# Is it a multi-value attribute (i.e. a list of values)?
|
|
5477
|
-
if isinstance(attribute_value, list):
|
|
5478
|
-
if value in attribute_value:
|
|
5479
|
-
# Create a "results" dict that is compatible with normal REST calls
|
|
5480
|
-
# to not break get_result_value() method that may be called on the result:
|
|
5481
|
-
results["results"].append(node)
|
|
5482
|
-
# If not a multi-value attribute, check for equality:
|
|
5483
|
-
elif value == attribute_value:
|
|
5484
|
-
# Create a results dict that is compatible with normal REST calls
|
|
5485
|
-
# to not break get_result_value() method that may be called on the result:
|
|
5486
|
-
results["results"].append(node)
|
|
5487
|
-
# end if set_key else
|
|
5488
|
-
# end for cat_data, cat_schema in zip(data, schema)
|
|
5489
|
-
# end for node in nodes
|
|
5508
|
+
# end if set_key ... else
|
|
5509
|
+
# end for cat_data in data:
|
|
5510
|
+
# end for attribute_schema in attribute_schemas:
|
|
5511
|
+
# end for node in self.get_subnodes_iterator()
|
|
5490
5512
|
|
|
5491
5513
|
self.logger.debug(
|
|
5492
5514
|
"Couldn't find a node with the value -> '%s' in the attribute -> '%s' of category -> '%s' in parent with node ID -> %d.",
|
|
@@ -7516,7 +7538,7 @@ class OTCS:
|
|
|
7516
7538
|
chunk_size: int = 8192,
|
|
7517
7539
|
overwrite: bool = True,
|
|
7518
7540
|
) -> bool:
|
|
7519
|
-
"""Download a document from OTCS to local file system.
|
|
7541
|
+
"""Download a document (version) from OTCS to local file system.
|
|
7520
7542
|
|
|
7521
7543
|
Args:
|
|
7522
7544
|
node_id (int):
|
|
@@ -7541,8 +7563,7 @@ class OTCS:
|
|
|
7541
7563
|
"""
|
|
7542
7564
|
|
|
7543
7565
|
if not version_number:
|
|
7544
|
-
# we retrieve the latest version - using V1 REST API. V2 has issues
|
|
7545
|
-
# request_url = self.config()["nodesUrlv2"] + "/" + str(node_id) + "/content"
|
|
7566
|
+
# we retrieve the latest version - using V1 REST API. V2 has issues with downloading files:
|
|
7546
7567
|
request_url = self.config()["nodesUrl"] + "/" + str(node_id) + "/content"
|
|
7547
7568
|
self.logger.debug(
|
|
7548
7569
|
"Download document with node ID -> %d (latest version); calling -> %s",
|
|
@@ -7550,10 +7571,7 @@ class OTCS:
|
|
|
7550
7571
|
request_url,
|
|
7551
7572
|
)
|
|
7552
7573
|
else:
|
|
7553
|
-
# we retrieve the given version - using V1 REST API. V2 has issues
|
|
7554
|
-
# request_url = (
|
|
7555
|
-
# self.config()["nodesUrlv2"] + "/" + str(node_id) + "/versions/" + str(version_number) + "/content"
|
|
7556
|
-
# )
|
|
7574
|
+
# we retrieve the given version - using V1 REST API. V2 has issues with downloading files:
|
|
7557
7575
|
request_url = (
|
|
7558
7576
|
self.config()["nodesUrl"] + "/" + str(node_id) + "/versions/" + str(version_number) + "/content"
|
|
7559
7577
|
)
|
|
@@ -7585,6 +7603,14 @@ class OTCS:
|
|
|
7585
7603
|
content_encoding = response.headers.get("Content-Encoding", "").lower()
|
|
7586
7604
|
is_compressed = content_encoding in ("gzip", "deflate", "br")
|
|
7587
7605
|
|
|
7606
|
+
self.logger.debug(
|
|
7607
|
+
"Downloading document with node ID -> %d to file -> '%s'; total size -> %s bytes; content encoding -> '%s'",
|
|
7608
|
+
node_id,
|
|
7609
|
+
file_path,
|
|
7610
|
+
total_size,
|
|
7611
|
+
content_encoding,
|
|
7612
|
+
)
|
|
7613
|
+
|
|
7588
7614
|
if os.path.exists(file_path) and not overwrite:
|
|
7589
7615
|
self.logger.warning(
|
|
7590
7616
|
"File -> '%s' already exists and overwrite is set to False, not downloading document.",
|
|
@@ -7617,6 +7643,9 @@ class OTCS:
|
|
|
7617
7643
|
)
|
|
7618
7644
|
return False
|
|
7619
7645
|
|
|
7646
|
+
# if we have a total size and the content is not compressed
|
|
7647
|
+
# we can do a sanity check if the downloaded size matches
|
|
7648
|
+
# the expected size:
|
|
7620
7649
|
if total_size and not is_compressed and bytes_downloaded != total_size:
|
|
7621
7650
|
self.logger.error(
|
|
7622
7651
|
"Downloaded size (%d bytes) does not match expected size (%d bytes) for file -> '%s'",
|
|
@@ -9458,7 +9487,7 @@ class OTCS:
|
|
|
9458
9487
|
expand_workspace_info: bool = True,
|
|
9459
9488
|
expand_templates: bool = True,
|
|
9460
9489
|
) -> dict | None:
|
|
9461
|
-
"""Get all workspace types configured in
|
|
9490
|
+
"""Get all workspace types configured in OTCS.
|
|
9462
9491
|
|
|
9463
9492
|
This REST API is very limited. It does not return all workspace type properties
|
|
9464
9493
|
you can see in OTCS business admin page.
|
|
@@ -9639,11 +9668,11 @@ class OTCS:
|
|
|
9639
9668
|
|
|
9640
9669
|
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="get_workspace_type_name")
|
|
9641
9670
|
def get_workspace_type_name(self, type_id: int) -> str | None:
|
|
9642
|
-
"""Get the name of a workspace type based on the provided type ID.
|
|
9671
|
+
"""Get the name of a workspace type based on the provided workspace type ID.
|
|
9643
9672
|
|
|
9644
|
-
The name is taken from a OTCS
|
|
9673
|
+
The name is taken from a OTCS object variable self._workspace_type_lookup if recorded there.
|
|
9645
9674
|
If not yet derived it is determined via the REST API and then stored
|
|
9646
|
-
in
|
|
9675
|
+
in self._workspace_type_lookup (as a lookup cache).
|
|
9647
9676
|
|
|
9648
9677
|
Args:
|
|
9649
9678
|
type_id (int):
|
|
@@ -9654,6 +9683,10 @@ class OTCS:
|
|
|
9654
9683
|
The name of the workspace type. Or None if the type ID
|
|
9655
9684
|
was ot found.
|
|
9656
9685
|
|
|
9686
|
+
Side effects:
|
|
9687
|
+
Caches the workspace type name in self._workspace_type_lookup
|
|
9688
|
+
for future calls.
|
|
9689
|
+
|
|
9657
9690
|
"""
|
|
9658
9691
|
|
|
9659
9692
|
workspace_type = self._workspace_type_lookup.get(type_id)
|
|
@@ -9663,6 +9696,7 @@ class OTCS:
|
|
|
9663
9696
|
workspace_type = self.get_workspace_type(type_id=type_id)
|
|
9664
9697
|
type_name = workspace_type.get("workspace_type")
|
|
9665
9698
|
if type_name:
|
|
9699
|
+
# Update the lookup cache:
|
|
9666
9700
|
self._workspace_type_lookup[type_id] = {"location": None, "name": type_name}
|
|
9667
9701
|
return type_name
|
|
9668
9702
|
|
|
@@ -9670,6 +9704,43 @@ class OTCS:
|
|
|
9670
9704
|
|
|
9671
9705
|
# end method definition
|
|
9672
9706
|
|
|
9707
|
+
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="get_workspace_type_by_name")
|
|
9708
|
+
def get_workspace_type_names(self, lower_case: bool = False, renew: bool = False) -> list[str] | None:
|
|
9709
|
+
"""Get a list of all workspace type names.
|
|
9710
|
+
|
|
9711
|
+
Args:
|
|
9712
|
+
lower_case (bool):
|
|
9713
|
+
Whether to return the names in lower case.
|
|
9714
|
+
renew (bool):
|
|
9715
|
+
Whether to renew the cached workspace type names.
|
|
9716
|
+
|
|
9717
|
+
Returns:
|
|
9718
|
+
list[str] | None:
|
|
9719
|
+
List of workspace type names or None if the request fails.
|
|
9720
|
+
|
|
9721
|
+
Side effects:
|
|
9722
|
+
Caches the workspace type names in self._workspace_type_names
|
|
9723
|
+
for future calls.
|
|
9724
|
+
|
|
9725
|
+
"""
|
|
9726
|
+
|
|
9727
|
+
if self._workspace_type_names and not renew:
|
|
9728
|
+
return self._workspace_type_names
|
|
9729
|
+
|
|
9730
|
+
workspace_types = self.get_workspace_types_iterator()
|
|
9731
|
+
workspace_type_names = [
|
|
9732
|
+
self.get_result_value(response=workspace_type, key="wksp_type_name") for workspace_type in workspace_types
|
|
9733
|
+
]
|
|
9734
|
+
if lower_case:
|
|
9735
|
+
workspace_type_names = [name.lower() for name in workspace_type_names]
|
|
9736
|
+
|
|
9737
|
+
# Update the cache:
|
|
9738
|
+
self._workspace_type_names = workspace_type_names
|
|
9739
|
+
|
|
9740
|
+
return workspace_type_names
|
|
9741
|
+
|
|
9742
|
+
# end method definition
|
|
9743
|
+
|
|
9673
9744
|
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="get_workspace_templates")
|
|
9674
9745
|
def get_workspace_templates(
|
|
9675
9746
|
self, type_id: int | None = None, type_name: str | None = None
|
|
@@ -13998,6 +14069,7 @@ class OTCS:
|
|
|
13998
14069
|
apply_action: str = "add_upgrade",
|
|
13999
14070
|
add_version: bool = False,
|
|
14000
14071
|
clear_existing_categories: bool = False,
|
|
14072
|
+
attribute_values: dict | None = None,
|
|
14001
14073
|
) -> bool:
|
|
14002
14074
|
"""Assign a category to a Content Server node.
|
|
14003
14075
|
|
|
@@ -14005,6 +14077,7 @@ class OTCS:
|
|
|
14005
14077
|
(if node_id is a container / folder / workspace).
|
|
14006
14078
|
If the category is already assigned to the node this method will
|
|
14007
14079
|
throw an error.
|
|
14080
|
+
Optionally set category attributes values.
|
|
14008
14081
|
|
|
14009
14082
|
Args:
|
|
14010
14083
|
node_id (int):
|
|
@@ -14025,6 +14098,9 @@ class OTCS:
|
|
|
14025
14098
|
True, if a document version should be added for the category change (default = False).
|
|
14026
14099
|
clear_existing_categories (bool, optional):
|
|
14027
14100
|
Defines, whether or not existing (other) categories should be removed (default = False).
|
|
14101
|
+
attribute_values (dict, optional):
|
|
14102
|
+
Dictionary containing "attribute_id":"value" pairs, to be populated during the category assignment.
|
|
14103
|
+
(In case of the category attributes being set as "Required" in xECM, providing corresponding values for those attributes will resolve inability to assign the category).
|
|
14028
14104
|
|
|
14029
14105
|
Returns:
|
|
14030
14106
|
bool:
|
|
@@ -14050,6 +14126,9 @@ class OTCS:
|
|
|
14050
14126
|
"category_id": category_id,
|
|
14051
14127
|
}
|
|
14052
14128
|
|
|
14129
|
+
if attribute_values is not None:
|
|
14130
|
+
category_post_data.update(attribute_values)
|
|
14131
|
+
|
|
14053
14132
|
self.logger.debug(
|
|
14054
14133
|
"Assign category with ID -> %d to item with ID -> %d; calling -> %s",
|
|
14055
14134
|
category_id,
|
|
@@ -17286,6 +17365,7 @@ class OTCS:
|
|
|
17286
17365
|
|
|
17287
17366
|
"""
|
|
17288
17367
|
|
|
17368
|
+
# If no sub-process ID is given, use the process ID:
|
|
17289
17369
|
if subprocess_id is None:
|
|
17290
17370
|
subprocess_id = process_id
|
|
17291
17371
|
|
|
@@ -17781,8 +17861,8 @@ class OTCS:
|
|
|
17781
17861
|
for subnode in subnodes:
|
|
17782
17862
|
subnode_id = self.get_result_value(response=subnode, key="id")
|
|
17783
17863
|
subnode_name = self.get_result_value(response=subnode, key="name")
|
|
17784
|
-
|
|
17785
|
-
self.logger.info("Traversing %s node -> '%s' (%s)",
|
|
17864
|
+
subnode_type_name = self.get_result_value(response=subnode, key="type_name")
|
|
17865
|
+
self.logger.info("Traversing %s node -> '%s' (%s)", subnode_type_name, subnode_name, subnode_id)
|
|
17786
17866
|
# Recursive call for current subnode:
|
|
17787
17867
|
result = self.traverse_node(
|
|
17788
17868
|
node=subnode,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyxecm
|
|
3
|
-
Version: 3.1.
|
|
3
|
+
Version: 3.1.1
|
|
4
4
|
Summary: A Python library to interact with Opentext Content Management Rest API
|
|
5
5
|
Project-URL: Homepage, https://github.com/opentext/pyxecm
|
|
6
6
|
Author-email: Kai Gatzweiler <kgatzweiler@opentext.com>, "Dr. Marc Diefenbruch" <mdiefenb@opentext.com>
|
|
@@ -3,8 +3,8 @@ pyxecm/avts.py,sha256=NEd1JdJCVmNohp_A-ZxAIMQZX5NCJFMhyKV6cdLFgTw,56960
|
|
|
3
3
|
pyxecm/coreshare.py,sha256=oz5SASlOC_e4-OIazFUrIA7gB6DfpC7RY4Ck7AHKcvQ,95999
|
|
4
4
|
pyxecm/otac.py,sha256=itsxIkIhIHdAiRCLRegmxCejE3kqpWxgwaJvAGNqzzg,22849
|
|
5
5
|
pyxecm/otawp.py,sha256=9kQghTIrnHP5I_GOmnAlvPhJk433b0dcdVzQHx0fSAM,112962
|
|
6
|
-
pyxecm/otca.py,sha256=
|
|
7
|
-
pyxecm/otcs.py,sha256=
|
|
6
|
+
pyxecm/otca.py,sha256=_HJG84yFr00CvdgMlvTnmAYNs-6w1APbzUzZb5udkuk,99597
|
|
7
|
+
pyxecm/otcs.py,sha256=5YPdvkl8CBDMRNjVOkUDRce-GnBxaVGF98mAGakrvSM,819480
|
|
8
8
|
pyxecm/otds.py,sha256=gTyHwCSFVJbEia3h-xcPY0UL1fS58VxuI0l9uA3lC6c,191581
|
|
9
9
|
pyxecm/otiv.py,sha256=I5lt4sz7TN3W7BCstCKQY2WQnei-t0tXdM4QjRQRmWI,2358
|
|
10
10
|
pyxecm/otkd.py,sha256=3c37FwQ-KDBN48O1wZVxX46BiQDRtb2-KrXZ7HG54Fg,47488
|
|
@@ -27,7 +27,7 @@ pyxecm_api/auth/functions.py,sha256=hdu4XPS975r8LsbH_3OKNQOEMvnbD40zhRh1nO1hIyQ,
|
|
|
27
27
|
pyxecm_api/auth/models.py,sha256=lKebaIHbALZ10quCCKQ3wf7w8V6k84tFXcPV1zbQsS0,271
|
|
28
28
|
pyxecm_api/auth/router.py,sha256=H58owZMM9lcskUITWSJPZYQr6IM66MyV7h9G36HPpQM,2140
|
|
29
29
|
pyxecm_api/common/__init__.py,sha256=ranS8DEliiC4Mlo-bVva9Maj5q08I0I6NJflUFIvOvw,19
|
|
30
|
-
pyxecm_api/common/functions.py,sha256
|
|
30
|
+
pyxecm_api/common/functions.py,sha256=snu6bFNvky4JUCRbWdeE64wUjIH2NWVihXbf9nfTpsQ,5209
|
|
31
31
|
pyxecm_api/common/metrics.py,sha256=kOJ1DZveZJ7xFd1pB12Zbkq6Z-evaTivpgO_ujVbsxM,2707
|
|
32
32
|
pyxecm_api/common/models.py,sha256=c76ysWUt40A875DirsFMXttxwjHvBYvjuOVEXePSZ9k,456
|
|
33
33
|
pyxecm_api/common/router.py,sha256=BXi3SMvkswCIftoVkJIP_Wx8gd3WKJsE3rq6Pult50I,3482
|
|
@@ -54,15 +54,15 @@ pyxecm_api/v1_payload/models.py,sha256=eD9A2K23L_cGhBDTO1FGVGJMQ1COaYWmcr-ELE66t
|
|
|
54
54
|
pyxecm_api/v1_payload/router.py,sha256=2twiLBeNNDGIEQ6SCZwLFgd8oYOzgbskS-Q3OL-lKe4,15441
|
|
55
55
|
pyxecm_customizer/__init__.py,sha256=x2NhDlNcubRC-jXzqT02j9kQGXBo36QAehjmcQuSbXw,721
|
|
56
56
|
pyxecm_customizer/__main__.py,sha256=QGQlJJAdLmJccArwPT8XEhBOMSJ-Z8gwLrtHPNfL8Ps,1407
|
|
57
|
-
pyxecm_customizer/browser_automation.py,sha256=
|
|
57
|
+
pyxecm_customizer/browser_automation.py,sha256=gWYJVkVMOer7LyQNq_lwyCb0-Ixaf3VaWM7WrpqKMKY,71360
|
|
58
58
|
pyxecm_customizer/customizer.py,sha256=Nzz3wndPp9r5GVpOzLtdNUOuIsCZ2GYdIS1HhWdboBM,84938
|
|
59
59
|
pyxecm_customizer/exceptions.py,sha256=YXX0in-UGXfJb6Fei01JQetOtAHqUiITb49OTSaZRkE,909
|
|
60
|
-
pyxecm_customizer/guidewire.py,sha256=
|
|
60
|
+
pyxecm_customizer/guidewire.py,sha256=u_mGZU5Nedm-cGf5hUCWE38U-75qoqwi7cq-G3qHty0,67167
|
|
61
61
|
pyxecm_customizer/k8s.py,sha256=DcxLKKuvV3S4DYqP4jUygn32aeGtD0c1FOXumZzqqdY,55837
|
|
62
|
-
pyxecm_customizer/knowledge_graph.py,sha256=
|
|
62
|
+
pyxecm_customizer/knowledge_graph.py,sha256=Jdm-WVftunoLztinWSPUNhRZ8DhRAVIEcQJJz57opGk,46429
|
|
63
63
|
pyxecm_customizer/log.py,sha256=2DmGF3b-ZIOAJCPSmZmxGD7BNxY4-mZm0H_X0HeJedE,916
|
|
64
64
|
pyxecm_customizer/m365.py,sha256=xQGdY9ti44e93c5tPbgCHjvrM-SjQr2F1iApJMb0-Pk,213957
|
|
65
|
-
pyxecm_customizer/payload.py,sha256=
|
|
65
|
+
pyxecm_customizer/payload.py,sha256=bMyXLMBlio1WuLgKfy9z8LICxFSKg05JDy8qrxRn6M4,1366630
|
|
66
66
|
pyxecm_customizer/payload_list.py,sha256=MCpCzarmQ-5u7FSW7djNOMSjv373Ltx-9rEDIuhEboI,28266
|
|
67
67
|
pyxecm_customizer/salesforce.py,sha256=lGkQcEYg5YIWc7SF2TELT6yfgrn4pnbCrf8wXGk-PMc,64557
|
|
68
68
|
pyxecm_customizer/sap.py,sha256=lD_riOZhYjbZ0_pUZyqhxP6guzBM__TcUjZhSgDowoE,6506
|
|
@@ -76,7 +76,7 @@ pyxecm_maintenance_page/app.py,sha256=pTOeZfgPPq6BT7P8naUjW-ZT9dXqwX6DWazIVL-9Fk
|
|
|
76
76
|
pyxecm_maintenance_page/settings.py,sha256=VRReZeNdza7i7lgnQ3wVojzoPDGXZnzr5rsMJY1EnHk,955
|
|
77
77
|
pyxecm_maintenance_page/static/favicon.avif,sha256=POuuPXKbjHVP3BjNLpFIx8MfkQg5z2LZA7sK6lejARg,1543
|
|
78
78
|
pyxecm_maintenance_page/templates/maintenance.html,sha256=0OAinv7jmj3Aa7GNCIoBLDGEMW1-_HdJfwWmkmb6Cs4,5581
|
|
79
|
-
pyxecm-3.1.
|
|
80
|
-
pyxecm-3.1.
|
|
81
|
-
pyxecm-3.1.
|
|
82
|
-
pyxecm-3.1.
|
|
79
|
+
pyxecm-3.1.1.dist-info/METADATA,sha256=9ZjC6c_J6K4UBCgohLC_Fz9THffRsXPPq41fPl5Wpoo,4566
|
|
80
|
+
pyxecm-3.1.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
81
|
+
pyxecm-3.1.1.dist-info/entry_points.txt,sha256=prc1mDdpd3bQk98VRBozI363mDUgSwDibDKXGNqKqgI,151
|
|
82
|
+
pyxecm-3.1.1.dist-info/RECORD,,
|
pyxecm_api/common/functions.py
CHANGED
|
@@ -2,15 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
|
-
import time
|
|
6
|
-
from datetime import UTC, datetime
|
|
7
5
|
from typing import Annotated
|
|
8
6
|
|
|
9
7
|
from fastapi import Depends
|
|
10
8
|
from pyxecm.otca import OTCA
|
|
11
9
|
from pyxecm.otcs import OTCS
|
|
12
10
|
from pyxecm_customizer import K8s, PayloadList, Settings
|
|
13
|
-
from pyxecm_customizer.knowledge_graph import KnowledgeGraph
|
|
14
11
|
|
|
15
12
|
from pyxecm_api.auth.functions import get_otcsticket
|
|
16
13
|
from pyxecm_api.settings import CustomizerAPISettings, api_settings
|
|
@@ -22,100 +19,6 @@ LOGS_LOCK = {}
|
|
|
22
19
|
# Initialize the globel Payloadlist object
|
|
23
20
|
PAYLOAD_LIST = PayloadList(logger=logger)
|
|
24
21
|
|
|
25
|
-
# This object is initialized in the build_graph() function below.
|
|
26
|
-
KNOWLEDGEGRAPH_OBJECT: KnowledgeGraph = None
|
|
27
|
-
|
|
28
|
-
# The following ontology is fed into the knowledge graph tool description.
|
|
29
|
-
# This is currently hard-coded. Ideally this should be derived from OTCM
|
|
30
|
-
# or provided via a payload file:
|
|
31
|
-
|
|
32
|
-
KNOWLEDGEGRAPH_ONTOLOGY = {
|
|
33
|
-
("Vendor", "Material", "child"): ["offers", "supplies", "provides"],
|
|
34
|
-
("Vendor", "Purchase Order", "child"): ["supplies", "provides"],
|
|
35
|
-
("Vendor", "Purchase Contract", "child"): ["signs", "owns"],
|
|
36
|
-
("Material", "Vendor", "parent"): ["is supplied by"],
|
|
37
|
-
("Purchase Order", "Material", "child"): ["includes", "is part of"],
|
|
38
|
-
("Customer", "Sales Order", "child"): ["has ordered"],
|
|
39
|
-
("Customer", "Sales Contract", "child"): ["signs", "owns"],
|
|
40
|
-
("Sales Order", "Customer", "parent"): ["belongs to", "is initiated by"],
|
|
41
|
-
("Sales Order", "Material", "child"): ["includes", "consists of"],
|
|
42
|
-
("Sales Order", "Delivery", "child"): ["triggers", "is followed by"],
|
|
43
|
-
("Sales Order", "Production Order", "child"): ["triggers", "is followed by"],
|
|
44
|
-
("Sales Contract", "Material", "child"): ["includes", "consists of"],
|
|
45
|
-
("Production Order", "Material", "child"): ["includes", "consists of"],
|
|
46
|
-
("Production Order", "Delivery", "child"): ["triggers", "is followed by"],
|
|
47
|
-
("Production Order", "Goods Movement", "child"): ["triggers", "is followed by"],
|
|
48
|
-
("Delivery", "Goods Movement", "child"): ["triggers", "is followed by"],
|
|
49
|
-
("Delivery", "Material", "child"): ["triggers", "is followed by"],
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
### Functions
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def get_ontology() -> dict:
|
|
57
|
-
"""Get the ontology for the knowledge graph.
|
|
58
|
-
|
|
59
|
-
Returns:
|
|
60
|
-
dict: The ontology as a dictionary.
|
|
61
|
-
|
|
62
|
-
"""
|
|
63
|
-
|
|
64
|
-
return KNOWLEDGEGRAPH_ONTOLOGY
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def get_knowledgegraph_object() -> KnowledgeGraph:
|
|
68
|
-
"""Get the Knowledge Graph object."""
|
|
69
|
-
|
|
70
|
-
global KNOWLEDGEGRAPH_OBJECT # noqa: PLW0603
|
|
71
|
-
|
|
72
|
-
if KNOWLEDGEGRAPH_OBJECT is None:
|
|
73
|
-
KNOWLEDGEGRAPH_OBJECT = KnowledgeGraph(otcs_object=get_otcs_object(), ontology=KNOWLEDGEGRAPH_ONTOLOGY)
|
|
74
|
-
|
|
75
|
-
return KNOWLEDGEGRAPH_OBJECT
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def build_graph() -> None:
|
|
79
|
-
"""Build the knowledge Graph. And keep it updated every hour."""
|
|
80
|
-
|
|
81
|
-
def build() -> None:
|
|
82
|
-
"""Build the knowledge graph once."""
|
|
83
|
-
|
|
84
|
-
logger.info("Starting knowledge graph build...")
|
|
85
|
-
start_time = datetime.now(UTC)
|
|
86
|
-
result = get_knowledgegraph_object().build_graph(
|
|
87
|
-
workspace_type_exclusions=None,
|
|
88
|
-
workspace_type_inclusions=[
|
|
89
|
-
"Vendor",
|
|
90
|
-
"Purchase Contract",
|
|
91
|
-
"Purchase Order",
|
|
92
|
-
"Material",
|
|
93
|
-
"Customer",
|
|
94
|
-
"Sales Order",
|
|
95
|
-
"Sales Contract",
|
|
96
|
-
"Delivery",
|
|
97
|
-
"Goods Movement",
|
|
98
|
-
],
|
|
99
|
-
workers=20, # for multi-threaded traversal
|
|
100
|
-
filter_at_traversal=True, # also filter for workspace types if following relationships
|
|
101
|
-
relationship_types=["child"], # only go from parent to child
|
|
102
|
-
strategy="BFS", # Breadth-First-Search
|
|
103
|
-
metadata=True, # don't include workspace metadata
|
|
104
|
-
)
|
|
105
|
-
end_time = datetime.now(UTC)
|
|
106
|
-
logger.info(
|
|
107
|
-
"Knowledge graph completed in %s. Processed %d workspace nodes and traversed %d workspace relationships.",
|
|
108
|
-
str(end_time - start_time),
|
|
109
|
-
result["processed"],
|
|
110
|
-
result["traversed"],
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
# Endless loop to build knowledge graph and update it every hour:
|
|
114
|
-
while True:
|
|
115
|
-
build()
|
|
116
|
-
logger.info("Waiting for 1 hour before rebuilding the knowledge graph...")
|
|
117
|
-
time.sleep(3600)
|
|
118
|
-
|
|
119
22
|
|
|
120
23
|
def get_k8s_object() -> K8s:
|
|
121
24
|
"""Get an instance of a K8s object.
|
|
@@ -98,6 +98,12 @@ REQUEST_MAX_RETRIES = 3
|
|
|
98
98
|
class BrowserAutomation:
|
|
99
99
|
"""Class to automate settings via a browser interface."""
|
|
100
100
|
|
|
101
|
+
page: Page = None
|
|
102
|
+
browser: Browser = None
|
|
103
|
+
context: BrowserContext = None
|
|
104
|
+
playwright = None
|
|
105
|
+
proxy = None
|
|
106
|
+
|
|
101
107
|
logger: logging.Logger = default_logger
|
|
102
108
|
|
|
103
109
|
def __init__(
|
|
@@ -186,7 +192,6 @@ class BrowserAutomation:
|
|
|
186
192
|
if self.take_screenshots and not os.path.exists(self.screenshot_directory):
|
|
187
193
|
os.makedirs(self.screenshot_directory)
|
|
188
194
|
|
|
189
|
-
self.proxy = None
|
|
190
195
|
if os.getenv("HTTP_PROXY"):
|
|
191
196
|
self.proxy = {
|
|
192
197
|
"server": os.getenv("HTTP_PROXY"),
|
|
@@ -197,8 +202,9 @@ class BrowserAutomation:
|
|
|
197
202
|
self.logger.info("Using browser -> '%s'...", browser)
|
|
198
203
|
|
|
199
204
|
if not self.setup_playwright(browser=browser):
|
|
200
|
-
|
|
201
|
-
|
|
205
|
+
msg = "Failed to initialize Playwright browser automation!"
|
|
206
|
+
self.logger.error(msg)
|
|
207
|
+
raise RuntimeError(msg)
|
|
202
208
|
|
|
203
209
|
self.logger.info("Creating browser context...")
|
|
204
210
|
self.context: BrowserContext = self.browser.new_context(
|
|
@@ -378,19 +384,21 @@ class BrowserAutomation:
|
|
|
378
384
|
|
|
379
385
|
# end method definition
|
|
380
386
|
|
|
381
|
-
def take_screenshot(self) -> bool:
|
|
387
|
+
def take_screenshot(self, suffix: str = "") -> bool:
|
|
382
388
|
"""Take a screenshot of the current browser window and save it as PNG file.
|
|
383
389
|
|
|
390
|
+
Args:
|
|
391
|
+
suffix (str, optional):
|
|
392
|
+
Optional suffix to append to the screenshot filename.
|
|
393
|
+
|
|
384
394
|
Returns:
|
|
385
395
|
bool:
|
|
386
396
|
True if successful, False otherwise
|
|
387
397
|
|
|
388
398
|
"""
|
|
389
399
|
|
|
390
|
-
screenshot_file = "{}/{}-{:02d}.png".format(
|
|
391
|
-
self.screenshot_directory,
|
|
392
|
-
self.screenshot_names,
|
|
393
|
-
self.screenshot_counter,
|
|
400
|
+
screenshot_file = "{}/{}-{:02d}{}.png".format(
|
|
401
|
+
self.screenshot_directory, self.screenshot_names, self.screenshot_counter, suffix
|
|
394
402
|
)
|
|
395
403
|
self.logger.debug("Save browser screenshot to -> %s", screenshot_file)
|
|
396
404
|
|
|
@@ -681,6 +689,12 @@ class BrowserAutomation:
|
|
|
681
689
|
wait_state (str, optional):
|
|
682
690
|
Defines if we wait for attached (element is part of DOM) or
|
|
683
691
|
if we wait for elem to be visible (attached, displayed, and has non-zero size).
|
|
692
|
+
Possible values are:
|
|
693
|
+
* "attached" - the element is present in the DOM.
|
|
694
|
+
* "detached" - the element is not present in the DOM.
|
|
695
|
+
* "visible" - the element is visible (attached, displayed, and has non-zero size).
|
|
696
|
+
* "hidden" - the element is hidden (attached, but not displayed).
|
|
697
|
+
Default is "visible".
|
|
684
698
|
exact_match (bool | None, optional):
|
|
685
699
|
If an exact matching is required. Default is None (not set).
|
|
686
700
|
regex (bool, optional):
|
|
@@ -705,14 +719,17 @@ class BrowserAutomation:
|
|
|
705
719
|
|
|
706
720
|
"""
|
|
707
721
|
|
|
708
|
-
failure_message = "Cannot find page element with selector -> '{}' ({}){}{}{}".format(
|
|
722
|
+
failure_message = "Cannot find {} page element with selector -> '{}' ({}){}{}{}{}".format(
|
|
723
|
+
"occurence #{} of".format(occurrence) if occurrence > 1 else "any",
|
|
709
724
|
selector,
|
|
710
725
|
selector_type,
|
|
711
726
|
" and role type -> '{}'".format(role_type) if role_type else "",
|
|
712
727
|
" in iframe -> '{}'".format(iframe) if iframe else "",
|
|
713
728
|
", occurrence -> {}".format(occurrence) if occurrence > 1 else "",
|
|
729
|
+
", waiting for state -> '{}'".format(wait_state),
|
|
714
730
|
)
|
|
715
|
-
success_message = "Found page element with selector -> '{}' ('{}'){}{}{}".format(
|
|
731
|
+
success_message = "Found {} page element with selector -> '{}' ('{}'){}{}{}".format(
|
|
732
|
+
"occurence #{} of".format(occurrence) if occurrence > 1 else "a",
|
|
716
733
|
selector,
|
|
717
734
|
selector_type,
|
|
718
735
|
" and role type -> '{}'".format(role_type) if role_type else "",
|
|
@@ -811,6 +828,7 @@ class BrowserAutomation:
|
|
|
811
828
|
is_page_close_trigger: bool = False,
|
|
812
829
|
wait_until: str | None = None,
|
|
813
830
|
wait_time: float = 0.0,
|
|
831
|
+
wait_state: str = "visible",
|
|
814
832
|
exact_match: bool | None = None,
|
|
815
833
|
regex: bool = False,
|
|
816
834
|
hover_only: bool = False,
|
|
@@ -858,8 +876,17 @@ class BrowserAutomation:
|
|
|
858
876
|
This seems to be the safest one for OpenText Content Server.
|
|
859
877
|
* "domcontentloaded" - waits for the DOMContentLoaded event (HTML is parsed,
|
|
860
878
|
but subresources may still load).
|
|
861
|
-
wait_time (float):
|
|
862
|
-
Time in seconds to wait for elements to appear.
|
|
879
|
+
wait_time (float, optional):
|
|
880
|
+
Time in seconds to wait for elements to appear. Default is 0.0 (no wait).
|
|
881
|
+
wait_state (str, optional):
|
|
882
|
+
Defines if we wait for attached (element is part of DOM) or
|
|
883
|
+
if we wait for elem to be visible (attached, displayed, and has non-zero size).
|
|
884
|
+
Possible values are:
|
|
885
|
+
* "attached" - the element is present in the DOM.
|
|
886
|
+
* "detached" - the element is not present in the DOM.
|
|
887
|
+
* "visible" - the element is visible (attached, displayed, and has non-zero size).
|
|
888
|
+
* "hidden" - the element is hidden (attached, but not displayed).
|
|
889
|
+
Default is "visible".
|
|
863
890
|
exact_match (bool | None, optional):
|
|
864
891
|
If an exact matching is required. Default is None (not set).
|
|
865
892
|
regex (bool, optional):
|
|
@@ -896,6 +923,14 @@ class BrowserAutomation:
|
|
|
896
923
|
|
|
897
924
|
"""
|
|
898
925
|
|
|
926
|
+
if not selector:
|
|
927
|
+
failure_message = "Missing element selector! Cannot find page element!"
|
|
928
|
+
if show_error:
|
|
929
|
+
self.logger.error(failure_message)
|
|
930
|
+
else:
|
|
931
|
+
self.logger.warning(failure_message)
|
|
932
|
+
return False
|
|
933
|
+
|
|
899
934
|
success = True # Final return value
|
|
900
935
|
|
|
901
936
|
# If no specific wait until strategy is provided in the
|
|
@@ -909,18 +944,14 @@ class BrowserAutomation:
|
|
|
909
944
|
self.logger.info("Wait for %d milliseconds before clicking...", wait_time * 1000)
|
|
910
945
|
self.page.wait_for_timeout(wait_time * 1000)
|
|
911
946
|
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
if show_error:
|
|
915
|
-
self.logger.error(failure_message)
|
|
916
|
-
else:
|
|
917
|
-
self.logger.warning(failure_message)
|
|
918
|
-
return False
|
|
947
|
+
if self.take_screenshots:
|
|
948
|
+
self.take_screenshot(suffix="_wait_before_click")
|
|
919
949
|
|
|
920
950
|
elem = self.find_elem(
|
|
921
951
|
selector=selector,
|
|
922
952
|
selector_type=selector_type,
|
|
923
953
|
role_type=role_type,
|
|
954
|
+
wait_state=wait_state,
|
|
924
955
|
exact_match=exact_match,
|
|
925
956
|
regex=regex,
|
|
926
957
|
occurrence=occurrence,
|
|
@@ -1222,7 +1253,7 @@ class BrowserAutomation:
|
|
|
1222
1253
|
self.logger.error("Download failed; error -> %s", str(e))
|
|
1223
1254
|
return None
|
|
1224
1255
|
|
|
1225
|
-
self.logger.info("
|
|
1256
|
+
self.logger.info("Downloaded file to -> %s", save_path)
|
|
1226
1257
|
|
|
1227
1258
|
return save_path
|
|
1228
1259
|
|
|
@@ -1273,13 +1304,13 @@ class BrowserAutomation:
|
|
|
1273
1304
|
Is the element in an iFrame? Then provide the name of the iframe with this parameter.
|
|
1274
1305
|
min_count (int):
|
|
1275
1306
|
Minimum number of required matches (# elements on page).
|
|
1276
|
-
wait_time (float):
|
|
1277
|
-
Time in seconds to wait for elements to appear.
|
|
1307
|
+
wait_time (float, optional):
|
|
1308
|
+
Time in seconds to wait for elements to appear. Default is 0.0 (no wait).
|
|
1278
1309
|
wait_state (str, optional):
|
|
1279
1310
|
Defines if we wait for attached (element is part of DOM) or
|
|
1280
1311
|
if we wait for elem to be visible (attached, displayed, and has non-zero size).
|
|
1281
|
-
show_error (bool):
|
|
1282
|
-
Whether to log warnings/errors.
|
|
1312
|
+
show_error (bool, optional):
|
|
1313
|
+
Whether to log warnings/errors. Default is True.
|
|
1283
1314
|
|
|
1284
1315
|
Returns:
|
|
1285
1316
|
bool | None:
|
|
@@ -1306,10 +1337,9 @@ class BrowserAutomation:
|
|
|
1306
1337
|
iframe=iframe,
|
|
1307
1338
|
)
|
|
1308
1339
|
if not locator:
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
)
|
|
1340
|
+
self.logger.error(
|
|
1341
|
+
"Failed to check if elements -> '%s' (%s) exist! Locator is undefined.", selector, selector_type
|
|
1342
|
+
)
|
|
1313
1343
|
return (None, 0)
|
|
1314
1344
|
|
|
1315
1345
|
self.logger.info(
|
|
@@ -1369,8 +1399,9 @@ class BrowserAutomation:
|
|
|
1369
1399
|
return (None, 0)
|
|
1370
1400
|
|
|
1371
1401
|
self.logger.info(
|
|
1372
|
-
"Found %s
|
|
1402
|
+
"Found %d element%s matching selector -> '%s' (%s%s).",
|
|
1373
1403
|
count,
|
|
1404
|
+
"s" if count > 1 else "",
|
|
1374
1405
|
selector,
|
|
1375
1406
|
"selector type -> '{}'".format(selector_type),
|
|
1376
1407
|
", role type -> '{}'".format(role_type) if role_type else "",
|
|
@@ -1386,7 +1417,7 @@ class BrowserAutomation:
|
|
|
1386
1417
|
|
|
1387
1418
|
matching_elems = []
|
|
1388
1419
|
|
|
1389
|
-
# Iterate over all elements found by the locator and
|
|
1420
|
+
# Iterate over all elements found by the locator and check if
|
|
1390
1421
|
# they comply with the additional value conditions (if provided).
|
|
1391
1422
|
# We collect all matching elements in a list:
|
|
1392
1423
|
for i in range(count):
|
|
@@ -1424,8 +1455,9 @@ class BrowserAutomation:
|
|
|
1424
1455
|
else:
|
|
1425
1456
|
success = True
|
|
1426
1457
|
self.logger.info(
|
|
1427
|
-
"Found %d matching
|
|
1458
|
+
"Found %d matching element%s.%s",
|
|
1428
1459
|
matching_elements_count,
|
|
1460
|
+
"s" if matching_elements_count > 1 else "",
|
|
1429
1461
|
" This is {} the minimum {} element{} probed for.".format(
|
|
1430
1462
|
"exactly" if matching_elements_count == min_count else "more than",
|
|
1431
1463
|
min_count,
|
|
@@ -1542,9 +1574,9 @@ class BrowserAutomation:
|
|
|
1542
1574
|
|
|
1543
1575
|
"""
|
|
1544
1576
|
|
|
1545
|
-
self.logger.debug("Setting default timeout to ->
|
|
1577
|
+
self.logger.debug("Setting default timeout to -> %.2f seconds...", wait_time)
|
|
1546
1578
|
self.page.set_default_timeout(wait_time * 1000)
|
|
1547
|
-
self.logger.debug("Setting navigation timeout to ->
|
|
1579
|
+
self.logger.debug("Setting navigation timeout to -> %.2f seconds...", wait_time)
|
|
1548
1580
|
self.page.set_default_navigation_timeout(wait_time * 1000)
|
|
1549
1581
|
|
|
1550
1582
|
# end method definition
|
pyxecm_customizer/guidewire.py
CHANGED
|
@@ -358,44 +358,44 @@ class Guidewire:
|
|
|
358
358
|
index: int = 0,
|
|
359
359
|
show_error: bool = True,
|
|
360
360
|
) -> str | None:
|
|
361
|
-
"""Read an item value from the REST API response.
|
|
361
|
+
"""Read an item value from the Guidewire REST API response.
|
|
362
362
|
|
|
363
363
|
Args:
|
|
364
364
|
response (dict):
|
|
365
|
-
REST API response object.
|
|
365
|
+
Guidewire REST API response object.
|
|
366
366
|
key (str):
|
|
367
367
|
Key to find (e.g., "id", "name").
|
|
368
368
|
index (int, optional):
|
|
369
369
|
Index to use if a list of results is delivered (1st element has index 0).
|
|
370
370
|
Defaults to 0.
|
|
371
371
|
show_error (bool, optional):
|
|
372
|
-
Whether an error or just a warning should be logged.
|
|
372
|
+
Whether an error or just a warning should be logged. Defaults to True.
|
|
373
373
|
|
|
374
374
|
Returns:
|
|
375
|
-
str:
|
|
375
|
+
str | None:
|
|
376
376
|
Value of the item with the given key, or None if no value is found.
|
|
377
377
|
|
|
378
378
|
"""
|
|
379
379
|
|
|
380
380
|
# First do some sanity checks:
|
|
381
381
|
if not response:
|
|
382
|
-
self.logger.debug("Empty response - no results found!")
|
|
382
|
+
self.logger.debug("Empty Guidewire response - no results found!")
|
|
383
383
|
return None
|
|
384
384
|
|
|
385
385
|
# To support also iterators that yield from results,
|
|
386
|
-
# we wrap an
|
|
386
|
+
# we wrap an "attributes" element into a "data" element
|
|
387
387
|
# to make the following code work like for direct REST responses:
|
|
388
388
|
if "attributes" in response:
|
|
389
389
|
response = {"data": response}
|
|
390
390
|
|
|
391
391
|
if "data" not in response:
|
|
392
392
|
if show_error:
|
|
393
|
-
self.logger.error("No 'data' key in REST response
|
|
393
|
+
self.logger.error("No 'data' key in Guidewire REST response -> %s. Returning None.", str(response))
|
|
394
394
|
return None
|
|
395
395
|
|
|
396
396
|
results = response["data"]
|
|
397
397
|
if not results:
|
|
398
|
-
self.logger.debug("No results found! Empty data element.")
|
|
398
|
+
self.logger.debug("No results found in the Guidewire response! Empty 'data' element.")
|
|
399
399
|
return None
|
|
400
400
|
|
|
401
401
|
# check if results is a list or a dict (both is possible - iterator responses will be dict):
|
|
@@ -267,14 +267,14 @@ class KnowledgeGraph:
|
|
|
267
267
|
"""
|
|
268
268
|
|
|
269
269
|
if not self._attribute_schemas:
|
|
270
|
-
self.
|
|
270
|
+
self.build_categories()
|
|
271
271
|
|
|
272
272
|
return list(self._attribute_schemas.get(category, {}).get("attributes", {}).keys())
|
|
273
273
|
|
|
274
274
|
# end method definition
|
|
275
275
|
|
|
276
276
|
def get_category_id(self, category: str) -> str | None:
|
|
277
|
-
"""Return the
|
|
277
|
+
"""Return the category ID for a category name.
|
|
278
278
|
|
|
279
279
|
Args:
|
|
280
280
|
category (str):
|
|
@@ -288,7 +288,7 @@ class KnowledgeGraph:
|
|
|
288
288
|
def get_attribute_id(
|
|
289
289
|
self, category: str, attribute: str | None = None, set_attribute: str | None = None, row: int | None = None
|
|
290
290
|
) -> str | None:
|
|
291
|
-
"""Return the attribute
|
|
291
|
+
"""Return the attribute ID (or category ID if attribute is None).
|
|
292
292
|
|
|
293
293
|
Args:
|
|
294
294
|
category (str):
|
|
@@ -307,21 +307,18 @@ class KnowledgeGraph:
|
|
|
307
307
|
"""
|
|
308
308
|
|
|
309
309
|
if not self._attribute_schemas:
|
|
310
|
-
self.
|
|
310
|
+
self.build_categories()
|
|
311
311
|
|
|
312
312
|
# Lookup the category schema by the category name:
|
|
313
313
|
category = self._attribute_schemas.get(category, {})
|
|
314
314
|
if not category:
|
|
315
315
|
return None
|
|
316
316
|
if not attribute:
|
|
317
|
-
# If
|
|
317
|
+
# If no specific attribute is requested return the category ID:
|
|
318
318
|
return category.get("id", None)
|
|
319
319
|
attributes = category.get("attributes", {})
|
|
320
320
|
# Construct the attribute lookup key. For set attributes we use the format "Set:Attribute".
|
|
321
321
|
lookup_key = attribute if not set_attribute else f"{set_attribute}:{attribute}"
|
|
322
|
-
if lookup_key not in attributes:
|
|
323
|
-
return None
|
|
324
|
-
|
|
325
322
|
if lookup_key not in attributes:
|
|
326
323
|
self.logger.error("Attribute '%s' not found in category '%s'!", lookup_key, category.get("id", "unknown"))
|
|
327
324
|
return None
|
|
@@ -1065,8 +1062,8 @@ class KnowledgeGraph:
|
|
|
1065
1062
|
# end method definition
|
|
1066
1063
|
|
|
1067
1064
|
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="build_attribute")
|
|
1068
|
-
def
|
|
1069
|
-
"""Build a dictionary of all
|
|
1065
|
+
def build_attributes(self, node: dict, **kwargs: dict) -> tuple[bool, bool]: # noqa: ARG002
|
|
1066
|
+
"""Build a dictionary of all attributes in a given category in OTCS.
|
|
1070
1067
|
|
|
1071
1068
|
Args:
|
|
1072
1069
|
node (dict):
|
|
@@ -1101,11 +1098,11 @@ class KnowledgeGraph:
|
|
|
1101
1098
|
if success:
|
|
1102
1099
|
# Initialize the entry on the category level:
|
|
1103
1100
|
self._attribute_schemas[node_name] = {"id": node_id, "attributes": {}}
|
|
1104
|
-
personal_volume = self._otcs.get_volume(volume_type=self._otcs.VOLUME_TYPE_PERSONAL_WORKSPACE)
|
|
1105
|
-
personal_volume_id = self._otcs.get_result_value(response=personal_volume, key="id")
|
|
1106
1101
|
# Retrieve the attribute schema for the given category.
|
|
1107
|
-
# We use the
|
|
1102
|
+
# We use the Personal Volume as node_id to have a predictable
|
|
1108
1103
|
# node ID that is always available:
|
|
1104
|
+
personal_volume = self._otcs.get_volume(volume_type=self._otcs.VOLUME_TYPE_PERSONAL_WORKSPACE)
|
|
1105
|
+
personal_volume_id = self._otcs.get_result_value(response=personal_volume, key="id")
|
|
1109
1106
|
attributes = self._otcs.get_node_category_form(
|
|
1110
1107
|
node_id=personal_volume_id, category_id=node_id, operation="create"
|
|
1111
1108
|
)
|
|
@@ -1144,7 +1141,7 @@ class KnowledgeGraph:
|
|
|
1144
1141
|
# end method definition
|
|
1145
1142
|
|
|
1146
1143
|
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="build_attributes")
|
|
1147
|
-
def
|
|
1144
|
+
def build_categories(self) -> dict:
|
|
1148
1145
|
"""Build a dictionary of all attribute schemas in OTCS.
|
|
1149
1146
|
|
|
1150
1147
|
This method populates the `self._attribute_schemas` dictionary with
|
|
@@ -1186,14 +1183,14 @@ class KnowledgeGraph:
|
|
|
1186
1183
|
|
|
1187
1184
|
"""
|
|
1188
1185
|
|
|
1189
|
-
self.logger.
|
|
1186
|
+
self.logger.debug("Start building attribute data lookup...")
|
|
1190
1187
|
|
|
1191
1188
|
result = self._otcs.traverse_node(
|
|
1192
1189
|
node=self._otcs.get_volume(volume_type=self._otcs.VOLUME_TYPE_CATEGORIES_VOLUME),
|
|
1193
|
-
executables=[self.
|
|
1190
|
+
executables=[self.build_attributes],
|
|
1194
1191
|
)
|
|
1195
1192
|
|
|
1196
|
-
self.logger.
|
|
1193
|
+
self.logger.debug("Attribute data lookup completed.")
|
|
1197
1194
|
|
|
1198
1195
|
return result
|
|
1199
1196
|
|
pyxecm_customizer/payload.py
CHANGED
|
@@ -12127,7 +12127,7 @@ class Payload:
|
|
|
12127
12127
|
)
|
|
12128
12128
|
else: # BO found
|
|
12129
12129
|
self.logger.debug(
|
|
12130
|
-
"Retrieved ID -> %s for Salesforce object type -> '%s' (looking up -> '%s' in field -> '%s')",
|
|
12130
|
+
"Retrieved business object ID -> %s for Salesforce object type -> '%s' (looking up -> '%s' in field -> '%s')",
|
|
12131
12131
|
bo_id,
|
|
12132
12132
|
object_type,
|
|
12133
12133
|
search_value,
|
|
@@ -12223,7 +12223,7 @@ class Payload:
|
|
|
12223
12223
|
)
|
|
12224
12224
|
else: # BO found
|
|
12225
12225
|
self.logger.info(
|
|
12226
|
-
"Retrieved ID -> %s for Guidewire object type -> '%s' (looking up -> '%s' in field -> '%s').",
|
|
12226
|
+
"Retrieved business object ID -> %s for Guidewire object type -> '%s' (looking up -> '%s' in field -> '%s').",
|
|
12227
12227
|
bo_id,
|
|
12228
12228
|
object_type,
|
|
12229
12229
|
search_value,
|
|
@@ -13570,24 +13570,27 @@ class Payload:
|
|
|
13570
13570
|
for result in results:
|
|
13571
13571
|
if not result["success"]:
|
|
13572
13572
|
self.logger.info(
|
|
13573
|
-
"Thread -> '%s' had %s
|
|
13573
|
+
"Thread -> '%s' had %s failure%s and completed %s workspace%s successfully.",
|
|
13574
13574
|
result["thread_name"],
|
|
13575
13575
|
result["failure_counter"],
|
|
13576
|
+
"s" if result["failure_counter"] != 1 else "",
|
|
13576
13577
|
result["success_counter"],
|
|
13578
|
+
"s" if result["success_counter"] != 1 else "",
|
|
13577
13579
|
)
|
|
13578
13580
|
success = False # mark the complete processing as "failure" for the status file.
|
|
13579
13581
|
else:
|
|
13580
13582
|
self.logger.info(
|
|
13581
|
-
"Thread -> '%s' completed %s
|
|
13583
|
+
"Thread -> '%s' completed %s workspace%s successfully.",
|
|
13582
13584
|
result["thread_name"],
|
|
13583
13585
|
result["success_counter"],
|
|
13586
|
+
"s" if result["success_counter"] != 1 else "",
|
|
13584
13587
|
)
|
|
13585
13588
|
else: # no multi-threading
|
|
13586
13589
|
for workspace in self._workspaces:
|
|
13587
13590
|
try:
|
|
13588
13591
|
result = self.process_workspace(workspace=workspace)
|
|
13589
13592
|
except Exception:
|
|
13590
|
-
self.logger.
|
|
13593
|
+
self.logger.error("Failed to process workspace -> %s", workspace)
|
|
13591
13594
|
result = False
|
|
13592
13595
|
success = success and result # if a single result is False then mark this in 'success' variable.
|
|
13593
13596
|
|
|
@@ -18558,8 +18561,8 @@ class Payload:
|
|
|
18558
18561
|
node_name=document_name,
|
|
18559
18562
|
)
|
|
18560
18563
|
if response["results"]:
|
|
18561
|
-
self.logger.
|
|
18562
|
-
"
|
|
18564
|
+
self.logger.info(
|
|
18565
|
+
"Document -> '%s' does already exist in workspace folder with ID -> %s. Skipping...",
|
|
18563
18566
|
document_name,
|
|
18564
18567
|
workspace_folder_id,
|
|
18565
18568
|
)
|
|
@@ -18589,6 +18592,8 @@ class Payload:
|
|
|
18589
18592
|
workspace_id,
|
|
18590
18593
|
exec_as_user,
|
|
18591
18594
|
)
|
|
18595
|
+
# end for workspace_instance in workspace_instances:
|
|
18596
|
+
# end for doc_generator in self._doc_generators:
|
|
18592
18597
|
|
|
18593
18598
|
if authenticated_user != "admin":
|
|
18594
18599
|
# Impersonate as the admin user:
|
|
@@ -19185,18 +19190,19 @@ class Payload:
|
|
|
19185
19190
|
automation_type,
|
|
19186
19191
|
wait_until,
|
|
19187
19192
|
)
|
|
19188
|
-
|
|
19189
|
-
|
|
19190
|
-
|
|
19191
|
-
|
|
19192
|
-
|
|
19193
|
-
|
|
19194
|
-
|
|
19195
|
-
|
|
19196
|
-
|
|
19197
|
-
|
|
19198
|
-
|
|
19199
|
-
|
|
19193
|
+
try:
|
|
19194
|
+
browser_automation_object = BrowserAutomation(
|
|
19195
|
+
base_url=base_url,
|
|
19196
|
+
user_name=user_name,
|
|
19197
|
+
user_password=password,
|
|
19198
|
+
automation_name=name,
|
|
19199
|
+
take_screenshots=debug_automation,
|
|
19200
|
+
headless=browser_automation.get("headless", self._browser_headless),
|
|
19201
|
+
logger=self.logger,
|
|
19202
|
+
wait_until=wait_until,
|
|
19203
|
+
browser=browser_automation.get("browser"), # None is acceptable
|
|
19204
|
+
)
|
|
19205
|
+
except Exception:
|
|
19200
19206
|
self.logger.error(
|
|
19201
19207
|
"Cannot execute browser automation -> '%s'. Initialization of browser automation object failed!",
|
|
19202
19208
|
name,
|
|
@@ -19212,9 +19218,9 @@ class Payload:
|
|
|
19212
19218
|
browser_automation_object.set_timeout(wait_time=wait_time)
|
|
19213
19219
|
if "wait_time" in browser_automation:
|
|
19214
19220
|
self.logger.info(
|
|
19215
|
-
"%s automation wait time ->
|
|
19221
|
+
"%s automation wait time -> %.2f seconds.",
|
|
19216
19222
|
automation_type,
|
|
19217
|
-
|
|
19223
|
+
wait_time,
|
|
19218
19224
|
)
|
|
19219
19225
|
|
|
19220
19226
|
# Initialize overall result status:
|
|
@@ -19392,6 +19398,7 @@ class Payload:
|
|
|
19392
19398
|
checkbox_state = automation_step.get("checkbox_state", None)
|
|
19393
19399
|
wait_until = automation_step.get("wait_until", None)
|
|
19394
19400
|
wait_time = automation_step.get("wait_time", 0.0)
|
|
19401
|
+
wait_state = automation_step.get("wait_state", "visible")
|
|
19395
19402
|
role_type = automation_step.get("role_type", None)
|
|
19396
19403
|
occurrence = automation_step.get("occurrence", 1)
|
|
19397
19404
|
scroll_to_element = automation_step.get("scroll_to_element", True)
|
|
@@ -19417,6 +19424,7 @@ class Payload:
|
|
|
19417
19424
|
is_page_close_trigger=close_window,
|
|
19418
19425
|
wait_until=wait_until,
|
|
19419
19426
|
wait_time=wait_time,
|
|
19427
|
+
wait_state=wait_state,
|
|
19420
19428
|
exact_match=exact_match,
|
|
19421
19429
|
regex=regex,
|
|
19422
19430
|
hover_only=hover_only,
|
|
@@ -19441,8 +19449,10 @@ class Payload:
|
|
|
19441
19449
|
self.logger.warning(message)
|
|
19442
19450
|
continue
|
|
19443
19451
|
self.logger.info(
|
|
19444
|
-
"Successfully %s %s element selected by -> '%s' (%s%s).",
|
|
19452
|
+
"Successfully %s%s %s%s element selected by -> '%s' (%s%s).",
|
|
19453
|
+
"force " if force else "",
|
|
19445
19454
|
"clicked" if not hover_only else "hovered over",
|
|
19455
|
+
"occurrence #{} of ".format(occurrence) if occurrence > 1 else "",
|
|
19446
19456
|
"navigational" if navigation else "non-navigational",
|
|
19447
19457
|
selector,
|
|
19448
19458
|
"selector type -> '{}'".format(selector_type),
|
|
@@ -19555,7 +19565,7 @@ class Payload:
|
|
|
19555
19565
|
iframe=iframe,
|
|
19556
19566
|
min_count=min_count,
|
|
19557
19567
|
wait_time=wait_time, # time to wait before the check is actually done
|
|
19558
|
-
show_error=
|
|
19568
|
+
show_error=want_exist, # if element is not found that we do not want to find it is not an error
|
|
19559
19569
|
)
|
|
19560
19570
|
# Check if we didn't get what we want:
|
|
19561
19571
|
if (not result and want_exist) or (result and not want_exist):
|
|
File without changes
|
|
File without changes
|