The public API of python_openarch_api: the clients, their methods, the models and the errors. For
an introduction and examples, see the README. For the REST API itself, see the spec,
openapi/openarch-api.yaml.
- Clients
- Authentication
- Conventions
- Documents
- Download
- Query
- Users and groups
- Repository model
- Security policies
- Vocabularies
- Upload
- Models
- Errors
from python_openarch_api import OpenArchClient, AsyncOpenArchClientOpenArchClient (sync) and AsyncOpenArchClient (async) have the same constructor and the same
methods. On the async client every method is a coroutine (await oa.get_document(...)).
OpenArchClient(
base_url: str = "http://localhost:8080",
*,
repository: str | None = "default",
username: str | None = None,
password: str | None = None,
token: str | Callable[[], str] | None = None,
auth: httpx.Auth | None = None,
timeout: float | httpx.Timeout = 30.0,
http_client: httpx.Client | None = None, # httpx.AsyncClient for the async client
transport: httpx.BaseTransport | None = None, # httpx.AsyncBaseTransport for the async client
headers: Mapping[str, str] | None = None,
)| Argument | |
|---|---|
base_url |
The OpenArch server. |
repository |
Sent as the HXCS-REPOSITORY header on every request. Set to None to leave the header out. The repository= argument of a method overrides it for that call. |
username, password, token, auth |
See Authentication. Give at most one of them (username and password count as one). |
timeout |
The httpx timeout, in seconds or as an httpx.Timeout. |
http_client |
An httpx client to use instead of the one built here. The OpenArch client then does not close it, and base_url, the auth arguments, timeout, transport and headers are ignored. |
transport |
An httpx transport for the client built here: proxies, or httpx.MockTransport in tests. |
headers |
Extra headers for every request. The defaults are Accept: application/json and User-Agent: python-openarch-api/<version>. Hyland's hosted Content REST API wants a [product]/[version] User-Agent: pass headers={"User-Agent": "MyProduct/1.0"}. |
Use the client as a context manager, or close it yourself:
with OpenArchClient("http://localhost:8080", username="admin", password="password") as oa:
...
async with AsyncOpenArchClient("https://openarch.example.com", token=get_access_token) as oa:
...| Sync | Async | |
|---|---|---|
close() |
aclose() |
Closes the underlying httpx client, unless it came from http_client. |
| Argument | Sends | For |
|---|---|---|
username=, password= |
HTTP Basic | A local OpenArch: the users of openarch-app's openarch-users.yml, for instance admin / password. The spec's BasicAuth scheme. |
token= |
Authorization: Bearer <token> |
A hosted OpenArch (the cloud CIC), with a JWT from its identity provider. The spec's SecurityScheme. |
auth= |
Whatever the httpx.Auth sends |
Anything else. |
token is a string, or a function that returns the current token. The function is called on every
request, so a caller that refreshes tokens passes a function, not a value.
BearerAuth(token) is the httpx.Auth behind token=; it is exported for use with your own httpx
client.
Giving more than one of auth, token and username, or a username without a password,
raises ValueError.
- Method names follow the spec's operationIds in snake case. A few are shortened, for instance
getDocumentByIdisget_document. Every operation of the spec has a method (a test checks this). - Request bodies take a model or a plain dict. A model is sent with its wire names (its
aliases, such as
sys_primaryType), withNonefields left out. A dict is sent as it is, so it must use the wire names. - Responses come back as models: documents as
OpenArchDocument, query results asOpenArchQueryResult, and the rest as the models generated from the spec. Responses that the spec leaves untyped come back as parsed JSON (Any). repository=, on most methods, overrides the client's repository for that call.- Ids are escaped as one path segment. Paths keep their slashes, and a leading
/is optional:"/docs/a.txt"and"docs/a.txt"are the same document. - JSON Patch arguments are lists of operations, such as
[{"op": "replace", "path": "/sys_title", "value": "New title"}], sent asapplication/json-patch+json. - Errors: any non-2xx answer raises an
OpenArchError.
The document writes update_document, patch_document, move and their _by_path versions take:
| Argument | Query parameter | |
|---|---|---|
condition= |
condition |
An HXQL predicate the document must match. |
if_= |
if |
An expression the document must match, for instance 'content.sys_title == "report.pdf"'. |
upsert= |
upsert |
PUT only: True creates the document when it does not exist (HTTP 201). |
user_change= |
userChange |
False marks a technical update: sys_ugen is not incremented. |
condition and if_ are mutually exclusive. When the document does not match, the server answers
412 and the client raises OpenArchPreconditionError.
| Method | HTTP | Returns |
|---|---|---|
get_document(doc_id) |
GET /api/documents/{docId} |
OpenArchDocument |
create_document(parent_id, document, *, enforce_sys_name=None, files=None) |
POST /api/documents/{parentId} |
OpenArchDocument |
update_document(doc_id, document, *, condition=None, if_=None, upsert=None, user_change=None, files=None) |
PUT /api/documents/{docId} |
OpenArchDocument |
patch_document(doc_id, patch, *, condition=None, if_=None, user_change=None) |
PATCH /api/documents/{docId} |
OpenArchDocument |
delete_document(doc_id) |
DELETE /api/documents/{docId} |
None |
All take repository= too.
create_document creates document in the folder parent_id. enforce_sys_name is sent as the
enforceSysName query parameter. update_document replaces the document's properties;
patch_document applies a JSON Patch.
| Method | HTTP | Returns |
|---|---|---|
get_document_by_path(path) |
GET /api/documents/path/{path} |
OpenArchDocument |
create_document_by_path(parent_path, document, *, enforce_sys_name=None, files=None) |
POST /api/documents/path/{path} |
OpenArchDocument |
update_document_by_path(path, document, *, condition=None, if_=None, upsert=None, user_change=None, files=None) |
PUT /api/documents/path/{path} |
OpenArchDocument |
patch_document_by_path(path, patch, *, condition=None, if_=None, user_change=None) |
PATCH /api/documents/path/{path} |
OpenArchDocument |
delete_document_by_path(path) |
DELETE /api/documents/path/{path} |
None |
exists_by_path(path) |
GET /api/documents/path/{path} |
bool |
All take repository= too. exists_by_path is a convenience method: True if the document can
be read, False on a 404. Other errors raise.
create_document, update_document and their _by_path versions take files=: a mapping of part
name to an httpx file value ((file_name, data, mime_type)). With files, the request is
multipart: the document goes in a JSON part named main, and each file in its own part. The part
name is free; a blob property of the document refers to it by uploadId:
oa.create_document_by_path(
"/docs",
{"sys_primaryType": "SysFile", "sys_name": "a.txt", "sysfile_blob": {"uploadId": "f1"}},
files={"f1": ("a.txt", data, "text/plain")},
)The id of an earlier upload works the same way, without files.
| Method | HTTP | Returns |
|---|---|---|
get_ancestors(doc_id) |
GET /api/documents/{docId}/ancestors |
DocumentAncestors |
checkin(doc_id, *, minor=None, user_change=None) |
POST /api/documents/{docId}/checkin |
OpenArchDocument |
copy(doc_id, command) |
POST /api/documents/{docId}/copy |
OpenArchDocument |
move(doc_id, command, *, condition=None, if_=None, user_change=None) |
POST /api/documents/{docId}/move |
OpenArchDocument |
restore_version(version_id, *, user_change=None) |
POST /api/documents/{versionId}/restore |
OpenArchDocument |
get_embedding(doc_id, embedding_id) |
GET /api/documents/{docId}/embedding/{embeddingId} |
Embedding |
get_full_effective_acl(doc_id, usernames=None) |
GET /api/documents/{docId}/fulleffectiveacl |
ListEffectiveACLDTO |
get_full_effective_permissions(doc_id, usernames=None) |
GET /api/documents/{docId}/fulleffectivepermissions |
ListEffectivePermissionsDTO |
All take repository= too.
copytakes aCopyCommand(target_parent_id, optionalname) or the dict{"targetParentId": ..., "name": ...}.movetakes aMoveCommand(target_parent_id) or{"targetParentId": ...}.checkinof a document that is not versionable raisesOpenArchPreconditionError(412).usernameslimits the effective ACL or permissions to those users.
| Method | HTTP | Returns |
|---|---|---|
download(doc_id, property_xpath_and_filename, *, inline=None) |
GET /api/download/{docId}/{xpath} |
bytes |
download_info(doc_id, property_xpath_and_filename) |
HEAD /api/download/{docId}/{xpath} |
httpx.Headers |
download_url(doc_id, property_xpath_and_filename, *, expiration=None, inline=None) |
GET /api/download/url/{docId}/{xpath} |
str |
All take repository= too.
property_xpath_and_filename is the blob property followed by the file name, for instance
"sysfile_blob/report.pdf". download_info returns the response headers (length, type,
disposition) without the content.
download_url returns a pre-signed URL, valid for expiration seconds. It needs a blob provider
with pre-signed URLs, such as S3; the local filesystem store answers 501.
| Method | HTTP | Returns |
|---|---|---|
query(query, *, limit=None, offset=None, sort=None, track_total_count=None, syntax=None, search_after=None, sync=None) |
POST /api/query |
OpenArchQueryResult |
advanced_query(query, *, sync=None) |
POST /api/query/advanced |
OpenArchQueryResult |
named_query(query, *, sync=None) |
POST /api/query/named |
OpenArchQueryResult |
list_named_queries() |
GET /api/query/named |
Any (parsed JSON) |
get_named_query(name) |
GET /api/query/named/{name} |
NamedQueryDefinition |
terms_aggregation(query) |
POST /api/query/termsAggregation |
OpenArchQueryResult |
vector_search(query) |
POST /api/query/embeddings |
VectorSearchResult |
wait_for_indexing(*, refresh=None, timeout=None, wait_for_audit=None, repository=None) |
GET /api/query/waitForFullTextSearchIndexing |
Any (parsed JSON) |
query is an HXQL string, a Query model, or a dict with the Query wire names. With a string,
the keyword arguments fill in the rest of the Query, and its repositoryId is the client's
repository. With a model or a dict, the keyword arguments other than sync are ignored.
| Argument | |
|---|---|
limit |
Page size. The server's default is 10. |
offset |
Index of the first result. Ignored when search_after is given. |
sort |
Sort clauses. |
track_total_count |
Count every match, in total_count. |
syntax |
"hxql" (the default) or "expr" for an expression query; a QuerySyntax works too. |
search_after |
The next_search_after of the previous page, to read the next one. |
sync |
The sync query parameter. |
result = oa.query("SELECT * FROM SysContent WHERE cin_sourceId = 'repo-1'", limit=50)
while result.documents:
for doc in result.documents:
...
if not result.next_search_after:
break
result = oa.query("SELECT * FROM SysContent WHERE cin_sourceId = 'repo-1'", limit=50,
search_after=result.next_search_after)advanced_querytakes anAdvancedQuery: a query with parameters, quick filter clauses and a where clause definition.named_queryruns a query defined on the server, byquery_name, with itsparametersandselected_quick_filters(aNamedQuery).list_named_queriesandget_named_queryread the definitions.terms_aggregationtakes aTermsAggregationsQuery(query,terms_aggregation_property, optionalsearch_termandlimit). The counts per term are in the result'saggregations_buckets, asTermsBuckets.vector_searchtakes aVectorQuery(vector, optionalquery,embedding_type,chunk_fts,limit, ...) and returns the nearest embeddings.wait_for_indexingblocks until full-text indexing has caught up, for tests and read-after-write.
| Method | HTTP | Returns |
|---|---|---|
search_users(name) |
GET /api/user?name= |
list[User] |
get_user(user_id) |
GET /api/user/{userId} |
User |
search_groups(name) |
GET /api/group?name= |
list[Group] |
get_group(group_id) |
GET /api/group/{groupId} |
Group |
hello(*, protected=False) |
GET /hello or GET /hello/protected |
str |
hello is a connectivity check. With protected=True it also checks the credentials (401 if they
are wrong).
| Method | HTTP | Returns |
|---|---|---|
get_model(*, project_id=None) |
GET /api/repository/model[/project/{projectId}] |
Model |
set_model(model, *, project_id=None, validate_only=None) |
PUT /api/repository/model[/project/{projectId}] |
Model | None |
patch_model(patch, *, validate_only=None) |
PATCH /api/repository/model |
Model | None |
get_model_v2(*, project_id=None) |
GET /api/v2/repository/model[/project/{projectId}] |
Model |
set_model_v2(model, *, project_id=None, validate_only=None) |
PUT /api/v2/repository/model[/project/{projectId}] |
Model | None |
All take repository= too. project_id reads or writes the model of one project.
validate_only=True checks the model without saving it. The set_ and patch_ methods return
None when the server answers with an empty body.
v1 and v2. The v2 methods carry a version for optimistic locking. Read the model with
get_model_v2, change it, and send the same object back with set_model_v2. If someone else saved
the model in between, the server answers 409 and the client raises OpenArchConflictError.
model = oa.get_model_v2()
# ... change model.schemas, model.types, ...
oa.set_model_v2(model)| Method | HTTP | Returns |
|---|---|---|
get_security_policies() |
GET /api/repository/security/policies |
Any (parsed JSON) |
set_security_policies(policies, *, project_id=None, validate_only=None) |
PUT /api/repository/security/policies[/project/{projectId}] |
Any (parsed JSON) |
patch_security_policies(patch, *, validate_only=None) |
PATCH /api/repository/security/policies |
Any (parsed JSON) |
get_security_policies_v2() |
GET /api/v2/repository/security/policies |
SecurityPolicies |
set_security_policies_v2(policies, *, project_id=None, validate_only=None) |
PUT /api/v2/repository/security/policies[/project/{projectId}] |
SecurityPolicies | None |
get_external_user_relations(policy_name) |
GET /api/repository/security/policies/{policyName}/externaluserrelations |
ExternalUserRelationsSecurityPolicyRelations |
set_external_user_relations(policy_name, relations) |
PUT /api/repository/security/policies/{policyName}/externaluserrelations |
None |
get_external_user_relations_status(policy_name) |
GET /api/repository/security/policies/{policyName}/externaluserrelations/status |
Any (parsed JSON) |
All take repository= too. The spec leaves the v1 policies untyped, so they are plain JSON. The
v2 methods work like the model's: SecurityPolicies carries policies and a
version, and a stale version raises OpenArchConflictError.
| Method | HTTP | Returns |
|---|---|---|
list_vocabularies() |
GET /api/vocabularies |
list[VocabularyId] |
create_vocabulary(vocabulary) |
POST /api/vocabularies |
Vocabulary |
delete_vocabulary(vocabulary_id) |
DELETE /api/vocabularies/{vocabularyId} |
None |
get_vocabulary_entries(vocabulary_id) |
GET /api/vocabularies/{vocabularyId} |
list[VocabularyEntry] |
create_vocabulary_entry(vocabulary_id, entry) |
POST /api/vocabularies/{vocabularyId} |
VocabularyEntry |
update_vocabulary_entry(vocabulary_id, entry) |
PUT /api/vocabularies/{vocabularyId} |
VocabularyEntry |
get_vocabulary_entry(vocabulary_id, key) |
GET /api/vocabularies/{vocabularyId}/{key} |
VocabularyEntry |
delete_vocabulary_entry(vocabulary_id, key) |
DELETE /api/vocabularies/{vocabularyId}/{key} |
None |
All take repository= too. A Vocabulary has a sysvocab_id; a VocabularyEntryRequest has a
sysvocab_key and a sysvocab_label.
| Method | HTTP | Returns |
|---|---|---|
create_upload() |
POST /api/upload/create |
Upload |
upload(data, *, file_name=None, mime_type=None, upload_id=None, chunked=None, chunk_index=None, chunk_count=None, encoding=None, body_encoding=None) |
POST /api/upload |
Upload |
get_upload(upload_id) |
GET /api/upload/{uploadId} |
Upload | None |
complete_upload(upload_id, info) |
POST /api/upload/{uploadId}/complete |
None |
renew_upload_credentials(upload_id) |
POST /api/upload/{uploadId}/renewCredentials |
Any (parsed JSON) |
delete_upload(upload_id) |
DELETE /api/upload/{uploadId} |
None |
upload sends data (bytes) as application/octet-stream. Uploaded whole, it returns an Upload
whose id a blob property can refer to:
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}})For a chunked upload, pass upload_id, chunked=True, chunk_index and chunk_count with each
chunk. complete_upload takes an UploadFileInfo (file_key, file_name, mime_type).
from python_openarch_api import OpenArchDocument, OpenArchQueryResult, TermsBucket, MIXIN_CIN_REMOTE
from python_openarch_api.models import Query, CopyCommand, Model # ... any generated modelThe models are Pydantic v2. The generated ones, in python_openarch_api.models (from
python_openarch_api/models/generated.py), come from the spec's schemas. Their fields are snake
case in Python, with the spec's names as aliases (sys_primary_type / sys_primaryType). You can
build them with either name, and they are sent with the alias.
A document. It extends the generated Document.
-
The
sys_*fields of the schema:sys_id,sys_name,sys_title,sys_primary_type,sys_mixin_types,sys_path,sys_parent_id,sys_created,sys_modified,sys_gen,sys_ugen,sys_acl,sys_racl,sys_is_trashed,sysgov_is_record, ... -
The
CinRemotefields: the source-system identity of an ingested document.Field Wire name cin_idcin_idId of the document in its source system. cin_source_idcin_sourceIdId of the source system. cin_pathscin_pathsPaths of the document in the source system. cin_ingest_propertiescin_ingestPropertiesProperties from the source system. cin_ingest_property_namescin_ingestPropertyNamesTheir names. cin_readcin_readAuthorities that may read the document in the source system. cin_denycin_denyAuthorities denied reading it. -
Any other property (repository-model fields,
sysembed_*) is kept as is.
| Member | |
|---|---|
properties |
The properties not declared on the class, under their wire names: {"my_customField": 42, ...}. |
get(name, default=None) |
Any property by its wire name: doc.get("sys_primaryType"), doc.get("cin_sourceId"), doc.get("my_customField"). |
with_cin_remote() |
A copy of the document with CinRemote in sys_mixin_types. |
to_json() |
The request body: wire names, None fields left out. |
OpenArch rejects (400) a document with cin_* fields unless sys_mixinTypes includes
CinRemote (the constant MIXIN_CIN_REMOTE). with_cin_remote() adds it:
doc = OpenArchDocument(sys_primary_type="SysFile", sys_name="report.pdf",
cin_id="node-1", cin_source_id="repo-1").with_cin_remote()A query result. It extends the generated QueryResult.
| Field | |
|---|---|
documents |
list[OpenArchDocument] |
count |
Number of documents in this page. |
total_count, total_count_is_truncated |
The total, when track_total_count was set. |
limit, offset, query, repository_id, syntax |
The query as the server ran it. |
next_search_after |
The cursor for the next page, to pass back as search_after. The spec refers to it but QueryResult does not declare it. |
aggregations_buckets |
list[TermsBucket], from terms_aggregation. |
One terms-aggregation bucket: key (the term) and doc_count (wire name docCount). The spec's
StringTermsBucket has no properties, so this one is written by hand.
from python_openarch_api import (
OpenArchError, OpenArchAuthError, OpenArchNotFoundError,
OpenArchConflictError, OpenArchPreconditionError,
)Every non-2xx answer raises OpenArchError or one of its subclasses:
| Exception | Status | When |
|---|---|---|
OpenArchAuthError |
401, 403 | Missing, expired or insufficient credentials. |
OpenArchNotFoundError |
404 | No such document, path, user, ... |
OpenArchConflictError |
409 | A set_*_v2 with a stale version. |
OpenArchPreconditionError |
412 | A condition / if_ that did not match, or checkin of a non-versionable document. |
OpenArchError |
any other |
| Attribute | |
|---|---|
status_code |
The HTTP status. |
error |
The parsed RepositoryErrorResponse (status, message, exception, entity_type, stack_trace), or None when the body was not one. |
response |
The httpx.Response. |
The message has the method, the path, the status and the server's message:
try:
oa.get_document_by_path("/missing")
except OpenArchNotFoundError as e:
print(e.status_code, e.error.message if e.error else e)Network failures and timeouts are not wrapped: they raise httpx's own exceptions
(httpx.ConnectError, httpx.TimeoutException, ...).