pyxecm 2.0.2__py3-none-any.whl → 2.0.3__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/otcs.py CHANGED
@@ -148,6 +148,24 @@ class OTCS:
148
148
  ITEM_TYPE_WORKFLOW_MAP = 128
149
149
  ITEM_TYPE_WORKFLOW_STATUS = 190
150
150
 
151
+ PERMISSION_TYPES = [
152
+ "see",
153
+ "see_contents",
154
+ "modify",
155
+ "edit_attributes",
156
+ "add_items",
157
+ "reserve",
158
+ "add_major_version",
159
+ "delete_versions",
160
+ "delete",
161
+ "edit_permissions",
162
+ ]
163
+ PERMISSION_ASSIGNEE_TYPES = [
164
+ "owner",
165
+ "group",
166
+ "public",
167
+ "custom",
168
+ ]
151
169
  _config: dict
152
170
  _otcs_ticket = None
153
171
  _otds_ticket = None
@@ -858,6 +876,9 @@ class OTCS:
858
876
  # a cookie that is in process of being renewed
859
877
  # by another thread:
860
878
  with self._session_lock:
879
+ if not self.cookie():
880
+ self.logger.error("Cannot call -> %s - user is not authenticatd!", url)
881
+ return None
861
882
  # IMPORTANT: this needs to be a copy - dicts are mutable and
862
883
  # we need to preserve the old value to detect in reauthenticate()
863
884
  # if the cookie has been renewed already or not:
@@ -1671,11 +1692,8 @@ class OTCS:
1671
1692
  "Requesting OTCS ticket with existing OTDS ticket; calling -> %s",
1672
1693
  request_url,
1673
1694
  )
1674
- request_header = {
1675
- "Content-Type": "application/x-www-form-urlencoded",
1676
- "Accept": "application/json",
1677
- "OTDSTicket": self._otds_ticket,
1678
- }
1695
+ # Add the OTDS ticket to the request headers:
1696
+ request_header = REQUEST_FORM_HEADERS | {"OTDSTicket": self._otds_ticket}
1679
1697
 
1680
1698
  try:
