Metadata-Version: 2.5
Name: python-openarch-api
Version: 0.1.0
Summary: Python client for the Hyland OpenArch repository REST API, with Pydantic v2 models
Project-URL: Homepage, https://github.com/stevereiner/python-openarch-api
Project-URL: Repository, https://github.com/stevereiner/python-openarch-api
Project-URL: Issues, https://github.com/stevereiner/python-openarch-api/issues
Project-URL: Documentation, https://github.com/stevereiner/python-openarch-api/blob/main/docs/api.md
Author: Steve Reiner
License: Apache-2.0
License-File: LICENSE
Keywords: content-lake,hxpr,hyland,nuxeo,openarch,pydantic,rest-api
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2.7
Provides-Extra: dev
Requires-Dist: datamodel-code-generator[black,isort]==0.83.0; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: pyyaml>=6; extra == 'dev'
Description-Content-Type: text/markdown

# python-openarch-api

Python client for the REST API of [Hyland OpenArch](https://github.com/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](docs/openapi/README.md). Unit-tested with mocks and tested against a
local OpenArch in Docker (`tests/test_live.py`).

## Install

```bash
uv pip install python-openarch-api
```

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

## Use

Every method, model and error is described in the API reference, [docs/api.md](docs/api.md).

```python
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:

```python
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:

```python
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:

```python
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:

```bash
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):

```powershell
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:

```bash
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](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:

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

Windows (PowerShell):

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

Windows (Command Prompt):

```bat
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.
