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 CHANGED
@@ -2240,9 +2240,14 @@ class OTCA:
2240
2240
  }
2241
2241
  },
2242
2242
  },
2243
- "responseTemplate": {},
2244
- "agents": ["retrieverAgent"],
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
- return self.parse_request_response(response_object=response)
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
- "Connection error (%s)! Retrying in %d seconds... %d/%d",
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
- self.logger.error(message)
1218
- else:
1219
- self.logger.debug(message)
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
- else:
1222
- return dict_object
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
- attribute_schema = next(
5412
- (cat_elem for cat_elem in category_schema.values() if cat_elem.get("name") == attribute),
5413
- None,
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
- if attribute_set:
5429
- set_schema = next(
5430
- (
5431
- cat_elem
5432
- for cat_elem in category_schema.values()
5433
- if cat_elem.get("name") == attribute_set and cat_elem.get("persona") == "set"
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
- prefix = set_key + "_" if set_key else category_key + "_"
5451
-
5452
- data = node["data"]["categories"]
5453
- for cat_data in data:
5454
- if set_key:
5455
- for i in range(1, int(set_schema["multi_value_length_max"])):
5456
- key = prefix + str(i) + "_" + attribute_id
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
- break
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
- # end if set_key
5471
- else:
5472
- key = prefix + attribute_id
5473
- attribute_value = cat_data.get(key)
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 here.:
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 here.:
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 Extended ECM.
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 global variable if recorded there.
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 the global list.
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
- subnode_type = self.get_result_value(response=subnode, key="type")
17785
- self.logger.info("Traversing %s node -> '%s' (%s)", subnode_type, subnode_name, subnode_id)
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.0
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=8rBAnLGNcEK9UAdRz6bSzDqe5Smvvp0lLl8oaV7yQbE,99352
7
- pyxecm/otcs.py,sha256=GIXA1BIV8oTZEO3-30MG0eG0TQLNtuvjAzpNmBa2JCE,815063
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=-WpWLKkiHzv2WmR3zIBrF2Z1ADcDlC5ifJWLyG7cu-s,8963
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=q1305oZ3a_JI8fLpc0u0T5D_8YSFW0C8-LQpOY0pwXc,69540
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=1smNNDW-9FdrpqVu0r74KqgEqtq9M7LgpopUJvRSLrE,67049
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=9Yu-Y7Co7A54qEayRT9e0SKYHlMfDmDVAH12k5gSf3A,46469
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=JKye3d-ac-Z6IFInFPh50AnI47mREGHZhYdLeyQH8qM,1365927
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.0.dist-info/METADATA,sha256=C6gl_je1-QeK7NROKoR0Sm_hk4pQXPuXabc6is81GYA,4566
80
- pyxecm-3.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
81
- pyxecm-3.1.0.dist-info/entry_points.txt,sha256=prc1mDdpd3bQk98VRBozI363mDUgSwDibDKXGNqKqgI,151
82
- pyxecm-3.1.0.dist-info/RECORD,,
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,,
@@ -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
- self.logger.error("Failed to initialize Playwright browser automation!")
201
- return
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
- if not selector:
913
- failure_message = "Missing element selector! Cannot find page element!"
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("Download file to -> %s", save_path)
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
- if show_error:
1310
- self.logger.error(
1311
- "Failed to check if elements -> '%s' (%s) exist! Locator is undefined.", selector, selector_type
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 elements matching selector -> '%s' (%s%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 checkif
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 elements.%s",
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 -> %s seconds...", str(wait_time))
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 -> %s seconds...", str(wait_time))
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
@@ -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 attributea element into a data element
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 - returning None")
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.build_attributes()
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 attribute schemas dictionary.
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 schemas dictionary.
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.build_attributes()
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 now specific attribute is requested return the category ID:
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 build_attribute(self, node: dict, **kwargs: dict) -> tuple[bool, bool]: # noqa: ARG002
1069
- """Build a dictionary of all attribute schemas in OTCS.
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 Enterprise Vault (2000) as node_id to have a predictable
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 build_attributes(self) -> dict:
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.info("Starting attribute data lookup build...")
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.build_attribute],
1190
+ executables=[self.build_attributes],
1194
1191
  )
1195
1192
 
1196
- self.logger.info("Attributes data lookup build completed.")
1193
+ self.logger.debug("Attribute data lookup completed.")
1197
1194
 
1198
1195
  return result
1199
1196
 
@@ -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 failures and completed %s workspaces successfully.",
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 workspaces successfully.",
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.exception("Failed process workspace -> %s", workspace)
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.warning(
18562
- "Node -> '%s' does already exist in workspace folder with ID -> %s",
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
- browser_automation_object = BrowserAutomation(
19189
- base_url=base_url,
19190
- user_name=user_name,
19191
- user_password=password,
19192
- automation_name=name,
19193
- take_screenshots=debug_automation,
19194
- headless=browser_automation.get("headless", self._browser_headless),
19195
- logger=self.logger,
19196
- wait_until=wait_until,
19197
- browser=browser_automation.get("browser"), # None is acceptable
19198
- )
19199
- if not browser_automation_object:
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 -> '%s' configured.",
19221
+ "%s automation wait time -> %.2f seconds.",
19216
19222
  automation_type,
19217
- str(wait_time),
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=not want_exist, # if element is not found that we do not want to find it is not an 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