1681
1699
  response = requests.get(
@@ -1923,7 +1941,7 @@ class OTCS:
1923
1941
  """
1924
1942
 
1925
1943
  request_url = self.config()["serverInfoUrl"]
1926
- request_header = self.request_form_header() # self.cookie()
1944
+ request_header = self.request_form_header()
1927
1945
 
1928
1946
  self.logger.debug(
1929
1947
  "Retrieve Content Server information; calling -> %s",
@@ -2019,12 +2037,18 @@ class OTCS:
2019
2037
  # end method definition
2020
2038
 
2021
2039
  @cache
2022
- def get_user(self, name: str, show_error: bool = False) -> dict | None:
2040
+ def get_user(self, name: str, user_type: int = 0, show_error: bool = False) -> dict | None:
2023
2041
  """Look up an Content Server user based on the login name.
2024
2042
 
2025
2043
  Args:
2026
2044
  name (str):
2027
2045
  Name of the user (login).
2046
+ user_type (int, optional):
2047
+ Type ID of user:
2048
+ 0 - Regular User
2049
+ 17 - Service User
2050
+ Defaults to 0 -> (Regular User)
2051
+
2028
2052
  show_error (bool, optional):
2029
2053
  If True, treat as an error if the user is not found. Defaults to False.
2030
2054
 
@@ -2082,8 +2106,8 @@ class OTCS:
2082
2106
  """
2083
2107
 
2084
2108
  # Add query parameters (these are NOT passed via JSon body!)
2085
- # type = 0 ==> User
2086
- query = {"where_type": 0, "where_name": name}
2109
+ # type = 0 ==> regular User
2110
+ query = {"where_type": user_type, "where_name": name}
2087
2111
  encoded_query = urllib.parse.urlencode(query=query, doseq=True)
2088
2112
  request_url = self.config()["membersUrlv2"] + "?{}".format(encoded_query)
2089
2113
 
@@ -2122,18 +2146,26 @@ class OTCS:
2122
2146
  """Add Content Server user.
2123
2147
 
2124
2148
  Args:
2125
- name (str): login name of the user
2126
- password (str): password of the user
2127
- first_name (str): first name of the user
2128
- last_name (str): last name of the user
2129
- email (str): email address of the user
2130
- title (str): title of the user
2131
- base_group (int): base group id of the user (e.g. department)
2149
+ name (str):
2150
+ The login name of the user.
2151
+ password (str):
2152
+ The password of the user.
2153
+ first_name (str):
2154
+ The first name of the user.
2155
+ last_name (str):
2156
+ The last name of the user.
2157
+ email (str):
2158
+ The email address of the user.
2159
+ title (str):
2160
+ The title of the user.
2161
+ base_group (int):
2162
+ The base group id of the user (e.g. department)
2132
2163
  privileges (list, optional):
2133
2164
  Possible values are Login, Public Access, Content Manager,
2134
2165
  Modify Users, Modify Groups, User Admin Rights,
2135
2166
  Grant Discovery, System Admin Rights
2136
- user_type (int, optional): id of user_type 0-User, 17-ServiceUser, ...
2167
+ user_type (int, optional):
2168
+ The ID of the user type. 0 = regular user, 17 = service user.
2137
2169
 
2138
2170
  Returns:
2139
2171
  dict | None:
@@ -2268,9 +2300,12 @@ class OTCS:
2268
2300
  """Update a defined field for a user.
2269
2301
 
2270
2302
  Args:
2271
- user_id (int): ID of the user
2272
- value (str): field value
2273
- field (str): user field
2303
+ user_id (int):
2304
+ The ID of the user to update.
2305
+ field (str):
2306
+ The user data field to update.
2307
+ value (str):
2308
+ The new value for user data field.
2274
2309
 
2275
2310
  Returns:
2276
2311
  dict | None:
@@ -3669,7 +3704,8 @@ class OTCS:
3669
3704
  """Get a node based on the workspace ID (= node ID) and path (list of folder names).
3670
3705
 
3671
3706
  Args:
3672
- workspace_id (int): node ID of the workspace
3707
+ workspace_id (int):
3708
+ The node ID of the workspace.
3673
3709
  path (list):
3674
3710
  A list of container items (top down).
3675
3711
  The last item is name of to be retrieved item.
@@ -3871,8 +3907,10 @@ class OTCS:
3871
3907
  """Get a node based on the nickname.
3872
3908
 
3873
3909
  Args:
3874
- nickname (str): The nickname of the node.
3875
- show_error (bool): If True, treat as error if node is not found.
3910
+ nickname (str):
3911
+ The nickname of the node.
3912
+ show_error (bool):
3913
+ If True, treat as error if node is not found.
3876
3914
 
3877
3915
  Returns:
3878
3916
  dict | None:
@@ -4315,7 +4353,7 @@ class OTCS:
4315
4353
  The name of the attribute that includes the value to match with
4316
4354
  value (str):
4317
4355
  The lookup value that is matched agains the node attribute value.
4318
- attribute_set (str, optional):
4356
+ attribute_set (str | None, optional):
4319
4357
  The name of the attribute set
4320
4358
 
4321
4359
  Returns:
@@ -4343,7 +4381,7 @@ class OTCS:
4343
4381
  )
4344
4382
  if not category_schema:
4345
4383
  self.logger.debug(
4346
- "Node -> '%s' (%s) does not have category -> '%s'. Cannot lookup -> '%s'. Skipping...",
4384
+ "Node -> '%s' (%s) does not have category -> '%s'. Cannot lookup value -> '%s'. Skipping...",
4347
4385
  node_name,
4348
4386
  node_id,
4349
4387
  category,
@@ -4365,6 +4403,8 @@ class OTCS:
4365
4403
  )
4366
4404
  continue
4367
4405
  attribute_key = attribute_schema["key"]
4406
+ # Split the attribute key once (1) at the first underscore from the right.
4407
+ # rsplit delivers a list and [-1] delivers the last list item:
4368
4408
  attribute_id = attribute_key.rsplit("_", 1)[-1]
4369
4409
 
4370
4410
  if attribute_set:
@@ -4399,6 +4439,7 @@ class OTCS:
4399
4439
  attribute_value = cat_data.get(key)
4400
4440
  if not attribute_value:
4401
4441
  break
4442
+ # Is it a multi-value attribute (i.e. a list of values)?
4402
4443
  if isinstance(attribute_value, list):
4403
4444
  if value in attribute_value:
4404
4445
  # Create a "results" dict that is compatible with normal REST calls
@@ -4437,89 +4478,6 @@ class OTCS:
4437
4478
 
4438
4479
  # end method definition
4439
4480
 
4440
- def lookup_node_old(
4441
- self,
4442
- parent_node_id: int,
4443
- category: str,
4444
- attribute: str,
4445
- value: str,
4446
- ) -> dict | None:
4447
- """Lookup the node under a parent node that has a specified value in a category attribute.
4448
-
4449
- Args:
4450
- parent_node_id (int):
4451
- The node ID of the parent (typically folder or workspace).
4452
- category (str):
4453
- The name of the category.
4454
- attribute (str):
4455
- The name of the attribute that includes the value to match with
4456
- value (str):
4457
- The lookup value that is matched agains the node attribute value.
4458
-
4459
- Returns:
4460
- dict | None:
4461
- Node wrapped in dictionary with "results" key or None if the REST API fails.
4462
-
4463
- """
4464
-
4465
- # get_subnodes_iterator() returns a python generator that we use for iterating over all nodes
4466
- # in an efficient way avoiding to retrieve all nodes at once (which could be a large number):
4467
- for node in self.get_subnodes_iterator(
4468
- parent_node_id=parent_node_id,
4469
- fields=["properties", "categories"],
4470
- metadata=True,
4471
- ):
4472
- schema = node["metadata"]["categories"]
4473
- data = node["data"]["categories"]
4474
- for cat_data, cat_schema in zip(data, schema, strict=False):
4475
- data_values = list(cat_data.values())
4476
- schema_values = list(cat_schema.values())
4477
- # Schema has one additional element (the first one) representing
4478
- # the category object itself. This includes the name. We need
4479
- # to remove (pop) it from the schema list to make sure the schema list
4480
- # and the data list have the same number of items. Otherwise
4481
- # the following for loop with zip() would not properly align the
4482
- # two lists:
4483
- category_name = schema_values.pop(0)["name"]
4484
- # Set attributes (standing for the set itself, not it's contained attributes)
4485
- # are only in the schema values, not in the data values. We need to remove
4486
- # them as well to avoid mis-alignment:
4487
- schema_values = [schema_value for schema_value in schema_values if schema_value.get("persona") != "set"]
4488
- if category_name == category:
4489
- for attr_data, attr_schema in zip(
4490
- data_values,
4491
- schema_values,
4492
- strict=False,
4493
- ):
4494
- attr_name = attr_schema["name"]
4495
- if attr_name == attribute:
4496
- if isinstance(attr_data, list):
4497
- if value in attr_data:
4498
- # Create a "results" dict that is compatible with normal REST calls
4499
- # to not break get_result_value() method that may be called on the result:
4500
- return {"results": node}
4501
- elif value == attr_data:
4502
- # Create a results dict that is compatible with normal REST calls
4503
- # to not break get_result_value() method that may be called on the result:
4504
- return {"results": node}
4505
- # we can break here and continue with the next node
4506
- # as we had the right category but did not find the matching value
4507
- break
4508
- # end for cat_data, cat_schema in zip(data, schema)
4509
- # end for node in nodes
4510
-
4511
- self.logger.debug(
4512
- "Couldn't find a node with the value -> '%s' in the attribute -> '%s' of category -> '%s' in parent with node ID -> %s.",
4513
- value,
4514
- attribute,
4515
- category,
4516
- parent_node_id,
4517
- )
4518
-
4519
- return None
4520
-
4521
- # end method definition
4522
-
4523
4481
  def lookup_node_by_regex(
4524
4482
  self,
4525
4483
  parent_node_id: int,
@@ -4861,13 +4819,18 @@ class OTCS:
4861
4819
  node_id (int):
4862
4820
  ID of the node. You can use the get_volume() function below to
4863
4821
  to the node id for a volume.
4864
- name (str): New name of the node.
4865
- description (str): New description of the node.
4866
- name_multilingual (dict, optional): multi-lingual node names
4867
- description_multilingual (dict, optional): multi-lingual description
4822
+ name (str):
4823
+ New name of the node.
4824
+ description (str):
4825
+ New description of the node.
4826
+ name_multilingual (dict | None, optional):
4827
+ The multi-lingual node names.
4828
+ description_multilingual (dict | None, optional):
4829
+ The multi-lingual descriptions.
4868
4830
 
4869
4831
  Returns:
4870
- dict | None: Request response or None if the renaming fails.
4832
+ dict | None:
4833
+ Request response or None if the renaming fails.
4871
4834
 
4872
4835
  """
4873
4836
 
@@ -5044,6 +5007,285 @@ class OTCS:
5044
5007
 
5045
5008
  # end method definition
5046
5009
 
5010
+ def get_node_audit(
5011
+ self,
5012
+ node_id: int,
5013
+ filter_event_type: int | None = None,
5014
+ filter_user_id: int | None = None,
5015
+ filter_date_start: str | None = None,
5016
+ filter_date_end: str | None = None,
5017
+ limit: int = 100,
5018
+ page: int = 1,
5019
+ sort: str = "desc_audit_date",
5020
+ ) -> dict | None:
5021
+ """Get the audit information for a given node ID.
5022
+
5023
+ Args:
5024
+ node_id (int):
5025
+ The ID of the node to get the audit for.
5026
+ filter_event_type (int | None, optional):
5027
+ Type of audit events to filter by. Possible values:
5028
+ - 9 : Permission Changed
5029
+ - 10 : Attribute Value Changed
5030
+ - 92 : Create from Copy
5031
+ - 264 : Classification Applied
5032
+ - 301 : Deployed from Warehouse
5033
+ - 416 : XML Import
5034
+ - 6000 : Content Sharing - Shared with external system
5035
+ - 6014 : Content Sharing - Share Coordinator changed
5036
+ - ...
5037
+ filter_user_id (int, optional):
5038
+ Filter audit events by user ID. Defaults to no filter.
5039
+ The date should be provided in YYYY-MM-DD notation. Time
5040
+ is not considered (only days)
5041
+ filter_date_start (str | None, optional):
5042
+ Filter audit events by start date. Defaults to no filter.
5043
+ The date should be provided in YYYY-MM-DD notation. Time
5044
+ is not considered (only days)
5045
+ filter_date_end (str | None, optional):
5046
+ Filter audit events by end date. Defaults to no filter.
5047
+ limit (int, optional):
5048
+ The maximum number of results to return. Defaults to 100.
5049
+ page (int, optional):
5050
+ The page of results to retrieve. Defaults to 1 (first page).
5051
+ sort (str, optional):
5052
+ Sort order of audit results. Format can be sort=desc_audit_date or sort=asc_audit_date.
5053
+ Results are sorted in descending order by default.
5054
+
5055
+ Returns:
5056
+ dict | None:
5057
+ Subnode information as a dictionary, or None if no nodes with
5058
+ the given parent ID are found.
5059
+
5060
+ Example:
5061
+ {
5062
+ 'collection': {
5063
+ 'paging': {
5064
+ 'limit': 100,
5065
+ 'page': 1,
5066
+ 'page_total': 1,
5067
+ 'range_max': 23,
5068
+ 'range_min': 1,
5069
+ 'total_count': 23
5070
+ },
5071
+ 'sorting': {
5072
+ 'sort': [
5073
+ {
5074
+ 'key': 'sort',
5075
+ 'value': 'desc_audit_date'
5076
+ }
5077
+ ]
5078
+ }
5079
+ },
5080
+ 'links': {
5081
+ 'data': {
5082
+ 'self': {
5083
+ 'body': '',
5084
+ 'content_type': '',
5085
+ 'href': '/api/v2/nodes/29572/audit?fields=properties&limit=100&sort=desc_audit_date',
5086
+ 'method': 'GET',
5087
+ 'name': ''
5088
+ }
5089
+ }
5090
+ },
5091
+ 'results': {
5092
+ 'data': {
5093
+ 'audit': [
5094
+ {
5095
+ 'id': 29572,
5096
+ 'event_type': 6000,
5097
+ 'audit_date': '2025-05-23T10:20:56Z',
5098
+ 'user_id': 8306,
5099
+ 'agent_id': None,
5100
+ 'audit_language_code': None,
5101
+ 'target_user_id': None,
5102
+ 'audit_name': 'Shared with Microsoft Teams Content Sharing Provider'
5103
+ },
5104
+ ...
5105
+ ],
5106
+ 'audit_event_types': [
5107
+ {
5108
+ 'id': 92,
5109
+ 'name': 'Create from Copy'
5110
+ },
5111
+ {
5112
+ 'id': 6014,
5113
+ 'name': 'Content Sharing - Share Coordinators Changed'
5114
+ },
5115
+ {
5116
+ 'id': 301,
5117
+ 'name': 'Deployed from Warehouse'
5118
+ },
5119
+ ...
5120
+ ]
5121
+ }
5122
+ }
5123
+ }
5124
+
5125
+ """
5126
+
5127
+ # Add query parameters (these are NOT passed via JSon body!)
5128
+ query = {"limit": limit, "sort": sort}
5129
+ if filter_event_type:
5130
+ query["where_type"] = filter_event_type
5131
+ if filter_user_id:
5132
+ query["where_user_id"] = filter_user_id
5133
+ if filter_date_start:
5134
+ query["where_audit_date_start"] = filter_date_start
5135
+ if filter_date_end:
5136
+ query["where_audit_date_end"] = filter_date_end
5137
+ if page > 1:
5138
+ query["page"] = page
5139
+
5140
+ encoded_query = urllib.parse.urlencode(query=query, doseq=True)
5141
+
5142
+ request_url = self.config()["nodesUrlv2"] + "/" + str(node_id) + "/audit" + "?{}".format(encoded_query)
5143
+
5144
+ request_header = self.request_form_header()
5145
+
5146
+ self.logger.debug(
5147
+ "Get audit of node with ID -> %s (page -> %d, item limit -> %d); calling -> %s",
5148
+ str(node_id),
5149
+ page,
5150
+ limit,
5151
+ request_url,
5152
+ )
5153
+
5154
+ return self.do_request(
5155
+ url=request_url,
5156
+ method="GET",
5157
+ headers=request_header,
5158
+ timeout=None,
5159
+ failure_message="Failed to get audit for node with ID -> {}".format(
5160
+ node_id,
5161
+ ),
5162
+ )
5163
+
5164
+ # end method definition
5165
+
5166
+ def get_node_audit_iterator(
5167
+ self,
5168
+ node_id: int,
5169
+ filter_event_type: int | None = None,
5170
+ filter_user_id: int | None = None,
5171
+ filter_date_start: str | None = None,
5172
+ filter_date_end: str | None = None,
5173
+ page_size: int = 25,
5174
+ sort: str = "desc_audit_date",
5175
+ ) -> iter:
5176
+ """Get an iterator object that can be used to traverse subnodes.
5177
+
5178
+ Filters can be applied that are given by the "filter" parameters.
5179
+
5180
+ Using a generator avoids loading a large number of nodes into memory at once.
5181
+ Instead you can iterate over the potential large list of subnodes.
5182
+
5183
+ Example usage:
5184
+ ```python
5185
+ audit_entries = otcs_object.get_node_audit_iterator(node_id=15838)
5186
+ for audit_entry in audit_entries:
5187
+ logger.info("Audit entry -> '%s'", ...)
5188
+ ```
5189
+
5190
+ Args:
5191
+ node_id (int):
5192
+ The ID of the node to get the audit for.
5193
+ filter_event_type (int, optional):
5194
+ Type of audit events to filter by. Possible values:
5195
+ - 9 : Permission Changed
5196
+ - 10 : Attribute Value Changed
5197
+ - 92 : Create from Copy
5198
+ - 264 : Classification Applied
5199
+ - 301 : Deployed from Warehouse
5200
+ - 416 : XML Import
5201
+ - 6000 : Content Sharing - Shared with external system
5202
+ - 6014 : Content Sharing - Share Coordinator changed
5203
+ - ...
5204
+ filter_user_id (int, optional):
5205
+ Filter audit events by user ID. Defaults to no filter.
5206
+ The date should be provided in YYYY-MM-DD notation. Time
5207
+ is not considered (only days)
5208
+ filter_date_start (str, optional):
5209
+ Filter audit events by start date. Defaults to no filter.
5210
+ The date should be provided in YYYY-MM-DD notation. Time
5211
+ is not considered (only days)
5212
+ filter_date_end (str, optional):
5213
+ Filter audit events by end date. Defaults to no filter.
5214
+ limit (int, optional):
5215
+ The maximum number of results to return. Defaults to 100.
5216
+ page (int, optional):
5217
+ The page of results to retrieve. Defaults to 1 (first page).
5218
+ sort (str, optional):
5219
+ Sort order of audit results. Format can be sort=desc_audit_date or sort=asc_audit_date.
5220
+ Results are sorted in descending order by default.
5221
+ page_size (int, optional):
5222
+ The number of subnodes that are requested per page.
5223
+ For the iterator this is basically the chunk size.
5224
+
5225
+ Returns:
5226
+ iter:
5227
+ A generator yielding one node per iteration under the parent.
5228
+ If the REST API fails, returns no value.
5229
+
5230
+ """
5231
+
5232
+ response = self.get_node_audit(
5233
+ node_id=node_id,
5234
+ filter_event_type=filter_event_type,
5235
+ filter_user_id=filter_user_id,
5236
+ filter_date_start=filter_date_start,
5237
+ filter_date_end=filter_date_end,
5238
+ )
5239
+ if (
5240
+ not response
5241
+ or "collection" not in response
5242
+ or "paging" not in response["collection"]
5243
+ or not response["collection"]["paging"].get("total_count")
5244
+ ):
5245
+ self.logger.debug(
5246
+ "Item with node ID -> %s has no audit information! Cannot iterate audit.",
5247
+ str(node_id),
5248
+ )
5249
+ # Don't return None! Plain return is what we need for iterators.
5250
+ # Natural Termination: If the generator does not yield, it behaves
5251
+ # like an empty iterable when used in a loop or converted to a list:
5252
+ return
5253
+
5254
+ audit_size = response["collection"]["paging"]["total_count"]
5255
+
5256
+ # If the container has many items we need to go through all pages
5257
+ # Adding page_size - 1 ensures that any remainder from the division is
5258
+ # accounted for, effectively rounding up. Integer division (//) performs floor division,
5259
+ # giving the desired number of pages:
5260
+ total_pages = (audit_size + page_size - 1) // page_size
5261
+
5262
+ for page in range(1, total_pages + 1):
5263
+ # Get the next page of sub node items:
5264
+ response = self.get_node_audit(
5265
+ node_id=node_id,
5266
+ filter_event_type=filter_event_type,
5267
+ filter_user_id=filter_user_id,
5268
+ filter_date_start=filter_date_start,
5269
+ filter_date_end=filter_date_end,
5270
+ limit=page_size,
5271
+ page=page,
5272
+ sort=sort,
5273
+ )
5274
+ if not response or not response.get("results", None):
5275
+ self.logger.warning(
5276
+ "Failed to retrieve audit for node ID -> %d (page -> %d)",
5277
+ node_id,
5278
+ page,
5279
+ )
5280
+ return None
5281
+
5282
+ # Yield nodes one at a time
5283
+ yield from response["results"]["data"]["audit"]
5284
+
5285
+ # end for page in range(1, total_pages + 1)
5286
+
5287
+ # end method definition
5288
+
5047
5289
  def get_volumes(self) -> dict | None:
5048
5290
  """Get all Volumes.
5049
5291
 
@@ -5123,11 +5365,14 @@ class OTCS:
5123
5365
  """Get Volume information based on the volume type ID.
5124
5366
 
5125
5367
  Args:
5126
- volume_type (int): ID of the volume type
5127
- timeout (int, optional): timeout for the request in seconds
5368
+ volume_type (int):
5369
+ The ID of the volume type.
5370
+ timeout (int, optional):
5371
+ The timeout for the request in seconds.
5128
5372
 
5129
5373
  Returns:
5130
- dict | None: Volume Details or None if volume is not found.
5374
+ dict | None:
5375
+ Volume details or None if volume is not found.
5131
5376
 
5132
5377
  Example:
5133
5378
  ["results"]["data"]["properties"]["id"] is the node ID of the volume.
@@ -5411,7 +5656,7 @@ class OTCS:
5411
5656
  "12508_9": "MS Word", # Text drop-down
5412
5657
  }
