CMIS Browser Binding fixes

This commit is contained in:
2026-05-11 12:28:36 +02:00
parent 59aa2a49a8
commit dc32be36fb
8 changed files with 1422 additions and 121 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -58,6 +58,7 @@ class CMISAction(str, Enum):
CREATE_FOLDER = "create_folder"
CREATE_DOCUMENT = "create_document"
UPDATE_PROPERTIES = "update_properties"
MOVE_OBJECT = "move_object"
DELETE_OBJECT = "delete_object"
DELETE_TREE = "delete_tree"
SET_CONTENT_STREAM = "set_content_stream"
@@ -89,6 +90,7 @@ ACTION_CAPABILITIES: dict[CMISAction, CMISCapability] = {
CMISAction.CREATE_FOLDER: CMISCapability.OBJECT_WRITE,
CMISAction.CREATE_DOCUMENT: CMISCapability.OBJECT_WRITE,
CMISAction.UPDATE_PROPERTIES: CMISCapability.OBJECT_WRITE,
CMISAction.MOVE_OBJECT: CMISCapability.OBJECT_WRITE,
CMISAction.DELETE_OBJECT: CMISCapability.OBJECT_WRITE,
CMISAction.DELETE_TREE: CMISCapability.OBJECT_WRITE,
CMISAction.SET_CONTENT_STREAM: CMISCapability.CONTENT_STREAM_WRITE,
@@ -111,6 +113,7 @@ IMPLEMENTED_CMIS_ACTIONS: frozenset[CMISAction] = frozenset(
CMISAction.CREATE_FOLDER,
CMISAction.CREATE_DOCUMENT,
CMISAction.UPDATE_PROPERTIES,
CMISAction.MOVE_OBJECT,
CMISAction.DELETE_OBJECT,
CMISAction.DELETE_TREE,
CMISAction.SET_CONTENT_STREAM,
@@ -120,6 +123,7 @@ MUTATION_ACTIONS = {
CMISAction.CREATE_DOCUMENT,
CMISAction.CREATE_FOLDER,
CMISAction.UPDATE_PROPERTIES,
CMISAction.MOVE_OBJECT,
CMISAction.DELETE_OBJECT,
CMISAction.DELETE_TREE,
CMISAction.SET_CONTENT_STREAM,
@@ -884,7 +888,6 @@ class CMISDomainMapper:
"cmis:changeToken": asset.current_version_id,
"cmis:description": asset.metadata.get("description", asset.title),
"cmis:secondaryObjectTypeIds": list(asset.metadata.get("cmis_secondary_object_type_ids", ())),
"cmis:path": self.asset_path(asset),
"cmis:contentStreamLength": content_stream.get("length") if content_stream else None,
"cmis:contentStreamMimeType": content_stream.get("mime_type") if content_stream else None,
"cmis:contentStreamFileName": content_stream.get("file_name") if content_stream else None,
@@ -905,6 +908,10 @@ class CMISDomainMapper:
compacted.setdefault("cmis:createdBy", "system")
compacted.setdefault("cmis:lastModifiedBy", "system")
compacted.setdefault("cmis:secondaryObjectTypeIds", [])
compacted.setdefault("cmis:contentStreamLength", None)
compacted.setdefault("cmis:contentStreamMimeType", None)
compacted.setdefault("cmis:contentStreamFileName", None)
compacted.setdefault("cmis:contentStreamId", None)
compacted.setdefault("kontextual:owner", "")
compacted.setdefault("kontextual:topics", [])
compacted.setdefault("kontextual:reviewState", "")
@@ -980,6 +987,7 @@ class CMISDomainMapper:
CMISAction.GET_RELATIONSHIPS,
CMISAction.GET_OBJECT_PARENTS,
CMISAction.UPDATE_PROPERTIES,
CMISAction.MOVE_OBJECT,
CMISAction.DELETE_OBJECT,
CMISAction.SET_CONTENT_STREAM,
]
@@ -1209,39 +1217,61 @@ def cmis_browser_type_definition_by_id(
raise KeyError(type_id)
def cmis_browser_object(projection: dict[str, Any]) -> dict[str, Any]:
properties = _browser_object_properties(projection)
def cmis_browser_object(
projection: dict[str, Any],
*,
property_filter: str | None = None,
include_allowable_actions: bool = True,
include_acl: bool = True,
) -> dict[str, Any]:
unfiltered_properties = _browser_object_properties(projection)
properties = _filter_browser_properties(unfiltered_properties, property_filter)
result = {
"properties": {
key: cmis_browser_property_value(key, value)
for key, value in properties.items()
},
"succinctProperties": properties,
"allowableActions": cmis_browser_allowable_actions(
projection.get("allowable_actions", []),
base_type_id=properties.get("cmis:baseTypeId"),
),
}
acl = projection.get("acl")
if include_allowable_actions:
result["allowableActions"] = cmis_browser_allowable_actions(
projection.get("allowable_actions", []),
base_type_id=unfiltered_properties.get("cmis:baseTypeId"),
)
acl = projection.get("acl") if include_acl else None
if isinstance(acl, dict) and acl:
result["acl"] = cmis_browser_acl(acl)
result["exactACL"] = bool(acl.get("is_exact", True))
return compact_dict(result)
def cmis_browser_object_in_folder_list(children: dict[str, Any]) -> dict[str, Any]:
def cmis_browser_object_in_folder_list(
children: dict[str, Any],
*,
property_filter: str | None = None,
include_allowable_actions: bool = True,
include_acl: bool = True,
include_path_segment: bool = True,
) -> dict[str, Any]:
objects = []
for item in children.get("objects", []):
entry = {
"object": cmis_browser_object(
item,
property_filter=property_filter,
include_allowable_actions=include_allowable_actions,
include_acl=include_acl,
),
}
if include_path_segment:
entry["pathSegment"] = item.get("name") or item.get("object_id")
objects.append(
{
"object": cmis_browser_object(item),
"pathSegment": item.get("name") or item.get("object_id"),
}
entry
)
return {
"objects": objects,
"hasMoreItems": bool(children.get("has_more_items", False)),
"numItems": int(children.get("num_items", len(objects))),
"numItems": int(children.get("total_num_items", children.get("num_items", len(objects)))),
}
@@ -1253,15 +1283,17 @@ def cmis_browser_object_list(objects: list[dict[str, Any]], *, has_more_items: b
}
def cmis_browser_parent_list(parents: dict[str, Any]) -> list[dict[str, Any]]:
def cmis_browser_parent_list(
parents: dict[str, Any],
*,
include_relative_path_segment: bool = True,
) -> list[dict[str, Any]]:
items = []
for parent in parents.get("parents", []):
items.append(
{
"object": cmis_browser_object(parent),
"relativePathSegment": parent.get("name"),
}
)
item = {"object": cmis_browser_object(parent)}
if include_relative_path_segment:
item["relativePathSegment"] = parent.get("relative_path_segment") or parent.get("name")
items.append(item)
return items
@@ -1270,7 +1302,7 @@ def cmis_browser_query_result(query_result: dict[str, Any]) -> dict[str, Any]:
return {
"results": results,
"hasMoreItems": bool(query_result.get("has_more_items", False)),
"numItems": int(query_result.get("num_items", len(results))),
"numItems": int(query_result.get("total_num_items", query_result.get("num_items", len(results)))),
}
@@ -1351,7 +1383,7 @@ def cmis_browser_allowable_actions(
"canGetFolderTree": False,
"canGetFolderParent": is_folder and CMISAction.GET_OBJECT_PARENTS.value in native,
"canGetRenditions": False,
"canMoveObject": False,
"canMoveObject": CMISAction.MOVE_OBJECT.value in native,
"canAddObjectToFolder": False,
"canRemoveObjectFromFolder": False,
"canCheckOut": False,
@@ -1362,7 +1394,7 @@ def cmis_browser_allowable_actions(
"canRemovePolicy": False,
"canGetAppliedPolicies": False,
"canApplyACL": False,
"canDeleteContentStream": False,
"canDeleteContentStream": CMISAction.SET_CONTENT_STREAM.value in native,
}
@@ -1600,6 +1632,23 @@ def _browser_object_properties(projection: dict[str, Any]) -> dict[str, Any]:
return properties
def _filter_browser_properties(properties: dict[str, Any], property_filter: str | None) -> dict[str, Any]:
if property_filter is None:
return properties
requested = {
part.strip()
for part in property_filter.split(",")
if part.strip()
}
if not requested or "*" in requested:
return properties
return {
key: value
for key, value in properties.items()
if key in requested or any(pattern.endswith(":*") and key.startswith(pattern[:-1]) for pattern in requested)
}
def _browser_property_type(property_id: str, value: Any) -> str:
if property_id in {
"cmis:objectId",
@@ -1677,11 +1726,12 @@ def _property_definitions(base_type_id: CMISBaseType) -> dict[str, dict[str, Any
"cmis:lastModificationDate": {"property_type": "datetime", "cardinality": "single", "required": False},
"cmis:changeToken": {"property_type": "string", "cardinality": "single", "required": False},
"cmis:secondaryObjectTypeIds": {"property_type": "id", "cardinality": "multi", "required": False},
"cmis:path": {"property_type": "string", "cardinality": "single", "required": False},
"cmis:description": {"property_type": "string", "cardinality": "single", "required": False},
"kontextual:sensitivity": {"property_type": "string", "cardinality": "single", "required": False},
"kontextual:lifecycle": {"property_type": "string", "cardinality": "single", "required": False},
}
if base_type_id == CMISBaseType.FOLDER:
definitions["cmis:path"] = {"property_type": "string", "cardinality": "single", "required": False}
if base_type_id == CMISBaseType.DOCUMENT:
definitions.update(
{