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.
- pyxecm/__init__.py +2 -1
- pyxecm/avts.py +79 -33
- pyxecm/customizer/api/app.py +45 -796
- pyxecm/customizer/api/auth/__init__.py +1 -0
- pyxecm/customizer/api/{auth.py → auth/functions.py} +2 -64
- pyxecm/customizer/api/auth/router.py +78 -0
- pyxecm/customizer/api/common/__init__.py +1 -0
- pyxecm/customizer/api/common/functions.py +47 -0
- pyxecm/customizer/api/{metrics.py → common/metrics.py} +1 -1
- pyxecm/customizer/api/common/models.py +21 -0
- pyxecm/customizer/api/{payload_list.py → common/payload_list.py} +6 -1
- pyxecm/customizer/api/common/router.py +72 -0
- pyxecm/customizer/api/settings.py +25 -0
- pyxecm/customizer/api/terminal/__init__.py +1 -0
- pyxecm/customizer/api/terminal/router.py +87 -0
- pyxecm/customizer/api/v1_csai/__init__.py +1 -0
- pyxecm/customizer/api/v1_csai/router.py +87 -0
- pyxecm/customizer/api/v1_maintenance/__init__.py +1 -0
- pyxecm/customizer/api/v1_maintenance/functions.py +100 -0
- pyxecm/customizer/api/v1_maintenance/models.py +12 -0
- pyxecm/customizer/api/v1_maintenance/router.py +76 -0
- pyxecm/customizer/api/v1_otcs/__init__.py +1 -0
- pyxecm/customizer/api/v1_otcs/functions.py +61 -0
- pyxecm/customizer/api/v1_otcs/router.py +179 -0
- pyxecm/customizer/api/v1_payload/__init__.py +1 -0
- pyxecm/customizer/api/v1_payload/functions.py +179 -0
- pyxecm/customizer/api/v1_payload/models.py +51 -0
- pyxecm/customizer/api/v1_payload/router.py +499 -0
- pyxecm/customizer/browser_automation.py +568 -326
- pyxecm/customizer/customizer.py +204 -430
- pyxecm/customizer/guidewire.py +907 -43
- pyxecm/customizer/k8s.py +243 -56
- pyxecm/customizer/m365.py +104 -15
- pyxecm/customizer/payload.py +1943 -885
- pyxecm/customizer/pht.py +19 -2
- pyxecm/customizer/servicenow.py +22 -5
- pyxecm/customizer/settings.py +9 -6
- pyxecm/helper/xml.py +69 -0
- pyxecm/otac.py +1 -1
- pyxecm/otawp.py +2104 -1535
- pyxecm/otca.py +569 -0
- pyxecm/otcs.py +201 -37
- pyxecm/otds.py +35 -13
- {pyxecm-2.0.0.dist-info → pyxecm-2.0.1.dist-info}/METADATA +6 -29
- pyxecm-2.0.1.dist-info/RECORD +76 -0
- {pyxecm-2.0.0.dist-info → pyxecm-2.0.1.dist-info}/WHEEL +1 -1
- pyxecm-2.0.0.dist-info/RECORD +0 -54
- /pyxecm/customizer/api/{models.py → auth/models.py} +0 -0
- {pyxecm-2.0.0.dist-info → pyxecm-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {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 =
|
|
74
|
-
REQUEST_MAX_RETRIES =
|
|
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.
|
|
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["
|
|
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 +
|
|
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 +
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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 +
|
|
14886
|
+
column_header = prefix + column_key
|
|
14746
14887
|
else:
|
|
14747
|
-
# Construct the final
|
|
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 + "_",
|
|
14750
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
) ->
|
|
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": [
|
|
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.
|
|
15253
|
-
"Start
|
|
15254
|
-
|
|
15255
|
-
|
|
15256
|
-
|
|
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
|
-
|
|
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
|
|
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["
|
|
15491
|
+
uri = self._config["femeUri"]
|
|
15333
15492
|
task = _inner(
|
|
15334
15493
|
uri=uri,
|
|
15335
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
"
|
|
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
|
-
|
|
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=
|
|
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.
|
|
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
|
-
|
|
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
|
|