5413
5658
  }
5414
- classifications (list):
5659
+ classifications (list | None, optional):
5415
5660
  List of classification item IDs to apply to the new item.
5416
5661
  description (str, optional):
5417
5662
  A description of the document.
@@ -5972,13 +6217,16 @@ class OTCS:
5972
6217
  """Get document content from Extended ECM and read content as JSON.
5973
6218
 
5974
6219
  Args:
5975
- node_id (int): The node ID of the document to download
5976
- version_number (str, optional): The version of the document to download.
5977
- If version = "" then download the latest
5978
- version.
6220
+ node_id (int):
6221
+ The node ID of the document to download
6222
+ version_number (str, optional):
6223
+ The version of the document to download.
6224
+ If version = "" then download the latest
6225
+ version.
5979
6226
 
5980
6227
  Returns:
5981
- list | dict | None: Content of the file or None in case of an error.
6228
+ list | dict | None:
6229
+ Content of the file or None in case of an error.
5982
6230
 
5983
6231
  """
5984
6232
 
@@ -6528,11 +6776,14 @@ class OTCS:
6528
6776
  """Get Extended ECM external system connection (e.g. SAP, Salesforce, SuccessFactors).
6529
6777
 
6530
6778
  Args:
6531
- connection_name (str): Name of the connection
6532
- show_error (bool, optional): If True, treat as error if connection is not found.
6779
+ connection_name (str):
6780
+ The name of the connection to an external system.
6781
+ show_error (bool, optional):
6782
+ If True, treat as error if connection is not found.
6533
6783
 
6534
6784
  Returns:
6535
- dict | None: External system Details or None if the REST call fails.
6785
+ dict | None:
6786
+ External system Details or None if the REST call fails.
6536
6787
 
6537
6788
  """
