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

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

Potentially problematic release.


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

Files changed (50) hide show
  1. pyxecm/__init__.py +2 -1
  2. pyxecm/avts.py +79 -33
  3. pyxecm/customizer/api/app.py +45 -796
  4. pyxecm/customizer/api/auth/__init__.py +1 -0
  5. pyxecm/customizer/api/{auth.py → auth/functions.py} +2 -64
  6. pyxecm/customizer/api/auth/router.py +78 -0
  7. pyxecm/customizer/api/common/__init__.py +1 -0
  8. pyxecm/customizer/api/common/functions.py +47 -0
  9. pyxecm/customizer/api/{metrics.py → common/metrics.py} +1 -1
  10. pyxecm/customizer/api/common/models.py +21 -0
  11. pyxecm/customizer/api/{payload_list.py → common/payload_list.py} +6 -1
  12. pyxecm/customizer/api/common/router.py +72 -0
  13. pyxecm/customizer/api/settings.py +25 -0
  14. pyxecm/customizer/api/terminal/__init__.py +1 -0
  15. pyxecm/customizer/api/terminal/router.py +87 -0
  16. pyxecm/customizer/api/v1_csai/__init__.py +1 -0
  17. pyxecm/customizer/api/v1_csai/router.py +87 -0
  18. pyxecm/customizer/api/v1_maintenance/__init__.py +1 -0
  19. pyxecm/customizer/api/v1_maintenance/functions.py +100 -0
  20. pyxecm/customizer/api/v1_maintenance/models.py +12 -0
  21. pyxecm/customizer/api/v1_maintenance/router.py +76 -0
  22. pyxecm/customizer/api/v1_otcs/__init__.py +1 -0
  23. pyxecm/customizer/api/v1_otcs/functions.py +61 -0
  24. pyxecm/customizer/api/v1_otcs/router.py +179 -0
  25. pyxecm/customizer/api/v1_payload/__init__.py +1 -0
  26. pyxecm/customizer/api/v1_payload/functions.py +179 -0
  27. pyxecm/customizer/api/v1_payload/models.py +51 -0
  28. pyxecm/customizer/api/v1_payload/router.py +499 -0
  29. pyxecm/customizer/browser_automation.py +568 -326
  30. pyxecm/customizer/customizer.py +204 -430
  31. pyxecm/customizer/guidewire.py +907 -43
  32. pyxecm/customizer/k8s.py +243 -56
  33. pyxecm/customizer/m365.py +104 -15
  34. pyxecm/customizer/payload.py +1943 -885
  35. pyxecm/customizer/pht.py +19 -2
  36. pyxecm/customizer/servicenow.py +22 -5
  37. pyxecm/customizer/settings.py +9 -6
  38. pyxecm/helper/xml.py +69 -0
  39. pyxecm/otac.py +1 -1
  40. pyxecm/otawp.py +2104 -1535
  41. pyxecm/otca.py +569 -0
  42. pyxecm/otcs.py +201 -37
  43. pyxecm/otds.py +35 -13
  44. {pyxecm-2.0.0.dist-info → pyxecm-2.0.1.dist-info}/METADATA +6 -29
  45. pyxecm-2.0.1.dist-info/RECORD +76 -0
  46. {pyxecm-2.0.0.dist-info → pyxecm-2.0.1.dist-info}/WHEEL +1 -1
  47. pyxecm-2.0.0.dist-info/RECORD +0 -54
  48. /pyxecm/customizer/api/{models.py → auth/models.py} +0 -0
  49. {pyxecm-2.0.0.dist-info → pyxecm-2.0.1.dist-info}/licenses/LICENSE +0 -0
  50. {pyxecm-2.0.0.dist-info → pyxecm-2.0.1.dist-info}/top_level.txt +0 -0
pyxecm/otcs.py CHANGED
@@ -70,8 +70,8 @@ REQUEST_DOWNLOAD_HEADERS = {
70
70
  }
71
71
 
72
72
  REQUEST_TIMEOUT = 60
73
- REQUEST_RETRY_DELAY = 20
74
- REQUEST_MAX_RETRIES = 2
73
+ REQUEST_RETRY_DELAY = 30
74
+ REQUEST_MAX_RETRIES = 4
75
75
 
