CMIS Browser Binding serializer layer

This commit is contained in:
2026-05-08 12:27:26 +02:00
parent 6382a5a7ab
commit 54a26cdb02
9 changed files with 1172 additions and 40 deletions

View File

@@ -51,6 +51,17 @@ from kontextual_engine.core import (
stable_json_dumps,
utc_now,
)
from kontextual_engine.core.cmis import (
cmis_browser_object,
cmis_browser_object_in_folder_list,
cmis_browser_parent_list,
cmis_browser_query_result,
cmis_browser_root_folder,
cmis_browser_service_document,
cmis_browser_type_children,
cmis_browser_type_descendants,
cmis_browser_type_definition_by_id,
)
from kontextual_engine.errors import AuthorizationError, KontextualError, NotFoundError, ValidationError
from kontextual_engine.ports import AllowAllPolicyGateway, AssetRegistryRepository, BlobStorage, PolicyGateway
from kontextual_engine.services import (
@@ -339,10 +350,131 @@ class ServiceRuntime:
def cmis_repository_info(self, access_point_id: str) -> dict[str, Any]:
return self._cmis_mapper(access_point_id).repository_info()
def cmis_browser_service_document(
self,
access_point_id: str,
*,
repository_url: str,
root_folder_url: str,
) -> dict[str, Any]:
return cmis_browser_service_document(
self.cmis_repository_info(access_point_id),
repository_url=repository_url,
root_folder_url=root_folder_url,
)
def cmis_type_definitions(self, access_point_id: str) -> dict[str, Any]:
definitions = self._cmis_mapper(access_point_id).type_definitions()
return {"items": definitions, "count": len(definitions)}
def cmis_browser_type_children(
self,
access_point_id: str,
*,
type_id: str | None = None,
skip_count: int = 0,
max_items: int = 100,
include_property_definitions: bool = False,
) -> dict[str, Any]:
return cmis_browser_type_children(
self._cmis_mapper(access_point_id).type_definitions(),
type_id=type_id,
skip_count=skip_count,
max_items=max_items,
include_property_definitions=include_property_definitions,
)
def cmis_browser_type_descendants(
self,
access_point_id: str,
*,
type_id: str | None = None,
include_property_definitions: bool = False,
) -> list[dict[str, Any]]:
return cmis_browser_type_descendants(
self._cmis_mapper(access_point_id).type_definitions(),
type_id=type_id,
include_property_definitions=include_property_definitions,
)
def cmis_browser_type_definition(
self,
access_point_id: str,
*,
type_id: str | None,
) -> dict[str, Any]:
try:
return cmis_browser_type_definition_by_id(
self._cmis_mapper(access_point_id).type_definitions(),
type_id,
)
except KeyError as exc:
raise NotFoundError(
"CMIS type definition not found",
details={"access_point_id": access_point_id, "type_id": type_id},
) from exc
def cmis_browser_root_object(self, access_point_id: str) -> dict[str, Any]:
return cmis_browser_object(cmis_browser_root_folder(self._cmis_access_point(access_point_id)))
def cmis_browser_object(
self,
access_point_id: str,
object_id: str | None,
context: OperationContext,
) -> dict[str, Any]:
if object_id in (None, "", "cmis-root", "root", "/"):
return self.cmis_browser_root_object(access_point_id)
if object_id.startswith("cmis:folder:"):
folder_path = _cmis_folder_path(object_id) or "/"
return cmis_browser_object(self._cmis_mapper(access_point_id).folder_projection(folder_path))
return cmis_browser_object(self.cmis_object(access_point_id, object_id, context))
def cmis_browser_children(
self,
access_point_id: str,
context: OperationContext,
*,
object_id: str | None = None,
skip_count: int = 0,
max_items: int = 100,
) -> dict[str, Any]:
children = self.cmis_children(
access_point_id,
context,
folder_id=object_id,
skip_count=skip_count,
max_items=max_items,
)
return cmis_browser_object_in_folder_list(children)
def cmis_browser_parents(
self,
access_point_id: str,
object_id: str,
context: OperationContext,
) -> list[dict[str, Any]]:
return cmis_browser_parent_list(self.cmis_object_parents(access_point_id, object_id, context))
def cmis_browser_query(
self,
access_point_id: str,
query: str,
context: OperationContext,
*,
skip_count: int = 0,
max_items: int = 100,
) -> dict[str, Any]:
return cmis_browser_query_result(
self.cmis_query(
access_point_id,
query,
context,
skip_count=skip_count,
max_items=max_items,
)
)
def cmis_children(
self,
access_point_id: str,
@@ -700,9 +832,12 @@ class ServiceRuntime:
}
def _cmis_mapper(self, access_point_id: str) -> CMISDomainMapper:
return CMISDomainMapper(self._cmis_access_point(access_point_id))
def _cmis_access_point(self, access_point_id: str) -> CMISAccessPoint:
for profile in _cmis_profiles():
if profile.name == access_point_id:
return CMISDomainMapper(_cmis_access_point(profile))
return _cmis_access_point(profile)
raise NotFoundError(
"CMIS access point not found",
details={"access_point_id": access_point_id, "available": [profile.name for profile in _cmis_profiles()]},
@@ -2074,12 +2209,14 @@ class ServiceRuntime:
def create_app(runtime: ServiceRuntime | None = None):
try:
from fastapi import Depends, FastAPI, Header, HTTPException, Query
from fastapi import Depends, FastAPI, Header, HTTPException, Query, Request
from fastapi.responses import JSONResponse, StreamingResponse
except ImportError as exc: # pragma: no cover - exercised when optional extra is absent
raise RuntimeError(
"FastAPI service dependencies are not installed. Install kontextual-engine[service]."
) from exc
globals()["Request"] = Request
globals()["StreamingResponse"] = StreamingResponse
runtime = runtime or ServiceRuntime()
app = FastAPI(
@@ -2205,9 +2342,129 @@ def create_app(runtime: ServiceRuntime | None = None):
def cmis_access_points() -> dict[str, Any]:
return response(runtime.cmis_access_points)
def browser_urls(request: Request, access_point_id: str) -> tuple[str, str]:
return (
str(request.url_for("cmis_browser_entry", access_point_id=access_point_id)),
str(request.url_for("cmis_browser_root", access_point_id=access_point_id)),
)
def unsupported_browser_selector(selector: str | None) -> dict[str, Any]:
raise ValidationError(
"Unsupported CMIS Browser Binding selector",
details={
"cmisselector": selector,
"supported": [
"repositoryInfo",
"typeChildren",
"typeDescendants",
"typeDefinition",
"query",
"object",
"children",
"parents",
"properties",
"allowableActions",
"policies",
"content",
],
},
)
@app.get("/cmis/{access_point_id}/browser", tags=["cmis"])
def cmis_repository_info(access_point_id: str) -> dict[str, Any]:
return response(runtime.cmis_repository_info, access_point_id)
def cmis_browser_entry(
access_point_id: str,
request: Request,
cmisselector: str | None = Query(None),
typeId: str | None = Query(None),
includePropertyDefinitions: bool = Query(False),
q: str | None = Query(None),
skipCount: int = Query(0),
maxItems: int = Query(100),
context: OperationContext = Depends(context_from_headers),
) -> Any:
repository_url, root_folder_url = browser_urls(request, access_point_id)
if cmisselector in (None, "", "repositoryInfo"):
return response(
runtime.cmis_browser_service_document,
access_point_id,
repository_url=repository_url,
root_folder_url=root_folder_url,
)
if cmisselector == "typeChildren":
return response(
runtime.cmis_browser_type_children,
access_point_id,
type_id=typeId,
skip_count=skipCount,
max_items=maxItems,
include_property_definitions=includePropertyDefinitions,
)
if cmisselector == "typeDescendants":
return response(
runtime.cmis_browser_type_descendants,
access_point_id,
type_id=typeId,
include_property_definitions=includePropertyDefinitions,
)
if cmisselector == "typeDefinition":
return response(runtime.cmis_browser_type_definition, access_point_id, type_id=typeId)
if cmisselector == "query":
return response(
runtime.cmis_browser_query,
access_point_id,
q or "SELECT * FROM cmis:document",
context,
skip_count=skipCount,
max_items=maxItems,
)
return unsupported_browser_selector(cmisselector)
@app.get("/cmis/{access_point_id}/browser/root", tags=["cmis"])
def cmis_browser_root(
access_point_id: str,
cmisselector: str | None = Query(None),
objectId: str | None = Query(None),
skipCount: int = Query(0),
maxItems: int = Query(100),
context: OperationContext = Depends(context_from_headers),
) -> Any:
if cmisselector in (None, "", "object"):
return response(runtime.cmis_browser_object, access_point_id, objectId, context)
if cmisselector == "children":
return response(
runtime.cmis_browser_children,
access_point_id,
context,
object_id=objectId,
skip_count=skipCount,
max_items=maxItems,
)
if cmisselector == "parents":
if not objectId:
return []
return response(runtime.cmis_browser_parents, access_point_id, objectId, context)
if cmisselector == "properties":
return response(runtime.cmis_browser_object, access_point_id, objectId, context)["properties"]
if cmisselector == "allowableActions":
return response(runtime.cmis_browser_object, access_point_id, objectId, context)["allowableActions"]
if cmisselector == "policies":
return []
if cmisselector == "content":
if not objectId:
return unsupported_browser_selector(cmisselector)
result = response(runtime.cmis_content_stream_bytes, access_point_id, objectId, context)
representation = result.representation
return StreamingResponse(
result.chunks,
media_type=representation.media_type,
headers={
"Content-Length": str(representation.size_bytes),
"ETag": representation.digest,
"X-Kontextual-Representation-Id": representation.representation_id,
"X-Kontextual-Storage-Ref": representation.storage_ref or "",
},
)
return unsupported_browser_selector(cmisselector)
@app.get("/cmis/{access_point_id}/browser/types", tags=["cmis"])
def cmis_types(access_point_id: str) -> dict[str, Any]:
@@ -2251,7 +2508,7 @@ def create_app(runtime: ServiceRuntime | None = None):
access_point_id: str,
object_id: str,
context: OperationContext = Depends(context_from_headers),
) -> StreamingResponse:
) -> Any:
result = response(runtime.cmis_content_stream_bytes, access_point_id, object_id, context)
representation = result.representation
return StreamingResponse(
@@ -2391,7 +2648,7 @@ def create_app(runtime: ServiceRuntime | None = None):
asset_id: str,
representation_id: str,
context: OperationContext = Depends(context_from_headers),
) -> StreamingResponse:
) -> Any:
result = response(runtime.representation_content_stream, asset_id, representation_id, context)
representation = result.representation
return StreamingResponse(
@@ -2772,9 +3029,10 @@ def _cmis_profiles() -> tuple[CMISAccessProfile, ...]:
def _cmis_access_point(profile: CMISAccessProfile) -> CMISAccessPoint:
repository_id = profile.name if profile.name == "compat-tck" else f"kontextual-{profile.name}"
return CMISAccessPoint(
access_point_id=profile.name,
repository_id=f"kontextual-{profile.name}",
repository_id=repository_id,
profile=profile,
base_path=f"/cmis/{profile.name}/browser",
metadata={"repository_name": f"Kontextual Engine {profile.name}"},

View File

@@ -649,6 +649,7 @@ class CMISDomainMapper:
"common_name": "Profiled CMIS access points",
"version_label": "1.0",
"description": "Multiple Browser Binding access points can expose different governed profile slices.",
"url": "https://docs.oasis-open.org/cmis/CMIS/v1.1/CMIS-v1.1.html",
},
{
"id": "urn:kontextual:cmis:feature:projection-parentage",
@@ -658,6 +659,7 @@ class CMISDomainMapper:
"Assets may appear under multiple virtual folder projections; CMIS multi-filing mutation "
"services are not advertised."
),
"url": "https://docs.oasis-open.org/cmis/CMIS/v1.1/CMIS-v1.1.html",
},
]
@@ -824,6 +826,8 @@ class CMISDomainMapper:
def folder_projection(self, path: str) -> dict[str, Any]:
normalized = _normalize_path(path)
parent = _parent_path(normalized)
parent_id = self.access_point.root_folder_id if parent == "/" else self.folder_object_id(parent)
return {
"object_id": self.folder_object_id(normalized),
"base_type_id": CMISBaseType.FOLDER.value,
@@ -835,6 +839,18 @@ class CMISDomainMapper:
"cmis:name": _path_name(normalized),
"cmis:baseTypeId": CMISBaseType.FOLDER.value,
"cmis:objectTypeId": "kontextual:folder",
"cmis:createdBy": "system",
"cmis:lastModifiedBy": "system",
"cmis:creationDate": "1970-01-01T00:00:00+00:00",
"cmis:lastModificationDate": "1970-01-01T00:00:00+00:00",
"cmis:changeToken": f"folder:{normalized}",
"cmis:description": "Virtual CMIS folder projection",
"cmis:secondaryObjectTypeIds": [],
"cmis:parentId": parent_id,
"cmis:path": normalized,
"cmis:allowedChildObjectTypeIds": [CMISBaseType.DOCUMENT.value, CMISBaseType.FOLDER.value],
"kontextual:sensitivity": "internal",
"kontextual:lifecycle": LifecycleState.ACTIVE.value,
"kontextual:filingSource": _filing_source(normalized),
},
"allowable_actions": [CMISAction.GET_CHILDREN.value],
@@ -946,6 +962,623 @@ class CMISDomainMapper:
return tuple(actions)
def cmis_browser_service_document(
repository_info: dict[str, Any],
*,
repository_url: str,
root_folder_url: str,
) -> dict[str, Any]:
"""Serialize native repository info as a CMIS Browser Binding service root."""
browser_info = cmis_browser_repository_info(
repository_info,
repository_url=repository_url,
root_folder_url=root_folder_url,
)
return {browser_info["repositoryId"]: browser_info}
def cmis_browser_repository_info(
repository_info: dict[str, Any],
*,
repository_url: str,
root_folder_url: str,
) -> dict[str, Any]:
capabilities = repository_info.get("capabilities", {})
return compact_dict(
{
"repositoryId": repository_info.get("repository_id"),
"repositoryName": repository_info.get("repository_name"),
"repositoryDescription": repository_info.get("repository_description"),
"vendorName": repository_info.get("vendor_name"),
"productName": repository_info.get("product_name"),
"productVersion": repository_info.get("product_version"),
"rootFolderId": repository_info.get("root_folder_id"),
"repositoryUrl": repository_url,
"rootFolderUrl": root_folder_url,
"capabilities": cmis_browser_capabilities(capabilities),
"aclCapabilities": _browser_acl_capabilities(),
"latestChangeLogToken": "0",
"cmisVersionSupported": repository_info.get("cmis_version_supported"),
"thinClientURI": repository_url,
"changesIncomplete": True,
"changesOnType": ["cmis:document", "cmis:folder"],
"principalIdAnonymous": repository_info.get("principal_anonymous", "anonymous"),
"principalIdAnyone": repository_info.get("principal_anyone", "anyone"),
"extendedFeatures": cmis_browser_extended_features(repository_info),
}
)
def cmis_browser_capabilities(capabilities: dict[str, Any]) -> dict[str, Any]:
settable = capabilities.get("capability_new_type_settable_attributes", {})
return {
"capabilityContentStreamUpdatability": capabilities.get(
"capability_content_stream_updatability",
"none",
),
"capabilityChanges": capabilities.get("capability_changes", "none"),
"capabilityRenditions": capabilities.get("capability_renditions", "none"),
"capabilityGetDescendants": bool(capabilities.get("capability_get_descendants", False)),
"capabilityGetFolderTree": bool(capabilities.get("capability_get_folder_tree", False)),
"capabilityMultifiling": bool(capabilities.get("capability_multifiling", False)),
"capabilityUnfiling": bool(capabilities.get("capability_unfiling", False)),
"capabilityVersionSpecificFiling": bool(capabilities.get("capability_version_specific_filing", False)),
"capabilityPWCSearchable": bool(capabilities.get("capability_pwc_searchable", False)),
"capabilityPWCUpdatable": bool(capabilities.get("capability_pwc_updatable", False)),
"capabilityAllVersionsSearchable": bool(capabilities.get("capability_all_versions_searchable", False)),
"capabilityOrderBy": capabilities.get("capability_order_by", "none"),
"capabilityQuery": capabilities.get("capability_query", "none"),
"capabilityJoin": capabilities.get("capability_join", "none"),
"capabilityACL": capabilities.get("capability_acl", "none"),
"capabilityCreatablePropertyTypes": {"canCreate": []},
"capabilityNewTypeSettableAttributes": {
"id": bool(settable.get("id", False)),
"localName": bool(settable.get("local_name", False)),
"localNamespace": bool(settable.get("local_namespace", False)),
"displayName": bool(settable.get("display_name", False)),
"queryName": bool(settable.get("query_name", False)),
"description": bool(settable.get("description", False)),
"creatable": bool(settable.get("creatable", False)),
"fileable": bool(settable.get("fileable", False)),
"queryable": bool(settable.get("queryable", False)),
"fulltextIndexed": bool(settable.get("fulltext_indexed", False)),
"includedInSupertypeQuery": bool(settable.get("included_in_supertype_query", False)),
"controllablePolicy": bool(settable.get("controllable_policy", False)),
"controllableACL": bool(settable.get("controllable_acl", False)),
},
}
def cmis_browser_extended_features(repository_info: dict[str, Any]) -> list[dict[str, Any]]:
features = []
for feature in repository_info.get("repository_features", []):
if not isinstance(feature, dict):
continue
features.append(
compact_dict(
{
"id": feature.get("id"),
"commonName": feature.get("common_name"),
"versionLabel": feature.get("version_label"),
"description": feature.get("description"),
"url": feature.get("url", ""),
}
)
)
return features
def cmis_browser_type_definition(
type_definition: dict[str, Any],
*,
include_property_definitions: bool = True,
) -> dict[str, Any]:
base_id = type_definition.get("base_type_id")
type_id = _browser_type_id(type_definition)
is_document = base_id == CMISBaseType.DOCUMENT.value
is_folder = base_id == CMISBaseType.FOLDER.value
property_definitions = dict(type_definition.get("property_definitions", {}))
property_definitions.update(_browser_standard_property_definitions(str(base_id)))
result = {
"id": type_id,
"localName": type_id.split(":", 1)[-1],
"localNamespace": "http://docs.oasis-open.org/ns/cmis/core/200908/",
"displayName": _browser_type_display_name(type_definition),
"queryName": type_id,
"description": type_definition.get("display_name", type_id),
"baseId": base_id,
"parentId": None,
"creatable": bool(type_definition.get("creatable", False)),
"fileable": is_document or is_folder or bool(type_definition.get("fileable", False)),
"queryable": bool(type_definition.get("queryable", is_document)),
"fulltextIndexed": bool(type_definition.get("fulltext_indexed", False)),
"includedInSupertypeQuery": bool(type_definition.get("included_in_supertype_query", is_document)),
"controllablePolicy": bool(type_definition.get("controllable_policy", False)),
"controllableACL": bool(type_definition.get("controllable_acl", is_document)),
"typeMutability": {"create": False, "update": False, "delete": False},
}
if include_property_definitions:
result["propertyDefinitions"] = {
key: cmis_browser_property_definition(key, value)
for key, value in property_definitions.items()
}
if base_id == CMISBaseType.DOCUMENT.value:
result["versionable"] = bool(type_definition.get("versionable", False))
result["contentStreamAllowed"] = "allowed"
if base_id == CMISBaseType.RELATIONSHIP.value:
result["allowedSourceTypes"] = [CMISBaseType.DOCUMENT.value]
result["allowedTargetTypes"] = [CMISBaseType.DOCUMENT.value]
return compact_dict(result)
def cmis_browser_type_children(
type_definitions: list[dict[str, Any]],
*,
type_id: str | None = None,
skip_count: int = 0,
max_items: int = 100,
include_property_definitions: bool = False,
) -> dict[str, Any]:
definitions = _browser_type_definitions(
type_definitions,
include_property_definitions=include_property_definitions,
)
if type_id:
definitions = [
definition
for definition in definitions
if definition.get("parentId") == type_id
]
start = max(skip_count, 0)
limit = max(max_items, 0)
paged = definitions[start : start + limit]
return {
"types": paged,
"hasMoreItems": len(definitions) > start + len(paged),
"numItems": len(paged),
}
def cmis_browser_type_descendants(
type_definitions: list[dict[str, Any]],
*,
type_id: str | None = None,
include_property_definitions: bool = False,
) -> list[dict[str, Any]]:
definitions = _browser_type_definitions(
type_definitions,
include_property_definitions=include_property_definitions,
)
if type_id:
definitions = [
definition
for definition in definitions
if definition.get("parentId") == type_id
]
return [{"type": definition, "children": []} for definition in definitions]
def cmis_browser_type_definition_by_id(
type_definitions: list[dict[str, Any]],
type_id: str | None,
*,
include_property_definitions: bool = True,
) -> dict[str, Any]:
definitions = _browser_type_definitions(
type_definitions,
include_property_definitions=include_property_definitions,
)
if type_id is None:
return definitions[0]
for definition in definitions:
if definition["id"] == type_id:
return definition
for definition in definitions:
if definition.get("baseId") == type_id:
return definition
raise KeyError(type_id)
def cmis_browser_object(projection: dict[str, Any]) -> dict[str, Any]:
properties = _browser_object_properties(projection)
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", [])),
}
acl = projection.get("acl")
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]:
objects = []
for item in children.get("objects", []):
objects.append(
{
"object": cmis_browser_object(item),
"pathSegment": item.get("name") or item.get("object_id"),
}
)
return {
"objects": objects,
"hasMoreItems": bool(children.get("has_more_items", False)),
"numItems": int(children.get("num_items", len(objects))),
}
def cmis_browser_object_list(objects: list[dict[str, Any]], *, has_more_items: bool = False) -> dict[str, Any]:
return {
"objects": [cmis_browser_object(item) for item in objects],
"hasMoreItems": has_more_items,
"numItems": len(objects),
}
def cmis_browser_parent_list(parents: dict[str, Any]) -> list[dict[str, Any]]:
items = []
for parent in parents.get("parents", []):
items.append(
{
"object": cmis_browser_object(parent),
"relativePathSegment": parent.get("name"),
}
)
return items
def cmis_browser_query_result(query_result: dict[str, Any]) -> dict[str, Any]:
results = [{"succinctProperties": _browser_object_properties(item)} for item in query_result.get("results", [])]
return {
"results": results,
"hasMoreItems": bool(query_result.get("has_more_items", False)),
"numItems": int(query_result.get("num_items", len(results))),
}
def cmis_browser_acl(acl: dict[str, Any]) -> dict[str, Any]:
aces = []
for ace in acl.get("aces", []):
aces.append(
{
"principal": {"principalId": ace.get("principal_id")},
"permissions": list(ace.get("permissions", [])),
"isDirect": bool(ace.get("direct", True)),
}
)
return {"aces": aces, "isExact": bool(acl.get("is_exact", True))}
def cmis_browser_property_definition(
property_id: str,
definition: dict[str, Any],
) -> dict[str, Any]:
local_name = property_id.split(":", 1)[-1]
return {
"id": property_id,
"localName": local_name,
"displayName": local_name,
"queryName": property_id,
"description": property_id,
"propertyType": definition.get("property_type", "string"),
"cardinality": definition.get("cardinality", "single"),
"updatability": definition.get(
"updatability",
"readwrite" if not property_id.startswith("cmis:") else "readonly",
),
"inherited": bool(definition.get("inherited", False)),
"required": bool(definition.get("required", False)),
"queryable": bool(definition.get("queryable", True)),
"orderable": bool(definition.get("orderable", False)),
"openChoice": bool(definition.get("open_choice", True)),
}
def cmis_browser_property_value(property_id: str, value: Any) -> dict[str, Any]:
local_name = property_id.split(":", 1)[-1]
return {
"id": property_id,
"localName": local_name,
"displayName": local_name,
"queryName": property_id,
"type": _browser_property_type(property_id, value),
"cardinality": "multi" if isinstance(value, list) else "single",
"value": value,
}
def cmis_browser_allowable_actions(actions: list[str] | tuple[str, ...]) -> dict[str, bool]:
native = set(actions)
return {
"canGetObjectParents": CMISAction.GET_OBJECT_PARENTS.value in native,
"canGetProperties": CMISAction.GET_OBJECT.value in native,
"canGetObjectRelationships": CMISAction.GET_RELATIONSHIPS.value in native,
"canGetContentStream": CMISAction.GET_CONTENT_STREAM.value in native,
"canGetACL": CMISAction.GET_ACL.value in native,
"canUpdateProperties": CMISAction.UPDATE_PROPERTIES.value in native,
"canDeleteObject": CMISAction.DELETE_OBJECT.value in native,
"canSetContentStream": CMISAction.SET_CONTENT_STREAM.value in native,
"canGetChildren": CMISAction.GET_CHILDREN.value in native,
"canCreateDocument": CMISAction.CREATE_DOCUMENT.value in native,
"canCreateFolder": False,
"canCreateRelationship": False,
"canCreateItem": False,
"canDeleteTree": False,
"canGetDescendants": False,
"canGetFolderTree": False,
"canGetFolderParent": False,
"canGetRenditions": False,
"canMoveObject": False,
"canAddObjectToFolder": False,
"canRemoveObjectFromFolder": False,
"canCheckOut": False,
"canCancelCheckOut": False,
"canCheckIn": False,
"canGetAllVersions": False,
"canApplyPolicy": False,
"canRemovePolicy": False,
"canGetAppliedPolicies": False,
"canApplyACL": False,
"canDeleteContentStream": False,
}
def cmis_browser_root_folder(access_point: CMISAccessPoint) -> dict[str, Any]:
return {
"object_id": access_point.root_folder_id,
"base_type_id": CMISBaseType.FOLDER.value,
"type_id": CMISBaseType.FOLDER.value,
"name": "root",
"path": "/",
"properties": {
"cmis:objectId": access_point.root_folder_id,
"cmis:name": "root",
"cmis:baseTypeId": CMISBaseType.FOLDER.value,
"cmis:objectTypeId": CMISBaseType.FOLDER.value,
"cmis:path": "/",
"cmis:createdBy": "system",
"cmis:lastModifiedBy": "system",
"cmis:creationDate": "1970-01-01T00:00:00+00:00",
"cmis:lastModificationDate": "1970-01-01T00:00:00+00:00",
"cmis:changeToken": "root",
"cmis:description": "CMIS root folder",
"cmis:secondaryObjectTypeIds": [],
"cmis:parentId": None,
"cmis:allowedChildObjectTypeIds": [CMISBaseType.DOCUMENT.value, CMISBaseType.FOLDER.value],
"kontextual:sensitivity": "internal",
"kontextual:lifecycle": LifecycleState.ACTIVE.value,
},
"allowable_actions": [CMISAction.GET_OBJECT.value, CMISAction.GET_CHILDREN.value],
}
def _browser_acl_capabilities() -> dict[str, Any]:
return {
"supportedPermissions": "basic",
"propagation": "objectonly",
"permissions": [
{"permission": "cmis:read", "description": "Read"},
{"permission": "cmis:write", "description": "Write"},
{"permission": "cmis:all", "description": "All"},
],
"permissionMapping": [
{"key": "canGetProperties.Object", "permission": ["cmis:read"]},
{"key": "canViewContent.Object", "permission": ["cmis:read"]},
{"key": "canGetChildren.Folder", "permission": ["cmis:read"]},
{"key": "canGetParents.Folder", "permission": ["cmis:read"]},
{"key": "canUpdateProperties.Object", "permission": ["cmis:write"]},
{"key": "canDelete.Object", "permission": ["cmis:write"]},
{"key": "canSetContent.Document", "permission": ["cmis:write"]},
],
}
def _browser_type_definitions(
type_definitions: list[dict[str, Any]],
*,
include_property_definitions: bool = True,
) -> list[dict[str, Any]]:
by_base: dict[str, dict[str, Any]] = {}
for definition in type_definitions:
base_id = definition.get("base_type_id")
if isinstance(base_id, str) and base_id not in by_base:
by_base[base_id] = cmis_browser_type_definition(
definition,
include_property_definitions=include_property_definitions,
)
order = [
CMISBaseType.DOCUMENT.value,
CMISBaseType.FOLDER.value,
CMISBaseType.RELATIONSHIP.value,
CMISBaseType.POLICY.value,
CMISBaseType.ITEM.value,
CMISBaseType.SECONDARY.value,
]
return [by_base[item] for item in order if item in by_base]
def _browser_type_id(type_definition: dict[str, Any]) -> str:
base_id = type_definition.get("base_type_id")
if isinstance(base_id, str) and base_id.startswith("cmis:"):
return base_id
return str(type_definition.get("id"))
def _browser_type_display_name(type_definition: dict[str, Any]) -> str:
base_id = type_definition.get("base_type_id")
if base_id == CMISBaseType.DOCUMENT.value:
return "Document"
if base_id == CMISBaseType.FOLDER.value:
return "Folder"
if base_id == CMISBaseType.RELATIONSHIP.value:
return "Relationship"
if base_id == CMISBaseType.POLICY.value:
return "Policy"
if base_id == CMISBaseType.ITEM.value:
return "Item"
if base_id == CMISBaseType.SECONDARY.value:
return "Secondary"
return str(type_definition.get("display_name", type_definition.get("id", "CMIS Type")))
def _browser_standard_property_definitions(base_id: str) -> dict[str, dict[str, Any]]:
common = {
"cmis:objectId": _browser_propdef("id", required=False, updatability="readonly"),
"cmis:name": _browser_propdef("string", required=True, updatability="readwrite"),
"cmis:baseTypeId": _browser_propdef("id", required=False, updatability="readonly"),
"cmis:objectTypeId": _browser_propdef("id", required=True, updatability="oncreate"),
"cmis:createdBy": _browser_propdef(
"string",
required=False,
updatability="readonly",
orderable=True,
),
"cmis:creationDate": _browser_propdef(
"datetime",
required=False,
updatability="readonly",
orderable=True,
),
"cmis:lastModifiedBy": _browser_propdef(
"string",
required=False,
updatability="readonly",
orderable=True,
),
"cmis:lastModificationDate": _browser_propdef(
"datetime",
required=False,
updatability="readonly",
orderable=True,
),
"cmis:changeToken": _browser_propdef("string", required=False, updatability="readonly"),
"cmis:description": _browser_propdef("string", required=False, updatability="readwrite"),
"cmis:secondaryObjectTypeIds": _browser_propdef(
"id",
cardinality="multi",
required=False,
updatability="readwrite",
),
}
if base_id == CMISBaseType.FOLDER.value:
common.update(
{
"cmis:parentId": _browser_propdef("id", required=False, updatability="readonly"),
"cmis:path": _browser_propdef("string", required=False, updatability="readonly"),
"cmis:allowedChildObjectTypeIds": _browser_propdef(
"id",
cardinality="multi",
required=False,
updatability="readonly",
),
}
)
if base_id == CMISBaseType.DOCUMENT.value:
common.update(
{
"cmis:isImmutable": _browser_propdef("boolean", required=False, updatability="readonly"),
"cmis:isLatestVersion": _browser_propdef("boolean", required=False, updatability="readonly"),
"cmis:isMajorVersion": _browser_propdef("boolean", required=False, updatability="readonly"),
"cmis:isLatestMajorVersion": _browser_propdef("boolean", required=False, updatability="readonly"),
"cmis:versionLabel": _browser_propdef("string", required=False, updatability="readonly"),
"cmis:versionSeriesId": _browser_propdef("id", required=False, updatability="readonly"),
"cmis:isVersionSeriesCheckedOut": _browser_propdef(
"boolean",
required=False,
updatability="readonly",
),
"cmis:isPrivateWorkingCopy": _browser_propdef(
"boolean",
required=False,
updatability="readonly",
),
"cmis:versionSeriesCheckedOutBy": _browser_propdef(
"string",
required=False,
updatability="readonly",
),
"cmis:versionSeriesCheckedOutId": _browser_propdef("id", required=False, updatability="readonly"),
"cmis:checkinComment": _browser_propdef("string", required=False, updatability="readonly"),
"cmis:contentStreamLength": _browser_propdef("integer", required=False, updatability="readonly"),
"cmis:contentStreamMimeType": _browser_propdef("string", required=False, updatability="readonly"),
"cmis:contentStreamFileName": _browser_propdef("string", required=False, updatability="readonly"),
"cmis:contentStreamId": _browser_propdef("id", required=False, updatability="readonly"),
}
)
if base_id == CMISBaseType.RELATIONSHIP.value:
common.update(
{
"cmis:sourceId": _browser_propdef("id", required=True, updatability="readonly"),
"cmis:targetId": _browser_propdef("id", required=True, updatability="readonly"),
}
)
if base_id == CMISBaseType.POLICY.value:
common["cmis:policyText"] = _browser_propdef("string", required=False, updatability="readonly")
return common
def _browser_propdef(
property_type: str,
*,
cardinality: str = "single",
required: bool = False,
updatability: str = "readonly",
queryable: bool = True,
orderable: bool = False,
open_choice: bool = True,
) -> dict[str, Any]:
return {
"property_type": property_type,
"cardinality": cardinality,
"required": required,
"updatability": updatability,
"queryable": queryable,
"orderable": orderable,
"open_choice": open_choice,
}
def _browser_object_properties(projection: dict[str, Any]) -> dict[str, Any]:
properties = dict(projection.get("properties", {}))
base_type = projection.get("base_type_id") or properties.get("cmis:baseTypeId")
properties["cmis:objectId"] = projection.get("object_id", properties.get("cmis:objectId"))
properties["cmis:name"] = projection.get("name", properties.get("cmis:name"))
properties["cmis:baseTypeId"] = base_type
if base_type in {CMISBaseType.DOCUMENT.value, CMISBaseType.FOLDER.value}:
properties["cmis:objectTypeId"] = base_type
else:
properties["cmis:objectTypeId"] = projection.get("type_id", properties.get("cmis:objectTypeId"))
return properties
def _browser_property_type(property_id: str, value: Any) -> str:
if property_id in {
"cmis:objectId",
"cmis:baseTypeId",
"cmis:objectTypeId",
"cmis:contentStreamId",
"cmis:sourceId",
"cmis:targetId",
}:
return "id"
if property_id in {"cmis:creationDate", "cmis:lastModificationDate"}:
return "datetime"
if property_id == "cmis:contentStreamLength":
return "integer"
if isinstance(value, bool):
return "boolean"
if isinstance(value, int):
return "integer"
if isinstance(value, float):
return "decimal"
return "string"
def _read_capabilities() -> tuple[CMISCapability, ...]:
return (
CMISCapability.REPOSITORY,