6538
6789
  # Encode special characters in connection_name
@@ -6570,7 +6821,7 @@ class OTCS:
6570
6821
  base_url: str,
6571
6822
  username: str,
6572
6823
  password: str,
6573
- authentication_method: str = "BASIC", # either BASIC or OAUTH
6824
+ authentication_method: str = "BASIC",
6574
6825
  client_id: str | None = None,
6575
6826
  client_secret: str | None = None,
6576
6827
  ) -> dict | None:
@@ -6591,9 +6842,9 @@ class OTCS:
6591
6842
  The password (used for BASIC authentication)
6592
6843
  authentication_method (str, optional):
6593
6844
  Either BASIC (using username and password) or OAUTH.
6594
- client_id (str, optional):
6845
+ client_id (str | None, optional):
6595
6846
  The OAUTH Client ID (only required if authenticationMethod = OAUTH).
6596
- client_secret (str, optional):
6847
+ client_secret (str | None, optional):
6597
6848
  OAUTH Client Secret (only required if authenticationMethod = OAUTH).
6598
6849
 
6599
6850
  Returns:
@@ -6799,12 +7050,12 @@ class OTCS:
6799
7050
  Name of the transport package ZIP file.
6800
7051
  package_description (str, optional):
6801
7052
  Description of the transport package. Default is an empty string.