76
76
  default_logger = logging.getLogger(MODULE_NAME)
77
77
 
@@ -245,9 +245,11 @@ class OTCS:
245
245
  password: str | None = None,
246
246
  user_partition: str = "Content Server Members",
247
247
  resource_name: str = "cs",
248
+ resource_id: str = "",
248
249
  default_license: str = "X3",
249
250
  otds_ticket: str | None = None,
250
251
  base_path: str = "/cs/cs",
252
+ support_path: str = "/cssupport",
251
253
  thread_number: int = 3,
252
254
  download_dir: str | None = None,
253
255
  feme_uri: str | None = None,
@@ -273,7 +275,9 @@ class OTCS:
273
275
  The name of the OTDS partition for OTCS users.
274
276
  Default is "Content Server Members".
275
277
  resource_name (str, optional):
276
- The name of the OTDS resource for OTCS. Dault is "cs".
278
+ The name of the OTDS resource for OTCS. Default is "cs".
279
+ resource_id (str, optional):
280
+ The ID of the OTDS resource for OTCS. Default is "".
277
281
  default_license (str, optional):
278
282
  The name of the default user license. Default is "X3".
279
283
  otds_ticket (str, optional):
@@ -282,6 +286,9 @@ class OTCS:
282
286
  The base path segment of the Content Server URL.
283
287
  This typically is /cs/cs on a Linux deployment or /cs/cs.exe
284
288
  on a Windows deployment.
289
+ support_path (str, optional):
290
+ The support path of the Content Server. This is typically
291
+ /cssupport on a Linux deployment. This is also the default.
285
292
  thread_number (int, optional):
286
293
  The number of threads for parallel processing for data loads.
287
294
  download_dir (str | None, optional):
@@ -347,24 +354,29 @@ class OTCS:
347
354
  else:
348
355
  otcs_config["resource"] = ""
349
356
 
357
+ if resource_id:
358
+ otcs_config["resourceId"] = resource_id
359
+ else:
360
+ otcs_config["resourceId"] = None
361
+
350
362
  if default_license:
351
363
  otcs_config["license"] = default_license
352
364
  else:
353
365
  otcs_config["license"] = ""
354
366
 
355
- otcs_config["feme_uri"] = feme_uri
367
+ otcs_config["femeUri"] = feme_uri
356
368
 
357
369
  otcs_base_url = protocol + "://" + otcs_config["hostname"]
358
370
  if str(port) not in ["80", "443"]:
359
371
  otcs_base_url += ":{}".format(port)
360
372
  otcs_config["baseUrl"] = otcs_base_url
361
- otcs_support_url = otcs_base_url + "/cssupport"
373
+ otcs_support_url = otcs_base_url + support_path
362
374
  otcs_config["supportUrl"] = otcs_support_url
363
375
 
364
376
  if public_url is None:
365
377
  public_url = otcs_base_url
366
378
 
367
- otcs_public_support_url = public_url + "/cssupport"
379
+ otcs_public_support_url = public_url + support_path
368
380
  otcs_config["supportPublicUrl"] = otcs_public_support_url
369
381
 
370
382
  otcs_config["configuredUrl"] = otcs_support_url + "/csconfigured"
@@ -639,6 +651,58 @@ class OTCS:
639
651
 
640
652
  # end method definition
641
653
 
654
+ def partition_name(self) -> str:
655
+ """Return the OTDS user partition for Content Server.
656
+
657
+ Returns:
658
+ str:
659
+ The Content Server OTDS user partition.
660
+
661
+ """
662
+
663
+ return self.config()["partition"]
664
+
665
+ # end method definition
666
+
667
+ def resource_name(self) -> str:
668
+ """Return the OTDS resource name of Content Server.
669
+
670
+ Returns:
671
+ str:
672
+ The Content Server OTDS resource name.
673
+
674
+ """
675
+
676
+ return self.config()["resource"]
677
+
678
+ # end method definition
679
+
680
+ def resource_id(self) -> str:
681
+ """Return the OTDS resource ID of Content Server.
682
+
683
+ Returns:
684
+ str:
685
+ The Content Server OTDS resource ID.
686
+
687
+ """
688
+
689
+ return self.config()["resourceId"]
690
+
691
+ # end method definition
692
+
693
+ def set_resource_id(self, resource_id: str) -> None:
694
+ """Set the OTDS resource ID of Content Server.
695
+
696
+ Args:
697
+ resource_id (str):
698
+ The Content Server OTDS resource ID.
699
+
700
+ """
701
+
702
+ self.config()["resourceId"] = resource_id
703
+
704
+ # end method definition
705
+
642
706
  def get_data(self) -> Data:
