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.
@@ -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]")
@@ -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 KonectyAPIError, KonectyError, KonectyValidationError
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
- response.raise_for_status()
515
- result = await response.json()
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
- raise KonectyAPIError(errors)
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
- response.raise_for_status()
570
- result = response.json()
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
- raise KonectyAPIError(errors)
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,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: konecty_sdk_python
3
- Version: 2.1.2
3
+ Version: 2.1.4
4
4
  Summary: Konecty SDK Python
5
5
  Author-email: Leonardo Leal <leonardo.leal@konecty.com>, Derotino Silveira <derotino.silveira@konecty.com>
6
6
  License: MIT
@@ -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=LyPoNcsU3oz9P_eTAi4BdtJHWkbp-sNK_SP9W8Xua04,17923
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=i-7goKqsH-jvwxZfGeipTRFByOBAOhMHkuhjjdEN3kE,34034
8
- KonectySdkPython/lib/exceptions.py,sha256=RQ6H_zgbF8yeII1F75wCIEJ1LfO4lWfY5hnK30cwETY,1107
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.2.dist-info/METADATA,sha256=TfygAtKFhJJZvdH8AgpgtIBQnoFexqBLQk9FaXwDZ8Q,5442
33
- konecty_sdk_python-2.1.2.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
34
- konecty_sdk_python-2.1.2.dist-info/entry_points.txt,sha256=awf962DamFgy791G3PsJ354edBMLJg16Dqi-2Y1tb9s,58
35
- konecty_sdk_python-2.1.2.dist-info/RECORD,,
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.31.0
2
+ Generator: hatchling 1.32.4
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any