6802
- replacements (list of dicts, optional):
7053
+ replacements (list[dict] | None, optional):
6803
7054
  List of replacement values to be applied to all XML files in the transport.
6804
7055
  Each dictionary must contain:
6805
7056
  - 'placeholder': text to replace
6806
7057
  - 'value': text to replace with
6807
- extractions (list of dicts, optional):
7058
+ extractions (list[dict] | None, optional):
6808
7059
  List of XML subtrees to extract from each XML file in the transport.
6809
7060
  Each dictionary must contain:
6810
7061
  - 'xpath': defining the subtree to extract
@@ -7029,14 +7280,16 @@ class OTCS:
7029
7280
  """Search and replace strings in the XML files of the transport package.
7030
7281
 
7031
7282
  Args:
7032
- zip_file_path (str): Path to transport zip file.
7033
- replacements (list of dicts):
7283
+ zip_file_path (str):
7284
+ Path to transport zip file.
7285
+ replacements (list[dict]):
7034
7286
  List of replacement values; dict needs to have two values:
7035
7287
  - placeholder: The text to replace.
7036
7288
  - value: The replacement text.
7037
7289
 
7038
7290
  Returns:
7039
- bool: True = success, False = error.
7291
+ bool:
7292
+ True = success, False = error.
7040
7293
 
7041
7294
  """