643
707
  """Get the Data object that holds all loaded Content Server items (see method load_items()).
644
708
 
@@ -925,7 +989,19 @@ class OTCS:
925
989
  str(REQUEST_RETRY_DELAY),
926
990
  )
927
991
  retries += 1
928
- time.sleep(REQUEST_RETRY_DELAY) # Add a delay before retrying
992
+
993
+ if not self.is_ready():
994
+ self.logger.warning(
995
+ "Content Server is not ready to receive requests. Waiting for state change in %s seconds...",
996
+ str(REQUEST_RETRY_DELAY),
997
+ )
998
+
999
+ while not self.is_ready():
1000
+ time.sleep(REQUEST_RETRY_DELAY) # Add a delay before retrying
1001
+
1002
+ else:
1003
+ time.sleep(REQUEST_RETRY_DELAY) # Add a delay before retrying
1004
+
929
1005
  else:
930
1006
  self.logger.error(
931
1007
  "%s; connection error",
@@ -1116,7 +1192,8 @@ class OTCS:
1116
1192
  The name of the substructure that includes the values.
1117
1193
 
1118
1194
  Returns:
1119
- bool: True if the value was found, False otherwise
1195
+ bool:
1196
+ True if the value was found, False otherwise
1120
1197
 
1121
1198
  """
1122
1199
 
@@ -1581,10 +1658,9 @@ class OTCS:
1581
1658
  otcs_ticket = None
1582
1659
 
1583
1660
  if wait_for_ready:
1584
- self.logger.info("Check if OTCS is ready...")
1585
1661
  while not self.is_ready():
1586
1662
  self.logger.debug(
1587
- "OTCS is not ready to receive requests yet. Waiting additional 30 seconds...",
1663
+ "OTCS is not ready to receive requests yet. Waiting 30 seconds...",
1588
1664
  )
1589
1665
  time.sleep(30)
1590
1666
 
@@ -3833,7 +3909,7 @@ class OTCS:
3833
3909
  nickname: str,
3834
3910
  show_error: bool = False,
3835
3911
  ) -> dict | None:
3836
- """Assign a nickname to an Extended ECM node (e.g. workspace).
3912
+ """Assign a nickname to an OTCS node (e.g. workspace).
3837
3913
 
3838
3914
  Some naming conventions for the nickname are automatically applied:
3839
3915
  - replace "-" with "_"
@@ -6672,7 +6748,7 @@ class OTCS:
6672
6748
  request_url,
6673
6749
  )
6674
6750
 
6675
- return self.do_request(
6751
+ response = self.do_request(
6676
6752
  url=request_url,
6677
6753
  method="POST",
6678
6754
  headers=request_header,
@@ -6682,6 +6758,25 @@ class OTCS:
6682
6758
  ),
6683
6759
  )
6684
6760
 
6761
+ try:
6762
+ if response["results"]["data"]["status"]["error_count"] > 0:
6763
+ self.logger.error("Error occoured during package deployment")
6764
+ else:
6765
+ self.logger.info(
6766
+ "Transport successfully deployed %s items",
6767
+ response["results"]["data"]["status"]["success_count"],
6768
+ )
6769
+
6770
+ for error in response["results"]["data"]["status"]["errors"]:
6771
+ self.logger.error(
6772
+ "Transport deployment error: %s (%s) -> %s", error["name"], error["id"], error["error"]
6773
+ )
6774
+
6775
+ except Exception as e:
6776
+ self.logger.debug(e)
6777
+
6778
+ return response
6779
+
6685
6780
  # end method definition
6686
6781
 
