konecty_sdk_python 2.1.2__py3-none-any.whl → 2.1.4__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.
- KonectySdkPython/cli/apply.py +11 -2
- KonectySdkPython/lib/client.py +182 -8
- KonectySdkPython/lib/exceptions.py +51 -0
- KonectySdkPython/lib/services/admin.py +223 -0
- KonectySdkPython/lib/services/pat.py +46 -0
- {konecty_sdk_python-2.1.2.dist-info → konecty_sdk_python-2.1.4.dist-info}/METADATA +2 -2
- {konecty_sdk_python-2.1.2.dist-info → konecty_sdk_python-2.1.4.dist-info}/RECORD +9 -7
- {konecty_sdk_python-2.1.2.dist-info → konecty_sdk_python-2.1.4.dist-info}/WHEEL +1 -1
- {konecty_sdk_python-2.1.2.dist-info → konecty_sdk_python-2.1.4.dist-info}/entry_points.txt +0 -0
KonectySdkPython/cli/apply.py
CHANGED
|
@@ -191,12 +191,21 @@ async def apply_document(
|
|
|
191
191
|
|
|
192
192
|
# Hooks
|
|
193
193
|
for file_path in doc_files["hook"]:
|
|
194
|
-
data = file_path.read_text()
|
|
194
|
+
data: Any = file_path.read_text()
|
|
195
195
|
if data:
|
|
196
|
+
hook_name = file_path.stem
|
|
197
|
+
# `validationData.json` é objeto no core, não texto: gravado como string, o
|
|
198
|
+
# `Object.keys` do `processValidationScript` itera os caracteres. Comparar o
|
|
199
|
+
# objeto também faz o apply reparar o que a versão antiga gravou como string.
|
|
200
|
+
if file_path.suffix == ".json":
|
|
201
|
+
try:
|
|
202
|
+
data = json.loads(data)
|
|
203
|
+
except json.JSONDecodeError as error:
|
|
204
|
+
errors.append(f"✗ {doc_name}/{hook_name}: JSON inválido ({error})")
|
|
205
|
+
continue
|
|
196
206
|
existing_data = collection.find_one(
|
|
197
207
|
{"name": doc_name, "type": {"$in": ["composite", "document"]}}
|
|
198
208
|
)
|
|
199
|
-
hook_name = file_path.stem
|
|
200
209
|
if existing_data:
|
|
201
210
|
if existing_data.get(hook_name, None) == data:
|
|
202
211
|
skipped.append(f"⚡ {doc_name}/{hook_name} [identical]")
|
KonectySdkPython/lib/client.py
CHANGED
|
@@ -7,13 +7,19 @@ from typing import Any, AsyncGenerator, Dict, List, Literal, Optional, Union, ca
|
|
|
7
7
|
|
|
8
8
|
import aiohttp
|
|
9
9
|
|
|
10
|
-
from .exceptions import
|
|
10
|
+
from .exceptions import (
|
|
11
|
+
KonectyAPIError,
|
|
12
|
+
KonectyError,
|
|
13
|
+
KonectyValidationError,
|
|
14
|
+
raise_for_konecty_errors,
|
|
15
|
+
)
|
|
11
16
|
from .feature_types.kpi import KpiConfig
|
|
12
17
|
from .file_manager import FileManager
|
|
13
18
|
from .filters import KonectyFilter, KonectyFindParams
|
|
14
19
|
from .http import StreamResponse
|
|
15
20
|
from .http import request as _http_request
|
|
16
21
|
from .serialization import json_serial
|
|
22
|
+
from .services.admin import AdminService
|
|
17
23
|
from .services.aggregation import AggregationService
|
|
18
24
|
from .services.auth import AuthService
|
|
19
25
|
from .services.change_user import ChangeUserService
|
|
@@ -21,6 +27,7 @@ from .services.comments import CommentsService
|
|
|
21
27
|
from .services.export import ExportService
|
|
22
28
|
from .services.files import FilesService
|
|
23
29
|
from .services.notifications import NotificationsService
|
|
30
|
+
from .services.pat import PatService
|
|
24
31
|
from .services.query import QueryResult, QueryService
|
|
25
32
|
from .services.stream import FindStreamResult, StreamService
|
|
26
33
|
from .services.subscriptions import SubscriptionsService
|
|
@@ -31,12 +38,15 @@ logging.getLogger("urllib3.connectionpool").setLevel(logging.ERROR)
|
|
|
31
38
|
|
|
32
39
|
logger = logging.getLogger(__name__)
|
|
33
40
|
|
|
41
|
+
# `_updatedBy` is NOT stripped: the backend accepts it on update as an explicit
|
|
42
|
+
# authorship override for admin callers (automations crediting a write to the user
|
|
43
|
+
# it acted on behalf of). Non-admin callers have it ignored server-side, so letting
|
|
44
|
+
# it through costs nothing. See "Autoria da escrita" in docs/pt-BR/api.md (konecty/Konecty).
|
|
34
45
|
KONECTY_UPDATE_IGNORE_FIELDS = [
|
|
35
46
|
"_id",
|
|
36
47
|
"code",
|
|
37
48
|
"_updatedAt",
|
|
38
49
|
"_createdAt",
|
|
39
|
-
"_updatedBy",
|
|
40
50
|
"_createdBy",
|
|
41
51
|
]
|
|
42
52
|
KONECTY_CREATE_IGNORE_FIELDS = ["_updatedAt", "_createdAt", "_updatedBy", "_createdBy"]
|
|
@@ -52,6 +62,18 @@ def get_first_dict(items: List[Any]) -> Optional[KonectyDict]:
|
|
|
52
62
|
return None
|
|
53
63
|
|
|
54
64
|
|
|
65
|
+
async def _read_json_body(response: "aiohttp.ClientResponse") -> Optional[Dict[str, Any]]:
|
|
66
|
+
"""
|
|
67
|
+
Lê o corpo como JSON sem explodir quando não é JSON (proxy, gateway, HTML).
|
|
68
|
+
|
|
69
|
+
Devolve ``None`` nesse caso, e o chamador cai no tratamento por status.
|
|
70
|
+
"""
|
|
71
|
+
try:
|
|
72
|
+
return cast(Dict[str, Any], await response.json(content_type=None))
|
|
73
|
+
except Exception: # noqa: BLE001 - corpo não-JSON é caso esperado aqui
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
55
77
|
class KonectyClient:
|
|
56
78
|
def __init__(self, base_url: str, token: str = "") -> None:
|
|
57
79
|
self.base_url = base_url
|
|
@@ -417,6 +439,145 @@ class KonectyClient:
|
|
|
417
439
|
"""Get login method flags for the namespace (GET /api/auth/login-options)."""
|
|
418
440
|
return await self._auth.get_login_options()
|
|
419
441
|
|
|
442
|
+
@property
|
|
443
|
+
def _pat(self) -> PatService:
|
|
444
|
+
if not hasattr(self, "_pat_service"):
|
|
445
|
+
self._pat_service = PatService(self)
|
|
446
|
+
return self._pat_service
|
|
447
|
+
|
|
448
|
+
async def create_pat(
|
|
449
|
+
self, name: str, *, expires_at: Optional[str] = None
|
|
450
|
+
) -> Dict[str, Any]:
|
|
451
|
+
"""
|
|
452
|
+
Create a Personal Access Token for the caller (POST /rest/auth/pat).
|
|
453
|
+
|
|
454
|
+
Requires a session/OAuth authentication (a PAT cannot mint another PAT). The
|
|
455
|
+
plaintext token is only ever returned here — save it now, it is not recoverable
|
|
456
|
+
afterwards. `expires_at` is an ISO date string; omit for a PAT that never expires.
|
|
457
|
+
"""
|
|
458
|
+
return await self._pat.create_pat(name, expires_at=expires_at)
|
|
459
|
+
|
|
460
|
+
async def list_pats(self) -> Dict[str, Any]:
|
|
461
|
+
"""List the caller's own Personal Access Tokens (GET /rest/auth/pat)."""
|
|
462
|
+
return await self._pat.list_pats()
|
|
463
|
+
|
|
464
|
+
async def revoke_pat(self, pat_id: str) -> Dict[str, Any]:
|
|
465
|
+
"""Revoke one of the caller's own Personal Access Tokens (DELETE /rest/auth/pat/{pat_id})."""
|
|
466
|
+
return await self._pat.revoke_pat(pat_id)
|
|
467
|
+
|
|
468
|
+
@property
|
|
469
|
+
def _admin(self) -> AdminService:
|
|
470
|
+
if not hasattr(self, "_admin_service"):
|
|
471
|
+
self._admin_service = AdminService(self)
|
|
472
|
+
return self._admin_service
|
|
473
|
+
|
|
474
|
+
async def list_all_pats(self) -> Dict[str, Any]:
|
|
475
|
+
"""Admin: list every PAT and legacy perpetual token in the namespace (GET /api/admin/pats)."""
|
|
476
|
+
return await self._admin.list_all_pats()
|
|
477
|
+
|
|
478
|
+
async def revoke_user_pat(self, user_id: str, pat_id: str) -> Dict[str, Any]:
|
|
479
|
+
"""
|
|
480
|
+
Admin: revoke a PAT belonging to any user (DELETE /api/admin/pats/{user_id}/{pat_id}).
|
|
481
|
+
|
|
482
|
+
Named `revoke_user_pat` — not `revoke_pat` — because that flat name is already
|
|
483
|
+
taken by the self-service method above, and the two act on different resources
|
|
484
|
+
(`user_id` + `pat_id` here vs. a caller-scoped `pat_id`). Mirrors the TypeScript
|
|
485
|
+
SDK's equivalent method for the same reason.
|
|
486
|
+
"""
|
|
487
|
+
return await self._admin.revoke_user_pat(user_id, pat_id)
|
|
488
|
+
|
|
489
|
+
async def revoke_legacy_token(self, user_id: str, fingerprint: str) -> Dict[str, Any]:
|
|
490
|
+
"""Admin: revoke a legacy perpetual token by fingerprint (DELETE /api/admin/legacy-tokens/{user_id}/{fingerprint})."""
|
|
491
|
+
return await self._admin.revoke_legacy_token(user_id, fingerprint)
|
|
492
|
+
|
|
493
|
+
async def create_service_account(
|
|
494
|
+
self,
|
|
495
|
+
name: str,
|
|
496
|
+
username: str,
|
|
497
|
+
access_map: Optional[Dict[str, str]] = None,
|
|
498
|
+
) -> Dict[str, Any]:
|
|
499
|
+
"""
|
|
500
|
+
Admin: create a Service Account with a sovereign access map (POST /api/admin/service-accounts).
|
|
501
|
+
|
|
502
|
+
`access_map` maps document name -> 'read' | 'readWrite'.
|
|
503
|
+
"""
|
|
504
|
+
return await self._admin.create_service_account(name, username, access_map)
|
|
505
|
+
|
|
506
|
+
async def list_meta_documents(self) -> Dict[str, Any]:
|
|
507
|
+
"""Admin: os documentos de metadado do namespace (GET /api/admin/meta)."""
|
|
508
|
+
return await self._admin.list_meta_documents()
|
|
509
|
+
|
|
510
|
+
async def read_meta(self, document: str) -> Dict[str, Any]:
|
|
511
|
+
"""Admin: o metadado de um documento (GET /api/admin/meta/{document})."""
|
|
512
|
+
return await self._admin.read_meta(document)
|
|
513
|
+
|
|
514
|
+
async def upsert_meta(
|
|
515
|
+
self, document: str, type: str, body: Dict[str, Any]
|
|
516
|
+
) -> Dict[str, Any]:
|
|
517
|
+
"""
|
|
518
|
+
Admin: grava o metadado singleton de um tipo (PUT /api/admin/meta/{document}/{type}).
|
|
519
|
+
|
|
520
|
+
Campo de hook que o `body` omite é preservado do metadado já gravado e volta nomeado em
|
|
521
|
+
`data["preservedHooks"]`; para remover um hook, envie `None` naquele campo.
|
|
522
|
+
"""
|
|
523
|
+
return await self._admin.upsert_meta(document, type, body)
|
|
524
|
+
|
|
525
|
+
async def delete_meta(self, document: str, type: str) -> Dict[str, Any]:
|
|
526
|
+
"""Admin: remove e versiona a remoção (DELETE /api/admin/meta/{document}/{type})."""
|
|
527
|
+
return await self._admin.delete_meta(document, type)
|
|
528
|
+
|
|
529
|
+
async def list_meta_history(
|
|
530
|
+
self,
|
|
531
|
+
meta_id: str,
|
|
532
|
+
limit: Optional[int] = None,
|
|
533
|
+
offset: Optional[int] = None,
|
|
534
|
+
) -> Dict[str, Any]:
|
|
535
|
+
"""Admin: versões de um metadado, mais recentes primeiro (GET /api/admin/meta/{metaId}/history)."""
|
|
536
|
+
return await self._admin.list_meta_history(meta_id, limit, offset)
|
|
537
|
+
|
|
538
|
+
async def rollback_meta(self, meta_id: str, version: int) -> Dict[str, Any]:
|
|
539
|
+
"""Admin: restaura o conteúdo de uma versão anterior (POST /api/admin/meta/{metaId}/rollback)."""
|
|
540
|
+
return await self._admin.rollback_meta(meta_id, version)
|
|
541
|
+
|
|
542
|
+
async def list_service_accounts(self) -> Dict[str, Any]:
|
|
543
|
+
"""Admin: list every Service Account and its PATs (GET /api/admin/service-accounts)."""
|
|
544
|
+
return await self._admin.list_service_accounts()
|
|
545
|
+
|
|
546
|
+
async def update_service_account_access(
|
|
547
|
+
self, service_account_id: str, access_map: Dict[str, str]
|
|
548
|
+
) -> Dict[str, Any]:
|
|
549
|
+
"""Admin: replace a Service Account's access map (PUT /api/admin/service-accounts/{id}/access)."""
|
|
550
|
+
return await self._admin.update_service_account_access(
|
|
551
|
+
service_account_id, access_map
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
async def create_service_account_pat(
|
|
555
|
+
self,
|
|
556
|
+
service_account_id: str,
|
|
557
|
+
name: str,
|
|
558
|
+
*,
|
|
559
|
+
expires_at: Optional[str] = None,
|
|
560
|
+
) -> Dict[str, Any]:
|
|
561
|
+
"""Admin: mint a PAT on behalf of a Service Account (POST /api/admin/service-accounts/{id}/pats)."""
|
|
562
|
+
return await self._admin.create_service_account_pat(
|
|
563
|
+
service_account_id, name, expires_at=expires_at
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
async def get_mcp_access(self) -> Dict[str, Any]:
|
|
567
|
+
"""Admin: read the roles allowed to reach the MCP, split into read and write (GET /api/admin/mcp-access)."""
|
|
568
|
+
return await self._admin.get_mcp_access()
|
|
569
|
+
|
|
570
|
+
async def update_mcp_access(
|
|
571
|
+
self, read_role_ids: List[str], write_role_ids: List[str]
|
|
572
|
+
) -> Dict[str, Any]:
|
|
573
|
+
"""
|
|
574
|
+
Admin: replace both MCP role lists (PUT /api/admin/mcp-access).
|
|
575
|
+
|
|
576
|
+
A role in `write_role_ids` also gets read access. Mirrors the TypeScript SDK's
|
|
577
|
+
`updateMcpAccess`.
|
|
578
|
+
"""
|
|
579
|
+
return await self._admin.update_mcp_access(read_role_ids, write_role_ids)
|
|
580
|
+
|
|
420
581
|
@property
|
|
421
582
|
def _query(self) -> QueryService:
|
|
422
583
|
if not hasattr(self, "_query_service"):
|
|
@@ -511,12 +672,18 @@ class KonectyClient:
|
|
|
511
672
|
headers={"Authorization": self.headers["Authorization"]},
|
|
512
673
|
) as response,
|
|
513
674
|
):
|
|
514
|
-
|
|
515
|
-
|
|
675
|
+
# O corpo é lido ANTES do raise_for_status: a recusa de sort acima do
|
|
676
|
+
# teto chega como HTTP 400 com `errors[0].code`, e levantar pelo status
|
|
677
|
+
# primeiro descartava mensagem e código, deixando só "400 Bad Request".
|
|
678
|
+
result = await _read_json_body(response)
|
|
679
|
+
if result is None:
|
|
680
|
+
response.raise_for_status()
|
|
681
|
+
raise KonectyAPIError(f"{response.status} {response.reason}")
|
|
516
682
|
if not result.get("success", False):
|
|
517
683
|
errors = result.get("errors", [])
|
|
518
684
|
logger.error(errors)
|
|
519
|
-
|
|
685
|
+
raise_for_konecty_errors(errors)
|
|
686
|
+
response.raise_for_status()
|
|
520
687
|
data = result.get("data", [])
|
|
521
688
|
return cast(List[KonectyDict], data)
|
|
522
689
|
|
|
@@ -566,12 +733,19 @@ class KonectyClient:
|
|
|
566
733
|
params=params,
|
|
567
734
|
headers={"Authorization": self.headers["Authorization"]},
|
|
568
735
|
)
|
|
569
|
-
|
|
570
|
-
|
|
736
|
+
# Mesma ordem do `find` assíncrono: corpo primeiro, status depois.
|
|
737
|
+
try:
|
|
738
|
+
result = response.json()
|
|
739
|
+
except ValueError:
|
|
740
|
+
result = None
|
|
741
|
+
if result is None:
|
|
742
|
+
response.raise_for_status()
|
|
743
|
+
raise KonectyAPIError(f"{response.status_code} {response.reason}")
|
|
571
744
|
if not result.get("success", False):
|
|
572
745
|
errors = result.get("errors", [])
|
|
573
746
|
logger.error(errors)
|
|
574
|
-
|
|
747
|
+
raise_for_konecty_errors(errors)
|
|
748
|
+
response.raise_for_status()
|
|
575
749
|
data = result.get("data", [])
|
|
576
750
|
return cast(List[KonectyDict], data)
|
|
577
751
|
|
|
@@ -29,6 +29,57 @@ class KonectyGoogleSessionError(KonectyAPIError):
|
|
|
29
29
|
self.code = code
|
|
30
30
|
|
|
31
31
|
|
|
32
|
+
#: Código devolvido pelo Konecty quando a requisição pede ordenação arbitrária
|
|
33
|
+
#: acima do teto de página. Espelha ``SORT_ABOVE_MAX_PAGE_SIZE`` no SDK
|
|
34
|
+
#: TypeScript — manter os dois em sincronia.
|
|
35
|
+
SORT_ABOVE_MAX_PAGE_SIZE = "SORT_ABOVE_MAX_PAGE_SIZE"
|
|
36
|
+
|
|
37
|
+
#: Teto do servidor. Informativo: quem decide é a API, este valor só evita adivinhação.
|
|
38
|
+
MAX_SORTED_PAGE_SIZE = 1000
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class KonectySortLimitError(KonectyAPIError):
|
|
42
|
+
"""
|
|
43
|
+
Raised when the request asks for arbitrary ordering above the page cap.
|
|
44
|
+
|
|
45
|
+
Acima de ``MAX_SORTED_PAGE_SIZE`` registros — e no caso sem limite
|
|
46
|
+
(``limit=-1``) — o ``$sort`` teria de terminar antes de o primeiro documento
|
|
47
|
+
sair do cursor, então a API recusa com HTTP 400 em vez de trocar a ordenação
|
|
48
|
+
em silêncio, como fazia antes.
|
|
49
|
+
|
|
50
|
+
Carrega o ``code`` legível por máquina para o chamador ramificar sem
|
|
51
|
+
inspecionar a mensagem, como ``KonectyGoogleSessionError``. Subclasse de
|
|
52
|
+
``KonectyAPIError``, então ``except KonectyAPIError`` existente continua
|
|
53
|
+
pegando.
|
|
54
|
+
|
|
55
|
+
A saída é ordenar por ``_id`` (ascendente ou descendente) e paginar por faixa
|
|
56
|
+
de ``_id``, ou pedir no máximo ``MAX_SORTED_PAGE_SIZE`` registros.
|
|
57
|
+
|
|
58
|
+
Espelha ``KonectySortLimitError`` no SDK TypeScript — manter os dois em sincronia.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(self, message: str) -> None:
|
|
62
|
+
super().__init__(message)
|
|
63
|
+
self.code = SORT_ABOVE_MAX_PAGE_SIZE
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def raise_for_konecty_errors(errors: object) -> None:
|
|
67
|
+
"""
|
|
68
|
+
Lança a exceção mais específica que a lista de ``errors`` da API permitir.
|
|
69
|
+
|
|
70
|
+
Existe porque os caminhos de find liam o corpo só para logar e lançavam
|
|
71
|
+
``KonectyAPIError`` genérico — e, no caso de HTTP 400, o
|
|
72
|
+
``response.raise_for_status()`` disparava ANTES de o corpo ser lido, então
|
|
73
|
+
nem a mensagem nem o código chegavam a quem chamou.
|
|
74
|
+
"""
|
|
75
|
+
items = errors if isinstance(errors, list) else []
|
|
76
|
+
for error in items:
|
|
77
|
+
if isinstance(error, dict) and error.get("code") == SORT_ABOVE_MAX_PAGE_SIZE:
|
|
78
|
+
raise KonectySortLimitError(str(error.get("message", SORT_ABOVE_MAX_PAGE_SIZE)))
|
|
79
|
+
|
|
80
|
+
raise KonectyAPIError(errors)
|
|
81
|
+
|
|
82
|
+
|
|
32
83
|
class KonectyValidationError(KonectyError):
|
|
33
84
|
"""Raised for validation errors."""
|
|
34
85
|
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Admin API: cross-user PAT/legacy-token management and Service Accounts.
|
|
2
|
+
|
|
3
|
+
Mirrors the backend's `src/server/routes/api/admin/credentials.ts` and
|
|
4
|
+
`src/server/routes/api/admin/serviceAccounts.ts` (Konecty core repo). Every route here
|
|
5
|
+
requires an admin session — a regular user gets 403.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
from urllib.parse import quote
|
|
10
|
+
|
|
11
|
+
from .base import BaseService
|
|
12
|
+
|
|
13
|
+
ADMIN_PATS_PATH = "/api/admin/pats"
|
|
14
|
+
ADMIN_LEGACY_TOKENS_PATH = "/api/admin/legacy-tokens"
|
|
15
|
+
ADMIN_SERVICE_ACCOUNTS_PATH = "/api/admin/service-accounts"
|
|
16
|
+
ADMIN_MCP_ACCESS_PATH = "/api/admin/mcp-access"
|
|
17
|
+
ADMIN_META_PATH = "/api/admin/meta"
|
|
18
|
+
|
|
19
|
+
#: `409` — o deployment carrega metadados de um diretório (`METADATA_DIR`); nada é gravável.
|
|
20
|
+
META_ADMIN_CONFIG_READ_ONLY_CODE = "meta-admin-config-read-only"
|
|
21
|
+
|
|
22
|
+
#: `403` — a rota exige sessão first-party; PAT e token OAuth são recusados.
|
|
23
|
+
ADMIN_ROUTES_REQUIRE_SESSION_CODE = "admin-credential-routes-require-session"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _segment(value: str) -> str:
|
|
27
|
+
"""Codifica UM segmento de path.
|
|
28
|
+
|
|
29
|
+
`safe=""` de propósito, e **não** `quote_plus`: o TS usa `encodeURIComponent`, que produz `%20`
|
|
30
|
+
para espaço enquanto `quote_plus` produziria `+` — as duas URLs deixariam de bater byte a byte,
|
|
31
|
+
que é exatamente a classe de divergência que já mordeu este time. Codifica por segmento porque
|
|
32
|
+
um `_id` de meta carrega `:` (`Product:list:Default`) e codificar o caminho inteiro escaparia as
|
|
33
|
+
barras que o separam.
|
|
34
|
+
|
|
35
|
+
Diferente dos ids de PAT/service account acima, aqui o valor é nome de documento ou `_id` de
|
|
36
|
+
meta — entrada livre, não identificador gerado pelo servidor.
|
|
37
|
+
"""
|
|
38
|
+
return quote(value, safe="")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class AdminService(BaseService):
|
|
42
|
+
"""Service for namespace-wide credential and Service Account administration.
|
|
43
|
+
|
|
44
|
+
`user_id`/`pat_id`/`fingerprint`/`service_account_id` below are server-generated
|
|
45
|
+
identifiers (Mongo-style hex ids or slug-like alphanumeric/hyphen strings) —
|
|
46
|
+
interpolated raw into the path with an f-string, no percent-encoding, matching
|
|
47
|
+
the TS SDK's equivalent path-templating in adminCredentials.ts/
|
|
48
|
+
adminServiceAccounts.ts. They are never free-form user input.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
async def list_all_pats(self) -> Dict[str, Any]:
|
|
52
|
+
"""GET /api/admin/pats. Returns `data`: {pats: [...], legacyTokens: [...]} for the whole namespace."""
|
|
53
|
+
return await self._get(ADMIN_PATS_PATH)
|
|
54
|
+
|
|
55
|
+
async def revoke_user_pat(self, user_id: str, pat_id: str) -> Dict[str, Any]:
|
|
56
|
+
"""DELETE /api/admin/pats/{user_id}/{pat_id}. Revokes a PAT belonging to any user."""
|
|
57
|
+
path = f"{ADMIN_PATS_PATH}/{user_id}/{pat_id}"
|
|
58
|
+
return await self._delete(path)
|
|
59
|
+
|
|
60
|
+
async def revoke_legacy_token(self, user_id: str, fingerprint: str) -> Dict[str, Any]:
|
|
61
|
+
"""DELETE /api/admin/legacy-tokens/{user_id}/{fingerprint}. `fingerprint` comes from `list_all_pats`'s `legacyTokens` entries."""
|
|
62
|
+
path = f"{ADMIN_LEGACY_TOKENS_PATH}/{user_id}/{fingerprint}"
|
|
63
|
+
return await self._delete(path)
|
|
64
|
+
|
|
65
|
+
async def create_service_account(
|
|
66
|
+
self,
|
|
67
|
+
name: str,
|
|
68
|
+
username: str,
|
|
69
|
+
access_map: Optional[Dict[str, str]] = None,
|
|
70
|
+
) -> Dict[str, Any]:
|
|
71
|
+
"""
|
|
72
|
+
POST /api/admin/service-accounts. Wire body: {name, username, accessMap?}.
|
|
73
|
+
|
|
74
|
+
`access_map` maps document name -> 'read' | 'readWrite'; when omitted the
|
|
75
|
+
`accessMap` key itself is left out of the request body (matching the TS SDK,
|
|
76
|
+
which passes `accessMap: undefined` and drops it on `JSON.stringify`) — the
|
|
77
|
+
backend treats a missing map the same as an empty one: no access to anything.
|
|
78
|
+
Returns `data`: {_id, username, role, access, mcpRoleHint?} — 201 on success,
|
|
79
|
+
409 if the username is already in use.
|
|
80
|
+
"""
|
|
81
|
+
payload: Dict[str, Any] = {"name": name, "username": username}
|
|
82
|
+
if access_map is not None:
|
|
83
|
+
payload["accessMap"] = access_map
|
|
84
|
+
return await self._post(ADMIN_SERVICE_ACCOUNTS_PATH, json=payload)
|
|
85
|
+
|
|
86
|
+
async def list_service_accounts(self) -> Dict[str, Any]:
|
|
87
|
+
"""GET /api/admin/service-accounts. Returns `data`: list of {_id, name, username, active, access, pats}."""
|
|
88
|
+
return await self._get(ADMIN_SERVICE_ACCOUNTS_PATH)
|
|
89
|
+
|
|
90
|
+
async def update_service_account_access(
|
|
91
|
+
self, service_account_id: str, access_map: Dict[str, str]
|
|
92
|
+
) -> Dict[str, Any]:
|
|
93
|
+
"""
|
|
94
|
+
PUT /api/admin/service-accounts/{id}/access. Wire body: {accessMap}.
|
|
95
|
+
|
|
96
|
+
Replaces the whole access map (a document omitted from `access_map` loses
|
|
97
|
+
access). Returns `data`: {_id, access}.
|
|
98
|
+
"""
|
|
99
|
+
path = f"{ADMIN_SERVICE_ACCOUNTS_PATH}/{service_account_id}/access"
|
|
100
|
+
return await self._put(path, json={"accessMap": access_map})
|
|
101
|
+
|
|
102
|
+
async def create_service_account_pat(
|
|
103
|
+
self,
|
|
104
|
+
service_account_id: str,
|
|
105
|
+
name: str,
|
|
106
|
+
*,
|
|
107
|
+
expires_at: Optional[str] = None,
|
|
108
|
+
) -> Dict[str, Any]:
|
|
109
|
+
"""
|
|
110
|
+
POST /api/admin/service-accounts/{id}/pats. Wire body: {name, expiresAt?}.
|
|
111
|
+
|
|
112
|
+
Mints a PAT on behalf of a Service Account (never a human user — the backend
|
|
113
|
+
rejects that with 403, ADM-02). Returns `data`: {_id, token} — 201 on success.
|
|
114
|
+
"""
|
|
115
|
+
payload: Dict[str, Any] = {"name": name}
|
|
116
|
+
if expires_at is not None:
|
|
117
|
+
payload["expiresAt"] = expires_at
|
|
118
|
+
path = f"{ADMIN_SERVICE_ACCOUNTS_PATH}/{service_account_id}/pats"
|
|
119
|
+
return await self._post(path, json=payload)
|
|
120
|
+
|
|
121
|
+
async def get_mcp_access(self) -> Dict[str, Any]:
|
|
122
|
+
"""
|
|
123
|
+
GET /api/admin/mcp-access. Returns `data`: {roles, readRoleIds, writeRoleIds, readOnlyConfig}.
|
|
124
|
+
|
|
125
|
+
`readRoleIds` are the roles allowed to reach the MCP; `writeRoleIds` the subset
|
|
126
|
+
also allowed to use write/destructive tools (a role listed there reads too, whether
|
|
127
|
+
or not it also appears in `readRoleIds`). `readOnlyConfig` is true when the
|
|
128
|
+
deployment loads its namespace config from a metadata directory — `update_mcp_access`
|
|
129
|
+
then refuses with 409.
|
|
130
|
+
"""
|
|
131
|
+
return await self._get(ADMIN_MCP_ACCESS_PATH)
|
|
132
|
+
|
|
133
|
+
async def update_mcp_access(
|
|
134
|
+
self, read_role_ids: List[str], write_role_ids: List[str]
|
|
135
|
+
) -> Dict[str, Any]:
|
|
136
|
+
"""
|
|
137
|
+
PUT /api/admin/mcp-access. Wire body: {readRoleIds, writeRoleIds}.
|
|
138
|
+
|
|
139
|
+
Replaces both lists wholesale (a role omitted loses access). An unknown role id
|
|
140
|
+
is rejected with 400 and nothing is written; a deployment configured by a metadata
|
|
141
|
+
directory answers 409 with code `mcp-access-config-read-only`.
|
|
142
|
+
"""
|
|
143
|
+
return await self._put(
|
|
144
|
+
ADMIN_MCP_ACCESS_PATH,
|
|
145
|
+
json={"readRoleIds": read_role_ids, "writeRoleIds": write_role_ids},
|
|
146
|
+
)
|
|
147
|
+
# --- Meta Admin API (fatia mínima) ---------------------------------------------------------
|
|
148
|
+
#
|
|
149
|
+
# Seis métodos, não os 17 que `docs/features.json` declara. As demais rotas existem no core e
|
|
150
|
+
# ficam sem método de SDK até haver consumidor — YAGNI. Cada teste em `tests/test_admin.py`
|
|
151
|
+
# cita o equivalente em `src/__test__/api/adminMeta.test.ts` do `konecty-sdk`.
|
|
152
|
+
#
|
|
153
|
+
# Todas exigem **sessão first-party de admin**: PAT e token OAuth são recusados com `403` e o
|
|
154
|
+
# código `admin-credential-routes-require-session` — metadado decide quem lê e escreve o quê.
|
|
155
|
+
|
|
156
|
+
async def list_meta_documents(self) -> Dict[str, Any]:
|
|
157
|
+
"""GET /api/admin/meta. Returns `data`: [{_id, name, type, label}] do namespace."""
|
|
158
|
+
return await self._get(ADMIN_META_PATH)
|
|
159
|
+
|
|
160
|
+
async def read_meta(self, document: str) -> Dict[str, Any]:
|
|
161
|
+
"""GET /api/admin/meta/:document. `404` quando o metadado não existe."""
|
|
162
|
+
return await self._get(f"{ADMIN_META_PATH}/{_segment(document)}")
|
|
163
|
+
|
|
164
|
+
async def upsert_meta(self, document: str, type: str, body: Dict[str, Any]) -> Dict[str, Any]:
|
|
165
|
+
"""
|
|
166
|
+
PUT /api/admin/meta/:document/:type — grava o metadado singleton de um tipo
|
|
167
|
+
(`document`, `composite`, `namespace`).
|
|
168
|
+
|
|
169
|
+
Returns `data`: {matchedCount, modifiedCount, upsertedCount, versioned, version,
|
|
170
|
+
preservedHooks}. Escrita idêntica ao estado atual não cria versão (`versioned: False`).
|
|
171
|
+
Num deployment com `METADATA_DIR` responde `409` com `META_ADMIN_CONFIG_READ_ONLY_CODE`.
|
|
172
|
+
|
|
173
|
+
`preservedHooks` lista os campos de hook (`scriptBeforeValidation`, `validationScript`,
|
|
174
|
+
`scriptAfterSave`, `validationData`) que o `body` **omitiu** e vieram do metadado já
|
|
175
|
+
gravado. Hook é campo do metadado, mas nos repositórios de metadados ele vive em
|
|
176
|
+
`MetaObjects/<Doc>/hook/<nome>.js|json` e nunca aparece no `document.json`; por isso o core
|
|
177
|
+
preserva o hook cuja chave o payload não menciona, em vez de apagá-lo. Para remover um
|
|
178
|
+
hook, envie `None` explícito naquele campo. A chave está **ausente** quando o deployment é
|
|
179
|
+
anterior ao contrato — ausente significa "este servidor não informa", não "nada foi
|
|
180
|
+
preservado".
|
|
181
|
+
"""
|
|
182
|
+
return await self._put(f"{ADMIN_META_PATH}/{_segment(document)}/{_segment(type)}", json=body)
|
|
183
|
+
|
|
184
|
+
async def delete_meta(self, document: str, type: str) -> Dict[str, Any]:
|
|
185
|
+
"""
|
|
186
|
+
DELETE /api/admin/meta/:document/:type — remove e versiona a remoção.
|
|
187
|
+
|
|
188
|
+
Returns `data`: {deletedCount, version}. O documento `Namespace` não é deletável.
|
|
189
|
+
"""
|
|
190
|
+
return await self._delete(f"{ADMIN_META_PATH}/{_segment(document)}/{_segment(type)}")
|
|
191
|
+
|
|
192
|
+
async def list_meta_history(
|
|
193
|
+
self,
|
|
194
|
+
meta_id: str,
|
|
195
|
+
limit: Optional[int] = None,
|
|
196
|
+
offset: Optional[int] = None,
|
|
197
|
+
) -> Dict[str, Any]:
|
|
198
|
+
"""
|
|
199
|
+
GET /api/admin/meta/:metaId/history — versões, mais recentes primeiro.
|
|
200
|
+
|
|
201
|
+
`limit`/`offset` são omitidos da query quando não informados, igual ao TS. Um metadado nunca
|
|
202
|
+
escrito desde o deploy devolve **lista vazia**, não `404`: ausência de histórico não é
|
|
203
|
+
ausência de metadado.
|
|
204
|
+
"""
|
|
205
|
+
params: Dict[str, Any] = {}
|
|
206
|
+
if limit is not None:
|
|
207
|
+
params["limit"] = limit
|
|
208
|
+
if offset is not None:
|
|
209
|
+
params["offset"] = offset
|
|
210
|
+
|
|
211
|
+
return await self._get(
|
|
212
|
+
f"{ADMIN_META_PATH}/{_segment(meta_id)}/history",
|
|
213
|
+
params=params or None,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
async def rollback_meta(self, meta_id: str, version: int) -> Dict[str, Any]:
|
|
217
|
+
"""
|
|
218
|
+
POST /api/admin/meta/:metaId/rollback — restaura o conteúdo de uma versão anterior.
|
|
219
|
+
|
|
220
|
+
Returns `data`: {version, restoredFrom}. Forward-only: restaurar a v2 **cria** a v5 e nunca
|
|
221
|
+
reescreve nem apaga versão anterior.
|
|
222
|
+
"""
|
|
223
|
+
return await self._post(f"{ADMIN_META_PATH}/{_segment(meta_id)}/rollback", json={"version": version})
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Self-service Personal Access Token (PAT) API: create, list, revoke.
|
|
2
|
+
|
|
3
|
+
Mirrors the backend's `src/server/routes/rest/auth/patApi.ts` (Konecty core repo). These
|
|
4
|
+
routes require a session/OAuth authentication — a PAT itself cannot mint or revoke PATs
|
|
5
|
+
(server-side rule D4/PAT-01), the SDK does not enforce that client-side.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Dict, Optional
|
|
9
|
+
|
|
10
|
+
from .base import BaseService
|
|
11
|
+
|
|
12
|
+
PAT_PATH = "/rest/auth/pat"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PatService(BaseService):
|
|
16
|
+
"""Service for the caller's own Personal Access Tokens.
|
|
17
|
+
|
|
18
|
+
`pat_id` below is a server-generated identifier (Mongo-style hex id) —
|
|
19
|
+
interpolated raw into the path with an f-string, no percent-encoding, matching
|
|
20
|
+
the TS SDK's equivalent path-templating in pat.ts. It is never free-form user
|
|
21
|
+
input.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
async def create_pat(
|
|
25
|
+
self, name: str, *, expires_at: Optional[str] = None
|
|
26
|
+
) -> Dict[str, Any]:
|
|
27
|
+
"""
|
|
28
|
+
POST /rest/auth/pat. Wire body: {name, expiresAt?}.
|
|
29
|
+
|
|
30
|
+
The plaintext token only ever appears in this response's `data.token` — it is
|
|
31
|
+
never recoverable afterwards. `expires_at` is an ISO date string; omit for a
|
|
32
|
+
PAT that never expires.
|
|
33
|
+
"""
|
|
34
|
+
payload: Dict[str, Any] = {"name": name}
|
|
35
|
+
if expires_at is not None:
|
|
36
|
+
payload["expiresAt"] = expires_at
|
|
37
|
+
return await self._post(PAT_PATH, json=payload)
|
|
38
|
+
|
|
39
|
+
async def list_pats(self) -> Dict[str, Any]:
|
|
40
|
+
"""GET /rest/auth/pat. Returns `data`: list of {_id, name, createdAt, expiresAt, lastUsedAt} — never the hashed token."""
|
|
41
|
+
return await self._get(PAT_PATH)
|
|
42
|
+
|
|
43
|
+
async def revoke_pat(self, pat_id: str) -> Dict[str, Any]:
|
|
44
|
+
"""DELETE /rest/auth/pat/{pat_id}. Self-scoped: another user's PAT id 404s."""
|
|
45
|
+
path = f"{PAT_PATH}/{pat_id}"
|
|
46
|
+
return await self._delete(path)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
KonectySdkPython/__init__.py,sha256=E8bSSpoh-XogtSp4oDAhhID0J0UtctOlGS9LhI5F5d0,1022
|
|
2
2
|
KonectySdkPython/cli/__init__.py,sha256=Ap83UThRejLC44VaM-l31NhKcpkzx0Y4CgT042OJCBE,761
|
|
3
|
-
KonectySdkPython/cli/apply.py,sha256=
|
|
3
|
+
KonectySdkPython/cli/apply.py,sha256=OSCsMZ6t8TCWvdlHksjUmTESjfZQV7FFvVvA0K6dL6w,18482
|
|
4
4
|
KonectySdkPython/cli/backup.py,sha256=rcla3EWfPsy-vWgmDK844EaM5u3h7JF-u7-b2HUBMgA,4785
|
|
5
5
|
KonectySdkPython/cli/pull.py,sha256=n4gIkKtuEpPKULb3zcYu5tZlBLbt_4oNQDeu2l6tHxU,9529
|
|
6
6
|
KonectySdkPython/lib/__init__.py,sha256=Du8jLNfFG1bQuHX5u7pAhbyV9TjC7Ie9K1Xo1KSUqqc,995
|
|
7
|
-
KonectySdkPython/lib/client.py,sha256=
|
|
8
|
-
KonectySdkPython/lib/exceptions.py,sha256=
|
|
7
|
+
KonectySdkPython/lib/client.py,sha256=55di-db_5zUwdPVuIseILcWFrv__9VqMUM5Ulg9U9Ig,41861
|
|
8
|
+
KonectySdkPython/lib/exceptions.py,sha256=WkXRm0ZzKZdOdrHmgGxB_T_yvLH6agsHTsph3bTA0Vs,3251
|
|
9
9
|
KonectySdkPython/lib/file_manager.py,sha256=CLpOJWdA1tb2iq0qOIeRyEEIuACRq9tIo7Rls1YT2ig,9986
|
|
10
10
|
KonectySdkPython/lib/filters.py,sha256=PsFaUFCEyKgxgzD566LdTLvuMJevoF_i9U1WnpJZzPQ,4482
|
|
11
11
|
KonectySdkPython/lib/http.py,sha256=atwyuKSFniulvAspi_VhinUhtPJGNyZYkIkaEgqfIqo,3377
|
|
@@ -18,6 +18,7 @@ KonectySdkPython/lib/feature_types/cross_module_query.py,sha256=kG21qBMgCWd2gE1M
|
|
|
18
18
|
KonectySdkPython/lib/feature_types/kpi.py,sha256=67RpwwwnzU8piIPRyFb6Kn0hqSRpT9RFbYqiRnvYkFk,626
|
|
19
19
|
KonectySdkPython/lib/feature_types/query_json.py,sha256=yZgMPmkMdiBLm-dIrmU1AeZe5G9zQLwLtKUPMb7IOhU,8139
|
|
20
20
|
KonectySdkPython/lib/services/__init__.py,sha256=fluQuxs6LnIZwcjYTuKNeXCFTKuNF3At3X19fIB68p8,121
|
|
21
|
+
KonectySdkPython/lib/services/admin.py,sha256=DyJWjDNVE9HSd3FaESLbMW6EviTwYbu6iocUhMMIlDc,10747
|
|
21
22
|
KonectySdkPython/lib/services/aggregation.py,sha256=igK1mleMFU2_rDQCAU1yKyvDoOjhTO1SO0Nn4T_DokA,5610
|
|
22
23
|
KonectySdkPython/lib/services/auth.py,sha256=seb2QgdeWT_ALcVLR8bDMgO1ycI9dz75OhuIjahzy7o,5173
|
|
23
24
|
KonectySdkPython/lib/services/base.py,sha256=tdVyWb7Y77cBhVc8Mz2BQMN6kAyZIohoxBxdwDnRsYs,1820
|
|
@@ -26,10 +27,11 @@ KonectySdkPython/lib/services/comments.py,sha256=RNevCg4de2FWNk7mD60-pk3r8f2sJ1N
|
|
|
26
27
|
KonectySdkPython/lib/services/export.py,sha256=WAtSOUYx4uK1ujGHmITP77B1_l69UykhxvUBXEOG_wE,2074
|
|
27
28
|
KonectySdkPython/lib/services/files.py,sha256=YvaN2yyh48pcPfv0t4PtZLxaDxQlQrGnzIAwnEYN5Ag,1843
|
|
28
29
|
KonectySdkPython/lib/services/notifications.py,sha256=7AtCBbBl-YAz_RBIW6bzoKvuwsYqAUVnLBnS4uAlMWQ,1446
|
|
30
|
+
KonectySdkPython/lib/services/pat.py,sha256=yelMEubMX4G5aB5-4h-QAYjeQrqJp48AsapM6nK519s,1824
|
|
29
31
|
KonectySdkPython/lib/services/query.py,sha256=vCqbf0QdV8Blh8Hfsvq6Ne1-YFuC5siw1gVa7ZBGs_k,6605
|
|
30
32
|
KonectySdkPython/lib/services/stream.py,sha256=1DMVxcg9Ymk2NgX0Qfvpjrr6vZJo5iFovPFT1NA3_CY,4737
|
|
31
33
|
KonectySdkPython/lib/services/subscriptions.py,sha256=_Jq8jW7YkL_hwLNte1bWgrr4D2EMZUUSOXUgnRoGGK4,1022
|
|
32
|
-
konecty_sdk_python-2.1.
|
|
33
|
-
konecty_sdk_python-2.1.
|
|
34
|
-
konecty_sdk_python-2.1.
|
|
35
|
-
konecty_sdk_python-2.1.
|
|
34
|
+
konecty_sdk_python-2.1.4.dist-info/METADATA,sha256=b_dxxgMQsJVnVkGm9sXAkjc5qCvPJTAV3abh069hXDg,5442
|
|
35
|
+
konecty_sdk_python-2.1.4.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
36
|
+
konecty_sdk_python-2.1.4.dist-info/entry_points.txt,sha256=awf962DamFgy791G3PsJ354edBMLJg16Dqi-2Y1tb9s,58
|
|
37
|
+
konecty_sdk_python-2.1.4.dist-info/RECORD,,
|
|
File without changes
|