7042
7295
 
@@ -7175,7 +7428,8 @@ class OTCS:
7175
7428
  """Search and extract XML data from the transport package.
7176
7429
 
7177
7430
  Args:
7178
- zip_file_path (str): Path to transport zip file.
7431
+ zip_file_path (str):
7432
+ Path to transport zip file.
7179
7433
  extractions (list of dicts):
7180
7434
  List of extraction values; dict needs to have two values:
7181
7435
  - xpath: structure to find
@@ -7419,9 +7673,9 @@ class OTCS:
7419
7673
  where_clauses (dict | None, optional):
7420
7674
  Filter the results based on one or multiple where clauses.
7421
7675
  TODO: NAME CONVENTION FOR THE FIELDS
7422
- limit (int, optional):
7676
+ limit (int | None, optional):
7423
7677
  The maximum number of result items.
7424
- page (int, optional):
7678
+ page (int | None, optional):
7425
7679
  The page number for a chunked result list.
7426
7680
 
7427
7681
  Returns:
@@ -8393,11 +8647,11 @@ class OTCS:
8393
8647
  Args:
8394
8648
  workspace_id (int):
8395
8649
  The ID of the workspace.
8396
- external_system_id (str, optional):
8650
+ external_system_id (str | None, optional):
8397
8651
  Identifier of the external system (None if no external system).
8398
- bo_type (str, optional):
8652
+ bo_type (str | None, optional):
8399
8653
  Business object type (None if no external system)
8400
- bo_id (str, optional):
8654
+ bo_id (str | None, optional):
8401
8655
  Business object identifier / key (None if no external system)
8402
8656
  show_error (bool, optional):
8403
8657
  Log an error if workspace cration fails. Otherwise log a warning.
@@ -9213,10 +9467,12 @@ class OTCS:
9213
9467
  """Get the Workspace roles.
9214
9468
 
9215
9469
  Args:
9216
- workspace_id (int): ID of the workspace template or workspace
9470
+ workspace_id (int):
9471
+ The ID of the workspace template or workspace.
9217
9472
 
9218
9473
  Returns:
9219
- dict | None: Workspace Roles data or None if the request fails.
9474
+ dict | None:
9475
+ Workspace Roles data or None if the request fails.
9220
9476
 
9221
9477
  """
9222
9478
 
@@ -9245,11 +9501,14 @@ class OTCS:
9245
9501
  """Get the Workspace members of a given role.
9246
9502
 
9247
9503
  Args:
9248
- workspace_id (int): ID of the workspace template
9249
- role_id (int): ID of the role
9504
+ workspace_id (int):
9505
+ The ID of the workspace.
9506
+ role_id (int):
9507
+ The ID of the workspace role.
9250
9508
 
9251
9509
  Returns:
9252
- dict | None: Workspace member data or None if the request fails.
9510
+ dict | None:
9511
+ Workspace member data or None if the request fails.
9253
9512
 
9254
9513
  """
9255
9514
 
@@ -9283,13 +9542,18 @@ class OTCS:
9283
9542
  """Add member to a workspace role. Check that the user/group is not yet a member.
9284
9543
 
9285
9544
  Args:
9286
- workspace_id (int): ID of the workspace
9287
- role_id (int): ID of the role
9288
- member_id (int): User ID or Group ID
9289
- show_warning (bool, optional): If True logs a warning if member is already in role
9545
+ workspace_id (int):
9546
+ The ID of the workspace.
9547
+ role_id (int):
9548
+ The ID of the workspace role.
9549
+ member_id (int):
9550
+ The user ID or group ID.
9551
+ show_warning (bool, optional):
9552
+ If True logs a warning if member is already in role.
9290
9553
 
9291
9554
  Returns:
9292
- dict | None: Workspace Role Membership or None if the request fails.
9555
+ dict | None:
9556
+ Workspace Role Membership or None if the request fails.
9293
9557
 
9294
9558
  """
9295
9559
 
@@ -9357,13 +9621,18 @@ class OTCS:
9357
9621
  """Remove a member from a workspace role. Check that the user is currently a member.
9358
9622
 
9359
9623
  Args:
9360
- workspace_id (int): ID of the workspace
9361
- role_id (int): ID of the role
9362
- member_id (int): User or Group Id
9363
- show_warning (bool, optional): If True logs a warning if member is not in role
9624
+ workspace_id (int):
9625
+ The ID of the workspace.
9626
+ role_id (int):
9627
+ The ID of the workspace role.
9628
+ member_id (int):
9629
+ The user or Group ID.
9630
+ show_warning (bool, optional):
9631
+ If True logs a warning if member is not in role.
9364
9632
 
9365
9633
  Returns:
9366
- dict | None: Workspace Role Membership or None if the request fails.
9634
+ dict | None:
9635
+ Workspace Role Membership or None if the request fails.
9367
9636
 
9368
9637
  """
9369
9638
 
@@ -9431,12 +9700,16 @@ class OTCS:
9431
9700
  """Remove all members from a workspace role. Check that the user is currently a member.
9432
9701
 
9433
9702
  Args:
9434
- workspace_id (int): ID of the workspace
9435
- role_id (int): ID of the role
9436
- show_warning (bool, optional): If True, logs a warning if member is not in role
9703
+ workspace_id (int):
9704
+ The ID of the workspace.
9705
+ role_id (int):
9706
+ The ID of the workspace role.
9707
+ show_warning (bool, optional):
9708
+ If True, logs a warning if member is not in role.
9437
9709
 
9438
9710
  Returns:
9439
- bool: True if success or False if the request fails.
9711
+ bool:
9712
+ True if success or False if the request fails.
9440
9713
 