6687
6782
  def deploy_transport(
@@ -14714,7 +14809,29 @@ class OTCS:
14714
14809
  row (dict):
14715
14810
  The row data to extend with keys for each attribute.
14716
14811
  categories (dict):
14717
- The categories of the node.
14812
+ The categories of the node. This is a structure like
14813
+ {
14814
+ "links" = {}
14815
+ "results" = [
14816
+ {
14817
+ "data" = {
14818
+ "categories" = {
14819
+ "14885_10_1_11 = "Aerospace", # Multi-value set row 1, attribute 11
14820
+ "14885_10_2_11 = "Automotive" # Multi-value set row 2, attribute 11
14821
+ "14885_14" = ["Content"] # Multi-value attribute
14822
+ "14885_15" = "Test" # Single value attribute
14823
+ }
14824
+ }
14825
+ "metadata" = {
14826
+ "categories" = {
14827
+ "14885_10" = {...} # Definition of the set
14828
+ "14885_10_x_11" = {...} # Definition of the set attribute
14829
+ "144885_14" = {...} # Definition of multi-value attribute
14830
+ }
14831
+ }
14832
+ }
14833
+ ]
14834
+ }
14718
14835
  prefix (str):
14719
14836
  The prefix string. Either "workspace_" or "item_" to
14720
14837
  differentiate attributes on workspace level and attributes
@@ -14726,6 +14843,19 @@ class OTCS:
14726
14843
 
14727
14844
  """
14728
14845
 
14846
+ def get_attribute_identifier(s: str) -> str:
14847
+ # Get the prefix of two numbers separated by an underscore:
14848
+ match = re.match(r"^([^_]+_[^_]+)", s)
14849
+ return match.group(1) if match else ""
14850
+
14851
+ def get_column_identifier(s: str) -> str:
14852
+ # Cut out the third number if there's a third number. Used for set attributes
14853
+ match = re.match(r"^([^_]+_[^_]+)(?:.*_([0-9]+))?$", s)
14854
+ return f"{match.group(1)}_{match.group(2)}" if match and match.group(2) else match.group(1)
14855
+
14856
+ def get_set_identifier(s: str) -> str:
14857
+ return re.sub(r"^([^_]+_[^_]+_)[^_]+", r"\1x", s)
14858
+
14729
14859
  if not categories or "results" not in categories:
14730
14860
  return False
14731
14861
 
@@ -14739,15 +14869,37 @@ class OTCS:
14739
14869
  # Replace non-aphanumeric characters in the name with underscores
14740
14870
  # but avoid having multiple underscores following each other:
14741
14871
  category_name = re.sub(r"[^a-z0-9]+", "_", category_name.lower())
14872
+
14873
+ # Iterate over all attributes. For multi-value sets (which are basically a matrix or table)
14874
+ # we do a special handling. The values of each column of such a table are written as a value list in a
14875
+ # separate data frame column. So we slice the multi-value set by columns (not rows).
14876
+ # This way this can later on recombined by "columns_to_add_table" in the payload.
14742
14877
  for key in attributes:
14743
14878
  value = attributes[key]
14879
+ # We don't want "_x_" in the column identifiers as we create a list:
14880
+ column_key = get_column_identifier(key)
14881
+ # The attribute key should just be the category ID (before first "_")
14882
+ # and the attribute or set ID (after first "_"):
14883
+ attribute_key = get_attribute_identifier(key)
14884
+ meta = metadata[attribute_key]
14744
14885
  if self._use_numeric_category_identifier: # this value is set be the class initializer
14745
- column_header = prefix + key
14886
+ column_header = prefix + column_key
14746
14887
  else:
14747
- # Construct the final coolumn name by replacing the leading <cat_num>_ with the
14888
+ # Construct the final column name by replacing the leading <cat_num>_ with the
14748
14889
  # normalized name of the category.
14749
- column_header = prefix + re.sub(r"^[^_]+_", category_name + "_", key)
14750
- row[column_header] = value
14890
+ column_header = prefix + re.sub(r"^[^_]+_", category_name + "_", column_key)
14891
+ # Check if meta is the schema for a multi-value set. This is the
14892
+ # case if "multi_value" is True _and_ "persona" is "set":
14893
+ if meta.get("multi_value", False) and meta.get("persona") == "set":
14894
+ # Is it the first value line? Then we need to initialize the list...
14895
+ if column_header not in row:
14896
+ row[column_header] = []
14897
+ row[column_header].append(value)
14898
+ else:
14899
+ # Not a multi-value set. We just write the value
14900
+ # into the data frame (in case it is a multi-value
14901
+ # a)
14902
+ row[column_header] = value
14751
14903
 
14752
14904
  return True
14753
14905
 
@@ -14889,7 +15041,7 @@ class OTCS:
14889
15041
  # We try to avoid calculating the node categories more than once
14890
15042
  # by doing it here and use it for filtering _and_ for
14891
15043
  # data frame columns. We only need the category metadata if we
14892
- # have category/attribute filters:
15044
+ # have category/attribute filters or if we want columns with attributze names instead of IDs:
14893
15045
  if workspace_metadata or filter_workspace_category or filter_workspace_attributes:
14894
15046
  categories = self.get_node_categories(
14895
15047
  node_id=subnode["id"],
@@ -15172,7 +15324,7 @@ class OTCS:
15172
15324
 
15173
15325
  # end method definition
15174
15326
 
15175
- def feme_embedd_metadata(
15327
+ def aviator_embed_metadata(
15176
15328
  self,
15177
15329
  node_id: int,
15178
15330
  node: dict | None = None,
@@ -15186,7 +15338,7 @@ class OTCS:
15186
15338
  workspace_metadata: bool = True,
15187
15339
  remove_existing: bool = False,
15188
15340
  ) -> None:
15189
- """Run FEME metadata embedding on provided node for Content Aviator.
15341
+ """Run Content Aviator metadata embedding on provided node with FEME tool.
15190
15342
 
15191
15343
  Args:
15192
15344
  node_id (int):
@@ -15215,9 +15367,11 @@ class OTCS:
15215
15367
 
15216
15368
  """
15217
15369
 
15370
+ success = True
15371
+
15218
15372
  async def _inner(
15219
15373
  uri: str,
15220
- node: dict,
15374
+ node_properties: dict,
15221
15375
  crawl: bool,
15222
15376
  wait_for_completion: bool,
15223
15377
  message_override: dict | None,
@@ -15227,7 +15381,11 @@ class OTCS:
15227
15381
  image_prompt: str = "",
15228
15382
  workspace_metadata: bool = True,
15229
15383
  remove_existing: bool = False,
15230
- ) -> None:
15384
+ ) -> bool:
15385
+ # This is important as the sub-method needs to write
15386
+ # to the 'success' variable:
15387
+ nonlocal success
15388
+
15231
15389
  self.logger.debug("Open WebSocket connection to -> %s", uri)
