pyxecm 3.4.7__tar.gz → 3.4.9__tar.gz
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.
- {pyxecm-3.4.7 → pyxecm-3.4.9}/PKG-INFO +1 -1
- {pyxecm-3.4.7 → pyxecm-3.4.9}/pyproject.toml +1 -1
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm/otcs.py +219 -26
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm/otds.py +94 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_customizer/payload.py +3 -1
- {pyxecm-3.4.7 → pyxecm-3.4.9}/.gitignore +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/README.md +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm/__init__.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm/avts.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm/coreshare.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm/helper/__init__.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm/helper/assoc.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm/helper/data.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm/helper/logadapter.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm/helper/web.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm/helper/xml.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm/otac.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm/otawp.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm/otca.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm/otiv.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm/otkd.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm/otmm.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm/otpd.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm/py.typed +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_api/__init__.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_api/__main__.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_api/app.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_api/auth/__init__.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_api/auth/functions.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_api/auth/models.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_api/auth/router.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_api/common/__init__.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_api/common/functions.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_api/common/metrics.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_api/common/models.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_api/common/router.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_api/settings.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_api/terminal/__init__.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_api/terminal/router.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_api/v1_maintenance/__init__.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_api/v1_maintenance/functions.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_api/v1_maintenance/models.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_api/v1_maintenance/router.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_api/v1_otcs/__init__.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_api/v1_otcs/functions.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_api/v1_otcs/router.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_api/v1_payload/__init__.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_api/v1_payload/functions.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_api/v1_payload/models.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_api/v1_payload/router.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_customizer/__init__.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_customizer/__main__.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_customizer/browser_automation.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_customizer/customizer.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_customizer/exceptions.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_customizer/guidewire.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_customizer/k8s.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_customizer/log.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_customizer/m365.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_customizer/payload_list.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_customizer/salesforce.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_customizer/sap.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_customizer/servicenow.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_customizer/settings.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_customizer/successfactors.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_customizer/translate.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_maintenance_page/__init__.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_maintenance_page/__main__.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_maintenance_page/app.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_maintenance_page/settings.py +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_maintenance_page/static/favicon.avif +0 -0
- {pyxecm-3.4.7 → pyxecm-3.4.9}/src/pyxecm_maintenance_page/templates/maintenance.html +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyxecm
|
|
3
|
-
Version: 3.4.
|
|
3
|
+
Version: 3.4.9
|
|
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>
|
|
@@ -998,7 +998,7 @@ class OTCS:
|
|
|
998
998
|
if not self._workspace_type_lookup or workspace_type_id not in self._workspace_type_lookup:
|
|
999
999
|
return None
|
|
1000
1000
|
|
|
1001
|
-
return self._workspace_type_lookup.get(workspace_type_id)
|
|
1001
|
+
return self._workspace_type_lookup.get(workspace_type_id)
|
|
1002
1002
|
|
|
1003
1003
|
# end method definition
|
|
1004
1004
|
|
|
@@ -7185,13 +7185,21 @@ class OTCS:
|
|
|
7185
7185
|
# if the set row does not exist we can break:
|
|
7186
7186
|
break
|
|
7187
7187
|
attribute_value = cat_data.get(key)
|
|
7188
|
+
# Skip empty set cells (None / empty string) so they don't
|
|
7189
|
+
# accidentally match (e.g. str(None) == "None" under substring).
|
|
7190
|
+
# We keep checking the remaining set rows.
|
|
7191
|
+
if attribute_value is None or attribute_value == "":
|
|
7192
|
+
continue
|
|
7193
|
+
# Compare as strings to be type-tolerant: the lookup value is
|
|
7194
|
+
# always a string (e.g. from bulk payload placeholders) while the
|
|
7195
|
+
# stored attribute value can be an int/float for numeric attributes.
|
|
7188
7196
|
# Is it a multi-value attribute (i.e. a list of values)?
|
|
7189
7197
|
if isinstance(attribute_value, list):
|
|
7190
|
-
if value in attribute_value:
|
|
7198
|
+
if any(str(value) == str(item) for item in attribute_value):
|
|
7191
7199
|
node_matched = True
|
|
7192
7200
|
break
|
|
7193
|
-
elif (substring and value in attribute_value) or (
|
|
7194
|
-
not substring and value == attribute_value
|
|
7201
|
+
elif (substring and str(value) in str(attribute_value)) or (
|
|
7202
|
+
not substring and str(value) == str(attribute_value)
|
|
7195
7203
|
):
|
|
7196
7204
|
node_matched = True
|
|
7197
7205
|
break
|
|
@@ -7199,14 +7207,19 @@ class OTCS:
|
|
|
7199
7207
|
else:
|
|
7200
7208
|
key = prefix + attribute_id
|
|
7201
7209
|
attribute_value = cat_data.get(key)
|
|
7202
|
-
if not attribute_value
|
|
7210
|
+
# Note: we intentionally do not use "if not attribute_value" here as
|
|
7211
|
+
# that would also skip a legitimate numeric value of 0.
|
|
7212
|
+
if attribute_value is None or attribute_value == "":
|
|
7203
7213
|
continue
|
|
7214
|
+
# Compare as strings to be type-tolerant (see comment above).
|
|
7204
7215
|
# Is it a multi-value attribute (i.e. a list of values)?
|
|
7205
7216
|
if isinstance(attribute_value, list):
|
|
7206
|
-
if value in attribute_value:
|
|
7217
|
+
if any(str(value) == str(item) for item in attribute_value):
|
|
7207
7218
|
node_matched = True
|
|
7208
7219
|
# If not a multi-value attribute, check for equality:
|
|
7209
|
-
elif (substring and value in attribute_value) or (
|
|
7220
|
+
elif (substring and str(value) in str(attribute_value)) or (
|
|
7221
|
+
not substring and str(value) == str(attribute_value)
|
|
7222
|
+
):
|
|
7210
7223
|
node_matched = True
|
|
7211
7224
|
if node_matched:
|
|
7212
7225
|
break
|
|
@@ -13587,15 +13600,15 @@ class OTCS:
|
|
|
13587
13600
|
|
|
13588
13601
|
"""
|
|
13589
13602
|
|
|
13590
|
-
|
|
13591
|
-
if
|
|
13592
|
-
return
|
|
13603
|
+
type_name = self._workspace_type_lookup.get(type_id)
|
|
13604
|
+
if type_name and not renew:
|
|
13605
|
+
return type_name
|
|
13593
13606
|
|
|
13594
13607
|
workspace_type = self.get_workspace_type(type_id=type_id)
|
|
13595
13608
|
type_name = workspace_type.get("workspace_type")
|
|
13596
13609
|
if type_name:
|
|
13597
13610
|
# Update the lookup cache:
|
|
13598
|
-
self._workspace_type_lookup[type_id] =
|
|
13611
|
+
self._workspace_type_lookup[type_id] = type_name
|
|
13599
13612
|
return type_name
|
|
13600
13613
|
|
|
13601
13614
|
return None
|
|
@@ -13639,6 +13652,165 @@ class OTCS:
|
|
|
13639
13652
|
|
|
13640
13653
|
# end method definition
|
|
13641
13654
|
|
|
13655
|
+
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="get_workspace_type_lookup")
|
|
13656
|
+
def get_workspace_type_lookup(self, renew: bool = False) -> dict:
|
|
13657
|
+
"""Get the lookup dictionary that maps workspace type IDs to their names.
|
|
13658
|
+
|
|
13659
|
+
The lookup maps each workspace type ID to its name,
|
|
13660
|
+
e.g. {848: 'Sales Contract'}.
|
|
13661
|
+
It is cached in the OTCS object variable self._workspace_type_lookup and
|
|
13662
|
+
also populated on demand by get_workspace_type_name().
|
|
13663
|
+
|
|
13664
|
+
Args:
|
|
13665
|
+
renew (bool):
|
|
13666
|
+
Whether to refresh the cached lookup via the REST API. If the
|
|
13667
|
+
cache is empty it is always populated regardless of this flag.
|
|
13668
|
+
Passing True re-fetches the current workspace types and overwrites
|
|
13669
|
+
existing entries. This is important because self._workspace_type_lookup
|
|
13670
|
+
has no expiry and is shared for the lifetime of this OTCS instance,
|
|
13671
|
+
so without a refresh a renamed workspace type would keep resolving
|
|
13672
|
+
to its stale old name here and via get_workspace_type_name().
|
|
13673
|
+
|
|
13674
|
+
Returns:
|
|
13675
|
+
dict:
|
|
13676
|
+
The workspace type lookup dictionary
|
|
13677
|
+
(type_id -> type_name).
|
|
13678
|
+
|
|
13679
|
+
Side effects:
|
|
13680
|
+
Caches the workspace type lookup in self._workspace_type_lookup
|
|
13681
|
+
for future calls.
|
|
13682
|
+
|
|
13683
|
+
"""
|
|
13684
|
+
|
|
13685
|
+
if self._workspace_type_lookup and not renew:
|
|
13686
|
+
return self._workspace_type_lookup
|
|
13687
|
+
|
|
13688
|
+
workspace_types = self.get_workspace_types_iterator(expand_workspace_info=False, expand_templates=False)
|
|
13689
|
+
for workspace_type in workspace_types:
|
|
13690
|
+
wksp_type_id = self.get_result_value(response=workspace_type, key="wksp_type_id")
|
|
13691
|
+
wksp_type_name = self.get_result_value(response=workspace_type, key="wksp_type_name")
|
|
13692
|
+
# Overwrite (not just fill-if-missing) so a renamed workspace type does
|
|
13693
|
+
# not keep resolving to its stale old name here or via get_workspace_type_name():
|
|
13694
|
+
self._workspace_type_lookup[wksp_type_id] = wksp_type_name
|
|
13695
|
+
|
|
13696
|
+
return self._workspace_type_lookup
|
|
13697
|
+
|
|
13698
|
+
# end method definition
|
|
13699
|
+
|
|
13700
|
+
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="resolve_workspace_type_ids")
|
|
13701
|
+
def resolve_workspace_type_ids(self, workspace_types: str | int | list | None) -> set[int]:
|
|
13702
|
+
"""Resolve workspace type names and/or IDs into a set of workspace type IDs.
|
|
13703
|
+
|
|
13704
|
+
Items may be workspace type IDs (int) or workspace type names (str). Names are
|
|
13705
|
+
resolved via the workspace type lookup (self._workspace_type_lookup, populated
|
|
13706
|
+
on demand via get_workspace_type_lookup()). Unresolvable names are silently
|
|
13707
|
+
skipped.
|
|
13708
|
+
|
|
13709
|
+
Args:
|
|
13710
|
+
workspace_types (str | int | list | None):
|
|
13711
|
+
A single workspace type name or ID, or a list of them.
|
|
13712
|
+
None or empty yields an empty set.
|
|
13713
|
+
|
|
13714
|
+
Returns:
|
|
13715
|
+
set[int]:
|
|
13716
|
+
The set of resolved workspace type IDs.
|
|
13717
|
+
|
|
13718
|
+
"""
|
|
13719
|
+
|
|
13720
|
+
if not workspace_types:
|
|
13721
|
+
return set()
|
|
13722
|
+
if isinstance(workspace_types, (str, int)):
|
|
13723
|
+
workspace_types = [workspace_types]
|
|
13724
|
+
|
|
13725
|
+
name_to_id = {name: type_id for type_id, name in self.get_workspace_type_lookup().items()}
|
|
13726
|
+
|
|
13727
|
+
type_ids = set()
|
|
13728
|
+
for item in workspace_types:
|
|
13729
|
+
if isinstance(item, int):
|
|
13730
|
+
type_ids.add(item)
|
|
13731
|
+
elif item in name_to_id:
|
|
13732
|
+
type_ids.add(name_to_id[item])
|
|
13733
|
+
|
|
13734
|
+
return type_ids
|
|
13735
|
+
|
|
13736
|
+
# end method definition
|
|
13737
|
+
|
|
13738
|
+
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="resolve_workspace_type_filter")
|
|
13739
|
+
def resolve_workspace_type_filter(
|
|
13740
|
+
self,
|
|
13741
|
+
workspace_type_inclusions: str | int | list | None = None,
|
|
13742
|
+
workspace_type_exclusions: str | int | list | None = None,
|
|
13743
|
+
) -> list[int] | None:
|
|
13744
|
+
"""Resolve inclusion/exclusion workspace type filters into a list of allowed type IDs.
|
|
13745
|
+
|
|
13746
|
+
This is intended for server-side pre-filtering (e.g. the related_workspace_type_id
|
|
13747
|
+
parameter of get_workspace_relationships_iterator()). Inclusions define the base set
|
|
13748
|
+
(all workspace types if no inclusions are given), exclusions are then removed from it.
|
|
13749
|
+
|
|
13750
|
+
If an inclusion name cannot be resolved to a workspace type ID we cannot safely narrow
|
|
13751
|
+
server-side (we would risk dropping matching workspaces), so we return None so the
|
|
13752
|
+
caller can fall back to client-side filtering (see _check_filter()).
|
|
13753
|
+
|
|
13754
|
+
Args:
|
|
13755
|
+
workspace_type_inclusions (str | int | list | None):
|
|
13756
|
+
Workspace type names and/or IDs to include. None/empty = include all types.
|
|
13757
|
+
workspace_type_exclusions (str | int | list | None):
|
|
13758
|
+
Workspace type names and/or IDs to exclude.
|
|
13759
|
+
|
|
13760
|
+
Returns:
|
|
13761
|
+
list[int] | None:
|
|
13762
|
+
The list of allowed workspace type IDs, or None if the filter cannot be
|
|
13763
|
+
resolved to a safe server-side filter (caller should not narrow server-side).
|
|
13764
|
+
|
|
13765
|
+
"""
|
|
13766
|
+
|
|
13767
|
+
name_to_id = {name: type_id for type_id, name in self.get_workspace_type_lookup().items()}
|
|
13768
|
+
|
|
13769
|
+
# Normalize scalars to lists so we can inspect the inclusion names below:
|
|
13770
|
+
if isinstance(workspace_type_inclusions, (str, int)):
|
|
13771
|
+
workspace_type_inclusions = [workspace_type_inclusions]
|
|
13772
|
+
if isinstance(workspace_type_exclusions, (str, int)):
|
|
13773
|
+
workspace_type_exclusions = [workspace_type_exclusions]
|
|
13774
|
+
|
|
13775
|
+
# Inclusions define the base set (all types if no inclusions are given),
|
|
13776
|
+
# exclusions are then removed from it:
|
|
13777
|
+
included_ids = (
|
|
13778
|
+
self.resolve_workspace_type_ids(workspace_type_inclusions)
|
|
13779
|
+
if workspace_type_inclusions
|
|
13780
|
+
else set(name_to_id.values())
|
|
13781
|
+
)
|
|
13782
|
+
excluded_ids = self.resolve_workspace_type_ids(workspace_type_exclusions)
|
|
13783
|
+
allowed_ids = included_ids - excluded_ids
|
|
13784
|
+
|
|
13785
|
+
# If an inclusion name could not be resolved to a type ID we cannot safely
|
|
13786
|
+
# narrow server-side (we would risk dropping matching workspaces):
|
|
13787
|
+
unresolved_names = (
|
|
13788
|
+
[item for item in workspace_type_inclusions if isinstance(item, str) and item not in name_to_id]
|
|
13789
|
+
if workspace_type_inclusions
|
|
13790
|
+
else []
|
|
13791
|
+
)
|
|
13792
|
+
if unresolved_names:
|
|
13793
|
+
self.logger.warning(
|
|
13794
|
+
"Cannot resolve workspace type name(s) -> %s to a workspace type ID "
|
|
13795
|
+
"(available types -> %s). Falling back to client-side filtering.",
|
|
13796
|
+
unresolved_names,
|
|
13797
|
+
sorted(name_to_id.keys()),
|
|
13798
|
+
)
|
|
13799
|
+
return None
|
|
13800
|
+
|
|
13801
|
+
if not allowed_ids:
|
|
13802
|
+
self.logger.debug(
|
|
13803
|
+
"Workspace type filter (inclusions -> %s, exclusions -> %s) resolved to an empty set of allowed "
|
|
13804
|
+
"types. Falling back to client-side filtering.",
|
|
13805
|
+
workspace_type_inclusions,
|
|
13806
|
+
workspace_type_exclusions,
|
|
13807
|
+
)
|
|
13808
|
+
return None
|
|
13809
|
+
|
|
13810
|
+
return list(allowed_ids)
|
|
13811
|
+
|
|
13812
|
+
# end method definition
|
|
13813
|
+
|
|
13642
13814
|
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="update_workspace_type_relations")
|
|
13643
13815
|
def update_workspace_type_relations(
|
|
13644
13816
|
self,
|
|
@@ -15220,7 +15392,7 @@ class OTCS:
|
|
|
15220
15392
|
parse_error_response=parse_error_response,
|
|
15221
15393
|
)
|
|
15222
15394
|
|
|
15223
|
-
if "error" in response:
|
|
15395
|
+
if response and "error" in response:
|
|
15224
15396
|
return response
|
|
15225
15397
|
|
|
15226
15398
|
node_id = self.get_result_value(response=response, key="id")
|
|
@@ -24416,6 +24588,31 @@ class OTCS:
|
|
|
24416
24588
|
if isinstance(workspace_type_inclusions, str):
|
|
24417
24589
|
workspace_type_inclusions = [workspace_type_inclusions]
|
|
24418
24590
|
|
|
24591
|
+
# Precompute the full workspace type lookup (type_id -> name) up-front
|
|
24592
|
+
# (renew=True to pick up any renamed types on every traversal run). This lets us:
|
|
24593
|
+
# 1. Resolve the server-side related workspace type filter deterministically
|
|
24594
|
+
# before any worker starts (no race with the queue initializer thread).
|
|
24595
|
+
# 2. Have get_workspace_type_name() calls during traversal hit the cache.
|
|
24596
|
+
# 3. Iterate the workspace types for queue initialization without an
|
|
24597
|
+
# extra REST round-trip.
|
|
24598
|
+
self.get_workspace_type_lookup(renew=True)
|
|
24599
|
+
|
|
24600
|
+
# Resolve the server-side related workspace type filter once, now that the
|
|
24601
|
+
# full workspace type lookup is available. When filter_at_traversal is False
|
|
24602
|
+
# we keep this None so the relationship iterator is not narrowed server-side:
|
|
24603
|
+
# passing the full list of all workspace type IDs would only slow down the
|
|
24604
|
+
# server-side query without excluding anything (the REST API then returns all
|
|
24605
|
+
# related workspaces - the previous behavior). When it is True we resolve the
|
|
24606
|
+
# inclusion/exclusion filters (which may contain type names or type IDs) into a
|
|
24607
|
+
# concrete list of allowed workspace type IDs so the server pre-filters. The
|
|
24608
|
+
# client-side _check_filter() still runs as a safety net.
|
|
24609
|
+
related_type_id_filter: list[int] | None = None
|
|
24610
|
+
if filter_at_traversal:
|
|
24611
|
+
related_type_id_filter = self.resolve_workspace_type_filter(
|
|
24612
|
+
workspace_type_inclusions=workspace_type_inclusions,
|
|
24613
|
+
workspace_type_exclusions=workspace_type_exclusions,
|
|
24614
|
+
)
|
|
24615
|
+
|
|
24419
24616
|
lock = threading.Lock()
|
|
24420
24617
|
if strategy == "BFS":
|
|
24421
24618
|
task_queue = Queue()
|
|
@@ -24435,19 +24632,11 @@ class OTCS:
|
|
|
24435
24632
|
|
|
24436
24633
|
self.logger.debug("Initialize traversal queue...")
|
|
24437
24634
|
|
|
24438
|
-
# Enqueue initial nodes at depth 0
|
|
24439
|
-
|
|
24440
|
-
|
|
24441
|
-
|
|
24442
|
-
|
|
24443
|
-
# Refresh (not just fill-if-missing): this is a fresh, current listing of
|
|
24444
|
-
# workspace types, fetched on every traversal run. self._workspace_type_lookup
|
|
24445
|
-
# is a long-lived cache on this OTCS instance (spans many traversal runs over the
|
|
24446
|
-
# life of the process), so if we only filled in missing entries, a renamed
|
|
24447
|
-
# workspace type would keep resolving to its stale old name here - and via
|
|
24448
|
-
# get_workspace_type_name() - forever. Overwriting is free since we already have
|
|
24449
|
-
# the current name from this iteration; no extra API call is needed.
|
|
24450
|
-
self._workspace_type_lookup[wksp_type_id] = {"location": None, "name": wksp_type_name}
|
|
24635
|
+
# Enqueue initial nodes at depth 0. We reuse the workspace type lookup
|
|
24636
|
+
# (self._workspace_type_lookup) that was fully populated before the
|
|
24637
|
+
# workers started, so we neither re-query the REST API nor need to
|
|
24638
|
+
# repopulate the lookup here:
|
|
24639
|
+
for wksp_type_id, wksp_type_name in self._workspace_type_lookup.items():
|
|
24451
24640
|
if not self._check_filter(
|
|
24452
24641
|
workspace_type_name=wksp_type_name,
|
|
24453
24642
|
workspace_type_id=wksp_type_id,
|
|
@@ -24589,7 +24778,11 @@ class OTCS:
|
|
|
24589
24778
|
for rel_type in relationship_types:
|
|
24590
24779
|
# Get related workspaces of the current workspace and the current relationship type:
|
|
24591
24780
|
workspace_relationships = self.get_workspace_relationships_iterator(
|
|
24592
|
-
workspace_id=workspace_id,
|
|
24781
|
+
workspace_id=workspace_id,
|
|
24782
|
+
relationship_type=rel_type,
|
|
24783
|
+
related_workspace_type_id=related_type_id_filter,
|
|
24784
|
+
fields=fields,
|
|
24785
|
+
metadata=metadata,
|
|
24593
24786
|
)
|
|
24594
24787
|
|
|
24595
24788
|
# Traverse all related workspaces:
|
|
@@ -957,6 +957,100 @@ class OTDS:
|
|
|
957
957
|
|
|
958
958
|
# end method definition
|
|
959
959
|
|
|
960
|
+
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="authenticate_user_token")
|
|
961
|
+
def authenticate_user_token(
|
|
962
|
+
self,
|
|
963
|
+
username: str,
|
|
964
|
+
password: str,
|
|
965
|
+
scope: str = "otds:roles",
|
|
966
|
+
) -> str | None:
|
|
967
|
+
"""Authenticate a user via the OAuth2 password grant and return a JWT access token.
|
|
968
|
+
|
|
969
|
+
Unlike :meth:`authenticate` with ``grant_type="password"`` (which hits the
|
|
970
|
+
legacy credential endpoint and returns an OTDS SSO ticket), this method posts
|
|
971
|
+
``grant_type=password`` together with the configured OAuth client credentials to
|
|
972
|
+
the OAuth2 token endpoint (``/otdsws/oauth2/token``). This produces a signed
|
|
973
|
+
JWT access token that can be used as a standard ``Authorization: Bearer`` token.
|
|
974
|
+
|
|
975
|
+
The ``client_id`` and ``client_secret`` must be configured on the OTDS object
|
|
976
|
+
(passed to the constructor) before calling this method.
|
|
977
|
+
|
|
978
|
+
Args:
|
|
979
|
+
username (str):
|
|
980
|
+
The OTDS login name of the user to authenticate.
|
|
981
|
+
password (str):
|
|
982
|
+
The password for the OTDS user.
|
|
983
|
+
scope (str, optional):
|
|
984
|
+
OAuth2 scope string to request. Defaults to ``"otds:roles"``.
|
|
985
|
+
|
|
986
|
+
Returns:
|
|
987
|
+
str | None:
|
|
988
|
+
The JWT access token, or ``None`` if authentication fails.
|
|
989
|
+
The token is also stored in ``self._token``.
|
|
990
|
+
|
|
991
|
+
Example:
|
|
992
|
+
'eyJraWQiOiJvdGRzLXNpZ25pbmcta2V5IiwiYWxnIjoiUlMyNTYifQ...'
|
|
993
|
+
|
|
994
|
+
"""
|
|
995
|
+
|
|
996
|
+
request_url = self.token_url()
|
|
997
|
+
|
|
998
|
+
request_data = {
|
|
999
|
+
"grant_type": "password",
|
|
1000
|
+
"client_id": self.config()["clientId"],
|
|
1001
|
+
"client_secret": self.config()["clientSecret"],
|
|
1002
|
+
"username": username,
|
|
1003
|
+
"password": password,
|
|
1004
|
+
"scope": scope,
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
self.logger.debug(
|
|
1008
|
+
"Requesting OTDS user access token for -> '%s'; calling -> %s",
|
|
1009
|
+
username,
|
|
1010
|
+
request_url,
|
|
1011
|
+
)
|
|
1012
|
+
|
|
1013
|
+
try:
|
|
1014
|
+
response = requests.post(
|
|
1015
|
+
url=request_url,
|
|
1016
|
+
headers=REQUEST_FORM_HEADERS,
|
|
1017
|
+
data=request_data,
|
|
1018
|
+
timeout=REQUEST_TIMEOUT,
|
|
1019
|
+
)
|
|
1020
|
+
except requests.exceptions.RequestException as exception:
|
|
1021
|
+
self.logger.warning(
|
|
1022
|
+
"Unable to connect to OTDS token endpoint -> %s; error -> %s",
|
|
1023
|
+
request_url,
|
|
1024
|
+
str(exception),
|
|
1025
|
+
)
|
|
1026
|
+
return None
|
|
1027
|
+
|
|
1028
|
+
if not response.ok:
|
|
1029
|
+
self.logger.error(
|
|
1030
|
+
"Failed to request user access token for -> '%s'; error -> %s",
|
|
1031
|
+
username,
|
|
1032
|
+
response.text,
|
|
1033
|
+
)
|
|
1034
|
+
return None
|
|
1035
|
+
|
|
1036
|
+
token_dict = self.parse_request_response(response)
|
|
1037
|
+
if not token_dict:
|
|
1038
|
+
return None
|
|
1039
|
+
|
|
1040
|
+
access_token = token_dict.get("access_token")
|
|
1041
|
+
if not access_token:
|
|
1042
|
+
self.logger.error(
|
|
1043
|
+
"OTDS token response for user -> '%s' did not contain an access_token",
|
|
1044
|
+
username,
|
|
1045
|
+
)
|
|
1046
|
+
return None
|
|
1047
|
+
|
|
1048
|
+
self._token = access_token
|
|
1049
|
+
self._cookie = None
|
|
1050
|
+
return self._token
|
|
1051
|
+
|
|
1052
|
+
# end method definition
|
|
1053
|
+
|
|
960
1054
|
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="impersonate_user")
|
|
961
1055
|
def impersonate_user(
|
|
962
1056
|
self,
|
|
@@ -28177,7 +28177,9 @@ class Payload:
|
|
|
28177
28177
|
attribute_name=att_name,
|
|
28178
28178
|
set_name=set_name,
|
|
28179
28179
|
)
|
|
28180
|
-
|
|
28180
|
+
# Compare as strings to be type-tolerant: 'key' is always a string
|
|
28181
|
+
# while a numeric category attribute value is returned as an int/float.
|
|
28182
|
+
if str(key) == str(conflicting_key):
|
|
28181
28183
|
# We have a race condition as the two documents don't really clash but are identical.
|
|
28182
28184
|
# Just skip uploading the same document once more.
|
|
28183
28185
|
self.logger.warning(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|