9441
9714
  """
9442
9715
 
@@ -9478,9 +9751,12 @@ class OTCS:
9478
9751
  specifying whether to apply these permissions to the item itself, its sub-items, or both.
9479
9752
 
9480
9753
  Args:
9481
- workspace_id (int): ID of the workspace for which the role permissions are being assigned.
9482
- role_id (int): ID of the role to which the permissions will be assigned.
9483
- permissions (list of str): List of permissions to assign to the role. Valid permissions include:
9754
+ workspace_id (int):
9755
+ The ID of the workspace for which the role permissions are being assigned.
9756
+ role_id (int):
9757
+ The ID of the role to which the permissions will be assigned.
9758
+ permissions (list):
9759
+ List of permissions to assign to the role. Valid permissions include:
9484
9760
  - "see" : View the workspace
9485
9761
  - "see_contents" : View contents of the workspace
9486
9762
  - "modify" : Modify the workspace
@@ -9491,14 +9767,16 @@ class OTCS:
9491
9767
  - "delete_versions" : Delete versions of the workspace
9492
9768
  - "delete" : Delete the workspace
9493
9769
  - "edit_permissions" : Modify permissions for the workspace
9494
- apply_to (int, optional): Specifies the scope of permission assignment. Possible values:
9770
+ apply_to (int, optional):
9771
+ Specifies the scope of permission assignment. Possible values:
9495
9772
  - 0 = Apply to this item only
9496
9773
  - 1 = Apply to sub-items only
9497
9774
  - 2 = Apply to this item and its sub-items (default)
9498
9775
  - 3 = Apply to this item and its immediate sub-items
9499
9776
 
9500
9777
  Returns:
9501
- dict | None: Updated workspace role membership details or `None` if the request fails.
9778
+ dict | None:
9779
+ Updated workspace role membership details or `None` if the request fails.
9502
9780
 
9503
9781
  Notes:
9504
9782
  - If `apply_to` is set to `2`, both the workspace and its sub-items will inherit the updated permissions.
@@ -9549,12 +9827,16 @@ class OTCS:
9549
9827
  """Update a workspace with a with a new icon (which is uploaded).
9550
9828
 
9551
9829
  Args:
9552
- workspace_id (int): ID of the workspace
9553
- file_path (str): path + filename of icon file
9554
- file_mimetype (str, optional): mimetype of the image
9830
+ workspace_id (int):
9831
+ The ID of the workspace to update the icon for.
9832
+ file_path (str):
9833
+ The path + filename of icon file.
9834
+ file_mimetype (str, optional):
9835
+ The mimetype of the image.
9555
9836
 
9556
9837
  Returns:
9557
- dict | None: Node information or None if REST call fails.
9838
+ dict | None:
9839
+ Node information or None if REST call fails.
9558
9840
 
9559
9841
  """
9560
9842
 
@@ -9878,12 +10160,12 @@ class OTCS:
9878
10160
  Address of the URL item (if it is an URL item type).
9879
10161
  category_data (dict | None, optional):
9880
10162
  New category and attributes values.
9881
- classifications (list):
10163
+ classifications (list | None, optional):
9882
10164
  List of classification item IDs to apply to the new item.
9883
- body (bool):
10165
+ body (bool, optional):
9884
10166
  Should the payload be put in an body tag. Most V2 REST API methods
9885
10167
  do require this but some not (like Scheduled Bots)
9886
- **kwargs (dict):
10168
+ **kwargs (dict, optional):
9887
10169
  Add additional attributes to the body of the POST request
9888
10170
 
9889
10171
  Returns:
@@ -9971,9 +10253,9 @@ class OTCS:
9971
10253
  Args:
9972
10254
  parent_id (int):
9973
10255
  The node the category should be applied to.
9974
- subtype (int):
10256
+ subtype (int, optional):
9975
10257
  The subtype of the new node. Default is document.
9976
- category_ids (int | list[int]):
10258
+ category_ids (int | list[int], optional):
9977
10259
  The ID of the category or a list of category IDs.
9978
10260
 
9979
10261
  Returns:
@@ -10327,7 +10609,7 @@ class OTCS:
10327
10609
  # end method definition
10328
10610
 
10329
10611
  def get_web_report_parameters(self, nickname: str) -> list | None:
10330
- """Retrieve parameters of a Web Report in Extended ECM.
10612
+ """Retrieve parameters of a Web Report in OTCS.
10331
10613
 
10332
10614
  These parameters are defined on the Web Report node (Properties -> Parameters).
10333
10615
 
@@ -10384,14 +10666,17 @@ class OTCS:
10384
10666
  nickname: str,
10385
10667
  web_report_parameters: dict | None = None,
10386
10668
  ) -> dict | None:
10387
- """Run a Web Report that is identified by its nick name.
10669
+ """Run a Web Report that is identified by its nickname.
10388
10670
 
10389
10671
  Args:
10390
- nickname (str): nickname of the Web Reports node.
10391
- web_report_parameters (dict, optional): Parameters of the Web Report (names + value pairs)
10672
+ nickname (str):
10673
+ The nickname of the Web Reports node.
10674
+ web_report_parameters (dict, optional):
10675
+ Parameters of the Web Report (names + value pairs)
10392
10676
 
10393
10677
  Returns:
10394
- dict | None: Response of the run Web Report request or None if the Web Report execution has failed.
10678
+ dict | None:
10679
+ Response of the run Web Report request or None if the Web Report execution has failed.
10395
10680
 
10396
10681
  """
10397
10682
 
@@ -10403,7 +10688,7 @@ class OTCS:
10403
10688
  request_header = self.request_form_header()
10404
10689
 
10405
10690
  self.logger.debug(
10406
- "Running Web Report with nickname -> %s; calling -> %s",
10691
+ "Running Web Report with nickname -> '%s'; calling -> %s",
10407
10692
  nickname,
10408
10693
  request_url,
10409
10694
  )