15232
15390
  async with websockets.connect(uri) as websocket:
15233
15391
  # Define if one node (index), or all childs should be processed (crawl)
@@ -15235,7 +15393,7 @@ class OTCS:
15235
15393
 
15236
15394
  message = {
15237
15395
  "task": task, # either "index" or "crawl". "crawl" means traversing OTCS workspaces and folders.
15238
- "nodes": [node], # the list of (root) node IDs to process
15396
+ "nodes": [node_properties], # the list of (root) nodes to process
15239
15397
  "documents": document_metadata, # process metadata of documents
15240
15398
  "workspaces": workspace_metadata, # process metadata of workspaces
15241
15399
  "images": images, # enable image processing via LLM (Gemini) - just content of images
@@ -15249,11 +15407,11 @@ class OTCS:
15249
15407
  }
15250
15408
  if message_override:
15251
15409
  message.update(message_override)
15252
- self.logger.debug(
15253
- "Start FEME on -> %s (%s), type -> %s, crawl -> %s, wait for completion -> %s, workspaces -> %s, documents -> %s, images -> %s",
15254
- node["name"],
15255
- node["id"],
15256
- node["type"],
15410
+ self.logger.info(
15411
+ "Start Content Aviator embedding on -> '%s' (%s), type -> %s, crawl -> %s, wait for completion -> %s, workspaces -> %s, documents -> %s, images -> %s",
15412
+ node_properties["name"],
15413
+ node_properties["id"],
15414
+ node_properties["type"],
15257
15415
  crawl,
15258
15416
  wait_for_completion,
15259
15417
  workspace_metadata,
@@ -15275,6 +15433,7 @@ class OTCS:
15275
15433
  "Timeout Error during FEME WebSocket connection, WebSocket did not receive a message in time (%ss)",
15276
15434
  timeout,
15277
15435
  )
15436
+ success = False
15278
15437
  break
15279
15438
 
15280
15439
  self.logger.debug("Received WebSocket response -> %s", response)
@@ -15319,20 +15478,20 @@ class OTCS:
15319
15478
  "Cannot get node with ID -> %s, skipping FEME embedding!",
15320
15479
  node_id,
15321
15480
  )
