generated from coulomb/repo-seed
1871 lines
75 KiB
Python
1871 lines
75 KiB
Python
"""CMIS access-point profile primitives.
|
|
|
|
These classes model how a CMIS adapter may expose engine capabilities without
|
|
turning CMIS into a second domain model.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from enum import Enum
|
|
from typing import Any
|
|
|
|
from .actors import ActorType, OperationContext
|
|
from .assets import AssetRepresentation, KnowledgeAsset, RepresentationKind
|
|
from .metadata import LifecycleState, Sensitivity
|
|
from .policy import PolicyDecision
|
|
from .provenance import AssetVersion
|
|
from .relationships import CoreRelationship, RelationshipTargetKind
|
|
from .primitives import compact_dict
|
|
|
|
|
|
class CMISBinding(str, Enum):
|
|
BROWSER = "browser"
|
|
ATOMPUB = "atompub"
|
|
WEB_SERVICES = "web_services"
|
|
|
|
|
|
class CMISCapability(str, Enum):
|
|
REPOSITORY = "repository"
|
|
TYPE_DEFINITIONS = "type_definitions"
|
|
NAVIGATION = "navigation"
|
|
OBJECT_READ = "object_read"
|
|
OBJECT_WRITE = "object_write"
|
|
CONTENT_STREAM_READ = "content_stream_read"
|
|
CONTENT_STREAM_WRITE = "content_stream_write"
|
|
VERSIONING = "versioning"
|
|
DISCOVERY_QUERY = "discovery_query"
|
|
RELATIONSHIPS = "relationships"
|
|
ACL = "acl"
|
|
POLICY = "policy"
|
|
CHANGE_LOG = "change_log"
|
|
RENDITIONS = "renditions"
|
|
RETENTION_HOLD = "retention_hold"
|
|
BULK_UPDATE = "bulk_update"
|
|
|
|
|
|
class CMISAction(str, Enum):
|
|
GET_REPOSITORY_INFO = "get_repository_info"
|
|
GET_TYPE_DEFINITION = "get_type_definition"
|
|
GET_CHILDREN = "get_children"
|
|
GET_OBJECT_PARENTS = "get_object_parents"
|
|
GET_OBJECT = "get_object"
|
|
GET_CONTENT_STREAM = "get_content_stream"
|
|
GET_ACL = "get_acl"
|
|
QUERY = "query"
|
|
GET_RELATIONSHIPS = "get_relationships"
|
|
GET_CHANGE_LOG = "get_change_log"
|
|
CREATE_FOLDER = "create_folder"
|
|
CREATE_DOCUMENT = "create_document"
|
|
UPDATE_PROPERTIES = "update_properties"
|
|
DELETE_OBJECT = "delete_object"
|
|
DELETE_TREE = "delete_tree"
|
|
SET_CONTENT_STREAM = "set_content_stream"
|
|
APPLY_ACL = "apply_acl"
|
|
APPLY_POLICY = "apply_policy"
|
|
BULK_UPDATE_PROPERTIES = "bulk_update_properties"
|
|
|
|
|
|
class CMISBaseType(str, Enum):
|
|
DOCUMENT = "cmis:document"
|
|
FOLDER = "cmis:folder"
|
|
RELATIONSHIP = "cmis:relationship"
|
|
POLICY = "cmis:policy"
|
|
ITEM = "cmis:item"
|
|
SECONDARY = "cmis:secondary"
|
|
|
|
|
|
ACTION_CAPABILITIES: dict[CMISAction, CMISCapability] = {
|
|
CMISAction.GET_REPOSITORY_INFO: CMISCapability.REPOSITORY,
|
|
CMISAction.GET_TYPE_DEFINITION: CMISCapability.TYPE_DEFINITIONS,
|
|
CMISAction.GET_CHILDREN: CMISCapability.NAVIGATION,
|
|
CMISAction.GET_OBJECT_PARENTS: CMISCapability.NAVIGATION,
|
|
CMISAction.GET_OBJECT: CMISCapability.OBJECT_READ,
|
|
CMISAction.GET_CONTENT_STREAM: CMISCapability.CONTENT_STREAM_READ,
|
|
CMISAction.GET_ACL: CMISCapability.ACL,
|
|
CMISAction.QUERY: CMISCapability.DISCOVERY_QUERY,
|
|
CMISAction.GET_RELATIONSHIPS: CMISCapability.RELATIONSHIPS,
|
|
CMISAction.GET_CHANGE_LOG: CMISCapability.CHANGE_LOG,
|
|
CMISAction.CREATE_FOLDER: CMISCapability.OBJECT_WRITE,
|
|
CMISAction.CREATE_DOCUMENT: CMISCapability.OBJECT_WRITE,
|
|
CMISAction.UPDATE_PROPERTIES: CMISCapability.OBJECT_WRITE,
|
|
CMISAction.DELETE_OBJECT: CMISCapability.OBJECT_WRITE,
|
|
CMISAction.DELETE_TREE: CMISCapability.OBJECT_WRITE,
|
|
CMISAction.SET_CONTENT_STREAM: CMISCapability.CONTENT_STREAM_WRITE,
|
|
CMISAction.APPLY_ACL: CMISCapability.ACL,
|
|
CMISAction.APPLY_POLICY: CMISCapability.POLICY,
|
|
CMISAction.BULK_UPDATE_PROPERTIES: CMISCapability.BULK_UPDATE,
|
|
}
|
|
IMPLEMENTED_CMIS_ACTIONS: frozenset[CMISAction] = frozenset(
|
|
{
|
|
CMISAction.GET_REPOSITORY_INFO,
|
|
CMISAction.GET_TYPE_DEFINITION,
|
|
CMISAction.GET_CHILDREN,
|
|
CMISAction.GET_OBJECT_PARENTS,
|
|
CMISAction.GET_OBJECT,
|
|
CMISAction.GET_CONTENT_STREAM,
|
|
CMISAction.GET_ACL,
|
|
CMISAction.QUERY,
|
|
CMISAction.GET_RELATIONSHIPS,
|
|
CMISAction.GET_CHANGE_LOG,
|
|
CMISAction.CREATE_FOLDER,
|
|
CMISAction.CREATE_DOCUMENT,
|
|
CMISAction.UPDATE_PROPERTIES,
|
|
CMISAction.DELETE_OBJECT,
|
|
CMISAction.DELETE_TREE,
|
|
CMISAction.SET_CONTENT_STREAM,
|
|
}
|
|
)
|
|
MUTATION_ACTIONS = {
|
|
CMISAction.CREATE_DOCUMENT,
|
|
CMISAction.CREATE_FOLDER,
|
|
CMISAction.UPDATE_PROPERTIES,
|
|
CMISAction.DELETE_OBJECT,
|
|
CMISAction.DELETE_TREE,
|
|
CMISAction.SET_CONTENT_STREAM,
|
|
CMISAction.APPLY_ACL,
|
|
CMISAction.APPLY_POLICY,
|
|
CMISAction.BULK_UPDATE_PROPERTIES,
|
|
}
|
|
NEW_TYPE_SETTABLE_ATTRIBUTES: dict[str, bool] = {
|
|
"id": False,
|
|
"local_name": False,
|
|
"local_namespace": False,
|
|
"display_name": False,
|
|
"query_name": False,
|
|
"description": False,
|
|
"creatable": False,
|
|
"fileable": False,
|
|
"queryable": False,
|
|
"fulltext_indexed": False,
|
|
"included_in_supertype_query": False,
|
|
"controllable_policy": False,
|
|
"controllable_acl": False,
|
|
}
|
|
UNSUPPORTED_FEATURES: dict[str, dict[str, Any]] = {
|
|
"atompub": {"status": "unsupported", "reason": "binding_not_supported", "intent": "deferred"},
|
|
"web_services": {"status": "unsupported", "reason": "binding_not_supported", "intent": "deferred"},
|
|
"get_descendants": {
|
|
"status": "unsupported",
|
|
"reason": "capability_not_supported",
|
|
"standard_flag": "capability_get_descendants",
|
|
},
|
|
"get_folder_tree": {
|
|
"status": "unsupported",
|
|
"reason": "capability_not_supported",
|
|
"standard_flag": "capability_get_folder_tree",
|
|
},
|
|
"multifiling": {
|
|
"status": "projection_only",
|
|
"reason": "mutation_capability_not_supported",
|
|
"standard_flag": "capability_multifiling",
|
|
},
|
|
"unfiling": {
|
|
"status": "unsupported",
|
|
"reason": "capability_not_supported",
|
|
"standard_flag": "capability_unfiling",
|
|
},
|
|
"versioning_services": {
|
|
"status": "unsupported",
|
|
"reason": "capability_not_supported",
|
|
"standard_flag": "document_type.versionable",
|
|
},
|
|
"private_working_copy": {
|
|
"status": "unsupported",
|
|
"reason": "capability_not_supported",
|
|
"standard_flags": ["capability_pwc_searchable", "capability_pwc_updatable"],
|
|
},
|
|
"all_versions_search": {
|
|
"status": "unsupported",
|
|
"reason": "capability_not_supported",
|
|
"standard_flag": "capability_all_versions_searchable",
|
|
},
|
|
"full_cmis_sql_joins": {
|
|
"status": "unsupported",
|
|
"reason": "query_not_supported",
|
|
"standard_flag": "capability_join",
|
|
},
|
|
"order_by": {"status": "unsupported", "reason": "query_not_supported", "standard_flag": "capability_order_by"},
|
|
"append_content_stream": {
|
|
"status": "unsupported",
|
|
"reason": "capability_not_supported",
|
|
"standard_flag": "capability_content_stream_updatability",
|
|
},
|
|
"apply_acl": {"status": "unsupported", "reason": "operation_not_implemented", "standard_flag": "capability_acl"},
|
|
"apply_policy": {"status": "unsupported", "reason": "capability_not_supported"},
|
|
"remove_policy": {"status": "unsupported", "reason": "capability_not_supported"},
|
|
"retention_hold_mutation": {"status": "unsupported", "reason": "capability_not_supported"},
|
|
"bulk_update_properties": {
|
|
"status": "unsupported",
|
|
"reason": "operation_not_implemented",
|
|
"standard_service": "bulkUpdateProperties",
|
|
},
|
|
"rendition_streams": {
|
|
"status": "unsupported",
|
|
"reason": "capability_not_supported",
|
|
"standard_flag": "capability_renditions",
|
|
},
|
|
"type_mutability": {
|
|
"status": "unsupported",
|
|
"reason": "capability_not_supported",
|
|
"standard_flag": "capability_new_type_settable_attributes",
|
|
},
|
|
}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class CMISAccessProfile:
|
|
name: str
|
|
binding: CMISBinding | str = CMISBinding.BROWSER
|
|
capabilities: tuple[CMISCapability | str, ...] = ()
|
|
allow_mutations: bool = False
|
|
visible_sensitivities: tuple[Sensitivity | str, ...] = (Sensitivity.PUBLIC, Sensitivity.INTERNAL)
|
|
denied_sensitivities: tuple[Sensitivity | str, ...] = (Sensitivity.CONFIDENTIAL, Sensitivity.RESTRICTED)
|
|
visible_asset_types: tuple[str, ...] = ()
|
|
visible_topics: tuple[str, ...] = ()
|
|
visible_source_systems: tuple[str, ...] = ()
|
|
denied_metadata: dict[str, tuple[Any, ...]] = field(default_factory=dict)
|
|
required_actor_types: tuple[ActorType | str, ...] = ()
|
|
metadata: dict[str, Any] = field(default_factory=dict)
|
|
|
|
def __post_init__(self) -> None:
|
|
object.__setattr__(self, "binding", CMISBinding(self.binding))
|
|
object.__setattr__(
|
|
self,
|
|
"capabilities",
|
|
tuple(CMISCapability(capability) for capability in self.capabilities),
|
|
)
|
|
object.__setattr__(
|
|
self,
|
|
"visible_sensitivities",
|
|
tuple(Sensitivity(value) for value in self.visible_sensitivities),
|
|
)
|
|
object.__setattr__(
|
|
self,
|
|
"denied_sensitivities",
|
|
tuple(Sensitivity(value) for value in self.denied_sensitivities),
|
|
)
|
|
object.__setattr__(
|
|
self,
|
|
"required_actor_types",
|
|
tuple(ActorType(value) for value in self.required_actor_types),
|
|
)
|
|
object.__setattr__(
|
|
self,
|
|
"denied_metadata",
|
|
{key: tuple(values) for key, values in self.denied_metadata.items()},
|
|
)
|
|
|
|
@classmethod
|
|
def readonly_browser(cls) -> "CMISAccessProfile":
|
|
return cls(
|
|
name="readonly-browser",
|
|
capabilities=_read_capabilities(),
|
|
allow_mutations=False,
|
|
)
|
|
|
|
@classmethod
|
|
def governed_authoring(cls) -> "CMISAccessProfile":
|
|
return cls(
|
|
name="governed-authoring",
|
|
capabilities=_read_capabilities()
|
|
+ (
|
|
CMISCapability.OBJECT_WRITE,
|
|
CMISCapability.CONTENT_STREAM_WRITE,
|
|
),
|
|
allow_mutations=True,
|
|
)
|
|
|
|
@classmethod
|
|
def admin_export(cls) -> "CMISAccessProfile":
|
|
return cls(
|
|
name="admin-export",
|
|
capabilities=_read_capabilities(),
|
|
allow_mutations=False,
|
|
visible_sensitivities=(
|
|
Sensitivity.PUBLIC,
|
|
Sensitivity.INTERNAL,
|
|
Sensitivity.CONFIDENTIAL,
|
|
Sensitivity.RESTRICTED,
|
|
),
|
|
denied_sensitivities=(),
|
|
required_actor_types=(ActorType.SERVICE_ACCOUNT,),
|
|
)
|
|
|
|
@classmethod
|
|
def compat_tck(cls) -> "CMISAccessProfile":
|
|
return cls(
|
|
name="compat-tck",
|
|
capabilities=_read_capabilities()
|
|
+ (
|
|
CMISCapability.OBJECT_WRITE,
|
|
CMISCapability.CONTENT_STREAM_WRITE,
|
|
),
|
|
allow_mutations=True,
|
|
metadata={"compatibility": "selected-opencmis-tck-browser-subset"},
|
|
)
|
|
|
|
def has_capability(self, capability: CMISCapability | str) -> bool:
|
|
return CMISCapability(capability) in self.capabilities
|
|
|
|
def allows_action(self, action: CMISAction | str) -> bool:
|
|
cmis_action = CMISAction(action)
|
|
return self.action_denial(cmis_action) is None
|
|
|
|
def action_denial(self, action: CMISAction | str) -> tuple[str, dict[str, Any]] | None:
|
|
cmis_action = CMISAction(action)
|
|
capability = ACTION_CAPABILITIES[cmis_action]
|
|
if cmis_action in MUTATION_ACTIONS and not self.allow_mutations:
|
|
return (
|
|
"cmis_mutation_not_allowed",
|
|
{"profile": self.name, "capability": capability.value, "mutation": cmis_action.value},
|
|
)
|
|
if not self.has_capability(capability):
|
|
return (
|
|
"cmis_capability_not_supported",
|
|
{"profile": self.name, "capability": capability.value},
|
|
)
|
|
if cmis_action not in IMPLEMENTED_CMIS_ACTIONS:
|
|
return (
|
|
"cmis_operation_not_implemented",
|
|
{"profile": self.name, "capability": capability.value, "operation": cmis_action.value},
|
|
)
|
|
return None
|
|
|
|
def decide_action(
|
|
self,
|
|
action: CMISAction | str,
|
|
context: OperationContext,
|
|
*,
|
|
resource: str = "cmis:repository",
|
|
) -> PolicyDecision:
|
|
cmis_action = CMISAction(action)
|
|
if not self.allows_actor(context):
|
|
return PolicyDecision.deny(
|
|
context.actor.id,
|
|
cmis_action.value,
|
|
resource,
|
|
reason="actor_type_not_allowed_for_cmis_profile",
|
|
context={"profile": self.name},
|
|
)
|
|
if self.allows_action(cmis_action):
|
|
return PolicyDecision.allow(
|
|
context.actor.id,
|
|
cmis_action.value,
|
|
resource,
|
|
context={"profile": self.name},
|
|
)
|
|
reason, denial_context = self.action_denial(cmis_action) or (
|
|
"cmis_capability_not_supported",
|
|
{"profile": self.name},
|
|
)
|
|
return PolicyDecision.deny(
|
|
context.actor.id,
|
|
cmis_action.value,
|
|
resource,
|
|
reason=reason,
|
|
context=denial_context,
|
|
)
|
|
|
|
def allows_actor(self, context: OperationContext) -> bool:
|
|
return not self.required_actor_types or context.actor.actor_type in self.required_actor_types
|
|
|
|
def exposes_asset(self, asset: KnowledgeAsset, context: OperationContext) -> bool:
|
|
return self.decide_asset_visibility(asset, context).allowed
|
|
|
|
def decide_asset_visibility(
|
|
self,
|
|
asset: KnowledgeAsset,
|
|
context: OperationContext,
|
|
) -> PolicyDecision:
|
|
resource = f"asset:{asset.id}"
|
|
if not self.allows_actor(context):
|
|
return PolicyDecision.deny(
|
|
context.actor.id,
|
|
"cmis.expose_asset",
|
|
resource,
|
|
reason="actor_type_not_allowed_for_cmis_profile",
|
|
context={"profile": self.name},
|
|
)
|
|
classification = asset.classification
|
|
if asset.lifecycle == LifecycleState.DELETE_REQUESTED:
|
|
return PolicyDecision.deny(
|
|
context.actor.id,
|
|
"cmis.expose_asset",
|
|
resource,
|
|
reason="cmis_lifecycle_not_visible",
|
|
context={"profile": self.name, "lifecycle": _enum_value(asset.lifecycle)},
|
|
)
|
|
if classification.sensitivity in self.denied_sensitivities:
|
|
return PolicyDecision.deny(
|
|
context.actor.id,
|
|
"cmis.expose_asset",
|
|
resource,
|
|
reason="cmis_sensitivity_denied",
|
|
context={"profile": self.name, "sensitivity": _enum_value(classification.sensitivity)},
|
|
)
|
|
if classification.sensitivity not in self.visible_sensitivities:
|
|
return PolicyDecision.deny(
|
|
context.actor.id,
|
|
"cmis.expose_asset",
|
|
resource,
|
|
reason="cmis_sensitivity_not_visible",
|
|
context={"profile": self.name, "sensitivity": _enum_value(classification.sensitivity)},
|
|
)
|
|
if self.visible_asset_types and classification.asset_type not in self.visible_asset_types:
|
|
return PolicyDecision.deny(
|
|
context.actor.id,
|
|
"cmis.expose_asset",
|
|
resource,
|
|
reason="cmis_asset_type_not_visible",
|
|
context={"profile": self.name, "asset_type": classification.asset_type},
|
|
)
|
|
if self.visible_topics and not set(classification.topics).intersection(self.visible_topics):
|
|
return PolicyDecision.deny(
|
|
context.actor.id,
|
|
"cmis.expose_asset",
|
|
resource,
|
|
reason="cmis_topic_not_visible",
|
|
context={"profile": self.name, "topics": list(classification.topics)},
|
|
)
|
|
source_system = asset.metadata.get("source_system") or classification.metadata.get("source_system")
|
|
if self.visible_source_systems and source_system not in self.visible_source_systems:
|
|
return PolicyDecision.deny(
|
|
context.actor.id,
|
|
"cmis.expose_asset",
|
|
resource,
|
|
reason="cmis_source_system_not_visible",
|
|
context={"profile": self.name, "source_system": source_system},
|
|
)
|
|
for key, denied_values in self.denied_metadata.items():
|
|
value = asset.metadata.get(key, classification.metadata.get(key))
|
|
if value in denied_values:
|
|
return PolicyDecision.deny(
|
|
context.actor.id,
|
|
"cmis.expose_asset",
|
|
resource,
|
|
reason="cmis_metadata_denied",
|
|
context={"profile": self.name, "metadata_key": key},
|
|
)
|
|
return PolicyDecision.allow(
|
|
context.actor.id,
|
|
"cmis.expose_asset",
|
|
resource,
|
|
context={"profile": self.name},
|
|
)
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
return compact_dict(
|
|
{
|
|
"name": self.name,
|
|
"binding": self.binding.value,
|
|
"capabilities": [capability.value for capability in self.capabilities],
|
|
"allow_mutations": self.allow_mutations,
|
|
"visible_sensitivities": [item.value for item in self.visible_sensitivities],
|
|
"denied_sensitivities": [item.value for item in self.denied_sensitivities],
|
|
"visible_asset_types": list(self.visible_asset_types),
|
|
"visible_topics": list(self.visible_topics),
|
|
"visible_source_systems": list(self.visible_source_systems),
|
|
"denied_metadata": {key: list(values) for key, values in self.denied_metadata.items()},
|
|
"required_actor_types": [item.value for item in self.required_actor_types],
|
|
"metadata": dict(self.metadata),
|
|
}
|
|
)
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict[str, Any]) -> "CMISAccessProfile":
|
|
return cls(
|
|
name=data["name"],
|
|
binding=data.get("binding", CMISBinding.BROWSER.value),
|
|
capabilities=tuple(data.get("capabilities", [])),
|
|
allow_mutations=bool(data.get("allow_mutations", False)),
|
|
visible_sensitivities=tuple(data.get("visible_sensitivities", [])),
|
|
denied_sensitivities=tuple(data.get("denied_sensitivities", [])),
|
|
visible_asset_types=tuple(data.get("visible_asset_types", [])),
|
|
visible_topics=tuple(data.get("visible_topics", [])),
|
|
visible_source_systems=tuple(data.get("visible_source_systems", [])),
|
|
denied_metadata=dict(data.get("denied_metadata", {})),
|
|
required_actor_types=tuple(data.get("required_actor_types", [])),
|
|
metadata=dict(data.get("metadata", {})),
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class CMISAccessPoint:
|
|
access_point_id: str
|
|
repository_id: str
|
|
profile: CMISAccessProfile
|
|
base_path: str
|
|
root_folder_id: str = "cmis-root"
|
|
enabled: bool = True
|
|
metadata: dict[str, Any] = field(default_factory=dict)
|
|
|
|
def __post_init__(self) -> None:
|
|
normalized = "/" + self.base_path.strip("/")
|
|
object.__setattr__(self, "base_path", normalized)
|
|
|
|
def decide_action(
|
|
self,
|
|
action: CMISAction | str,
|
|
context: OperationContext,
|
|
*,
|
|
resource: str | None = None,
|
|
) -> PolicyDecision:
|
|
if not self.enabled:
|
|
return PolicyDecision.deny(
|
|
context.actor.id,
|
|
CMISAction(action).value,
|
|
resource or f"cmis:{self.repository_id}",
|
|
reason="cmis_access_point_disabled",
|
|
context={"access_point_id": self.access_point_id, "profile": self.profile.name},
|
|
)
|
|
return self.profile.decide_action(
|
|
action,
|
|
context,
|
|
resource=resource or f"cmis:{self.repository_id}",
|
|
)
|
|
|
|
def exposes_asset(self, asset: KnowledgeAsset, context: OperationContext) -> bool:
|
|
return self.enabled and self.profile.exposes_asset(asset, context)
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
return compact_dict(
|
|
{
|
|
"access_point_id": self.access_point_id,
|
|
"repository_id": self.repository_id,
|
|
"profile": self.profile.to_dict(),
|
|
"base_path": self.base_path,
|
|
"root_folder_id": self.root_folder_id,
|
|
"enabled": self.enabled,
|
|
"metadata": dict(self.metadata),
|
|
}
|
|
)
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict[str, Any]) -> "CMISAccessPoint":
|
|
return cls(
|
|
access_point_id=data["access_point_id"],
|
|
repository_id=data["repository_id"],
|
|
profile=CMISAccessProfile.from_dict(data["profile"]),
|
|
base_path=data["base_path"],
|
|
root_folder_id=data.get("root_folder_id", "cmis-root"),
|
|
enabled=bool(data.get("enabled", True)),
|
|
metadata=dict(data.get("metadata", {})),
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class CMISObjectProjection:
|
|
object_id: str
|
|
base_type_id: CMISBaseType
|
|
type_id: str
|
|
name: str
|
|
properties: dict[str, Any]
|
|
allowable_actions: tuple[CMISAction, ...] = ()
|
|
path: str | None = None
|
|
content_stream: dict[str, Any] | None = None
|
|
version: dict[str, Any] | None = None
|
|
relationships: tuple[str, ...] = ()
|
|
acl: dict[str, Any] | None = None
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
return compact_dict(
|
|
{
|
|
"object_id": self.object_id,
|
|
"base_type_id": self.base_type_id.value,
|
|
"type_id": self.type_id,
|
|
"name": self.name,
|
|
"path": self.path,
|
|
"properties": dict(self.properties),
|
|
"allowable_actions": [action.value for action in self.allowable_actions],
|
|
"content_stream": dict(self.content_stream or {}),
|
|
"version": dict(self.version or {}),
|
|
"relationships": list(self.relationships),
|
|
"acl": dict(self.acl or {}),
|
|
}
|
|
)
|
|
|
|
|
|
class CMISDomainMapper:
|
|
"""Project engine domain objects into CMIS-shaped envelopes."""
|
|
|
|
def __init__(self, access_point: CMISAccessPoint) -> None:
|
|
self.access_point = access_point
|
|
|
|
def repository_info(self) -> dict[str, Any]:
|
|
profile = self.access_point.profile
|
|
return {
|
|
"repository_id": self.access_point.repository_id,
|
|
"repository_name": self.access_point.metadata.get("repository_name", self.access_point.repository_id),
|
|
"repository_description": self.access_point.metadata.get(
|
|
"repository_description",
|
|
"Kontextual Engine CMIS Browser Binding access point",
|
|
),
|
|
"cmis_version_supported": "1.1",
|
|
"root_folder_id": self.access_point.root_folder_id,
|
|
"principal_anonymous": "anonymous",
|
|
"principal_anyone": "anyone",
|
|
"vendor_name": "Kontextual",
|
|
"product_name": "kontextual-engine",
|
|
"product_version": self.access_point.metadata.get("product_version", "0.1.0"),
|
|
"binding": profile.binding.value,
|
|
"capabilities": self.capability_flags(),
|
|
"repository_features": self.repository_features(),
|
|
"unsupported_features": self.unsupported_features(),
|
|
"compliance": {
|
|
"standard": "CMIS 1.1",
|
|
"binding": profile.binding.value,
|
|
"posture": "declared-browser-binding-subset",
|
|
"overclaim_policy": "unsupported_optional_capabilities_are_advertised_as false/none",
|
|
},
|
|
"profile": profile.name,
|
|
}
|
|
|
|
def capability_flags(self) -> dict[str, Any]:
|
|
profile = self.access_point.profile
|
|
return {
|
|
"capability_content_stream_updatability": (
|
|
"anytime"
|
|
if profile.allow_mutations and profile.has_capability(CMISCapability.CONTENT_STREAM_WRITE)
|
|
else "none"
|
|
),
|
|
"capability_changes": "objectidsonly"
|
|
if profile.has_capability(CMISCapability.CHANGE_LOG)
|
|
else "none",
|
|
"capability_renditions": "none",
|
|
"capability_get_descendants": False,
|
|
"capability_get_folder_tree": False,
|
|
"capability_order_by": "none",
|
|
"capability_multifiling": False,
|
|
"capability_unfiling": False,
|
|
"capability_version_specific_filing": False,
|
|
"capability_pwc_searchable": False,
|
|
"capability_pwc_updatable": False,
|
|
"capability_all_versions_searchable": False,
|
|
"capability_query": "metadataonly"
|
|
if profile.has_capability(CMISCapability.DISCOVERY_QUERY)
|
|
else "none",
|
|
"capability_join": "none",
|
|
"capability_acl": "discover" if profile.has_capability(CMISCapability.ACL) else "none",
|
|
"capability_new_type_settable_attributes": dict(NEW_TYPE_SETTABLE_ATTRIBUTES),
|
|
}
|
|
|
|
def repository_features(self) -> list[dict[str, Any]]:
|
|
return [
|
|
{
|
|
"id": "urn:kontextual:cmis:feature:profiled-access-points",
|
|
"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",
|
|
"common_name": "Projection-only parent folder maps",
|
|
"version_label": "1.0",
|
|
"description": (
|
|
"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",
|
|
},
|
|
]
|
|
|
|
def unsupported_features(self) -> dict[str, dict[str, Any]]:
|
|
return {key: dict(value) for key, value in UNSUPPORTED_FEATURES.items()}
|
|
|
|
def type_definitions(self) -> list[dict[str, Any]]:
|
|
can_write = self.access_point.profile.allow_mutations
|
|
return [
|
|
_type_definition(CMISBaseType.DOCUMENT, "kontextual:document", "Kontextual Document", can_write),
|
|
_type_definition(CMISBaseType.FOLDER, "kontextual:folder", "Kontextual Folder", can_write),
|
|
_type_definition(
|
|
CMISBaseType.RELATIONSHIP,
|
|
"kontextual:relationship",
|
|
"Kontextual Relationship",
|
|
can_write,
|
|
),
|
|
_type_definition(CMISBaseType.POLICY, "kontextual:policy", "Kontextual Policy", False),
|
|
_type_definition(CMISBaseType.ITEM, "kontextual:item", "Kontextual Item", False),
|
|
_type_definition(CMISBaseType.SECONDARY, "kontextual:secondary", "Kontextual Secondary", False),
|
|
]
|
|
|
|
def map_asset(
|
|
self,
|
|
asset: KnowledgeAsset,
|
|
context: OperationContext,
|
|
*,
|
|
representations: list[AssetRepresentation] | tuple[AssetRepresentation, ...] = (),
|
|
versions: list[AssetVersion] | tuple[AssetVersion, ...] = (),
|
|
relationship_ids: list[str] | tuple[str, ...] = (),
|
|
metadata_records: list[Any] | tuple[Any, ...] = (),
|
|
) -> CMISObjectProjection | None:
|
|
if not self.access_point.exposes_asset(asset, context):
|
|
return None
|
|
current_version = _current_version(asset, versions)
|
|
content_stream = self.map_content_stream(asset, representations)
|
|
return CMISObjectProjection(
|
|
object_id=self.asset_object_id(asset.id),
|
|
base_type_id=CMISBaseType.DOCUMENT,
|
|
type_id=f"kontextual:{asset.classification.asset_type}",
|
|
name=asset.title,
|
|
path=self.asset_path(asset),
|
|
properties=self.asset_properties(asset, metadata_records=metadata_records, content_stream=content_stream),
|
|
allowable_actions=self.allowable_actions(context, has_content_stream=content_stream is not None),
|
|
content_stream=content_stream,
|
|
version=self.version_properties(asset, current_version, versions),
|
|
relationships=tuple(relationship_ids),
|
|
acl=self.acl_for_asset(asset, context),
|
|
)
|
|
|
|
def map_relationship(
|
|
self,
|
|
relationship: CoreRelationship,
|
|
context: OperationContext,
|
|
) -> CMISObjectProjection | None:
|
|
decision = self.access_point.decide_action(
|
|
CMISAction.GET_RELATIONSHIPS,
|
|
context,
|
|
resource=f"relationship:{relationship.relationship_id}",
|
|
)
|
|
if not decision.allowed:
|
|
return None
|
|
source_id = self.asset_object_id(relationship.source_id)
|
|
target_id = (
|
|
self.asset_object_id(relationship.target_id)
|
|
if relationship.target_kind == RelationshipTargetKind.ASSET
|
|
else f"cmis:entity:{relationship.target_id}"
|
|
)
|
|
return CMISObjectProjection(
|
|
object_id=f"cmis:relationship:{relationship.relationship_id}",
|
|
base_type_id=CMISBaseType.RELATIONSHIP,
|
|
type_id="kontextual:relationship",
|
|
name=relationship.predicate,
|
|
properties={
|
|
"cmis:objectId": f"cmis:relationship:{relationship.relationship_id}",
|
|
"cmis:name": relationship.predicate,
|
|
"cmis:baseTypeId": CMISBaseType.RELATIONSHIP.value,
|
|
"cmis:objectTypeId": "kontextual:relationship",
|
|
"cmis:sourceId": source_id,
|
|
"cmis:targetId": target_id,
|
|
"kontextual:predicate": relationship.predicate,
|
|
"kontextual:confidence": relationship.confidence,
|
|
"kontextual:targetKind": relationship.target_kind.value,
|
|
},
|
|
allowable_actions=(CMISAction.GET_OBJECT, CMISAction.GET_RELATIONSHIPS),
|
|
)
|
|
|
|
def acl_for_asset(self, asset: KnowledgeAsset, context: OperationContext) -> dict[str, Any] | None:
|
|
visibility = self.access_point.profile.decide_asset_visibility(asset, context)
|
|
if not visibility.allowed:
|
|
return None
|
|
permissions = ["cmis:read"]
|
|
if self.access_point.profile.allow_mutations:
|
|
permissions.extend(["cmis:write", "cmis:delete"])
|
|
entries = [
|
|
{
|
|
"principal_id": context.actor.id,
|
|
"permissions": permissions,
|
|
"direct": True,
|
|
}
|
|
]
|
|
if asset.classification.sensitivity == Sensitivity.PUBLIC:
|
|
entries.append(
|
|
{
|
|
"principal_id": "anyone",
|
|
"permissions": ["cmis:read"],
|
|
"direct": False,
|
|
}
|
|
)
|
|
return {
|
|
"object_id": self.asset_object_id(asset.id),
|
|
"is_exact": True,
|
|
"aces": entries,
|
|
"derived_from": "kontextual-profile-policy",
|
|
"profile": self.access_point.profile.name,
|
|
}
|
|
|
|
def asset_object_id(self, asset_id: str) -> str:
|
|
return f"cmis:asset:{asset_id}"
|
|
|
|
def folder_object_id(self, path: str) -> str:
|
|
return "cmis:folder:" + _normalize_path(path).strip("/").replace("/", "::")
|
|
|
|
def asset_path(self, asset: KnowledgeAsset) -> str:
|
|
return self.asset_paths(asset)[0]
|
|
|
|
def asset_paths(self, asset: KnowledgeAsset) -> tuple[str, ...]:
|
|
paths: list[str] = []
|
|
explicit = asset.metadata.get("cmis_path")
|
|
if explicit:
|
|
paths.append(_normalize_path(str(explicit)))
|
|
for value in asset.metadata.get("cmis_paths", ()):
|
|
paths.append(_normalize_path(str(value)))
|
|
if asset.source_refs:
|
|
source_ref = asset.source_refs[0]
|
|
source_root = _safe_path_segment(source_ref.source_system)
|
|
if source_ref.path:
|
|
paths.append(_normalize_path(f"/sources/{source_root}/{source_ref.path}"))
|
|
if source_ref.external_id:
|
|
paths.append(_normalize_path(f"/sources/{source_root}/{source_ref.external_id}"))
|
|
for topic in asset.classification.topics:
|
|
paths.append(_normalize_path(f"/topics/{topic}/{asset.id}"))
|
|
if asset.classification.owner:
|
|
paths.append(_normalize_path(f"/owners/{asset.classification.owner}/{asset.id}"))
|
|
paths.append(_normalize_path(f"/lifecycle/{_enum_value(asset.lifecycle)}/{asset.id}"))
|
|
paths.append(_normalize_path(f"/assets/{asset.classification.asset_type}/{asset.id}"))
|
|
return tuple(dict.fromkeys(paths))
|
|
|
|
def parent_folders_for_asset(self, asset: KnowledgeAsset) -> tuple[dict[str, Any], ...]:
|
|
folders = []
|
|
for path in self.asset_paths(asset):
|
|
parent = _parent_path(path)
|
|
folders.append(
|
|
{
|
|
"object_id": self.folder_object_id(parent),
|
|
"path": parent,
|
|
"name": _path_name(parent),
|
|
"base_type_id": CMISBaseType.FOLDER.value,
|
|
"type_id": "kontextual:folder",
|
|
"filing_source": _filing_source(parent),
|
|
}
|
|
)
|
|
return tuple({folder["path"]: folder for folder in folders}.values())
|
|
|
|
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,
|
|
"type_id": "kontextual:folder",
|
|
"name": _path_name(normalized),
|
|
"path": normalized,
|
|
"properties": {
|
|
"cmis:objectId": self.folder_object_id(normalized),
|
|
"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],
|
|
}
|
|
|
|
def asset_properties(
|
|
self,
|
|
asset: KnowledgeAsset,
|
|
*,
|
|
metadata_records: list[Any] | tuple[Any, ...] = (),
|
|
content_stream: dict[str, Any] | None = None,
|
|
) -> dict[str, Any]:
|
|
classification = asset.classification
|
|
properties = {
|
|
"cmis:objectId": self.asset_object_id(asset.id),
|
|
"cmis:name": asset.title,
|
|
"cmis:baseTypeId": CMISBaseType.DOCUMENT.value,
|
|
"cmis:objectTypeId": f"kontextual:{classification.asset_type}",
|
|
"cmis:createdBy": asset.metadata.get("created_by"),
|
|
"cmis:lastModifiedBy": asset.metadata.get("updated_by"),
|
|
"cmis:creationDate": asset.created_at,
|
|
"cmis:lastModificationDate": asset.updated_at,
|
|
"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,
|
|
"cmis:contentStreamId": content_stream.get("stream_id") if content_stream else None,
|
|
"kontextual:assetId": asset.id,
|
|
"kontextual:assetType": classification.asset_type,
|
|
"kontextual:sensitivity": _enum_value(classification.sensitivity),
|
|
"kontextual:lifecycle": _enum_value(asset.lifecycle),
|
|
"kontextual:owner": classification.owner,
|
|
"kontextual:topics": list(classification.topics),
|
|
"kontextual:reviewState": classification.review_state,
|
|
}
|
|
for record in metadata_records:
|
|
key = getattr(record, "key", None)
|
|
if key:
|
|
properties[f"kontextual:metadata:{key}"] = getattr(record, "value", None)
|
|
compacted = compact_dict(properties)
|
|
compacted.setdefault("cmis:createdBy", "system")
|
|
compacted.setdefault("cmis:lastModifiedBy", "system")
|
|
compacted.setdefault("cmis:secondaryObjectTypeIds", [])
|
|
compacted.setdefault("kontextual:owner", "")
|
|
compacted.setdefault("kontextual:topics", [])
|
|
compacted.setdefault("kontextual:reviewState", "")
|
|
return compacted
|
|
|
|
def map_content_stream(
|
|
self,
|
|
asset: KnowledgeAsset,
|
|
representations: list[AssetRepresentation] | tuple[AssetRepresentation, ...],
|
|
) -> dict[str, Any] | None:
|
|
representation = _preferred_representation(representations)
|
|
if representation is None:
|
|
return None
|
|
return compact_dict(
|
|
{
|
|
"stream_id": representation.representation_id,
|
|
"file_name": asset.metadata.get("file_name", asset.title),
|
|
"mime_type": representation.media_type,
|
|
"length": representation.size_bytes,
|
|
"digest": representation.digest,
|
|
"storage_ref": representation.storage_ref,
|
|
"kind": representation.kind.value,
|
|
}
|
|
)
|
|
|
|
def version_properties(
|
|
self,
|
|
asset: KnowledgeAsset,
|
|
current_version: AssetVersion | None,
|
|
versions: list[AssetVersion] | tuple[AssetVersion, ...],
|
|
) -> dict[str, Any]:
|
|
if current_version is None:
|
|
return {
|
|
"cmis:isLatestVersion": True,
|
|
"cmis:isMajorVersion": True,
|
|
"cmis:isLatestMajorVersion": True,
|
|
"cmis:versionSeriesId": f"cmis:version-series:{asset.id}",
|
|
"cmis:versionLabel": "1",
|
|
"cmis:isVersionSeriesCheckedOut": False,
|
|
"cmis:isPrivateWorkingCopy": False,
|
|
"cmis:versionSeriesCheckedOutBy": None,
|
|
"cmis:versionSeriesCheckedOutId": None,
|
|
"cmis:checkinComment": "",
|
|
"cmis:isImmutable": False,
|
|
}
|
|
latest_sequence = max((version.sequence for version in versions), default=current_version.sequence)
|
|
return {
|
|
"cmis:isLatestVersion": current_version.sequence == latest_sequence,
|
|
"cmis:isMajorVersion": True,
|
|
"cmis:isLatestMajorVersion": current_version.sequence == latest_sequence,
|
|
"cmis:versionSeriesId": f"cmis:version-series:{asset.id}",
|
|
"cmis:versionLabel": str(current_version.sequence),
|
|
"cmis:isVersionSeriesCheckedOut": False,
|
|
"cmis:isPrivateWorkingCopy": False,
|
|
"cmis:versionSeriesCheckedOutBy": None,
|
|
"cmis:versionSeriesCheckedOutId": None,
|
|
"cmis:checkinComment": "",
|
|
"cmis:isImmutable": False,
|
|
"kontextual:versionId": current_version.version_id,
|
|
"kontextual:versionChangeType": current_version.change_type.value,
|
|
}
|
|
|
|
def allowable_actions(
|
|
self,
|
|
context: OperationContext,
|
|
*,
|
|
has_content_stream: bool,
|
|
) -> tuple[CMISAction, ...]:
|
|
candidates = [
|
|
CMISAction.GET_OBJECT,
|
|
CMISAction.GET_CONTENT_STREAM,
|
|
CMISAction.GET_ACL,
|
|
CMISAction.GET_RELATIONSHIPS,
|
|
CMISAction.GET_OBJECT_PARENTS,
|
|
CMISAction.UPDATE_PROPERTIES,
|
|
CMISAction.DELETE_OBJECT,
|
|
CMISAction.SET_CONTENT_STREAM,
|
|
]
|
|
actions: list[CMISAction] = []
|
|
for action in candidates:
|
|
if action == CMISAction.GET_CONTENT_STREAM and not has_content_stream:
|
|
continue
|
|
if self.access_point.decide_action(action, context).allowed:
|
|
actions.append(action)
|
|
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", []),
|
|
base_type_id=properties.get("cmis:baseTypeId"),
|
|
),
|
|
}
|
|
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, ...],
|
|
*,
|
|
base_type_id: str | None = None,
|
|
) -> dict[str, bool]:
|
|
native = set(actions)
|
|
is_folder = base_type_id == CMISBaseType.FOLDER.value
|
|
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": CMISAction.CREATE_FOLDER.value in native,
|
|
"canCreateRelationship": False,
|
|
"canCreateItem": False,
|
|
"canDeleteTree": CMISAction.DELETE_TREE.value in native,
|
|
"canGetDescendants": False,
|
|
"canGetFolderTree": False,
|
|
"canGetFolderParent": is_folder and CMISAction.GET_OBJECT_PARENTS.value in native,
|
|
"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]:
|
|
allowable_actions = [CMISAction.GET_OBJECT.value, CMISAction.GET_CHILDREN.value]
|
|
if access_point.profile.allow_mutations:
|
|
allowable_actions.extend([CMISAction.CREATE_DOCUMENT.value, CMISAction.CREATE_FOLDER.value])
|
|
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,
|
|
"kontextual:filingSource": "root",
|
|
"kontextual:workspaceFolder": False,
|
|
},
|
|
"allowable_actions": allowable_actions,
|
|
}
|
|
|
|
|
|
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", {}))
|
|
properties.update(projection.get("version", {}))
|
|
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,
|
|
CMISCapability.TYPE_DEFINITIONS,
|
|
CMISCapability.NAVIGATION,
|
|
CMISCapability.OBJECT_READ,
|
|
CMISCapability.CONTENT_STREAM_READ,
|
|
CMISCapability.DISCOVERY_QUERY,
|
|
CMISCapability.RELATIONSHIPS,
|
|
CMISCapability.ACL,
|
|
CMISCapability.CHANGE_LOG,
|
|
)
|
|
|
|
|
|
def _enum_value(value: Any) -> Any:
|
|
return getattr(value, "value", value)
|
|
|
|
|
|
def _type_definition(
|
|
base_type_id: CMISBaseType,
|
|
type_id: str,
|
|
display_name: str,
|
|
can_write: bool,
|
|
) -> dict[str, Any]:
|
|
is_document = base_type_id == CMISBaseType.DOCUMENT
|
|
return {
|
|
"id": type_id,
|
|
"local_name": type_id.split(":", 1)[-1],
|
|
"display_name": display_name,
|
|
"base_type_id": base_type_id.value,
|
|
"queryable": is_document,
|
|
"controllable_acl": is_document,
|
|
"controllable_policy": False,
|
|
"creatable": can_write and base_type_id in {CMISBaseType.DOCUMENT, CMISBaseType.FOLDER},
|
|
"fileable": base_type_id in {CMISBaseType.DOCUMENT, CMISBaseType.FOLDER},
|
|
"fulltext_indexed": False,
|
|
"included_in_supertype_query": is_document,
|
|
"versionable": False,
|
|
"property_definitions": _property_definitions(base_type_id),
|
|
}
|
|
|
|
|
|
def _property_definitions(base_type_id: CMISBaseType) -> dict[str, dict[str, Any]]:
|
|
definitions = {
|
|
"cmis:objectId": {"property_type": "id", "cardinality": "single", "required": True},
|
|
"cmis:name": {"property_type": "string", "cardinality": "single", "required": True},
|
|
"cmis:baseTypeId": {"property_type": "id", "cardinality": "single", "required": True},
|
|
"cmis:objectTypeId": {"property_type": "id", "cardinality": "single", "required": True},
|
|
"cmis:createdBy": {"property_type": "string", "cardinality": "single", "required": False},
|
|
"cmis:lastModifiedBy": {"property_type": "string", "cardinality": "single", "required": False},
|
|
"cmis:creationDate": {"property_type": "datetime", "cardinality": "single", "required": False},
|
|
"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.DOCUMENT:
|
|
definitions.update(
|
|
{
|
|
"cmis:contentStreamLength": {
|
|
"property_type": "integer",
|
|
"cardinality": "single",
|
|
"required": False,
|
|
},
|
|
"cmis:contentStreamMimeType": {
|
|
"property_type": "string",
|
|
"cardinality": "single",
|
|
"required": False,
|
|
},
|
|
"cmis:contentStreamFileName": {
|
|
"property_type": "string",
|
|
"cardinality": "single",
|
|
"required": False,
|
|
},
|
|
"cmis:contentStreamId": {
|
|
"property_type": "id",
|
|
"cardinality": "single",
|
|
"required": False,
|
|
},
|
|
"kontextual:assetId": {
|
|
"property_type": "id",
|
|
"cardinality": "single",
|
|
"required": False,
|
|
},
|
|
"kontextual:assetType": {
|
|
"property_type": "string",
|
|
"cardinality": "single",
|
|
"required": False,
|
|
},
|
|
"kontextual:owner": {
|
|
"property_type": "string",
|
|
"cardinality": "single",
|
|
"required": False,
|
|
},
|
|
"kontextual:topics": {
|
|
"property_type": "string",
|
|
"cardinality": "multi",
|
|
"required": False,
|
|
},
|
|
"kontextual:reviewState": {
|
|
"property_type": "string",
|
|
"cardinality": "single",
|
|
"required": False,
|
|
},
|
|
"cmis:isImmutable": {
|
|
"property_type": "boolean",
|
|
"cardinality": "single",
|
|
"required": False,
|
|
},
|
|
"cmis:isLatestVersion": {
|
|
"property_type": "boolean",
|
|
"cardinality": "single",
|
|
"required": False,
|
|
},
|
|
"cmis:isMajorVersion": {
|
|
"property_type": "boolean",
|
|
"cardinality": "single",
|
|
"required": False,
|
|
},
|
|
"cmis:isLatestMajorVersion": {
|
|
"property_type": "boolean",
|
|
"cardinality": "single",
|
|
"required": False,
|
|
},
|
|
"cmis:versionLabel": {
|
|
"property_type": "string",
|
|
"cardinality": "single",
|
|
"required": False,
|
|
},
|
|
"cmis:versionSeriesId": {
|
|
"property_type": "id",
|
|
"cardinality": "single",
|
|
"required": False,
|
|
},
|
|
"cmis:isVersionSeriesCheckedOut": {
|
|
"property_type": "boolean",
|
|
"cardinality": "single",
|
|
"required": False,
|
|
},
|
|
"cmis:isPrivateWorkingCopy": {
|
|
"property_type": "boolean",
|
|
"cardinality": "single",
|
|
"required": False,
|
|
},
|
|
"cmis:versionSeriesCheckedOutBy": {
|
|
"property_type": "string",
|
|
"cardinality": "single",
|
|
"required": False,
|
|
},
|
|
"cmis:versionSeriesCheckedOutId": {
|
|
"property_type": "id",
|
|
"cardinality": "single",
|
|
"required": False,
|
|
},
|
|
"cmis:checkinComment": {
|
|
"property_type": "string",
|
|
"cardinality": "single",
|
|
"required": False,
|
|
},
|
|
"kontextual:versionId": {
|
|
"property_type": "id",
|
|
"cardinality": "single",
|
|
"required": False,
|
|
},
|
|
"kontextual:versionChangeType": {
|
|
"property_type": "string",
|
|
"cardinality": "single",
|
|
"required": False,
|
|
},
|
|
}
|
|
)
|
|
if base_type_id == CMISBaseType.FOLDER:
|
|
definitions["kontextual:filingSource"] = {
|
|
"property_type": "string",
|
|
"cardinality": "single",
|
|
"required": False,
|
|
}
|
|
definitions["kontextual:workspaceFolder"] = {
|
|
"property_type": "boolean",
|
|
"cardinality": "single",
|
|
"required": False,
|
|
}
|
|
if base_type_id == CMISBaseType.RELATIONSHIP:
|
|
definitions["cmis:sourceId"] = {"property_type": "id", "cardinality": "single", "required": True}
|
|
definitions["cmis:targetId"] = {"property_type": "id", "cardinality": "single", "required": True}
|
|
return definitions
|
|
|
|
|
|
def _current_version(
|
|
asset: KnowledgeAsset,
|
|
versions: list[AssetVersion] | tuple[AssetVersion, ...],
|
|
) -> AssetVersion | None:
|
|
if asset.current_version_id:
|
|
for version in versions:
|
|
if version.version_id == asset.current_version_id:
|
|
return version
|
|
if versions:
|
|
return sorted(versions, key=lambda version: version.sequence)[-1]
|
|
return None
|
|
|
|
|
|
def _preferred_representation(
|
|
representations: list[AssetRepresentation] | tuple[AssetRepresentation, ...],
|
|
) -> AssetRepresentation | None:
|
|
if not representations:
|
|
return None
|
|
priority = {
|
|
RepresentationKind.SOURCE: 0,
|
|
RepresentationKind.NORMALIZED: 1,
|
|
RepresentationKind.DERIVED: 2,
|
|
}
|
|
best_priority = min(priority.get(item.kind, 99) for item in representations)
|
|
candidates = [item for item in representations if priority.get(item.kind, 99) == best_priority]
|
|
return sorted(candidates, key=lambda item: (item.created_at, item.representation_id), reverse=True)[0]
|
|
|
|
|
|
def _normalize_path(path: str) -> str:
|
|
parts = [_safe_path_segment(part) for part in path.replace("\\", "/").split("/") if part]
|
|
return "/" + "/".join(parts)
|
|
|
|
|
|
def _safe_path_segment(value: str) -> str:
|
|
return str(value).strip().strip("/") or "_"
|
|
|
|
|
|
def _parent_path(path: str) -> str:
|
|
parts = _normalize_path(path).strip("/").split("/")
|
|
if len(parts) <= 1:
|
|
return "/"
|
|
return "/" + "/".join(parts[:-1])
|
|
|
|
|
|
def _path_name(path: str) -> str:
|
|
normalized = _normalize_path(path)
|
|
if normalized == "/":
|
|
return "root"
|
|
return normalized.rsplit("/", 1)[-1]
|
|
|
|
|
|
def _filing_source(path: str) -> str:
|
|
parts = _normalize_path(path).strip("/").split("/")
|
|
return parts[0] if parts and parts[0] else "root"
|