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 (sync). GENERATED from ``_async/client.py`` by ``scripts/unasync.py``
2
+ -- do not edit; change the async client 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 OpenArchClient:
93
+ """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.Client | None = None,
112
+ transport: httpx.BaseTransport | 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.Client(
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
+ def __enter__(self) -> OpenArchClient:
126
+ return self
127
+
128
+ def __exit__(self, *exc: object) -> None:
129
+ self.close()
130
+
131
+ def close(self) -> None:
132
+ if self._owns_client:
133
+ self._http.close()
134
+
135
+ # ── plumbing ──────────────────────────────────────────────────────────────────────────
136
+
137
+ 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 = 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
+ response.read()
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
+ def get_document(self, doc_id: str, *, repository: str | None = None) -> OpenArchDocument:
182
+ r = self._send("GET", f"/api/documents/{_seg(doc_id)}", repository=repository)
183
+ return self._parse(r, OpenArchDocument)
184
+
185
+ 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 self._write_document(
201
+ "POST", f"/api/documents/{_seg(parent_id)}", document, files,
202
+ _params(enforceSysName=enforce_sys_name), repository,
203
+ )
204
+
205
+ 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 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
+ 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 = 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
+ def delete_document(self, doc_id: str, *, repository: str | None = None) -> None:
236
+ self._send("DELETE", f"/api/documents/{_seg(doc_id)}", repository=repository)
237
+
238
+ # ── Documents: by path ────────────────────────────────────────────────────────────────
239
+
240
+ def get_document_by_path(self, path: str, *, repository: str | None = None) -> OpenArchDocument:
241
+ r = self._send("GET", f"/api/documents/path/{_doc_path(path)}", repository=repository)
242
+ return self._parse(r, OpenArchDocument)
243
+
244
+ 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 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
+ 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 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
+ 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 = 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
+ def delete_document_by_path(self, path: str, *, repository: str | None = None) -> None:
286
+ self._send("DELETE", f"/api/documents/path/{_doc_path(path)}", repository=repository)
287
+
288
+ def exists_by_path(self, path: str, *, repository: str | None = None) -> bool:
289
+ from ..errors import OpenArchNotFoundError
290
+
291
+ try:
292
+ self.get_document_by_path(path, repository=repository)
293
+ return True
294
+ except OpenArchNotFoundError:
295
+ return False
296
+
297
+ 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 = self._send(method, url, files=parts, params=params, repository=repository)
306
+ else:
307
+ r = self._send(method, url, json=_body(document), params=params, repository=repository)
308
+ return self._parse(r, OpenArchDocument)
309
+
310
+ # ── Documents: other operations ───────────────────────────────────────────────────────
311
+
312
+ def get_ancestors(self, doc_id: str, *, repository: str | None = None) -> DocumentAncestors:
313
+ r = self._send("GET", f"/api/documents/{_seg(doc_id)}/ancestors", repository=repository)
314
+ return self._parse(r, DocumentAncestors)
315
+
316
+ 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 = 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
+ def copy(self, doc_id: str, command: CopyCommand | Body, *, repository: str | None = None) -> OpenArchDocument:
327
+ r = self._send("POST", f"/api/documents/{_seg(doc_id)}/copy", json=_body(command), repository=repository)
328
+ return self._parse(r, OpenArchDocument)
329
+
330
+ 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 = 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
+ def restore_version(
341
+ self, version_id: str, *, user_change: bool | None = None, repository: str | None = None
342
+ ) -> OpenArchDocument:
343
+ r = 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
+ def get_embedding(self, doc_id: str, embedding_id: str, *, repository: str | None = None) -> Embedding:
350
+ r = 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
+ def get_full_effective_acl(
356
+ self, doc_id: str, usernames: Sequence[str] | None = None, *, repository: str | None = None
357
+ ) -> ListEffectiveACLDTO:
358
+ r = 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
+ def get_full_effective_permissions(
365
+ self, doc_id: str, usernames: Sequence[str] | None = None, *, repository: str | None = None
366
+ ) -> ListEffectivePermissionsDTO:
367
+ r = 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
+ 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 = 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
+ def download_info(
387
+ self, doc_id: str, property_xpath_and_filename: str, *, repository: str | None = None
388
+ ) -> httpx.Headers:
389
+ r = 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
+ 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 = 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
+ 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 = self._send("POST", "/api/query", json=_body(query), params=_params(sync=sync))
435
+ return self._parse(r, OpenArchQueryResult)
436
+
437
+ def advanced_query(self, query: AdvancedQuery | Body, *, sync: bool | None = None) -> OpenArchQueryResult:
438
+ r = self._send("POST", "/api/query/advanced", json=_body(query), params=_params(sync=sync))
439
+ return self._parse(r, OpenArchQueryResult)
440
+
441
+ def named_query(self, query: NamedQuery | Body, *, sync: bool | None = None) -> OpenArchQueryResult:
442
+ r = self._send("POST", "/api/query/named", json=_body(query), params=_params(sync=sync))
443
+ return self._parse(r, OpenArchQueryResult)
444
+
445
+ def list_named_queries(self) -> Any:
446
+ return self._json(self._send("GET", "/api/query/named"))
447
+
448
+ def get_named_query(self, name: str) -> NamedQueryDefinition:
449
+ r = self._send("GET", f"/api/query/named/{_seg(name)}")
450
+ return self._parse(r, NamedQueryDefinition)
451
+
452
+ def terms_aggregation(self, query: TermsAggregationsQuery | Body) -> OpenArchQueryResult:
453
+ """Counts per term, in the result's ``aggregations_buckets``."""
454
+ r = self._send("POST", "/api/query/termsAggregation", json=_body(query))
455
+ return self._parse(r, OpenArchQueryResult)
456
+
457
+ def vector_search(self, query: VectorQuery | Body) -> VectorSearchResult:
458
+ """Nearest embeddings to a vector (``/api/query/embeddings``)."""
459
+ r = self._send("POST", "/api/query/embeddings", json=_body(query))
460
+ return self._parse(r, VectorSearchResult)
461
+
462
+ 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(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
+ def search_users(self, name: str) -> list[User]:
475
+ r = self._send("GET", "/api/user", params={"name": name})
476
+ return self._parse(r, list[User])
477
+
478
+ def get_user(self, user_id: str) -> User:
479
+ return self._parse(self._send("GET", f"/api/user/{_seg(user_id)}"), User)
480
+
481
+ def search_groups(self, name: str) -> list[Group]:
482
+ r = self._send("GET", "/api/group", params={"name": name})
483
+ return self._parse(r, list[Group])
484
+
485
+ def get_group(self, group_id: str) -> Group:
486
+ return self._parse(self._send("GET", f"/api/group/{_seg(group_id)}"), Group)
487
+
488
+ def hello(self, *, protected: bool = False) -> str:
489
+ """Connectivity check; ``protected=True`` also checks the credentials."""
490
+ r = 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
+ 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(self._send("GET", url, repository=repository), Model)
501
+
502
+ 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 = 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
+ def patch_model(
511
+ self, patch: JsonPatch, *, validate_only: bool | None = None, repository: str | None = None
512
+ ) -> Model | None:
513
+ r = 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
+ 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(self._send("GET", url, repository=repository), Model)
522
+
523
+ 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 = 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
+ def get_security_policies(self, *, repository: str | None = None) -> Any:
532
+ return self._json(self._send("GET", "/api/repository/security/policies", repository=repository))
533
+
534
+ 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(self._send(
540
+ "PUT", url, json=_body(policies), params=_params(validateOnly=validate_only), repository=repository
541
+ ))
542
+
543
+ def patch_security_policies(
544
+ self, patch: JsonPatch, *, validate_only: bool | None = None, repository: str | None = None
545
+ ) -> Any:
546
+ return self._json(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
+ def get_security_policies_v2(self, *, repository: str | None = None) -> SecurityPolicies:
552
+ r = self._send("GET", "/api/v2/repository/security/policies", repository=repository)
553
+ return self._parse(r, SecurityPolicies)
554
+
555
+ 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 = 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
+ def get_external_user_relations(
564
+ self, policy_name: str, *, repository: str | None = None
565
+ ) -> ExternalUserRelationsSecurityPolicyRelations:
566
+ r = self._send(
567
+ "GET", f"/api/repository/security/policies/{_seg(policy_name)}/externaluserrelations", repository=repository
568
+ )
569
+ return self._parse(r, ExternalUserRelationsSecurityPolicyRelations)
570
+
571
+ def set_external_user_relations(
572
+ self, policy_name: str, relations: ExternalUserRelationsSecurityPolicyRelations | Body, *,
573
+ repository: str | None = None,
574
+ ) -> None:
575
+ self._send(
576
+ "PUT", f"/api/repository/security/policies/{_seg(policy_name)}/externaluserrelations",
577
+ json=_body(relations), repository=repository,
578
+ )
579
+
580
+ def get_external_user_relations_status(self, policy_name: str, *, repository: str | None = None) -> Any:
581
+ return self._json(self._send(
582
+ "GET", f"/api/repository/security/policies/{_seg(policy_name)}/externaluserrelations/status",
583
+ repository=repository,
584
+ ))
585
+
586
+ # ── Vocabularies ──────────────────────────────────────────────────────────────────────
587
+
588
+ def list_vocabularies(self, *, repository: str | None = None) -> list[VocabularyId]:
589
+ return self._parse(self._send("GET", "/api/vocabularies", repository=repository), list[VocabularyId])
590
+
591
+ def create_vocabulary(self, vocabulary: Vocabulary | Body, *, repository: str | None = None) -> Vocabulary:
592
+ r = self._send("POST", "/api/vocabularies", json=_body(vocabulary), repository=repository)
593
+ return self._parse(r, Vocabulary)
594
+
595
+ def delete_vocabulary(self, vocabulary_id: str, *, repository: str | None = None) -> None:
596
+ self._send("DELETE", f"/api/vocabularies/{_seg(vocabulary_id)}", repository=repository)
597
+
598
+ def get_vocabulary_entries(self, vocabulary_id: str, *, repository: str | None = None) -> list[VocabularyEntry]:
599
+ r = self._send("GET", f"/api/vocabularies/{_seg(vocabulary_id)}", repository=repository)
600
+ return self._parse(r, list[VocabularyEntry])
601
+
602
+ def create_vocabulary_entry(
603
+ self, vocabulary_id: str, entry: VocabularyEntryRequest | Body, *, repository: str | None = None
604
+ ) -> VocabularyEntry:
605
+ r = self._send("POST", f"/api/vocabularies/{_seg(vocabulary_id)}", json=_body(entry), repository=repository)
606
+ return self._parse(r, VocabularyEntry)
607
+
608
+ def update_vocabulary_entry(
609
+ self, vocabulary_id: str, entry: VocabularyEntryRequest | Body, *, repository: str | None = None
610
+ ) -> VocabularyEntry:
611
+ r = self._send("PUT", f"/api/vocabularies/{_seg(vocabulary_id)}", json=_body(entry), repository=repository)
612
+ return self._parse(r, VocabularyEntry)
613
+
614
+ def get_vocabulary_entry(self, vocabulary_id: str, key: str, *, repository: str | None = None) -> VocabularyEntry:
615
+ r = self._send("GET", f"/api/vocabularies/{_seg(vocabulary_id)}/{_seg(key)}", repository=repository)
616
+ return self._parse(r, VocabularyEntry)
617
+
618
+ def delete_vocabulary_entry(self, vocabulary_id: str, key: str, *, repository: str | None = None) -> None:
619
+ self._send("DELETE", f"/api/vocabularies/{_seg(vocabulary_id)}/{_seg(key)}", repository=repository)
620
+
621
+ # ── Upload ────────────────────────────────────────────────────────────────────────────
622
+
623
+ def create_upload(self) -> Upload:
624
+ return self._parse(self._send("POST", "/api/upload/create"), Upload)
625
+
626
+ 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 = 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
+ def get_upload(self, upload_id: str) -> Upload | None:
650
+ r = self._send("GET", f"/api/upload/{_seg(upload_id)}")
651
+ return self._parse(r, Upload) if r.content else None
652
+
653
+ def complete_upload(self, upload_id: str, info: UploadFileInfo | Body) -> None:
654
+ self._send("POST", f"/api/upload/{_seg(upload_id)}/complete", json=_body(info))
655
+
656
+ def renew_upload_credentials(self, upload_id: str) -> Any:
657
+ return self._json(self._send("POST", f"/api/upload/{_seg(upload_id)}/renewCredentials"))
658
+
659
+ def delete_upload(self, upload_id: str) -> None:
660
+ self._send("DELETE", f"/api/upload/{_seg(upload_id)}")
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"