15322
- return
15481
+ return False
15323
15482
  try:
15324
- node_data = node["results"]["data"]["properties"]
15325
- except json.JSONDecodeError:
15483
+ node_properties = node["results"]["data"]["properties"] if "results" in node else node["data"]["properties"]
15484
+ except (json.JSONDecodeError, KeyError):
15326
15485
  self.logger.error(
15327
- "Cannot decode data for node with ID -> %s, skipping FEME embedding.",
15486
+ "Cannot decode data for node with ID -> %s, skipping embedding with FEME.",
15328
15487
  node_id,
15329
15488
  )
15330
- return
15489
+ return False
15331
15490
 
15332
- uri = self._config["feme_uri"]
15491
+ uri = self._config["femeUri"]
15333
15492
  task = _inner(
15334
15493
  uri=uri,
15335
- node=node_data,
15494
+ node_properties=node_properties,
15336
15495
  crawl=crawl,
15337
15496
  wait_for_completion=wait_for_completion,
15338
15497
  message_override=message_override,
@@ -15348,16 +15507,21 @@ class OTCS:
15348
15507
  event_loop.run_until_complete(task)
15349
15508
  except websockets.exceptions.ConnectionClosed: # :
15350
15509
  self.logger.error("WebSocket connection was closed!")
15510
+ success = False
15351
15511
 
15352
15512
  except TimeoutError:
15353
15513
  self.logger.error(
15354
15514
  "Timeout error during FEME WebSocket connection, WebSocket did not receive a message in time (%ss)",
15355
15515
  timeout,
15356
15516
  )
15517
+ success = False
15357
15518
 
15358
- except Exception:
15359
- self.logger.error("Error during FEME WebSocket connection!")
15519
+ except Exception as exc:
15520
+ self.logger.error("Error during FEME WebSocket connection! -> %s", exc)
15521
+ success = False
15360
15522
 
15361
15523
  event_loop.close()
15362
15524
 
15525
+ return success
15526
+
15363
15527
  # end method definition
pyxecm/otds.py CHANGED
@@ -81,6 +81,7 @@ class OTDS:
81
81
  password: str | None = None,
82
82
  otds_ticket: str | None = None,
83
83
  bind_password: str | None = None,
84
+ admin_partition: str = "otds.admin",
84
85
  logger: logging.Logger = default_logger,
85
86
  ) -> None:
