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