python-openarch-api 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- python_openarch_api/__init__.py +29 -0
- python_openarch_api/_async/__init__.py +0 -0
- python_openarch_api/_async/client.py +660 -0
- python_openarch_api/_sync/__init__.py +0 -0
- python_openarch_api/_sync/client.py +660 -0
- python_openarch_api/_version.py +1 -0
- python_openarch_api/auth.py +48 -0
- python_openarch_api/errors.py +52 -0
- python_openarch_api/models/__init__.py +7 -0
- python_openarch_api/models/document.py +91 -0
- python_openarch_api/models/generated.py +1380 -0
- python_openarch_api/py.typed +0 -0
- python_openarch_api-0.1.0.dist-info/METADATA +260 -0
- python_openarch_api-0.1.0.dist-info/RECORD +16 -0
- python_openarch_api-0.1.0.dist-info/WHEEL +4 -0
- python_openarch_api-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
"""The OpenArch REST client (async). The sync client in ``_sync/client.py`` is generated from this
|
|
2
|
+
file by ``scripts/unasync.py`` -- edit this one and regenerate.
|
|
3
|
+
|
|
4
|
+
Every operation of ``docs/openapi/openarch-api.yaml`` has a method here, named after its operationId in
|
|
5
|
+
snake case (a few shortened: ``get_document_by_id`` -> ``get_document``). Request bodies take a
|
|
6
|
+
model or a plain dict; responses come back as models (``OpenArchDocument`` for documents).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Mapping, Sequence
|
|
12
|
+
from typing import Any, TypeVar
|
|
13
|
+
from urllib.parse import quote
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
from pydantic import BaseModel, TypeAdapter
|
|
17
|
+
|
|
18
|
+
from .._version import __version__
|
|
19
|
+
from ..auth import TokenSource, build_auth
|
|
20
|
+
from ..errors import OpenArchError
|
|
21
|
+
from ..models.document import OpenArchDocument, OpenArchQueryResult
|
|
22
|
+
from ..models.generated import (
|
|
23
|
+
AdvancedQuery,
|
|
24
|
+
CopyCommand,
|
|
25
|
+
DocumentAncestors,
|
|
26
|
+
Embedding,
|
|
27
|
+
ExternalUserRelationsSecurityPolicyRelations,
|
|
28
|
+
Group,
|
|
29
|
+
ListEffectiveACLDTO,
|
|
30
|
+
ListEffectivePermissionsDTO,
|
|
31
|
+
Model,
|
|
32
|
+
MoveCommand,
|
|
33
|
+
NamedQuery,
|
|
34
|
+
NamedQueryDefinition,
|
|
35
|
+
Query,
|
|
36
|
+
QuerySyntax,
|
|
37
|
+
SecurityPolicies,
|
|
38
|
+
TermsAggregationsQuery,
|
|
39
|
+
Upload,
|
|
40
|
+
UploadFileInfo,
|
|
41
|
+
User,
|
|
42
|
+
VectorQuery,
|
|
43
|
+
VectorSearchResult,
|
|
44
|
+
Vocabulary,
|
|
45
|
+
VocabularyEntry,
|
|
46
|
+
VocabularyEntryRequest,
|
|
47
|
+
VocabularyId,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
T = TypeVar("T")
|
|
51
|
+
|
|
52
|
+
REPOSITORY_HEADER = "HXCS-REPOSITORY"
|
|
53
|
+
# Hyland's hosted Content REST API requires a "[product]/[version]" User-Agent; override it with
|
|
54
|
+
# headers={"User-Agent": "MyProduct/1.0"}.
|
|
55
|
+
USER_AGENT = f"python-openarch-api/{__version__}"
|
|
56
|
+
JSON_PATCH = "application/json-patch+json"
|
|
57
|
+
|
|
58
|
+
Body = BaseModel | Mapping[str, Any]
|
|
59
|
+
JsonPatch = Sequence[Mapping[str, Any]]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _seg(value: str) -> str:
|
|
63
|
+
"""One path segment (an id): every reserved character escaped."""
|
|
64
|
+
return quote(str(value), safe="")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _doc_path(path: str) -> str:
|
|
68
|
+
"""A repository path in the URL: its slashes stay unescaped (OpenArch reads them as the path)."""
|
|
69
|
+
return quote(str(path).lstrip("/"), safe="/")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _body(value: Body | None) -> Any:
|
|
73
|
+
if isinstance(value, BaseModel):
|
|
74
|
+
return value.model_dump(mode="json", by_alias=True, exclude_none=True)
|
|
75
|
+
return value
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _params(**kw: Any) -> dict[str, Any]:
|
|
79
|
+
return {k: (str(v).lower() if isinstance(v, bool) else v) for k, v in kw.items() if v is not None}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _write_params(
|
|
83
|
+
condition: str | None = None, if_: str | None = None, upsert: bool | None = None,
|
|
84
|
+
user_change: bool | None = None,
|
|
85
|
+
) -> dict[str, Any]:
|
|
86
|
+
"""The query parameters of a document write. ``condition`` (an HXQL predicate) and ``if_`` (an
|
|
87
|
+
expression, e.g. ``content.sys_title == "foo"``) are mutually exclusive; ``user_change=False``
|
|
88
|
+
marks a technical update (``sys_ugen`` is not incremented)."""
|
|
89
|
+
return _params(condition=condition, upsert=upsert, userChange=user_change, **{"if": if_})
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class AsyncOpenArchClient:
|
|
93
|
+
"""Async client for the OpenArch (Hyland repository) REST API.
|
|
94
|
+
|
|
95
|
+
Authenticate with ``username`` + ``password`` (Basic, a local OpenArch), a bearer ``token``
|
|
96
|
+
(a string, or a callable returning the current token), or any ``httpx.Auth``. ``repository``
|
|
97
|
+
is sent as the ``HXCS-REPOSITORY`` header; the per-call ``repository`` argument overrides it.
|
|
98
|
+
``http_client`` replaces the client built here; ``transport`` plugs into it (proxies, tests).
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
def __init__(
|
|
102
|
+
self,
|
|
103
|
+
base_url: str = "http://localhost:8080",
|
|
104
|
+
*,
|
|
105
|
+
repository: str | None = "default",
|
|
106
|
+
username: str | None = None,
|
|
107
|
+
password: str | None = None,
|
|
108
|
+
token: TokenSource | None = None,
|
|
109
|
+
auth: httpx.Auth | None = None,
|
|
110
|
+
timeout: float | httpx.Timeout = 30.0,
|
|
111
|
+
http_client: httpx.AsyncClient | None = None,
|
|
112
|
+
transport: httpx.AsyncBaseTransport | None = None,
|
|
113
|
+
headers: Mapping[str, str] | None = None,
|
|
114
|
+
):
|
|
115
|
+
self.repository = repository
|
|
116
|
+
self._owns_client = http_client is None
|
|
117
|
+
self._http = http_client or httpx.AsyncClient(
|
|
118
|
+
base_url=base_url.rstrip("/"),
|
|
119
|
+
auth=build_auth(username=username, password=password, token=token, auth=auth),
|
|
120
|
+
timeout=timeout,
|
|
121
|
+
transport=transport,
|
|
122
|
+
headers={"Accept": "application/json", "User-Agent": USER_AGENT, **dict(headers or {})},
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
async def __aenter__(self) -> AsyncOpenArchClient:
|
|
126
|
+
return self
|
|
127
|
+
|
|
128
|
+
async def __aexit__(self, *exc: object) -> None:
|
|
129
|
+
await self.aclose()
|
|
130
|
+
|
|
131
|
+
async def aclose(self) -> None:
|
|
132
|
+
if self._owns_client:
|
|
133
|
+
await self._http.aclose()
|
|
134
|
+
|
|
135
|
+
# ── plumbing ──────────────────────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
async def _send(
|
|
138
|
+
self,
|
|
139
|
+
method: str,
|
|
140
|
+
url: str,
|
|
141
|
+
*,
|
|
142
|
+
params: Mapping[str, Any] | None = None,
|
|
143
|
+
json: Any = None,
|
|
144
|
+
content: Any = None,
|
|
145
|
+
files: Any = None,
|
|
146
|
+
data: Any = None,
|
|
147
|
+
content_type: str | None = None,
|
|
148
|
+
repository: str | None = None,
|
|
149
|
+
accept: str | None = None,
|
|
150
|
+
) -> httpx.Response:
|
|
151
|
+
headers: dict[str, str] = {}
|
|
152
|
+
repo = repository if repository is not None else self.repository
|
|
153
|
+
if repo:
|
|
154
|
+
headers[REPOSITORY_HEADER] = repo
|
|
155
|
+
if content_type:
|
|
156
|
+
headers["Content-Type"] = content_type
|
|
157
|
+
if accept:
|
|
158
|
+
headers["Accept"] = accept
|
|
159
|
+
if json is not None and content_type:
|
|
160
|
+
import json as _json
|
|
161
|
+
|
|
162
|
+
content, json = _json.dumps(json), None
|
|
163
|
+
response = await self._http.request(
|
|
164
|
+
method, url, params=params, json=json, content=content, files=files, data=data, headers=headers
|
|
165
|
+
)
|
|
166
|
+
if response.is_error:
|
|
167
|
+
await response.aread()
|
|
168
|
+
raise OpenArchError.from_response(response)
|
|
169
|
+
return response
|
|
170
|
+
|
|
171
|
+
@staticmethod
|
|
172
|
+
def _parse(response: httpx.Response, model: type[T]) -> T:
|
|
173
|
+
return TypeAdapter(model).validate_python(response.json())
|
|
174
|
+
|
|
175
|
+
@staticmethod
|
|
176
|
+
def _json(response: httpx.Response) -> Any:
|
|
177
|
+
return response.json() if response.content else None
|
|
178
|
+
|
|
179
|
+
# ── Documents: by id ──────────────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
async def get_document(self, doc_id: str, *, repository: str | None = None) -> OpenArchDocument:
|
|
182
|
+
r = await self._send("GET", f"/api/documents/{_seg(doc_id)}", repository=repository)
|
|
183
|
+
return self._parse(r, OpenArchDocument)
|
|
184
|
+
|
|
185
|
+
async def create_document(
|
|
186
|
+
self,
|
|
187
|
+
parent_id: str,
|
|
188
|
+
document: Body,
|
|
189
|
+
*,
|
|
190
|
+
enforce_sys_name: bool | None = None,
|
|
191
|
+
files: Mapping[str, Any] | None = None,
|
|
192
|
+
repository: str | None = None,
|
|
193
|
+
) -> OpenArchDocument:
|
|
194
|
+
"""Create ``document`` under the folder ``parent_id``.
|
|
195
|
+
|
|
196
|
+
``files`` (part name -> an httpx file value) sends binary content in the same request: the
|
|
197
|
+
part name is free, and a blob property of the document points at it, e.g.
|
|
198
|
+
``{"sysfile_blob": {"uploadId": "f1"}}`` with ``files={"f1": ("a.txt", data, "text/plain")}``.
|
|
199
|
+
The id of an earlier :meth:`upload` works the same way, without ``files``."""
|
|
200
|
+
return await self._write_document(
|
|
201
|
+
"POST", f"/api/documents/{_seg(parent_id)}", document, files,
|
|
202
|
+
_params(enforceSysName=enforce_sys_name), repository,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
async def update_document(
|
|
206
|
+
self,
|
|
207
|
+
doc_id: str,
|
|
208
|
+
document: Body,
|
|
209
|
+
*,
|
|
210
|
+
condition: str | None = None,
|
|
211
|
+
if_: str | None = None,
|
|
212
|
+
upsert: bool | None = None,
|
|
213
|
+
user_change: bool | None = None,
|
|
214
|
+
files: Mapping[str, Any] | None = None,
|
|
215
|
+
repository: str | None = None,
|
|
216
|
+
) -> OpenArchDocument:
|
|
217
|
+
"""Replace the document's properties. ``condition`` / ``if_``: a predicate it must match;
|
|
218
|
+
``upsert=True`` creates it when it does not exist (HTTP 201)."""
|
|
219
|
+
return await self._write_document(
|
|
220
|
+
"PUT", f"/api/documents/{_seg(doc_id)}", document, files,
|
|
221
|
+
_write_params(condition, if_, upsert, user_change), repository,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
async def patch_document(
|
|
225
|
+
self, doc_id: str, patch: JsonPatch, *, condition: str | None = None, if_: str | None = None,
|
|
226
|
+
user_change: bool | None = None, repository: str | None = None,
|
|
227
|
+
) -> OpenArchDocument:
|
|
228
|
+
"""Apply a JSON Patch (``[{"op": "replace", "path": "/sys_title", "value": "x"}]``)."""
|
|
229
|
+
r = await self._send(
|
|
230
|
+
"PATCH", f"/api/documents/{_seg(doc_id)}", json=list(patch), content_type=JSON_PATCH,
|
|
231
|
+
params=_write_params(condition, if_, None, user_change), repository=repository,
|
|
232
|
+
)
|
|
233
|
+
return self._parse(r, OpenArchDocument)
|
|
234
|
+
|
|
235
|
+
async def delete_document(self, doc_id: str, *, repository: str | None = None) -> None:
|
|
236
|
+
await self._send("DELETE", f"/api/documents/{_seg(doc_id)}", repository=repository)
|
|
237
|
+
|
|
238
|
+
# ── Documents: by path ────────────────────────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
async def get_document_by_path(self, path: str, *, repository: str | None = None) -> OpenArchDocument:
|
|
241
|
+
r = await self._send("GET", f"/api/documents/path/{_doc_path(path)}", repository=repository)
|
|
242
|
+
return self._parse(r, OpenArchDocument)
|
|
243
|
+
|
|
244
|
+
async def create_document_by_path(
|
|
245
|
+
self,
|
|
246
|
+
parent_path: str,
|
|
247
|
+
document: Body,
|
|
248
|
+
*,
|
|
249
|
+
enforce_sys_name: bool | None = None,
|
|
250
|
+
files: Mapping[str, Any] | None = None,
|
|
251
|
+
repository: str | None = None,
|
|
252
|
+
) -> OpenArchDocument:
|
|
253
|
+
return await self._write_document(
|
|
254
|
+
"POST", f"/api/documents/path/{_doc_path(parent_path)}", document, files,
|
|
255
|
+
_params(enforceSysName=enforce_sys_name), repository,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
async def update_document_by_path(
|
|
259
|
+
self,
|
|
260
|
+
path: str,
|
|
261
|
+
document: Body,
|
|
262
|
+
*,
|
|
263
|
+
condition: str | None = None,
|
|
264
|
+
if_: str | None = None,
|
|
265
|
+
upsert: bool | None = None,
|
|
266
|
+
user_change: bool | None = None,
|
|
267
|
+
files: Mapping[str, Any] | None = None,
|
|
268
|
+
repository: str | None = None,
|
|
269
|
+
) -> OpenArchDocument:
|
|
270
|
+
return await self._write_document(
|
|
271
|
+
"PUT", f"/api/documents/path/{_doc_path(path)}", document, files,
|
|
272
|
+
_write_params(condition, if_, upsert, user_change), repository,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
async def patch_document_by_path(
|
|
276
|
+
self, path: str, patch: JsonPatch, *, condition: str | None = None, if_: str | None = None,
|
|
277
|
+
user_change: bool | None = None, repository: str | None = None,
|
|
278
|
+
) -> OpenArchDocument:
|
|
279
|
+
r = await self._send(
|
|
280
|
+
"PATCH", f"/api/documents/path/{_doc_path(path)}", json=list(patch), content_type=JSON_PATCH,
|
|
281
|
+
params=_write_params(condition, if_, None, user_change), repository=repository,
|
|
282
|
+
)
|
|
283
|
+
return self._parse(r, OpenArchDocument)
|
|
284
|
+
|
|
285
|
+
async def delete_document_by_path(self, path: str, *, repository: str | None = None) -> None:
|
|
286
|
+
await self._send("DELETE", f"/api/documents/path/{_doc_path(path)}", repository=repository)
|
|
287
|
+
|
|
288
|
+
async def exists_by_path(self, path: str, *, repository: str | None = None) -> bool:
|
|
289
|
+
from ..errors import OpenArchNotFoundError
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
await self.get_document_by_path(path, repository=repository)
|
|
293
|
+
return True
|
|
294
|
+
except OpenArchNotFoundError:
|
|
295
|
+
return False
|
|
296
|
+
|
|
297
|
+
async def _write_document(
|
|
298
|
+
self, method: str, url: str, document: Body, files: Mapping[str, Any] | None,
|
|
299
|
+
params: dict[str, Any], repository: str | None,
|
|
300
|
+
) -> OpenArchDocument:
|
|
301
|
+
if files:
|
|
302
|
+
import json as _json
|
|
303
|
+
|
|
304
|
+
parts = {"main": (None, _json.dumps(_body(document)), "application/json"), **dict(files)}
|
|
305
|
+
r = await self._send(method, url, files=parts, params=params, repository=repository)
|
|
306
|
+
else:
|
|
307
|
+
r = await self._send(method, url, json=_body(document), params=params, repository=repository)
|
|
308
|
+
return self._parse(r, OpenArchDocument)
|
|
309
|
+
|
|
310
|
+
# ── Documents: other operations ───────────────────────────────────────────────────────
|
|
311
|
+
|
|
312
|
+
async def get_ancestors(self, doc_id: str, *, repository: str | None = None) -> DocumentAncestors:
|
|
313
|
+
r = await self._send("GET", f"/api/documents/{_seg(doc_id)}/ancestors", repository=repository)
|
|
314
|
+
return self._parse(r, DocumentAncestors)
|
|
315
|
+
|
|
316
|
+
async def checkin(
|
|
317
|
+
self, doc_id: str, *, minor: bool | None = None, user_change: bool | None = None,
|
|
318
|
+
repository: str | None = None,
|
|
319
|
+
) -> OpenArchDocument:
|
|
320
|
+
r = await self._send(
|
|
321
|
+
"POST", f"/api/documents/{_seg(doc_id)}/checkin", params=_params(minor=minor, userChange=user_change),
|
|
322
|
+
repository=repository,
|
|
323
|
+
)
|
|
324
|
+
return self._parse(r, OpenArchDocument)
|
|
325
|
+
|
|
326
|
+
async def copy(self, doc_id: str, command: CopyCommand | Body, *, repository: str | None = None) -> OpenArchDocument:
|
|
327
|
+
r = await self._send("POST", f"/api/documents/{_seg(doc_id)}/copy", json=_body(command), repository=repository)
|
|
328
|
+
return self._parse(r, OpenArchDocument)
|
|
329
|
+
|
|
330
|
+
async def move(
|
|
331
|
+
self, doc_id: str, command: MoveCommand | Body, *, condition: str | None = None, if_: str | None = None,
|
|
332
|
+
user_change: bool | None = None, repository: str | None = None,
|
|
333
|
+
) -> OpenArchDocument:
|
|
334
|
+
r = await self._send(
|
|
335
|
+
"POST", f"/api/documents/{_seg(doc_id)}/move", json=_body(command),
|
|
336
|
+
params=_write_params(condition, if_, None, user_change), repository=repository,
|
|
337
|
+
)
|
|
338
|
+
return self._parse(r, OpenArchDocument)
|
|
339
|
+
|
|
340
|
+
async def restore_version(
|
|
341
|
+
self, version_id: str, *, user_change: bool | None = None, repository: str | None = None
|
|
342
|
+
) -> OpenArchDocument:
|
|
343
|
+
r = await self._send(
|
|
344
|
+
"POST", f"/api/documents/{_seg(version_id)}/restore", params=_params(userChange=user_change),
|
|
345
|
+
repository=repository,
|
|
346
|
+
)
|
|
347
|
+
return self._parse(r, OpenArchDocument)
|
|
348
|
+
|
|
349
|
+
async def get_embedding(self, doc_id: str, embedding_id: str, *, repository: str | None = None) -> Embedding:
|
|
350
|
+
r = await self._send(
|
|
351
|
+
"GET", f"/api/documents/{_seg(doc_id)}/embedding/{_seg(embedding_id)}", repository=repository
|
|
352
|
+
)
|
|
353
|
+
return self._parse(r, Embedding)
|
|
354
|
+
|
|
355
|
+
async def get_full_effective_acl(
|
|
356
|
+
self, doc_id: str, usernames: Sequence[str] | None = None, *, repository: str | None = None
|
|
357
|
+
) -> ListEffectiveACLDTO:
|
|
358
|
+
r = await self._send(
|
|
359
|
+
"GET", f"/api/documents/{_seg(doc_id)}/fulleffectiveacl",
|
|
360
|
+
params=_params(usernames=list(usernames) if usernames else None), repository=repository,
|
|
361
|
+
)
|
|
362
|
+
return self._parse(r, ListEffectiveACLDTO)
|
|
363
|
+
|
|
364
|
+
async def get_full_effective_permissions(
|
|
365
|
+
self, doc_id: str, usernames: Sequence[str] | None = None, *, repository: str | None = None
|
|
366
|
+
) -> ListEffectivePermissionsDTO:
|
|
367
|
+
r = await self._send(
|
|
368
|
+
"GET", f"/api/documents/{_seg(doc_id)}/fulleffectivepermissions",
|
|
369
|
+
params=_params(usernames=list(usernames) if usernames else None), repository=repository,
|
|
370
|
+
)
|
|
371
|
+
return self._parse(r, ListEffectivePermissionsDTO)
|
|
372
|
+
|
|
373
|
+
# ── Download ──────────────────────────────────────────────────────────────────────────
|
|
374
|
+
|
|
375
|
+
async def download(
|
|
376
|
+
self, doc_id: str, property_xpath_and_filename: str, *, inline: bool | None = None,
|
|
377
|
+
repository: str | None = None,
|
|
378
|
+
) -> bytes:
|
|
379
|
+
"""The blob at ``property_xpath_and_filename`` (e.g. ``file:content/report.pdf``)."""
|
|
380
|
+
r = await self._send(
|
|
381
|
+
"GET", f"/api/download/{_seg(doc_id)}/{_doc_path(property_xpath_and_filename)}",
|
|
382
|
+
params=_params(inline=inline), repository=repository, accept="*/*",
|
|
383
|
+
)
|
|
384
|
+
return r.content
|
|
385
|
+
|
|
386
|
+
async def download_info(
|
|
387
|
+
self, doc_id: str, property_xpath_and_filename: str, *, repository: str | None = None
|
|
388
|
+
) -> httpx.Headers:
|
|
389
|
+
r = await self._send(
|
|
390
|
+
"HEAD", f"/api/download/{_seg(doc_id)}/{_doc_path(property_xpath_and_filename)}",
|
|
391
|
+
repository=repository, accept="*/*",
|
|
392
|
+
)
|
|
393
|
+
return r.headers
|
|
394
|
+
|
|
395
|
+
async def download_url(
|
|
396
|
+
self, doc_id: str, property_xpath_and_filename: str, *, expiration: int | None = None,
|
|
397
|
+
inline: bool | None = None, repository: str | None = None,
|
|
398
|
+
) -> str:
|
|
399
|
+
"""A pre-signed URL for the blob (``expiration`` in seconds)."""
|
|
400
|
+
r = await self._send(
|
|
401
|
+
"GET", f"/api/download/url/{_seg(doc_id)}/{_doc_path(property_xpath_and_filename)}",
|
|
402
|
+
params=_params(expiration=expiration, inline=inline), repository=repository, accept="*/*",
|
|
403
|
+
)
|
|
404
|
+
return r.text
|
|
405
|
+
|
|
406
|
+
# ── Query ─────────────────────────────────────────────────────────────────────────────
|
|
407
|
+
|
|
408
|
+
async def query(
|
|
409
|
+
self,
|
|
410
|
+
query: str | Query | Body,
|
|
411
|
+
*,
|
|
412
|
+
limit: int | None = None,
|
|
413
|
+
offset: int | None = None,
|
|
414
|
+
sort: Sequence[str] | None = None,
|
|
415
|
+
track_total_count: bool | None = None,
|
|
416
|
+
syntax: QuerySyntax | str | None = None,
|
|
417
|
+
search_after: str | None = None,
|
|
418
|
+
sync: bool | None = None,
|
|
419
|
+
) -> OpenArchQueryResult:
|
|
420
|
+
"""Run an HXQL query (``"SELECT * FROM SysContent WHERE ..."``) or a ``Query``.
|
|
421
|
+
|
|
422
|
+
``syntax="expr"`` for an expression query; ``search_after`` is the ``next_search_after`` of
|
|
423
|
+
the previous page (``offset`` is then ignored). ``limit`` defaults to the server's 10."""
|
|
424
|
+
if isinstance(query, str):
|
|
425
|
+
fields = dict(
|
|
426
|
+
limit=limit, offset=offset, sort=list(sort) if sort else None, track_total_count=track_total_count,
|
|
427
|
+
syntax=QuerySyntax(syntax) if syntax else None, search_after=search_after,
|
|
428
|
+
)
|
|
429
|
+
query = Query(
|
|
430
|
+
query=query,
|
|
431
|
+
repository_id=self.repository or "default",
|
|
432
|
+
**{k: v for k, v in fields.items() if v is not None},
|
|
433
|
+
)
|
|
434
|
+
r = await self._send("POST", "/api/query", json=_body(query), params=_params(sync=sync))
|
|
435
|
+
return self._parse(r, OpenArchQueryResult)
|
|
436
|
+
|
|
437
|
+
async def advanced_query(self, query: AdvancedQuery | Body, *, sync: bool | None = None) -> OpenArchQueryResult:
|
|
438
|
+
r = await self._send("POST", "/api/query/advanced", json=_body(query), params=_params(sync=sync))
|
|
439
|
+
return self._parse(r, OpenArchQueryResult)
|
|
440
|
+
|
|
441
|
+
async def named_query(self, query: NamedQuery | Body, *, sync: bool | None = None) -> OpenArchQueryResult:
|
|
442
|
+
r = await self._send("POST", "/api/query/named", json=_body(query), params=_params(sync=sync))
|
|
443
|
+
return self._parse(r, OpenArchQueryResult)
|
|
444
|
+
|
|
445
|
+
async def list_named_queries(self) -> Any:
|
|
446
|
+
return self._json(await self._send("GET", "/api/query/named"))
|
|
447
|
+
|
|
448
|
+
async def get_named_query(self, name: str) -> NamedQueryDefinition:
|
|
449
|
+
r = await self._send("GET", f"/api/query/named/{_seg(name)}")
|
|
450
|
+
return self._parse(r, NamedQueryDefinition)
|
|
451
|
+
|
|
452
|
+
async def terms_aggregation(self, query: TermsAggregationsQuery | Body) -> OpenArchQueryResult:
|
|
453
|
+
"""Counts per term, in the result's ``aggregations_buckets``."""
|
|
454
|
+
r = await self._send("POST", "/api/query/termsAggregation", json=_body(query))
|
|
455
|
+
return self._parse(r, OpenArchQueryResult)
|
|
456
|
+
|
|
457
|
+
async def vector_search(self, query: VectorQuery | Body) -> VectorSearchResult:
|
|
458
|
+
"""Nearest embeddings to a vector (``/api/query/embeddings``)."""
|
|
459
|
+
r = await self._send("POST", "/api/query/embeddings", json=_body(query))
|
|
460
|
+
return self._parse(r, VectorSearchResult)
|
|
461
|
+
|
|
462
|
+
async def wait_for_indexing(
|
|
463
|
+
self, *, refresh: bool | None = None, timeout: int | None = None, wait_for_audit: bool | None = None,
|
|
464
|
+
repository: str | None = None,
|
|
465
|
+
) -> Any:
|
|
466
|
+
"""Block until full-text indexing has caught up (tests, read-after-write)."""
|
|
467
|
+
return self._json(await self._send(
|
|
468
|
+
"GET", "/api/query/waitForFullTextSearchIndexing",
|
|
469
|
+
params=_params(refresh=refresh, timeout=timeout, waitForAudit=wait_for_audit), repository=repository,
|
|
470
|
+
))
|
|
471
|
+
|
|
472
|
+
# ── Users, groups ─────────────────────────────────────────────────────────────────────
|
|
473
|
+
|
|
474
|
+
async def search_users(self, name: str) -> list[User]:
|
|
475
|
+
r = await self._send("GET", "/api/user", params={"name": name})
|
|
476
|
+
return self._parse(r, list[User])
|
|
477
|
+
|
|
478
|
+
async def get_user(self, user_id: str) -> User:
|
|
479
|
+
return self._parse(await self._send("GET", f"/api/user/{_seg(user_id)}"), User)
|
|
480
|
+
|
|
481
|
+
async def search_groups(self, name: str) -> list[Group]:
|
|
482
|
+
r = await self._send("GET", "/api/group", params={"name": name})
|
|
483
|
+
return self._parse(r, list[Group])
|
|
484
|
+
|
|
485
|
+
async def get_group(self, group_id: str) -> Group:
|
|
486
|
+
return self._parse(await self._send("GET", f"/api/group/{_seg(group_id)}"), Group)
|
|
487
|
+
|
|
488
|
+
async def hello(self, *, protected: bool = False) -> str:
|
|
489
|
+
"""Connectivity check; ``protected=True`` also checks the credentials."""
|
|
490
|
+
r = await self._send("GET", "/hello/protected" if protected else "/hello", accept="text/plain")
|
|
491
|
+
return r.text
|
|
492
|
+
|
|
493
|
+
# ── Repository model, security policies ───────────────────────────────────────────────
|
|
494
|
+
#
|
|
495
|
+
# The v1 methods send and return the model / policies alone. The ``_v2`` methods carry a
|
|
496
|
+
# ``version`` for optimistic locking: read it with get_*_v2 and send it back with set_*_v2.
|
|
497
|
+
|
|
498
|
+
async def get_model(self, *, project_id: str | None = None, repository: str | None = None) -> Model:
|
|
499
|
+
url = "/api/repository/model" + (f"/project/{_seg(project_id)}" if project_id else "")
|
|
500
|
+
return self._parse(await self._send("GET", url, repository=repository), Model)
|
|
501
|
+
|
|
502
|
+
async def set_model(
|
|
503
|
+
self, model: Model | Body, *, project_id: str | None = None, validate_only: bool | None = None,
|
|
504
|
+
repository: str | None = None,
|
|
505
|
+
) -> Model | None:
|
|
506
|
+
url = "/api/repository/model" + (f"/project/{_seg(project_id)}" if project_id else "")
|
|
507
|
+
r = await self._send("PUT", url, json=_body(model), params=_params(validateOnly=validate_only), repository=repository)
|
|
508
|
+
return self._parse(r, Model) if r.content else None
|
|
509
|
+
|
|
510
|
+
async def patch_model(
|
|
511
|
+
self, patch: JsonPatch, *, validate_only: bool | None = None, repository: str | None = None
|
|
512
|
+
) -> Model | None:
|
|
513
|
+
r = await self._send(
|
|
514
|
+
"PATCH", "/api/repository/model", json=list(patch), content_type=JSON_PATCH,
|
|
515
|
+
params=_params(validateOnly=validate_only), repository=repository,
|
|
516
|
+
)
|
|
517
|
+
return self._parse(r, Model) if r.content else None
|
|
518
|
+
|
|
519
|
+
async def get_model_v2(self, *, project_id: str | None = None, repository: str | None = None) -> Model:
|
|
520
|
+
url = "/api/v2/repository/model" + (f"/project/{_seg(project_id)}" if project_id else "")
|
|
521
|
+
return self._parse(await self._send("GET", url, repository=repository), Model)
|
|
522
|
+
|
|
523
|
+
async def set_model_v2(
|
|
524
|
+
self, model: Model | Body, *, project_id: str | None = None, validate_only: bool | None = None,
|
|
525
|
+
repository: str | None = None,
|
|
526
|
+
) -> Model | None:
|
|
527
|
+
url = "/api/v2/repository/model" + (f"/project/{_seg(project_id)}" if project_id else "")
|
|
528
|
+
r = await self._send("PUT", url, json=_body(model), params=_params(validateOnly=validate_only), repository=repository)
|
|
529
|
+
return self._parse(r, Model) if r.content else None
|
|
530
|
+
|
|
531
|
+
async def get_security_policies(self, *, repository: str | None = None) -> Any:
|
|
532
|
+
return self._json(await self._send("GET", "/api/repository/security/policies", repository=repository))
|
|
533
|
+
|
|
534
|
+
async def set_security_policies(
|
|
535
|
+
self, policies: Body, *, project_id: str | None = None, validate_only: bool | None = None,
|
|
536
|
+
repository: str | None = None,
|
|
537
|
+
) -> Any:
|
|
538
|
+
url = "/api/repository/security/policies" + (f"/project/{_seg(project_id)}" if project_id else "")
|
|
539
|
+
return self._json(await self._send(
|
|
540
|
+
"PUT", url, json=_body(policies), params=_params(validateOnly=validate_only), repository=repository
|
|
541
|
+
))
|
|
542
|
+
|
|
543
|
+
async def patch_security_policies(
|
|
544
|
+
self, patch: JsonPatch, *, validate_only: bool | None = None, repository: str | None = None
|
|
545
|
+
) -> Any:
|
|
546
|
+
return self._json(await self._send(
|
|
547
|
+
"PATCH", "/api/repository/security/policies", json=list(patch), content_type=JSON_PATCH,
|
|
548
|
+
params=_params(validateOnly=validate_only), repository=repository,
|
|
549
|
+
))
|
|
550
|
+
|
|
551
|
+
async def get_security_policies_v2(self, *, repository: str | None = None) -> SecurityPolicies:
|
|
552
|
+
r = await self._send("GET", "/api/v2/repository/security/policies", repository=repository)
|
|
553
|
+
return self._parse(r, SecurityPolicies)
|
|
554
|
+
|
|
555
|
+
async def set_security_policies_v2(
|
|
556
|
+
self, policies: SecurityPolicies | Body, *, project_id: str | None = None,
|
|
557
|
+
validate_only: bool | None = None, repository: str | None = None,
|
|
558
|
+
) -> SecurityPolicies | None:
|
|
559
|
+
url = "/api/v2/repository/security/policies" + (f"/project/{_seg(project_id)}" if project_id else "")
|
|
560
|
+
r = await self._send("PUT", url, json=_body(policies), params=_params(validateOnly=validate_only), repository=repository)
|
|
561
|
+
return self._parse(r, SecurityPolicies) if r.content else None
|
|
562
|
+
|
|
563
|
+
async def get_external_user_relations(
|
|
564
|
+
self, policy_name: str, *, repository: str | None = None
|
|
565
|
+
) -> ExternalUserRelationsSecurityPolicyRelations:
|
|
566
|
+
r = await self._send(
|
|
567
|
+
"GET", f"/api/repository/security/policies/{_seg(policy_name)}/externaluserrelations", repository=repository
|
|
568
|
+
)
|
|
569
|
+
return self._parse(r, ExternalUserRelationsSecurityPolicyRelations)
|
|
570
|
+
|
|
571
|
+
async def set_external_user_relations(
|
|
572
|
+
self, policy_name: str, relations: ExternalUserRelationsSecurityPolicyRelations | Body, *,
|
|
573
|
+
repository: str | None = None,
|
|
574
|
+
) -> None:
|
|
575
|
+
await self._send(
|
|
576
|
+
"PUT", f"/api/repository/security/policies/{_seg(policy_name)}/externaluserrelations",
|
|
577
|
+
json=_body(relations), repository=repository,
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
async def get_external_user_relations_status(self, policy_name: str, *, repository: str | None = None) -> Any:
|
|
581
|
+
return self._json(await self._send(
|
|
582
|
+
"GET", f"/api/repository/security/policies/{_seg(policy_name)}/externaluserrelations/status",
|
|
583
|
+
repository=repository,
|
|
584
|
+
))
|
|
585
|
+
|
|
586
|
+
# ── Vocabularies ──────────────────────────────────────────────────────────────────────
|
|
587
|
+
|
|
588
|
+
async def list_vocabularies(self, *, repository: str | None = None) -> list[VocabularyId]:
|
|
589
|
+
return self._parse(await self._send("GET", "/api/vocabularies", repository=repository), list[VocabularyId])
|
|
590
|
+
|
|
591
|
+
async def create_vocabulary(self, vocabulary: Vocabulary | Body, *, repository: str | None = None) -> Vocabulary:
|
|
592
|
+
r = await self._send("POST", "/api/vocabularies", json=_body(vocabulary), repository=repository)
|
|
593
|
+
return self._parse(r, Vocabulary)
|
|
594
|
+
|
|
595
|
+
async def delete_vocabulary(self, vocabulary_id: str, *, repository: str | None = None) -> None:
|
|
596
|
+
await self._send("DELETE", f"/api/vocabularies/{_seg(vocabulary_id)}", repository=repository)
|
|
597
|
+
|
|
598
|
+
async def get_vocabulary_entries(self, vocabulary_id: str, *, repository: str | None = None) -> list[VocabularyEntry]:
|
|
599
|
+
r = await self._send("GET", f"/api/vocabularies/{_seg(vocabulary_id)}", repository=repository)
|
|
600
|
+
return self._parse(r, list[VocabularyEntry])
|
|
601
|
+
|
|
602
|
+
async def create_vocabulary_entry(
|
|
603
|
+
self, vocabulary_id: str, entry: VocabularyEntryRequest | Body, *, repository: str | None = None
|
|
604
|
+
) -> VocabularyEntry:
|
|
605
|
+
r = await self._send("POST", f"/api/vocabularies/{_seg(vocabulary_id)}", json=_body(entry), repository=repository)
|
|
606
|
+
return self._parse(r, VocabularyEntry)
|
|
607
|
+
|
|
608
|
+
async def update_vocabulary_entry(
|
|
609
|
+
self, vocabulary_id: str, entry: VocabularyEntryRequest | Body, *, repository: str | None = None
|
|
610
|
+
) -> VocabularyEntry:
|
|
611
|
+
r = await self._send("PUT", f"/api/vocabularies/{_seg(vocabulary_id)}", json=_body(entry), repository=repository)
|
|
612
|
+
return self._parse(r, VocabularyEntry)
|
|
613
|
+
|
|
614
|
+
async def get_vocabulary_entry(self, vocabulary_id: str, key: str, *, repository: str | None = None) -> VocabularyEntry:
|
|
615
|
+
r = await self._send("GET", f"/api/vocabularies/{_seg(vocabulary_id)}/{_seg(key)}", repository=repository)
|
|
616
|
+
return self._parse(r, VocabularyEntry)
|
|
617
|
+
|
|
618
|
+
async def delete_vocabulary_entry(self, vocabulary_id: str, key: str, *, repository: str | None = None) -> None:
|
|
619
|
+
await self._send("DELETE", f"/api/vocabularies/{_seg(vocabulary_id)}/{_seg(key)}", repository=repository)
|
|
620
|
+
|
|
621
|
+
# ── Upload ────────────────────────────────────────────────────────────────────────────
|
|
622
|
+
|
|
623
|
+
async def create_upload(self) -> Upload:
|
|
624
|
+
return self._parse(await self._send("POST", "/api/upload/create"), Upload)
|
|
625
|
+
|
|
626
|
+
async def upload(
|
|
627
|
+
self,
|
|
628
|
+
data: bytes,
|
|
629
|
+
*,
|
|
630
|
+
file_name: str | None = None,
|
|
631
|
+
mime_type: str | None = None,
|
|
632
|
+
upload_id: str | None = None,
|
|
633
|
+
chunked: bool | None = None,
|
|
634
|
+
chunk_index: int | None = None,
|
|
635
|
+
chunk_count: int | None = None,
|
|
636
|
+
encoding: str | None = None,
|
|
637
|
+
body_encoding: str | None = None,
|
|
638
|
+
) -> Upload:
|
|
639
|
+
"""Upload a binary (whole, or one chunk of ``upload_id``)."""
|
|
640
|
+
r = await self._send(
|
|
641
|
+
"POST", "/api/upload", content=data, content_type="application/octet-stream",
|
|
642
|
+
params=_params(
|
|
643
|
+
fileName=file_name, mimeType=mime_type, id=upload_id, chunked=chunked, chunkIndex=chunk_index,
|
|
644
|
+
chunkCount=chunk_count, encoding=encoding, bodyEncoding=body_encoding,
|
|
645
|
+
),
|
|
646
|
+
)
|
|
647
|
+
return self._parse(r, Upload)
|
|
648
|
+
|
|
649
|
+
async def get_upload(self, upload_id: str) -> Upload | None:
|
|
650
|
+
r = await self._send("GET", f"/api/upload/{_seg(upload_id)}")
|
|
651
|
+
return self._parse(r, Upload) if r.content else None
|
|
652
|
+
|
|
653
|
+
async def complete_upload(self, upload_id: str, info: UploadFileInfo | Body) -> None:
|
|
654
|
+
await self._send("POST", f"/api/upload/{_seg(upload_id)}/complete", json=_body(info))
|
|
655
|
+
|
|
656
|
+
async def renew_upload_credentials(self, upload_id: str) -> Any:
|
|
657
|
+
return self._json(await self._send("POST", f"/api/upload/{_seg(upload_id)}/renewCredentials"))
|
|
658
|
+
|
|
659
|
+
async def delete_upload(self, upload_id: str) -> None:
|
|
660
|
+
await self._send("DELETE", f"/api/upload/{_seg(upload_id)}")
|
|
File without changes
|