@@ -10598,12 +10883,12 @@ class OTCS:
10598
10883
  def assign_permission(
10599
10884
  self,
10600
10885
  node_id: int,
10601
- assignee_type: str,
10602
- assignee: int,
10603
10886
  permissions: list,
10887
+ assignee_type: str,
10888
+ assignee: int = 0,
10604
10889
  apply_to: int = 0,
10605
10890
  ) -> dict | None:
10606
- """Assign permissions to a user or group for an Extended ECM item.
10891
+ """Assign permissions to a user or group for an Content Server item.
10607
10892
 
10608
10893
  This method allows you to assign specified permissions to a user or group for a given
10609
10894
  Content Server item (node). The permissions can be applied to the item itself, its sub-items,
@@ -10611,15 +10896,6 @@ class OTCS:
10611
10896
 
10612
10897
  Args:
10613
10898
  node_id (int): The ID of the Extended ECM item (node) to which permissions are being assigned.
10614
- assignee_type (str): The type of assignee. This can be one of the following:
10615
- - "owner": Permissions are assigned to the owner.
10616
- - "group": Permissions are assigned to the owner group.
10617
- - "public": Permissions are assigned to the public (all users).
10618
- - "custom": Permissions are assigned to a specific user or group (specified by `assignee`).
10619
- assignee (int):
10620
- The ID of the user or group (referred to as "right ID").
10621
- If `assignee` is 0 and `assignee_type` is "owner" or "group",
10622
- the owner or group will not be changed.
10623
10899
  permissions (list of str): A list of permissions to assign to the assignee. Valid permissions include:
10624
10900
  - "see" : View the item
10625
10901
  - "see_contents" : View the contents of the item
@@ -10631,6 +10907,15 @@ class OTCS:
10631
10907
  - "delete_versions" : Delete versions of the item
10632
10908
  - "delete" : Delete the item
10633
10909
  - "edit_permissions" : Modify permissions for the item
10910
+ assignee_type (str): The type of assignee. This can be one of the following:
10911
+ - "owner": Permissions are assigned to the owner.
10912
+ - "group": Permissions are assigned to the owner group.
10913
+ - "public": Permissions are assigned to the public (all users).
10914
+ - "custom": Permissions are assigned to a specific user or group (specified by `assignee`).
10915
+ assignee (int):
10916
+ The ID of the user or group (referred to as "right ID").
10917
+ If `assignee` is 0 and `assignee_type` is "owner" or "group",
10918
+ the owner or group will not be changed.
10634
10919
  apply_to (int, optional): The scope of the permission assignment. Possible values:
10635
10920
  - 0 = Apply to this item only (default)
10636
10921
  - 1 = Apply to sub-items only
@@ -10647,18 +10932,24 @@ class OTCS:
10647
10932
 
10648
10933
  """
10649
10934
 
10650
- if not assignee_type or assignee_type not in [
10651
- "owner",
10652
- "group",
10653
- "public",
10654
- "custom",
10655
- ]:
10935
+ if not assignee_type or assignee_type not in OTCS.PERMISSION_ASSIGNEE_TYPES:
10656
10936
  self.logger.error(
10657
- "Missing or wrong assignee type. Needs to be owner, group, public or custom!",
10937
+ "Missing or wrong assignee type. Needs to be one of %s!", str(OTCS.PERMISSION_ASSIGNEE_TYPES)
10658
10938
  )
10659
10939
  return None
10660
10940
  if assignee_type == "custom" and not assignee:
10661
- self.logger.error("Missing permission assignee!")
10941
+ self.logger.error("Assignee type is 'custom' but permission assignee is missing!")
10942
+ return None
10943
+
10944
+ if any(permission not in OTCS.PERMISSION_TYPES for permission in permissions):
10945
+ illegal_permissions = [permission for permission in permissions if permission not in OTCS.PERMISSION_TYPES]
10946
+ self.logger.error(
10947
+ "Illegal permission%s -> %s! Allowed permissions are -> %s. Cannot assign permissions to node with ID -> %d.",
10948
+ "s" if len(illegal_permissions) > 1 else "",
10949
+ str(illegal_permissions),
10950
+ str(OTCS.PERMISSION_TYPES),
10951
+ node_id,
10952
+ )
10662
10953
  return None
10663
10954
 
10664
10955
  permission_post_data = {
@@ -10676,10 +10967,11 @@ class OTCS:
10676
10967
  request_header = self.request_form_header()
10677
10968
 
10678
10969
  self.logger.debug(
10679
- "Assign permissions -> %s to item with ID -> %s; assignee type -> '%s'; calling -> %s",
10970
+ "Assign permissions -> %s to item with ID -> %s; assignee type -> '%s'; apply to -> '%d'; calling -> %s",
10680
10971
  str(permissions),
10681
10972
  str(node_id),
10682
10973
  assignee_type,
10974
+ apply_to,
10683
10975
  request_url,
10684
10976
  )
10685
10977
 
@@ -10692,9 +10984,8 @@ class OTCS:
10692
10984
  headers=request_header,
10693
10985
  data={"body": json.dumps(permission_post_data)},
10694
10986
  timeout=None,
10695
- failure_message="Failed to assign custom permissions -> {} to item with ID -> {}".format(
10696
- permissions,
10697
- node_id,
10987
+ failure_message="Failed to assign 'custom' permissions -> {} to item with ID -> {} (apply to -> {})".format(
10988
+ permissions, node_id, apply_to
10698
10989
  ),
10699
10990
  )
10700
10991
  else:
@@ -10705,9 +10996,8 @@ class OTCS:
10705
10996
  headers=request_header,
10706
10997
  data={"body": json.dumps(permission_post_data)},
10707
10998
  timeout=None,
10708
- failure_message="Failed to assign stadard permissions -> {} to item with ID -> {}".format(
10709
- permissions,
10710
- node_id,
10999
+ failure_message="Failed to assign -> '{}' permissions -> {} to item with ID -> {} (apply to -> {})".format(
11000
+ assignee_type, permissions, node_id, apply_to
10711
11001
  ),
10712
11002
  )
10713
11003