86
87
  """Initialize the OTDS object.
@@ -99,6 +100,8 @@ class OTDS:
99
100
  otds_ticket (str | None, optional):
100
101
  Authentication ticket of OTDS.
101
102
  bind_password (str | None, optional): TODO
103
+ admin_partition (str, optional):
104
+ Name of the admin partition. Default is "otds.admin".
102
105
  logger (logging.Logger, optional):
103
106
  The logging object to use for all log messages. Defaults to default_logger.
104
107
 
@@ -142,6 +145,8 @@ class OTDS:
142
145
  else:
143
146
  otds_config["bindPassword"] = ""
144
147
 
148
+ otds_config["adminPartition"] = admin_partition
149
+
145
150
  if otds_ticket:
146
151
  self._cookie = {"OTDSTicket": otds_ticket}
147
152
 
@@ -443,6 +448,19 @@ class OTDS:
443
448
 
444
449
  # end method definition
445
450
 
451
+ def admin_partition_name(self) -> str:
452
+ """Return OTDS admin partition name.
453
+
454
+ Returns:
455
+ str:
456
+ The OTDS admin partition name.
457
+
458
+ """
459
+
460
+ return self.config()["adminPartition"]
461
+
462
+ # end method definition
463
+
446
464
  def do_request(
447
465
  self,
448
466
  url: str,
@@ -544,7 +562,7 @@ class OTDS:
544
562
  in response.text # OTDS seems to return 400 and not 401 for token expiry (in some cases like impersonation)
545
563
  )
546
564
  ):
547
- self.logger.debug("Session has expired - try to re-authenticate...")
565
+ self.logger.info("Session has expired - try to re-authenticate...")
548
566
  self.authenticate(revalidate=True)
549
567
  retries += 1
550
568
  else:
@@ -619,7 +637,7 @@ class OTDS:
619
637
  else:
620
638
  return None
621
639
  # end try
622
- self.logger.debug(
640
+ self.logger.info(
623
641
  "Retrying REST API %s call -> %s... (retry = %s, cookie -> %s)",
624
642
  method,
625
643
  url,
@@ -2648,17 +2666,19 @@ class OTDS:
2648
2666
  if existing_license:
2649
2667
  request_url += "/" + existing_license[0]["id"]
2650
2668
  else:
2651
- self.logger.debug(
2652
- "No existing license found for resource -> '%s' - adding a new license...",
2669
+ self.logger.info(
2670
+ "No existing license found for product -> '%s' and resource -> '%s' - adding a new license...",
2671
+ product_name,
2653
2672
  resource_id,
2654
2673
  )
2655
2674
  # change strategy to create a new license:
2656
2675
  update = False
2657
2676
 
2658
2677
  self.logger.debug(
2659
- "Adding product license -> '%s' for product -> '%s' to resource ->'%s'; calling -> %s",
2678
+ "%s product license -> '%s' for product -> '%s' to resource ->'%s'; calling -> %s",
2679
+ "Adding" if not update else "Updating",
2660
2680
  path_to_license_file,
2661
- product_description,
2681
+ "{} ({})".format(product_name, product_description) if product_description else product_name,
2662
2682
  resource_id,
2663
2683
  request_url,
2664
2684
  )
@@ -2670,9 +2690,10 @@ class OTDS:
2670
2690
  method="PUT",
2671
2691
  json_data=license_post_body_json,
2672
2692
  timeout=None,
2673
- failure_message="Failed to update product license -> '{}' for product -> '{}'".format(
2693
+ failure_message="Failed to update product license -> '{}' for product -> '{}' to resource -> '{}'".format(
2674
2694
  path_to_license_file,
2675
- product_description,
2695
+ "{} ({})".format(product_name, product_description) if product_description else product_name,
2696
+ resource_id,
2676
2697
  ),
2677
2698
  )
2678
2699
  else:
@@ -2682,9 +2703,10 @@ class OTDS:
2682
2703
  method="POST",
2683
2704
  json_data=license_post_body_json,
2684
2705
  timeout=None,
2685
- failure_message="Failed to add product license -> '{}' for product -> '{}'".format(
2706
+ failure_message="Failed to add product license -> '{}' for product -> '{}' to resource -> '{}'".format(
2686
2707
  path_to_license_file,
2687
- product_description,
2708
+ "{} ({})".format(product_name, product_description) if product_description else product_name,
2709
+ resource_id,
2688
2710
  ),
2689
2711
  )
2690
2712
 
@@ -3871,17 +3893,17 @@ class OTDS:
3871
3893
  """
3872
3894
 
3873
3895
  encoded_client_secret = "{}:{}".format(client_id, client_secret).encode("utf-8")
