Python client for the REST API of Hyland OpenArch, an open source project that federates unstructured, AI-ready data from all sources across an organization and makes it available to AI services and agents while always respecting permissions. OpenArch can be self-managed and used with your own LLMs and choice of AI services. Sync and async, with Pydantic v2 models.
- Every operation of
docs/openapi/openarch-api.yaml: documents by id and path (with conditional writes and upsert), query / advanced / named / terms aggregation / vector search, download, users and groups, repository model and security policies (v1 and v2), vocabularies, upload. - Pydantic v2 models generated from the spec, plus hand-written
OpenArchDocumentandOpenArchQueryResult: documents carry dynamiccin_*properties the generated types do not type (the same choice content-lake-app makes in Java). OpenArchClient(sync) andAsyncOpenArchClient(async), on httpx. The sync client is generated from the async one, so the two never drift.- Basic auth (a local OpenArch), bearer token (the cloud CIC) — a string or a function returning
the current one — or any
httpx.Auth.
Status: alpha (0.1.0). Written against the spec generated by running OpenArch
(docs/openapi/openarch-api.yaml), first from main aa90475b (2026-10-13, 12.0-SNAPSHOT). The
spec was regenerated from main 34caff31 (2026-10-19), the version that added the BasicAuth
security scheme, its only change: until then the spec declared only the bearer token of the cloud
CIC (Hyland Content Innovation Cloud). Checked again on 2026-10-21 against 28f4b833 (unchanged);
see docs/openapi/README.md. Unit-tested with mocks and tested against a
local OpenArch in Docker (tests/test_live.py).
uv pip install python-openarch-apiFor development with the source, install from a clone instead: uv pip install -e ".[dev]" (see
Development).
Every method, model and error is described in the API reference, docs/api.md.
from python_openarch_api import OpenArchClient, OpenArchDocument
with OpenArchClient("http://localhost:8080", username="admin", password="password") as oa:
print(oa.hello(protected=True))
# Query (HXQL)
result = oa.query("SELECT * FROM SysContent WHERE cin_sourceId = 'repo-1'", limit=50)
for doc in result.documents:
print(doc.sys_id, doc.sys_name, doc.cin_id, doc.properties)
# Next page: pass the cursor back
more = oa.query("SELECT * FROM SysContent WHERE cin_sourceId = 'repo-1'", limit=50,
search_after=result.next_search_after)
# Create a document carrying source-system fields (needs the CinRemote mixin)
doc = OpenArchDocument(sys_primary_type="SysFile", sys_name="report.pdf",
cin_id="node-1", cin_source_id="repo-1").with_cin_remote()
created = oa.create_document_by_path("/alfresco-sync", doc)
# Partial update, only if the title is still the old one
oa.patch_document(created.sys_id, [{"op": "replace", "path": "/sys_title", "value": "Report"}],
if_='content.sys_title == "report.pdf"')Async:
from python_openarch_api import AsyncOpenArchClient
async with AsyncOpenArchClient("https://openarch.example.com", token=get_access_token) as oa:
doc = await oa.get_document("a1b2c3")OpenArchDocument has the schema's sys_* fields (snake_case in Python, e.g.
sys_primary_type; their wire names on the way out) and the CinRemote fields cin_id,
cin_source_id, cin_paths, cin_ingest_properties, cin_ingest_property_names, cin_read,
cin_deny. Any other property (repository-model fields, sysembed_*) is kept as is:
doc.properties # {"my_customField": 42, ...} (wire names)
doc.get("cin_sourceId") # any property by wire name
doc.to_json() # request body: wire names, None left outOpenArch rejects a document with cin_* fields unless sys_mixinTypes includes CinRemote;
with_cin_remote() adds it.
A blob property points at its content by uploadId. Send the content in the same request (the
part name is yours to pick), or upload it first:
oa.create_document_by_path("/docs", {"sys_primaryType": "SysFile", "sys_name": "a.txt",
"sysfile_blob": {"uploadId": "f1"}},
files={"f1": ("a.txt", data, "text/plain")})
up = oa.upload(data, file_name="a.txt", mime_type="text/plain")
oa.create_document_by_path("/docs", {"sys_primaryType": "SysFile", "sys_name": "a.txt",
"sysfile_blob": {"uploadId": up.id}})
oa.download(doc_id, "sysfile_blob/a.txt") # -> bytesupdate_document, patch_document, move (and the _by_path ones) take condition= (an HXQL
predicate) or if_= (an expression), user_change=False for technical updates that should not
bump sys_ugen, and on PUT upsert=True to create the document when it does not exist.
get_model / set_model / get_security_policies / set_security_policies are the v1 API. The
_v2 methods return and take a version for optimistic locking: read with get_*_v2, change,
and send the same object back with set_*_v2.
The HXCS-REPOSITORY header is sent on every request (repository="default"); pass
repository= to the client or to a call to use another one.
Non-2xx answers raise OpenArchError (status_code, error = the parsed
RepositoryErrorResponse), or its subclasses OpenArchAuthError (401/403),
OpenArchNotFoundError (404), OpenArchConflictError (409: a set_*_v2 with a stale version)
and OpenArchPreconditionError (412: a condition / if_ that did not match, or checkin of a
non-versionable document).
Create and activate a virtual environment, then install the package with the dev extra.
Linux / macOS:
uv venv venv-3.14 --python 3.14
source venv-3.14/bin/activate
uv pip install -e ".[dev]"
python -m pytestWindows (PowerShell or Command Prompt):
uv venv venv-3.14 --python 3.14
venv-3.14\Scripts\activate
uv pip install -e ".[dev]"
python -m pytestWith the virtual environment active, the same on every platform:
python scripts/generate_models.py # after updating docs/openapi/openarch-api.yaml
python scripts/unasync.py # after editing python_openarch_api/_async/client.pyThe dev extra (-e ".[dev]") is needed to run the tests and the scripts: it brings pytest,
PyYAML (a test reads the spec) and the pinned datamodel-code-generator. A plain
uv pip install -e . installs only httpx and Pydantic.
docs/openapi/openarch-api.yaml— the spec, downloaded from a running OpenArch (it ships no static spec); docs/openapi/README.md says how to regenerate it.python_openarch_api/models/generated.py— generated by datamodel-code-generator (pinned); do not edit.python_openarch_api/_async/client.py— the client; edit this one.python_openarch_api/_sync/client.py— generated from it; a test fails if it is stale.- A test checks that every
operationIdin the spec has a client method. tests/test_live.pyruns against a real OpenArch whenOPENARCH_URLis set (it creates and deletes a folder at the root). OpenArch's rootdocker-compose.ymlleaves the data dir unwritable, so every upload is a 500: startopenarch-appwithNUXEO_DATA_DIR=/tmp, as OpenArch's own README and CI do.
pytest collects 23 tests in total: 16 unit tests (tests/test_client.py, with mocks, no server)
and 7 live tests (tests/test_live.py), which are skipped unless OPENARCH_URL is set. To run the
live tests against a running OpenArch, set OPENARCH_URL in the shell where the virtual
environment is active. Use http://localhost:8080 for OpenArch's own default port, or
http://localhost:8088 for the OpenArch of KG Spaces Deployment (its OPENARCH_PORT, 8088 by
default).
Linux / macOS:
OPENARCH_URL=http://localhost:8080 python -m pytest tests/test_live.pyWindows (PowerShell):
$env:OPENARCH_URL = "http://localhost:8080"
python -m pytest tests/test_live.pyWindows (Command Prompt):
set OPENARCH_URL=http://localhost:8080
python -m pytest tests/test_live.pyWithout tests/test_live.py, python -m pytest runs the unit tests and the live tests together.
OPENARCH_USER / OPENARCH_PASSWORD set the Basic auth user; they default to the admin user of
the local openarch-app's openarch-users.yml. Set them the same way as OPENARCH_URL.
The client follows the server where the generated spec is short of it:
- Multipart document writes: the spec shows a placeholder part (
main1234); any name works, as long as a blob property references it byuploadId. nextSearchAfter: the query schemas refer to it butQueryResultdoes not declare it;OpenArchQueryResult.next_search_afterreads it.StringTermsBuckethas no properties in the spec;TermsBuckettypeskeyanddoc_count.- Responses the spec leaves untyped (named query list, v1 security policies) are returned as parsed JSON.
download_urlneeds a blob provider with pre-signed URLs (S3); the local filesystem store answers 501.
Apache-2.0, as is OpenArch, from which docs/openapi/openarch-api.yaml is generated.