generated from coulomb/repo-seed
CMIS Browser Binding serializer layer
This commit is contained in:
@@ -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}"},
|
||||
|
||||
Reference in New Issue
Block a user