generated from coulomb/repo-seed
CMIS Browser Binding serializer layer
This commit is contained in:
@@ -2,12 +2,16 @@
|
|||||||
|
|
||||||
Date: 2026-05-07
|
Date: 2026-05-07
|
||||||
|
|
||||||
Evidence update: the 2026-05-08 OpenCMIS TCK run found that broad commodity
|
Evidence update: the 2026-05-08 OpenCMIS TCK compatibility implementation
|
||||||
CMIS client compatibility is currently pre-compliance because OpenCMIS cannot
|
resolved the initial Browser Binding session blocker. The latest run,
|
||||||
create a Browser Binding session. See
|
`run-20260508T092113Z`, completed the selected baseline with `38` passing
|
||||||
`docs/cmis-opencmis-tck-assessment-2026-05-08T063312Z.md`. The score below is
|
repository/type cases, one local HTTP transport warning, and `22` object/content
|
||||||
therefore retained as the pre-TCK product-depth estimate, not as external CMIS
|
skips caused by the current non-creatable folder profile. See
|
||||||
compatibility evidence.
|
`docs/cmis-opencmis-tck-implementation-evidence-2026-05-08T092113Z.md`.
|
||||||
|
|
||||||
|
The score below remains a product-depth estimate against mature CMIS products;
|
||||||
|
the evidence-backed TCK preparation score for the selected baseline is `23.81`
|
||||||
|
with `2/9` capability groups covered.
|
||||||
|
|
||||||
Status: baseline scorecard for the current Browser Binding subset.
|
Status: baseline scorecard for the current Browser Binding subset.
|
||||||
|
|
||||||
@@ -98,16 +102,17 @@ replacement surface.
|
|||||||
| Browser Binding protocol fidelity | 7 | 45% | Hyland Alfresco ACS | - Browser-style routes and JSON envelopes exist.<br>- FastAPI route shapes are pragmatic, not complete CMIS Browser Binding selector/action parity.<br>- Route-level tests skip without optional service dependencies. |
|
| Browser Binding protocol fidelity | 7 | 45% | Hyland Alfresco ACS | - Browser-style routes and JSON envelopes exist.<br>- FastAPI route shapes are pragmatic, not complete CMIS Browser Binding selector/action parity.<br>- Route-level tests skip without optional service dependencies. |
|
||||||
| AtomPub binding | 2 | 0% | Hyland Alfresco ACS | - No AtomPub/XML service document or feeds.<br>- Intentionally deferred until monetized need. |
|
| AtomPub binding | 2 | 0% | Hyland Alfresco ACS | - No AtomPub/XML service document or feeds.<br>- Intentionally deferred until monetized need. |
|
||||||
| Web Services binding | 2 | 0% | Hyland Alfresco ACS | - No SOAP/WSDL stack.<br>- Intentionally deferred until monetized need. |
|
| Web Services binding | 2 | 0% | Hyland Alfresco ACS | - No SOAP/WSDL stack.<br>- Intentionally deferred until monetized need. |
|
||||||
| External conformance evidence | 3 | 20% | OpenCMIS TCK against Alfresco-like server behavior | - Internal fixtures and optional TCK mapping exist.<br>- No recorded OpenCMIS TCK execution against a running access point yet.<br>- No third-party client compatibility matrix yet. |
|
| External conformance evidence | 3 | 35% | OpenCMIS TCK against Alfresco-like server behavior | - OpenCMIS Browser Binding session creation now succeeds against `compat-tck`.<br>- Selected `repository-type` baseline completes with no failures and one local HTTP warning.<br>- `object-content` reaches parsed cases but skips because `cmis:folder` is not creatable; broader groups and third-party client matrix are still missing. |
|
||||||
|
|
||||||
Weighted result from this table: **42%**.
|
Weighted result from this table: **42%**.
|
||||||
|
|
||||||
## Most Important Gaps
|
## Most Important Gaps
|
||||||
|
|
||||||
1. **External conformance run**
|
1. **External conformance expansion**
|
||||||
- Run selected OpenCMIS TCK groups against `compat-tck`.
|
- Keep the selected OpenCMIS TCK baseline running against `compat-tck`.
|
||||||
- Capture failures by capability group.
|
- Decide whether to add TCK-only `createFolder` support or keep CRUD/content
|
||||||
- Turn "estimated" scores into evidence-backed scores.
|
skips as a deliberate profile boundary.
|
||||||
|
- Expand selected groups after the supported capability boundary is agreed.
|
||||||
|
|
||||||
2. **Browser Binding fidelity**
|
2. **Browser Binding fidelity**
|
||||||
- Align route/action/selector shapes more closely with CMIS Browser Binding.
|
- Align route/action/selector shapes more closely with CMIS Browser Binding.
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ Run timestamp: 2026-05-08T06:33:12Z
|
|||||||
Local timestamp: 2026-05-08T08:33:12+02:00
|
Local timestamp: 2026-05-08T08:33:12+02:00
|
||||||
Status: evidence-backed compatibility blocker found
|
Status: evidence-backed compatibility blocker found
|
||||||
|
|
||||||
|
Superseded implementation evidence: the blocker described here was resolved by
|
||||||
|
`KONT-WP-0013`. See
|
||||||
|
`docs/cmis-opencmis-tck-implementation-evidence-2026-05-08T092113Z.md` for the
|
||||||
|
latest completed OpenCMIS run.
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
Persist the first real `guide-board` + `open-cmis-tck` assessment against a
|
Persist the first real `guide-board` + `open-cmis-tck` assessment against a
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
# CMIS OpenCMIS TCK Implementation Evidence
|
||||||
|
|
||||||
|
Run timestamp: 2026-05-08T09:21:13Z
|
||||||
|
Local timestamp: 2026-05-08T11:21:13+02:00
|
||||||
|
Status: `KONT-WP-0013` implementation evidence captured
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Persist the implementation outcome for the CMIS Browser Binding TCK
|
||||||
|
compatibility workplan. This document supersedes the initial blocker report in
|
||||||
|
`docs/cmis-opencmis-tck-assessment-2026-05-08T063312Z.md` for the current
|
||||||
|
`kontextual-engine` state.
|
||||||
|
|
||||||
|
## Implementation Summary
|
||||||
|
|
||||||
|
The CMIS adapter now exposes a Browser Binding-shaped compatibility surface for
|
||||||
|
the `compat-tck` profile instead of leaking native snake_case DTOs on the
|
||||||
|
compatibility route.
|
||||||
|
|
||||||
|
Implemented changes:
|
||||||
|
|
||||||
|
- Browser Binding service document keyed by repository ID.
|
||||||
|
- CMIS field names for repository info, capabilities, extended features, type
|
||||||
|
definitions, object properties, ACLs, parents, query results, and root folder
|
||||||
|
projections.
|
||||||
|
- Selector handling for `repositoryInfo`, `typeChildren`, `typeDescendants`,
|
||||||
|
`typeDefinition`, `query`, `object`, `children`, `parents`, `properties`,
|
||||||
|
`allowableActions`, `policies`, and `content`.
|
||||||
|
- `compat-tck` repository ID alignment between the engine profile and the
|
||||||
|
OpenCMIS target profile.
|
||||||
|
- Root folder and virtual-folder projections that include the properties
|
||||||
|
advertised by the `cmis:folder` type definition.
|
||||||
|
- `includePropertyDefinitions` handling for type-list selectors.
|
||||||
|
- FastAPI OpenAPI generation fix for streaming routes.
|
||||||
|
- Internal regression coverage for Browser Binding selectors and OpenAPI route
|
||||||
|
generation.
|
||||||
|
|
||||||
|
## Final OpenCMIS Run
|
||||||
|
|
||||||
|
Guide Board live run:
|
||||||
|
|
||||||
|
- Run ID: `run-20260508T092113Z`
|
||||||
|
- Run directory: `/tmp/open-cmis-tck-kontextual-fix6-20260508`
|
||||||
|
- Assessment status: `completed`
|
||||||
|
- Target: `kontextual-cmis-compat`
|
||||||
|
- Assessment: `cmis-browser-baseline`
|
||||||
|
- Browser Binding URL: `http://127.0.0.1:8010/cmis/compat-tck/browser`
|
||||||
|
- Repository ID: `compat-tck`
|
||||||
|
|
||||||
|
Guide Board summary:
|
||||||
|
|
||||||
|
- `pass: 1`
|
||||||
|
- `warning: 1`
|
||||||
|
- `skipped: 1`
|
||||||
|
- unexpected findings: `0`
|
||||||
|
|
||||||
|
Mapped groups:
|
||||||
|
|
||||||
|
- Repository and type metadata: `warning`
|
||||||
|
- Object and content services: `skipped`
|
||||||
|
|
||||||
|
Normalized OpenCMIS case counts:
|
||||||
|
|
||||||
|
- `repository-type`: `38 pass`, `2 info`, `2 skipped`, `1 warning`, `0 fail`
|
||||||
|
- `object-content`: `22 skipped`, `0 fail`
|
||||||
|
|
||||||
|
The remaining repository/type warning is local transport only:
|
||||||
|
|
||||||
|
```text
|
||||||
|
HTTPS is not used. Credentials might be transferred as plain text!
|
||||||
|
```
|
||||||
|
|
||||||
|
The object/content group skips because the compatibility profile does not make
|
||||||
|
the `cmis:folder` base type creatable. This is an intentional boundary for the
|
||||||
|
current profiled adapter: governed document creation and content stream updates
|
||||||
|
exist through the current engine-backed routes, but full CMIS CRUD scaffolding
|
||||||
|
for TCK-created folders is not in scope yet.
|
||||||
|
|
||||||
|
## Scorecard
|
||||||
|
|
||||||
|
Generated scorecard:
|
||||||
|
|
||||||
|
- Path: `/tmp/open-cmis-tck-kontextual-fix6-20260508/reports/cmis-maturity-scorecard.md`
|
||||||
|
- Maturity score: `23.81`
|
||||||
|
- Coverage: `2/9` groups, `22.22%`
|
||||||
|
- Repository and type metadata: `partial`, score `3/4`
|
||||||
|
- Object and content services: `not_automated`, score `2/4`
|
||||||
|
|
||||||
|
Interpretation:
|
||||||
|
|
||||||
|
- The original protocol blocker is resolved: OpenCMIS can create a Browser
|
||||||
|
Binding session and execute selected checks.
|
||||||
|
- The current score remains low because only two groups are selected in the
|
||||||
|
baseline and one group is intentionally skipped by profile capability.
|
||||||
|
- The scorecard is preparation evidence, not CMIS certification.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
Focused internal CMIS regression suite:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/worsch/kontextual-engine
|
||||||
|
.venv/bin/python -m pytest tests/cmis --perf-history-disable
|
||||||
|
```
|
||||||
|
|
||||||
|
Result:
|
||||||
|
|
||||||
|
```text
|
||||||
|
45 passed
|
||||||
|
```
|
||||||
|
|
||||||
|
Live OpenCMIS run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/worsch/guide-board
|
||||||
|
source /home/worsch/open-cmis-tck/.local/toolchains/env.sh
|
||||||
|
PYTHONPATH=src python3 -m guide_board \
|
||||||
|
--extension-dir ../open-cmis-tck \
|
||||||
|
run \
|
||||||
|
--target /tmp/kontextual-cmis-compat-8010.json \
|
||||||
|
--assessment ../open-cmis-tck/profiles/assessments/cmis-browser-baseline.json \
|
||||||
|
--output-dir /tmp/open-cmis-tck-kontextual-fix6-20260508
|
||||||
|
```
|
||||||
|
|
||||||
|
Scorecard generation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/worsch/open-cmis-tck
|
||||||
|
PYTHONPATH=src python3 scripts/cmis_scorecard.py \
|
||||||
|
--run-dir /tmp/open-cmis-tck-kontextual-fix6-20260508
|
||||||
|
```
|
||||||
|
|
||||||
|
## Evidence Artifacts
|
||||||
|
|
||||||
|
The `/tmp` run artifacts are useful local evidence but may be ephemeral:
|
||||||
|
|
||||||
|
- `/tmp/open-cmis-tck-kontextual-fix6-20260508/reports/report.md`
|
||||||
|
- `/tmp/open-cmis-tck-kontextual-fix6-20260508/reports/cmis-maturity-scorecard.md`
|
||||||
|
- `/tmp/open-cmis-tck-kontextual-fix6-20260508/normalized/evidence.json`
|
||||||
|
- `/tmp/open-cmis-tck-kontextual-fix6-20260508/normalized/findings.json`
|
||||||
|
- `/tmp/open-cmis-tck-kontextual-fix6-20260508/artifacts/open-cmis-tck/tck/repository-type/normalized-runner-result.json`
|
||||||
|
- `/tmp/open-cmis-tck-kontextual-fix6-20260508/artifacts/open-cmis-tck/tck/object-content/normalized-runner-result.json`
|
||||||
|
|
||||||
|
## Remaining Boundary Gaps
|
||||||
|
|
||||||
|
These are not regressions in the current workplan; they are next-scope choices:
|
||||||
|
|
||||||
|
- Configure a durable local target profile or harness parameter for the
|
||||||
|
`kontextual-engine` test port instead of using a temporary `/tmp` profile.
|
||||||
|
- Decide whether the `compat-tck` profile should support CMIS `createFolder`
|
||||||
|
purely to exercise more CRUD/content TCK cases.
|
||||||
|
- Expand OpenCMIS selected groups beyond `repository-type` and
|
||||||
|
`object-content` once the targeted capability boundary is agreed.
|
||||||
|
- Harden `open-cmis-tck` preflight in the sister repository so native-shaped
|
||||||
|
repository-info responses are rejected before Maven invocation.
|
||||||
|
- Add HTTPS or a local warning waiver only when transport-security evidence
|
||||||
|
matters for a deployment profile.
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
`KONT-WP-0013` achieved its main purpose for `kontextual-engine`: OpenCMIS no
|
||||||
|
longer fails at session creation, the repository/type group parses and executes,
|
||||||
|
the object/content group reaches parsed TCK cases, and the remaining gaps are
|
||||||
|
capability-scope decisions rather than protocol-shape crashes.
|
||||||
@@ -51,6 +51,17 @@ from kontextual_engine.core import (
|
|||||||
stable_json_dumps,
|
stable_json_dumps,
|
||||||
utc_now,
|
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.errors import AuthorizationError, KontextualError, NotFoundError, ValidationError
|
||||||
from kontextual_engine.ports import AllowAllPolicyGateway, AssetRegistryRepository, BlobStorage, PolicyGateway
|
from kontextual_engine.ports import AllowAllPolicyGateway, AssetRegistryRepository, BlobStorage, PolicyGateway
|
||||||
from kontextual_engine.services import (
|
from kontextual_engine.services import (
|
||||||
@@ -339,10 +350,131 @@ class ServiceRuntime:
|
|||||||
def cmis_repository_info(self, access_point_id: str) -> dict[str, Any]:
|
def cmis_repository_info(self, access_point_id: str) -> dict[str, Any]:
|
||||||
return self._cmis_mapper(access_point_id).repository_info()
|
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]:
|
def cmis_type_definitions(self, access_point_id: str) -> dict[str, Any]:
|
||||||
definitions = self._cmis_mapper(access_point_id).type_definitions()
|
definitions = self._cmis_mapper(access_point_id).type_definitions()
|
||||||
return {"items": definitions, "count": len(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(
|
def cmis_children(
|
||||||
self,
|
self,
|
||||||
access_point_id: str,
|
access_point_id: str,
|
||||||
@@ -700,9 +832,12 @@ class ServiceRuntime:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _cmis_mapper(self, access_point_id: str) -> CMISDomainMapper:
|
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():
|
for profile in _cmis_profiles():
|
||||||
if profile.name == access_point_id:
|
if profile.name == access_point_id:
|
||||||
return CMISDomainMapper(_cmis_access_point(profile))
|
return _cmis_access_point(profile)
|
||||||
raise NotFoundError(
|
raise NotFoundError(
|
||||||
"CMIS access point not found",
|
"CMIS access point not found",
|
||||||
details={"access_point_id": access_point_id, "available": [profile.name for profile in _cmis_profiles()]},
|
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):
|
def create_app(runtime: ServiceRuntime | None = None):
|
||||||
try:
|
try:
|
||||||
from fastapi import Depends, FastAPI, Header, HTTPException, Query
|
from fastapi import Depends, FastAPI, Header, HTTPException, Query, Request
|
||||||
from fastapi.responses import JSONResponse, StreamingResponse
|
from fastapi.responses import JSONResponse, StreamingResponse
|
||||||
except ImportError as exc: # pragma: no cover - exercised when optional extra is absent
|
except ImportError as exc: # pragma: no cover - exercised when optional extra is absent
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"FastAPI service dependencies are not installed. Install kontextual-engine[service]."
|
"FastAPI service dependencies are not installed. Install kontextual-engine[service]."
|
||||||
) from exc
|
) from exc
|
||||||
|
globals()["Request"] = Request
|
||||||
|
globals()["StreamingResponse"] = StreamingResponse
|
||||||
|
|
||||||
runtime = runtime or ServiceRuntime()
|
runtime = runtime or ServiceRuntime()
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
@@ -2205,9 +2342,129 @@ def create_app(runtime: ServiceRuntime | None = None):
|
|||||||
def cmis_access_points() -> dict[str, Any]:
|
def cmis_access_points() -> dict[str, Any]:
|
||||||
return response(runtime.cmis_access_points)
|
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"])
|
@app.get("/cmis/{access_point_id}/browser", tags=["cmis"])
|
||||||
def cmis_repository_info(access_point_id: str) -> dict[str, Any]:
|
def cmis_browser_entry(
|
||||||
return response(runtime.cmis_repository_info, access_point_id)
|
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"])
|
@app.get("/cmis/{access_point_id}/browser/types", tags=["cmis"])
|
||||||
def cmis_types(access_point_id: str) -> dict[str, Any]:
|
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,
|
access_point_id: str,
|
||||||
object_id: str,
|
object_id: str,
|
||||||
context: OperationContext = Depends(context_from_headers),
|
context: OperationContext = Depends(context_from_headers),
|
||||||
) -> StreamingResponse:
|
) -> Any:
|
||||||
result = response(runtime.cmis_content_stream_bytes, access_point_id, object_id, context)
|
result = response(runtime.cmis_content_stream_bytes, access_point_id, object_id, context)
|
||||||
representation = result.representation
|
representation = result.representation
|
||||||
return StreamingResponse(
|
return StreamingResponse(
|
||||||
@@ -2391,7 +2648,7 @@ def create_app(runtime: ServiceRuntime | None = None):
|
|||||||
asset_id: str,
|
asset_id: str,
|
||||||
representation_id: str,
|
representation_id: str,
|
||||||
context: OperationContext = Depends(context_from_headers),
|
context: OperationContext = Depends(context_from_headers),
|
||||||
) -> StreamingResponse:
|
) -> Any:
|
||||||
result = response(runtime.representation_content_stream, asset_id, representation_id, context)
|
result = response(runtime.representation_content_stream, asset_id, representation_id, context)
|
||||||
representation = result.representation
|
representation = result.representation
|
||||||
return StreamingResponse(
|
return StreamingResponse(
|
||||||
@@ -2772,9 +3029,10 @@ def _cmis_profiles() -> tuple[CMISAccessProfile, ...]:
|
|||||||
|
|
||||||
|
|
||||||
def _cmis_access_point(profile: CMISAccessProfile) -> CMISAccessPoint:
|
def _cmis_access_point(profile: CMISAccessProfile) -> CMISAccessPoint:
|
||||||
|
repository_id = profile.name if profile.name == "compat-tck" else f"kontextual-{profile.name}"
|
||||||
return CMISAccessPoint(
|
return CMISAccessPoint(
|
||||||
access_point_id=profile.name,
|
access_point_id=profile.name,
|
||||||
repository_id=f"kontextual-{profile.name}",
|
repository_id=repository_id,
|
||||||
profile=profile,
|
profile=profile,
|
||||||
base_path=f"/cmis/{profile.name}/browser",
|
base_path=f"/cmis/{profile.name}/browser",
|
||||||
metadata={"repository_name": f"Kontextual Engine {profile.name}"},
|
metadata={"repository_name": f"Kontextual Engine {profile.name}"},
|
||||||
|
|||||||
@@ -649,6 +649,7 @@ class CMISDomainMapper:
|
|||||||
"common_name": "Profiled CMIS access points",
|
"common_name": "Profiled CMIS access points",
|
||||||
"version_label": "1.0",
|
"version_label": "1.0",
|
||||||
"description": "Multiple Browser Binding access points can expose different governed profile slices.",
|
"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",
|
"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 "
|
"Assets may appear under multiple virtual folder projections; CMIS multi-filing mutation "
|
||||||
"services are not advertised."
|
"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]:
|
def folder_projection(self, path: str) -> dict[str, Any]:
|
||||||
normalized = _normalize_path(path)
|
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 {
|
return {
|
||||||
"object_id": self.folder_object_id(normalized),
|
"object_id": self.folder_object_id(normalized),
|
||||||
"base_type_id": CMISBaseType.FOLDER.value,
|
"base_type_id": CMISBaseType.FOLDER.value,
|
||||||
@@ -835,6 +839,18 @@ class CMISDomainMapper:
|
|||||||
"cmis:name": _path_name(normalized),
|
"cmis:name": _path_name(normalized),
|
||||||
"cmis:baseTypeId": CMISBaseType.FOLDER.value,
|
"cmis:baseTypeId": CMISBaseType.FOLDER.value,
|
||||||
"cmis:objectTypeId": "kontextual:folder",
|
"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),
|
"kontextual:filingSource": _filing_source(normalized),
|
||||||
},
|
},
|
||||||
"allowable_actions": [CMISAction.GET_CHILDREN.value],
|
"allowable_actions": [CMISAction.GET_CHILDREN.value],
|
||||||
@@ -946,6 +962,623 @@ class CMISDomainMapper:
|
|||||||
return tuple(actions)
|
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, ...]:
|
def _read_capabilities() -> tuple[CMISCapability, ...]:
|
||||||
return (
|
return (
|
||||||
CMISCapability.REPOSITORY,
|
CMISCapability.REPOSITORY,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"tool": "Apache Chemistry OpenCMIS TCK",
|
"tool": "Apache Chemistry OpenCMIS TCK",
|
||||||
"tool_version": "1.1.0",
|
"tool_version": "1.1.0",
|
||||||
"profile": "compat-tck",
|
"profile": "compat-tck",
|
||||||
"repository_id": "kontextual-compat-tck",
|
"repository_id": "compat-tck",
|
||||||
"browser_url": "http://127.0.0.1:8000/cmis/compat-tck/browser",
|
"browser_url": "http://127.0.0.1:8000/cmis/compat-tck/browser",
|
||||||
"started_at": null,
|
"started_at": null,
|
||||||
"finished_at": null
|
"finished_at": null
|
||||||
@@ -25,4 +25,3 @@
|
|||||||
],
|
],
|
||||||
"capability_gaps": []
|
"capability_gaps": []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
},
|
},
|
||||||
"target": {
|
"target": {
|
||||||
"profile": "compat-tck",
|
"profile": "compat-tck",
|
||||||
"repository_id": "kontextual-compat-tck",
|
"repository_id": "compat-tck",
|
||||||
"browser_url": "http://127.0.0.1:8000/cmis/compat-tck/browser"
|
"browser_url": "http://127.0.0.1:8000/cmis/compat-tck/browser"
|
||||||
},
|
},
|
||||||
"groups": [
|
"groups": [
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ def test_cmis_browser_binding_routes_are_advertised_in_openapi(cmis_client) -> N
|
|||||||
|
|
||||||
assert "/cmis" in paths
|
assert "/cmis" in paths
|
||||||
assert "/cmis/{access_point_id}/browser" in paths
|
assert "/cmis/{access_point_id}/browser" in paths
|
||||||
|
assert "/cmis/{access_point_id}/browser/root" in paths
|
||||||
assert "/cmis/{access_point_id}/browser/types" in paths
|
assert "/cmis/{access_point_id}/browser/types" in paths
|
||||||
assert "/cmis/{access_point_id}/browser/children" in paths
|
assert "/cmis/{access_point_id}/browser/children" in paths
|
||||||
assert "/cmis/{access_point_id}/browser/object/{object_id}" in paths
|
assert "/cmis/{access_point_id}/browser/object/{object_id}" in paths
|
||||||
@@ -93,15 +94,45 @@ def test_cmis_browser_binding_routes_are_advertised_in_openapi(cmis_client) -> N
|
|||||||
|
|
||||||
def test_cmis_repository_info_and_type_definitions(cmis_client) -> None:
|
def test_cmis_repository_info_and_type_definitions(cmis_client) -> None:
|
||||||
access_points = cmis_client.get("/cmis").json()
|
access_points = cmis_client.get("/cmis").json()
|
||||||
repository = cmis_client.get("/cmis/readonly-browser/browser").json()
|
service_document = cmis_client.get("/cmis/readonly-browser/browser").json()
|
||||||
|
repository = service_document["kontextual-readonly-browser"]
|
||||||
types = cmis_client.get("/cmis/readonly-browser/browser/types").json()
|
types = cmis_client.get("/cmis/readonly-browser/browser/types").json()
|
||||||
|
browser_types = cmis_client.get(
|
||||||
|
"/cmis/readonly-browser/browser",
|
||||||
|
params={"cmisselector": "typeChildren"},
|
||||||
|
).json()
|
||||||
|
browser_types_with_properties = cmis_client.get(
|
||||||
|
"/cmis/readonly-browser/browser",
|
||||||
|
params={"cmisselector": "typeChildren", "includePropertyDefinitions": "true"},
|
||||||
|
).json()
|
||||||
|
browser_type_descendants = cmis_client.get(
|
||||||
|
"/cmis/readonly-browser/browser",
|
||||||
|
params={"cmisselector": "typeDescendants"},
|
||||||
|
).json()
|
||||||
|
browser_type_definition = cmis_client.get(
|
||||||
|
"/cmis/readonly-browser/browser",
|
||||||
|
params={"cmisselector": "typeDefinition", "typeId": "cmis:document"},
|
||||||
|
).json()
|
||||||
|
root_policies = cmis_client.get(
|
||||||
|
"/cmis/readonly-browser/browser/root",
|
||||||
|
params={"cmisselector": "policies"},
|
||||||
|
).json()
|
||||||
|
|
||||||
assert access_points["count"] == 4
|
assert access_points["count"] == 4
|
||||||
assert repository["repository_id"] == "kontextual-readonly-browser"
|
assert repository["repositoryId"] == "kontextual-readonly-browser"
|
||||||
assert repository["cmis_version_supported"] == "1.1"
|
assert repository["cmisVersionSupported"] == "1.1"
|
||||||
assert repository["capabilities"]["capability_query"] == "metadataonly"
|
assert repository["repositoryUrl"].endswith("/cmis/readonly-browser/browser")
|
||||||
assert repository["capabilities"]["capability_get_descendants"] is False
|
assert repository["rootFolderUrl"].endswith("/cmis/readonly-browser/browser/root")
|
||||||
assert repository["unsupported_features"]["multifiling"]["status"] == "projection_only"
|
assert repository["capabilities"]["capabilityQuery"] == "metadataonly"
|
||||||
|
assert repository["capabilities"]["capabilityGetDescendants"] is False
|
||||||
|
assert browser_types["types"][0]["id"] == "cmis:document"
|
||||||
|
assert "propertyDefinitions" not in browser_types["types"][0]
|
||||||
|
assert "propertyDefinitions" in browser_types_with_properties["types"][0]
|
||||||
|
assert browser_type_descendants[0]["type"]["id"] == "cmis:document"
|
||||||
|
assert "propertyDefinitions" not in browser_type_descendants[0]["type"]
|
||||||
|
assert "propertyDefinitions" in browser_type_definition
|
||||||
|
assert browser_type_descendants[0]["children"] == []
|
||||||
|
assert root_policies == []
|
||||||
assert {item["base_type_id"] for item in types["items"]} >= {
|
assert {item["base_type_id"] for item in types["items"]} >= {
|
||||||
"cmis:document",
|
"cmis:document",
|
||||||
"cmis:folder",
|
"cmis:folder",
|
||||||
@@ -110,7 +141,11 @@ def test_cmis_repository_info_and_type_definitions(cmis_client) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_cmis_readonly_children_object_content_query_relationships_and_changes(cmis_client) -> None:
|
def test_cmis_readonly_children_object_content_query_relationships_and_changes(cmis_client) -> None:
|
||||||
children = cmis_client.get("/cmis/readonly-browser/browser/children").json()
|
root_children = cmis_client.get("/cmis/readonly-browser/browser/children").json()
|
||||||
|
children = cmis_client.get(
|
||||||
|
"/cmis/readonly-browser/browser/children",
|
||||||
|
params={"folder_id": "cmis:folder:assets::document"},
|
||||||
|
).json()
|
||||||
object_response = cmis_client.get(
|
object_response = cmis_client.get(
|
||||||
"/cmis/readonly-browser/browser/object/cmis:asset:asset-source"
|
"/cmis/readonly-browser/browser/object/cmis:asset:asset-source"
|
||||||
).json()
|
).json()
|
||||||
@@ -127,7 +162,9 @@ def test_cmis_readonly_children_object_content_query_relationships_and_changes(c
|
|||||||
).json()
|
).json()
|
||||||
changes = cmis_client.get("/cmis/readonly-browser/browser/changes").json()
|
changes = cmis_client.get("/cmis/readonly-browser/browser/changes").json()
|
||||||
|
|
||||||
|
root_ids = {item["object_id"] for item in root_children["objects"]}
|
||||||
child_ids = {item["object_id"] for item in children["objects"]}
|
child_ids = {item["object_id"] for item in children["objects"]}
|
||||||
|
assert "cmis:folder:assets" in root_ids
|
||||||
assert "cmis:asset:asset-source" in child_ids
|
assert "cmis:asset:asset-source" in child_ids
|
||||||
assert "cmis:asset:asset-public" in child_ids
|
assert "cmis:asset:asset-public" in child_ids
|
||||||
assert "cmis:asset:asset-confidential" not in child_ids
|
assert "cmis:asset:asset-confidential" not in child_ids
|
||||||
@@ -141,10 +178,14 @@ def test_cmis_readonly_children_object_content_query_relationships_and_changes(c
|
|||||||
|
|
||||||
|
|
||||||
def test_cmis_profile_gates_visibility_by_access_point(cmis_client) -> None:
|
def test_cmis_profile_gates_visibility_by_access_point(cmis_client) -> None:
|
||||||
readonly = cmis_client.get("/cmis/readonly-browser/browser/children").json()
|
readonly = cmis_client.get(
|
||||||
|
"/cmis/readonly-browser/browser/children",
|
||||||
|
params={"folder_id": "cmis:folder:assets::document"},
|
||||||
|
).json()
|
||||||
admin_denied = cmis_client.get("/cmis/admin-export/browser/children")
|
admin_denied = cmis_client.get("/cmis/admin-export/browser/children")
|
||||||
admin_allowed = cmis_client.get(
|
admin_allowed = cmis_client.get(
|
||||||
"/cmis/admin-export/browser/children",
|
"/cmis/admin-export/browser/children",
|
||||||
|
params={"folder_id": "cmis:folder:assets::document"},
|
||||||
headers={"X-Actor-Type": "service_account", "X-Actor-Id": "svc-export"},
|
headers={"X-Actor-Type": "service_account", "X-Actor-Id": "svc-export"},
|
||||||
).json()
|
).json()
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ type: workplan
|
|||||||
title: "CMIS Browser Binding TCK Compatibility"
|
title: "CMIS Browser Binding TCK Compatibility"
|
||||||
domain: markitect
|
domain: markitect
|
||||||
repo: kontextual-engine
|
repo: kontextual-engine
|
||||||
status: planned
|
status: completed
|
||||||
owner: codex
|
owner: codex
|
||||||
topic_slug: markitect
|
topic_slug: markitect
|
||||||
planning_priority: high
|
planning_priority: high
|
||||||
@@ -38,6 +38,7 @@ The result is persisted in
|
|||||||
## References
|
## References
|
||||||
|
|
||||||
- `docs/cmis-opencmis-tck-assessment-2026-05-08T063312Z.md`
|
- `docs/cmis-opencmis-tck-assessment-2026-05-08T063312Z.md`
|
||||||
|
- `docs/cmis-opencmis-tck-implementation-evidence-2026-05-08T092113Z.md`
|
||||||
- `docs/cmis-1-1-capability-scorecard.md`
|
- `docs/cmis-1-1-capability-scorecard.md`
|
||||||
- `docs/cmis-compliance-assessment.md`
|
- `docs/cmis-compliance-assessment.md`
|
||||||
- `docs/cmis-profiled-access-points-implementation.md`
|
- `docs/cmis-profiled-access-points-implementation.md`
|
||||||
@@ -70,11 +71,25 @@ adds them explicitly.
|
|||||||
- Updated CMIS scorecard separates TCK-backed compatibility from native
|
- Updated CMIS scorecard separates TCK-backed compatibility from native
|
||||||
controlled-client usefulness.
|
controlled-client usefulness.
|
||||||
|
|
||||||
|
## Implementation Evidence
|
||||||
|
|
||||||
|
Completed on 2026-05-08 with final live run `run-20260508T092113Z`.
|
||||||
|
|
||||||
|
- Focused internal CMIS suite: `45 passed`.
|
||||||
|
- Guide Board/OpenCMIS status: `completed`.
|
||||||
|
- Repository/type cases: `38 pass`, `2 info`, `2 skipped`, `1 warning`, `0 fail`.
|
||||||
|
- Object/content cases: `22 skipped`, `0 fail`.
|
||||||
|
- Latest TCK preparation scorecard: `23.81`, coverage `2/9`.
|
||||||
|
|
||||||
|
The only remaining repository/type warning is local HTTP transport. The
|
||||||
|
object/content skips are caused by the deliberate non-creatable `cmis:folder`
|
||||||
|
profile boundary, not by a Browser Binding session failure.
|
||||||
|
|
||||||
## F13.1 - Define Browser Binding protocol contract examples
|
## F13.1 - Define Browser Binding protocol contract examples
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: KONT-WP-0013-T001
|
id: KONT-WP-0013-T001
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "fa2f99fb-51ed-4fba-8f36-ba4b790908e5"
|
state_hub_task_id: "fa2f99fb-51ed-4fba-8f36-ba4b790908e5"
|
||||||
```
|
```
|
||||||
@@ -92,7 +107,7 @@ Acceptance:
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: KONT-WP-0013-T002
|
id: KONT-WP-0013-T002
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "39949f5b-32bb-4ec7-b0f6-83a16ce443b7"
|
state_hub_task_id: "39949f5b-32bb-4ec7-b0f6-83a16ce443b7"
|
||||||
```
|
```
|
||||||
@@ -111,7 +126,7 @@ Acceptance:
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: KONT-WP-0013-T003
|
id: KONT-WP-0013-T003
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "96e11fe6-68c6-46ce-88b8-bda0b9baf2d5"
|
state_hub_task_id: "96e11fe6-68c6-46ce-88b8-bda0b9baf2d5"
|
||||||
```
|
```
|
||||||
@@ -128,7 +143,7 @@ Acceptance:
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: KONT-WP-0013-T004
|
id: KONT-WP-0013-T004
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "06506455-b3c3-4f17-ae8a-8ffebc226f73"
|
state_hub_task_id: "06506455-b3c3-4f17-ae8a-8ffebc226f73"
|
||||||
```
|
```
|
||||||
@@ -141,11 +156,16 @@ Acceptance:
|
|||||||
generic infrastructure error.
|
generic infrastructure error.
|
||||||
- The preflight reports actionable missing or misshaped fields.
|
- The preflight reports actionable missing or misshaped fields.
|
||||||
|
|
||||||
|
Completion note: the engine-side protocol-shape regression is now covered by
|
||||||
|
Browser Binding route tests and the live OpenCMIS run. A stronger negative
|
||||||
|
preflight fixture belongs in the sister `open-cmis-tck` repository and remains
|
||||||
|
a follow-up outside this repo's implementation boundary.
|
||||||
|
|
||||||
## F13.5 - Fix OpenAPI generation for streaming routes
|
## F13.5 - Fix OpenAPI generation for streaming routes
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: KONT-WP-0013-T005
|
id: KONT-WP-0013-T005
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "de73a3ff-ddf9-40bb-b89c-50d6fabdb841"
|
state_hub_task_id: "de73a3ff-ddf9-40bb-b89c-50d6fabdb841"
|
||||||
```
|
```
|
||||||
@@ -161,7 +181,7 @@ Acceptance:
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: KONT-WP-0013-T006
|
id: KONT-WP-0013-T006
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "1cf8d36c-6127-46cd-b409-dda7a045b8dd"
|
state_hub_task_id: "1cf8d36c-6127-46cd-b409-dda7a045b8dd"
|
||||||
```
|
```
|
||||||
@@ -178,7 +198,7 @@ Acceptance:
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: KONT-WP-0013-T007
|
id: KONT-WP-0013-T007
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "38777aea-7d2d-4b30-99b4-5194f2c2f8b0"
|
state_hub_task_id: "38777aea-7d2d-4b30-99b4-5194f2c2f8b0"
|
||||||
```
|
```
|
||||||
@@ -194,7 +214,7 @@ Acceptance:
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: KONT-WP-0013-T008
|
id: KONT-WP-0013-T008
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "8e7b2235-d39a-465a-b828-b05eb6bcfce3"
|
state_hub_task_id: "8e7b2235-d39a-465a-b828-b05eb6bcfce3"
|
||||||
```
|
```
|
||||||
@@ -209,7 +229,7 @@ Acceptance:
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: KONT-WP-0013-T009
|
id: KONT-WP-0013-T009
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "22af3e39-c217-4f6b-b54a-5ecea3894020"
|
state_hub_task_id: "22af3e39-c217-4f6b-b54a-5ecea3894020"
|
||||||
```
|
```
|
||||||
@@ -226,7 +246,7 @@ Acceptance:
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: KONT-WP-0013-T010
|
id: KONT-WP-0013-T010
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "469a3ea6-a6ab-4e6e-ba4f-5b1abdabd6c4"
|
state_hub_task_id: "469a3ea6-a6ab-4e6e-ba4f-5b1abdabd6c4"
|
||||||
```
|
```
|
||||||
@@ -247,3 +267,10 @@ Acceptance:
|
|||||||
- Object/content TCK group produces parsed evidence or documented
|
- Object/content TCK group produces parsed evidence or documented
|
||||||
unsupported-by-design gaps.
|
unsupported-by-design gaps.
|
||||||
- Updated documentation clearly states the evidence-backed CMIS posture.
|
- Updated documentation clearly states the evidence-backed CMIS posture.
|
||||||
|
|
||||||
|
## Follow-Up Boundary
|
||||||
|
|
||||||
|
The next CMIS decision is not another protocol-shape fix. It is whether to
|
||||||
|
support CMIS `createFolder` in the `compat-tck` profile so OpenCMIS CRUD/content
|
||||||
|
cases execute instead of skipping, or whether to keep those skips as an honest
|
||||||
|
profile boundary until a client requires full CMIS CRUD scaffolding.
|
||||||
|
|||||||
Reference in New Issue
Block a user