3874
- access_token_request_headers = {
3896
+
3897
+ request_header = {
3875
3898
  "Authorization": "Basic " + base64.b64encode(encoded_client_secret).decode("utf-8"),
3876
3899
  "Content-Type": "application/x-www-form-urlencoded",
3877
3900
  }
3878
-
3879
3901
  request_url = self.token_url()
3880
3902
 
3881
3903
  response = requests.post(
3882
3904
  url=request_url,
3883
3905
  data={"grant_type": "client_credentials"},
3884
- headers=access_token_request_headers,
3906
+ headers=request_header,
3885
3907
  timeout=REQUEST_TIMEOUT,
3886
3908
  )
3887
3909
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyxecm
3
- Version: 2.0.0
3
+ Version: 2.0.1
4
4
  Summary: A Python library to interact with Opentext Extended ECM REST API
5
5
  Author-email: Kai Gatzweiler <kgatzweiler@opentext.com>, "Dr. Marc Diefenbruch" <mdiefenb@opentext.com>
6
6
  Project-URL: Homepage, https://github.com/opentext/pyxecm
@@ -28,16 +28,17 @@ Requires-Dist: pandas
28
28
  Requires-Dist: python-magic
29
29
  Requires-Dist: websockets
30
30
  Requires-Dist: pydantic-settings
31
- Requires-Dist: fastapi
31
+ Requires-Dist: fastapi>=0.115.12
32
32
  Requires-Dist: uvicorn
33
33
  Requires-Dist: python-multipart
34
34
  Requires-Dist: aiofiles
35
35
  Requires-Dist: asyncio
36
36
  Requires-Dist: jinja2
37
37
  Requires-Dist: prometheus-fastapi-instrumentator
38
+ Requires-Dist: psycopg[binary,pool]>=3.2.6
38
39
  Provides-Extra: browserautomation
39
- Requires-Dist: selenium; extra == "browserautomation"
40
40
  Requires-Dist: chromedriver_autoinstaller; extra == "browserautomation"
41
+ Requires-Dist: playwright>=1.52.0; extra == "browserautomation"
41
42
  Provides-Extra: dataloader
42
43
  Requires-Dist: pandas; extra == "dataloader"
43
44
  Requires-Dist: pyyaml; extra == "dataloader"
@@ -45,6 +46,7 @@ Requires-Dist: python-hcl2; extra == "dataloader"
45
46
  Requires-Dist: tropycal; extra == "dataloader"
46
47
  Requires-Dist: shapely; extra == "dataloader"
47
48
  Requires-Dist: cartopy; extra == "dataloader"
49
+ Requires-Dist: psycopg; extra == "dataloader"
48
50
  Provides-Extra: sap
49
51
  Requires-Dist: pyrfc==2.8.3; extra == "sap"
50
52
  Provides-Extra: profiling
@@ -77,21 +79,6 @@ Create an `.env` file as described here: [sample-environment-variables](customiz
77
79
  python -m pyxecm.customizer.api
78
80
  ```
79
81
 
80
- ??? example "Sample Output"
81
- ```console
82
- INFO: Started server process [93861]
83
- INFO: Waiting for application startup.
84
- 31-Mar-2025 12:49:53 INFO [CustomizerAPI] [MainThread] Starting maintenance_page thread...
85
- 31-Mar-2025 12:49:53 INFO [CustomizerAPI] [MainThread] Starting processing thread...
86
- 31-Mar-2025 12:49:53 INFO [CustomizerAPI.payload_list] [customization_run_api] Starting 'Scheduler' thread for payload list processing...
87
- INFO: Application startup complete.
88
- 31-Mar-2025 12:49:53 INFO [CustomizerAPI.payload_list] [customization_run_api] Waiting for thread -> 'Scheduler' to complete...
89
- INFO: Started server process [93861]
90
- INFO: Waiting for application startup.
91
- INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
92
- INFO: Application startup complete.
93
- INFO: Uvicorn running on http://0.0.0.0:5555 (Press CTRL+C to quit)
94
- ```
95
82
 
96
83
  Access the Customizer API at [http://localhost:8000/api](http://localhost:8000/api)
97
84
 
@@ -117,17 +104,7 @@ nodes = otcs_object.get_subnodes(2000)
117
104
  for node in nodes["results"]:
118
105
  print(node["data"]["properties"]["id"], node["data"]["properties"]["name"])
119
106
  ```
120
- ??? example "Sample Output"
121
- ```console
122
- 13050 Administration
123
- 13064 Case Management
124
- 18565 Contract Management
125
- 18599 Customer Support
126
- 18536 Engineering & Construction
127
- 13107 Enterprise Asset Management
128
- 18632 Human Resources
129
- 6554 Inbox Folders
130
- ```
107
+
131
108
 
132
109
  # Disclaimer
133
110