Skip to content

About

Python client for the Hyland OpenArch REST API: documents by id and path, HXQL query / vector search / aggregations, users and groups, security policies, upload and download. Sync and async clients on httpx, Pydantic v2 models generated from the OpenAPI spec

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

Repository files navigation

python-openarch-api

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 OpenArchDocument and OpenArchQueryResult: documents carry dynamic cin_* properties the generated types do not type (the same choice content-lake-app makes in Java).
  • OpenArchClient (sync) and AsyncOpenArchClient (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).

Install

uv pip install python-openarch-api

For development with the source, install from a clone instead: uv pip install -e ".[dev]" (see Development).

Use

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")

Documents and dynamic properties

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 out

OpenArch rejects a document with cin_* fields unless sys_mixinTypes includes CinRemote; with_cin_remote() adds it.

Binary content

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")    # -> bytes

Writes

update_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.

Model and security policies: v1 and v2

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.

Repository

The HXCS-REPOSITORY header is sent on every request (repository="default"); pass repository= to the client or to a call to use another one.

Errors

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).

Development

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 pytest

Windows (PowerShell or Command Prompt):

uv venv venv-3.14 --python 3.14
venv-3.14\Scripts\activate
uv pip install -e ".[dev]"
python -m pytest

With 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.py

The 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 operationId in the spec has a client method.
  • tests/test_live.py runs against a real OpenArch when OPENARCH_URL is set (it creates and deletes a folder at the root). OpenArch's root docker-compose.yml leaves the data dir unwritable, so every upload is a 500: start openarch-app with NUXEO_DATA_DIR=/tmp, as OpenArch's own README and CI do.

Tests

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.py

Windows (PowerShell):

$env:OPENARCH_URL = "http://localhost:8080"
python -m pytest tests/test_live.py

Windows (Command Prompt):

set OPENARCH_URL=http://localhost:8080
python -m pytest tests/test_live.py

Without 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.

Where the spec and the server differ

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 by uploadId.
  • nextSearchAfter: the query schemas refer to it but QueryResult does not declare it; OpenArchQueryResult.next_search_after reads it.
  • StringTermsBucket has no properties in the spec; TermsBucket types key and doc_count.
  • Responses the spec leaves untyped (named query list, v1 security policies) are returned as parsed JSON.
  • download_url needs a blob provider with pre-signed URLs (S3); the local filesystem store answers 501.

License

Apache-2.0, as is OpenArch, from which docs/openapi/openarch-api.yaml is generated.

About

Python client for the Hyland OpenArch REST API: documents by id and path, HXQL query / vector search / aggregations, users and groups, security policies, upload and download. Sync and async clients on httpx, Pydantic v2 models generated from the OpenAPI spec

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages