This module provides a Python SDK for interacting with the SAP Document Management Service (DMS). It supports repository management, document and folder operations via the CMIS Browser Binding protocol, versioning, and access control management.
# Using pip
pip install sap-cloud-sdkSee further information about installation in the main documentation.
from sap_cloud_sdk.dms import create_client
from sap_cloud_sdk.dms.model import (
InternalRepoRequest, UpdateRepoRequest, Repository,
CreateConfigRequest, UpdateConfigRequest, RepositoryConfig, ConfigName,
UserClaim, Ace, Acl, Folder, Document, CmisObject, ChildrenPage,
)
from sap_cloud_sdk.dms.exceptions import (
DMSError, DMSObjectNotFoundException, DMSPermissionDeniedException,
DMSInvalidArgumentException, DMSConflictException,
DMSConnectionError, DMSRuntimeException,
)Use create_client() to get a client with automatic configuration detection:
from sap_cloud_sdk.dms import create_client
# Load credentials from mounted secrets or environment variables
client = create_client(instance="my-instance")You can also provide credentials directly:
from sap_cloud_sdk.dms import create_client
from sap_cloud_sdk.dms.model import DMSCredentials
creds = DMSCredentials(
instance_name="my-instance",
uri="https://api-sdm-di.cfapps.eu10.hana.ondemand.com",
client_id="your-client-id",
client_secret="your-client-secret",
token_url="https://your-subdomain.authentication.eu10.hana.ondemand.com/oauth/token",
identityzone="your-subdomain",
)
client = create_client(dms_cred=creds)
instancerefers to the instance name defined in your Cloud descriptor.This name determines which set of credentials or mounted secrets to resolve from the environment.
Many DMS operations accept an optional user_claim parameter to impersonate a user when the SDK authenticates via client_credentials grant. This forwards X-EcmUserEnc and X-EcmAddPrincipals headers to the service.
from sap_cloud_sdk.dms.model import UserClaim
claim = UserClaim(
x_ecm_user_enc="john.doe@example.com",
x_ecm_add_principals=["~GroupA", "~GroupB", "another.user@example.com"],
)
# Pass to any API call
repos = client.get_all_repositories(user_claim=claim)Groups are prefixed with ~. Omit the user_claim parameter to use the service identity.
from sap_cloud_sdk.dms.model import InternalRepoRequest
request = InternalRepoRequest(
displayName="My Repository",
description="Main document store",
isVersionEnabled=True,
isVirusScanEnabled=True,
)
repo = client.onboard_repository(request)
print(f"Created: {repo.name} (id={repo.id})")repos = client.get_all_repositories()
for repo in repos:
print(f"{repo.name} — {repo.repository_type} — id={repo.id}")
print(f" CMIS repo ID: {repo.cmis_repository_id}")
print(f" Versioning: {repo.get_param('isVersionEnabled')}")repo = client.get_repository("repository-uuid")
print(f"{repo.name}: {repo.repository_category}")from sap_cloud_sdk.dms.model import UpdateRepoRequest
request = UpdateRepoRequest(
description="Updated description",
isVirusScanEnabled=False,
)
repo = client.update_repository("repository-uuid", request)client.delete_repository("repository-uuid")from sap_cloud_sdk.dms.model import CreateConfigRequest, ConfigName
request = CreateConfigRequest(
config_name=ConfigName.BLOCKED_FILE_EXTENSIONS,
config_value="bat,dmg,exe",
)
config = client.create_config(request)
print(f"Config: {config.config_name} = {config.config_value}")configs = client.get_configs()
for cfg in configs:
print(f"{cfg.config_name} = {cfg.config_value} (id={cfg.id})")from sap_cloud_sdk.dms.model import UpdateConfigRequest
request = UpdateConfigRequest(
id="config-uuid",
config_name=ConfigName.BLOCKED_FILE_EXTENSIONS,
config_value="bat,dmg,exe,msi",
)
config = client.update_config("config-uuid", request)client.delete_config("config-uuid")All CMIS operations require a repository_id (the CMIS repository ID from repo.cmis_repository_id) and use the CMIS Browser Binding protocol under the hood.
folder = client.create_folder(
repository_id="cmis-repo-id",
parent_folder_id="parent-folder-object-id",
folder_name="My Folder",
description="Optional description",
)
print(f"Created folder: {folder.name} (objectId={folder.object_id})")import io
# From in-memory bytes
content = io.BytesIO(b"Hello, World!")
doc = client.create_document(
repository_id="cmis-repo-id",
parent_folder_id="parent-folder-id",
document_name="hello.txt",
file=content,
mime_type="text/plain",
)
print(f"Created: {doc.name} ({doc.content_stream_length} bytes)")
print(f"Version: {doc.version_label}")with open("/path/to/report.pdf", "rb") as f:
doc = client.create_document(
repository_id="cmis-repo-id",
parent_folder_id="parent-folder-id",
document_name="report.pdf",
file=f,
mime_type="application/pdf",
description="Q4 financial report",
)response = client.get_content(
repository_id="cmis-repo-id",
document_id="document-object-id",
download="attachment",
)
try:
with open("downloaded.pdf", "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
finally:
response.close()Optional parameters:
stream_id— download a specific rendition (e.g."sap:zipRendition")filename— override the filename in the response header
Versioning requires a version-enabled repository.
Creates a Private Working Copy (PWC):
pwc = client.check_out(
repository_id="cmis-repo-id",
document_id="document-object-id",
)
print(f"PWC objectId: {pwc.object_id}")
print(f"Is PWC: {pwc.is_private_working_copy}")Creates a new version from the PWC:
import io
new_content = io.BytesIO(b"Updated content for v2")
doc = client.check_in(
repository_id="cmis-repo-id",
document_id=pwc.object_id, # the PWC objectId
major=True,
file=new_content,
file_name="hello.txt",
mime_type="text/plain",
checkin_comment="Version 2.0 — updated content",
)
print(f"New version: {doc.version_label}")
print(f"Is latest: {doc.is_latest_version}")Discards the PWC without creating a new version:
client.cancel_check_out(
repository_id="cmis-repo-id",
document_id=pwc.object_id,
)acl = client.apply_acl(
repository_id="cmis-repo-id",
object_id="document-or-folder-id",
)
for ace in acl.aces:
print(f"{ace.principal_id}: {ace.permissions} (direct={ace.is_direct})")from sap_cloud_sdk.dms.model import Ace
acl = client.apply_acl(
repository_id="cmis-repo-id",
object_id="document-or-folder-id",
add_aces=[
Ace(principal_id="user@example.com", permissions=["cmis:read"]),
Ace(principal_id="admin@example.com", permissions=["cmis:all"]),
],
)acl = client.apply_acl(
repository_id="cmis-repo-id",
object_id="document-or-folder-id",
remove_aces=[
Ace(principal_id="user@example.com", permissions=["cmis:read"]),
],
)Control whether ACL changes propagate to children:
acl = client.apply_acl(
repository_id="cmis-repo-id",
object_id="folder-id",
add_aces=[Ace(principal_id="team@example.com", permissions=["cmis:read"])],
acl_propagation="propagate", # "propagate" | "objectonly" | "repositorydetermined"
)Returns Folder, Document, or CmisObject depending on the base type:
obj = client.get_object(
repository_id="cmis-repo-id",
object_id="some-object-id",
include_acl=True,
)
print(f"Type: {type(obj).__name__}")
print(f"Name: {obj.name}")
print(f"Modified: {obj.last_modification_date}")
if isinstance(obj, Document):
print(f"MIME: {obj.content_stream_mime_type}")
print(f"Size: {obj.content_stream_length}")
print(f"Version: {obj.version_label}")updated = client.update_properties(
repository_id="cmis-repo-id",
object_id="some-object-id",
properties={
"cmis:name": "Renamed Document.pdf",
"cmis:description": "Updated description",
},
)
print(f"New name: {updated.name}")Use change_token for optimistic concurrency control:
updated = client.update_properties(
repository_id="cmis-repo-id",
object_id="some-object-id",
properties={"cmis:name": "New Name"},
change_token="current-change-token-value",
)page = client.get_children(
repository_id="cmis-repo-id",
folder_id="parent-folder-id",
max_items=50,
skip_count=0,
order_by="cmis:creationDate ASC",
)
print(f"Items: {len(page.objects)}, hasMore: {page.has_more_items}, total: {page.num_items}")
for obj in page.objects:
kind = "Folder" if isinstance(obj, Folder) else "Document"
print(f" [{kind}] {obj.name} — {obj.object_id}")Paginate through all children:
skip = 0
page_size = 100
while True:
page = client.get_children(
repository_id="cmis-repo-id",
folder_id="parent-folder-id",
max_items=page_size,
skip_count=skip,
)
for obj in page.objects:
print(obj.name)
if not page.has_more_items:
break
skip += page_sizeAll operations support an optional tenant parameter for subscriber-scoped requests. The SDK resolves the token URL by replacing the provider's identity zone with the tenant subdomain:
# Provider context (default)
repos = client.get_all_repositories()
# Subscriber context
repos = client.get_all_repositories(tenant="subscriber-subdomain")The DMS module provides specific exceptions for different error scenarios:
from sap_cloud_sdk.dms.exceptions import (
DMSError,
DMSObjectNotFoundException,
DMSPermissionDeniedException,
DMSInvalidArgumentException,
DMSConflictException,
DMSConnectionError,
DMSRuntimeException,
)
try:
obj = client.get_object("repo-uuid", "missing-id")
except DMSObjectNotFoundException as e:
print(f"Not found ({e.status_code}): {e}")
except DMSConflictException as e:
print(f"Conflict ({e.status_code}): {e}")
except DMSPermissionDeniedException as e:
print(f"Access denied ({e.status_code}): {e}")
except DMSInvalidArgumentException as e:
print(f"Bad request ({e.status_code}): {e}")
except DMSConnectionError as e:
print(f"Network error: {e}")
except DMSRuntimeException as e:
print(f"Server error ({e.status_code}): {e}")| Exception | HTTP Status | Description |
|---|---|---|
DMSError |
any | Base exception for all DMS errors |
DMSObjectNotFoundException |
404 | Repository, document, or folder not found |
DMSPermissionDeniedException |
401, 403 | Invalid or expired access token |
DMSInvalidArgumentException |
400 | Invalid request payload or parameters |
DMSConflictException |
409 | Resource state conflict (e.g. duplicate name in versioned repo) |
DMSConnectionError |
— | Network failure, timeout, or connection refused |
DMSRuntimeException |
500 | Server-side internal error |
All exceptions carry:
status_code: HTTP status code (when applicable)error_content: Raw response body for debugging
The SDK extracts the server's error message from JSON responses (the "message" field) when available, providing specific error descriptions rather than generic messages.
InternalRepoRequest: Create a new repository —displayName(required),description,isVersionEnabled,isVirusScanEnabled,repositoryCategory, etc.UpdateRepoRequest: Update repository metadata —description,isVirusScanEnabled,isThumbnailEnabled,isAIEnabled, etc.Repository: Repository entity —id,name,cmis_repository_id,repository_type,repository_category,repository_params. Userepo.get_param("paramName")for dynamic parameter access.
CreateConfigRequest:config_name(useConfigNameenum),config_valueUpdateConfigRequest:id,config_name,config_valueRepositoryConfig:id,config_name,config_value, timestampsConfigNameenum:BLOCKED_FILE_EXTENSIONS,TEMPSPACE_MAX_CONTENT_SIZE,IS_CROSS_DOMAIN_MAPPING_ALLOWED
CmisObject: Base model —object_id,name,base_type_id,object_type_id,created_by,creation_date,last_modified_by,last_modification_date,change_token,parent_ids,description,properties(full raw dict)Folder: ExtendsCmisObject(no additional fields)Document: ExtendsCmisObject— content fields (content_stream_length,content_stream_mime_type,content_stream_file_name) and versioning fields (version_label,version_series_id,is_latest_version,is_major_version,is_private_working_copy,checkin_comment,is_version_series_checked_out, etc.)
Ace: Access control entry —principal_id,permissions(list of strings),is_directAcl: Access control list —aces(list ofAce),is_exact
ChildrenPage: Paginated result —objects(list),has_more_items,num_itemsUserClaim: User identity —x_ecm_user_enc,x_ecm_add_principalsDMSCredentials: Service credentials —instance_name,uri,client_id,client_secret,token_url,identityzone
- Mount path:
$SERVICE_BINDING_ROOT/sdm/{instance}/(defaults to/etc/secrets/appfnd/sdm/{instance}/) - Required Keys:
uri(DMS API base URL),uaa(JSON string with XSUAA credentials) - Env var fallback:
CLOUD_SDK_CFG_SDM_{INSTANCE}_{FIELD}(uppercased, hyphens in instance replaced with_)
Note:
SERVICE_BINDING_ROOTdefaults to/etc/secrets/appfndwhen not set. See the Secret Resolver guide for details.
$SERVICE_BINDING_ROOT/sdm/{instance}/
├── uri
└── uaa
# Example for DMS with instance name "default"
export CLOUD_SDK_CFG_SDM_DEFAULT_URI="https://api.dms.example.com"
export CLOUD_SDK_CFG_SDM_DEFAULT_UAA='{"clientid":"...","clientsecret":"...","url":"https://...","identityzone":"..."}'The uaa key must contain a JSON string with the XSUAA credentials:
{
"clientid": "sb-xxx!bxxx|sdm-di-xxx!bxxx",
"clientsecret": "xxx",
"url": "https://subdomain.authentication.region.hana.ondemand.com",
"identityzone": "subdomain"
}