Skip to content

Latest commit

 

History

History
454 lines (360 loc) · 22.8 KB

api.md

File metadata and controls

454 lines (360 loc) · 22.8 KB

API reference

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

from python_openarch_api import OpenArchClient, AsyncOpenArchClient

OpenArchClient (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.

Authentication

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.

Conventions

  • Method names follow the spec's operationIds in snake case. A few are shortened, for instance getDocumentById is get_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), with None fields 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 as OpenArchQueryResult, 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 as application/json-patch+json.
  • Errors: any non-2xx answer raises an OpenArchError.

Conditional and technical writes

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.

Documents

By id

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.

By path

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.

Binary content with a write

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.

Other document operations

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.

  • copy takes a CopyCommand (target_parent_id, optional name) or the dict {"targetParentId": ..., "name": ...}.
  • move takes a MoveCommand (target_parent_id) or {"targetParentId": ...}.
  • checkin of a document that is not versionable raises OpenArchPreconditionError (412).
  • usernames limits the effective ACL or permissions to those users.

Download

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.

Query

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

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)

Other queries

  • advanced_query takes an AdvancedQuery: a query with parameters, quick filter clauses and a where clause definition.
  • named_query runs a query defined on the server, by query_name, with its parameters and selected_quick_filters (a NamedQuery). list_named_queries and get_named_query read the definitions.
  • terms_aggregation takes a TermsAggregationsQuery (query, terms_aggregation_property, optional search_term and limit). The counts per term are in the result's aggregations_buckets, as TermsBuckets.
  • vector_search takes a VectorQuery (vector, optional query, embedding_type, chunk_fts, limit, ...) and returns the nearest embeddings.
  • wait_for_indexing blocks until full-text indexing has caught up, for tests and read-after-write.

Users and groups

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

Repository model

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)

Security policies

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.

Vocabularies

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.

Upload

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

Models

from python_openarch_api import OpenArchDocument, OpenArchQueryResult, TermsBucket, MIXIN_CIN_REMOTE
from python_openarch_api.models import Query, CopyCommand, Model  # ... any generated model

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

OpenArchDocument

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 CinRemote fields: the source-system identity of an ingested document.

    Field Wire name
    cin_id cin_id Id of the document in its source system.
    cin_source_id cin_sourceId Id of the source system.
    cin_paths cin_paths Paths of the document in the source system.
    cin_ingest_properties cin_ingestProperties Properties from the source system.
    cin_ingest_property_names cin_ingestPropertyNames Their names.
    cin_read cin_read Authorities that may read the document in the source system.
    cin_deny cin_deny Authorities 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()

OpenArchQueryResult

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.

TermsBucket

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.

Errors

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