konecty_sdk_python 2.1.3__tar.gz → 2.1.5__tar.gz

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.
Files changed (38) hide show
  1. {konecty_sdk_python-2.1.3 → konecty_sdk_python-2.1.5}/KonectySdkPython/cli/apply.py +11 -2
  2. {konecty_sdk_python-2.1.3 → konecty_sdk_python-2.1.5}/KonectySdkPython/lib/client.py +178 -7
  3. konecty_sdk_python-2.1.5/KonectySdkPython/lib/exceptions.py +93 -0
  4. {konecty_sdk_python-2.1.3 → konecty_sdk_python-2.1.5}/KonectySdkPython/lib/http.py +7 -2
  5. konecty_sdk_python-2.1.5/KonectySdkPython/lib/services/admin.py +223 -0
  6. konecty_sdk_python-2.1.5/KonectySdkPython/lib/services/pat.py +46 -0
  7. {konecty_sdk_python-2.1.3 → konecty_sdk_python-2.1.5}/PKG-INFO +1 -1
  8. {konecty_sdk_python-2.1.3 → konecty_sdk_python-2.1.5}/pyproject.toml +1 -1
  9. konecty_sdk_python-2.1.3/KonectySdkPython/lib/exceptions.py +0 -42
  10. {konecty_sdk_python-2.1.3 → konecty_sdk_python-2.1.5}/.gitignore +0 -0
  11. {konecty_sdk_python-2.1.3 → konecty_sdk_python-2.1.5}/KonectySdkPython/__init__.py +0 -0
  12. {konecty_sdk_python-2.1.3 → konecty_sdk_python-2.1.5}/KonectySdkPython/cli/__init__.py +0 -0
  13. {konecty_sdk_python-2.1.3 → konecty_sdk_python-2.1.5}/KonectySdkPython/cli/backup.py +0 -0
  14. {konecty_sdk_python-2.1.3 → konecty_sdk_python-2.1.5}/KonectySdkPython/cli/pull.py +0 -0
  15. {konecty_sdk_python-2.1.3 → konecty_sdk_python-2.1.5}/KonectySdkPython/lib/__init__.py +0 -0
  16. {konecty_sdk_python-2.1.3 → konecty_sdk_python-2.1.5}/KonectySdkPython/lib/feature_types/__init__.py +0 -0
  17. {konecty_sdk_python-2.1.3 → konecty_sdk_python-2.1.5}/KonectySdkPython/lib/feature_types/cross_module_query.py +0 -0
  18. {konecty_sdk_python-2.1.3 → konecty_sdk_python-2.1.5}/KonectySdkPython/lib/feature_types/kpi.py +0 -0
  19. {konecty_sdk_python-2.1.3 → konecty_sdk_python-2.1.5}/KonectySdkPython/lib/feature_types/query_json.py +0 -0
  20. {konecty_sdk_python-2.1.3 → konecty_sdk_python-2.1.5}/KonectySdkPython/lib/file_manager.py +0 -0
  21. {konecty_sdk_python-2.1.3 → konecty_sdk_python-2.1.5}/KonectySdkPython/lib/filters.py +0 -0
  22. {konecty_sdk_python-2.1.3 → konecty_sdk_python-2.1.5}/KonectySdkPython/lib/model.py +0 -0
  23. {konecty_sdk_python-2.1.3 → konecty_sdk_python-2.1.5}/KonectySdkPython/lib/serialization.py +0 -0
  24. {konecty_sdk_python-2.1.3 → konecty_sdk_python-2.1.5}/KonectySdkPython/lib/services/__init__.py +0 -0
  25. {konecty_sdk_python-2.1.3 → konecty_sdk_python-2.1.5}/KonectySdkPython/lib/services/aggregation.py +0 -0
  26. {konecty_sdk_python-2.1.3 → konecty_sdk_python-2.1.5}/KonectySdkPython/lib/services/auth.py +0 -0
  27. {konecty_sdk_python-2.1.3 → konecty_sdk_python-2.1.5}/KonectySdkPython/lib/services/base.py +0 -0
  28. {konecty_sdk_python-2.1.3 → konecty_sdk_python-2.1.5}/KonectySdkPython/lib/services/change_user.py +0 -0
  29. {konecty_sdk_python-2.1.3 → konecty_sdk_python-2.1.5}/KonectySdkPython/lib/services/comments.py +0 -0
  30. {konecty_sdk_python-2.1.3 → konecty_sdk_python-2.1.5}/KonectySdkPython/lib/services/export.py +0 -0
  31. {konecty_sdk_python-2.1.3 → konecty_sdk_python-2.1.5}/KonectySdkPython/lib/services/files.py +0 -0
  32. {konecty_sdk_python-2.1.3 → konecty_sdk_python-2.1.5}/KonectySdkPython/lib/services/notifications.py +0 -0
  33. {konecty_sdk_python-2.1.3 → konecty_sdk_python-2.1.5}/KonectySdkPython/lib/services/query.py +0 -0
  34. {konecty_sdk_python-2.1.3 → konecty_sdk_python-2.1.5}/KonectySdkPython/lib/services/stream.py +0 -0
  35. {konecty_sdk_python-2.1.3 → konecty_sdk_python-2.1.5}/KonectySdkPython/lib/services/subscriptions.py +0 -0
  36. {konecty_sdk_python-2.1.3 → konecty_sdk_python-2.1.5}/KonectySdkPython/lib/settings.py +0 -0
  37. {konecty_sdk_python-2.1.3 → konecty_sdk_python-2.1.5}/KonectySdkPython/lib/types.py +0 -0
  38. {konecty_sdk_python-2.1.3 → konecty_sdk_python-2.1.5}/README.md +0 -0
@@ -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
@@ -55,6 +62,18 @@ def get_first_dict(items: List[Any]) -> Optional[KonectyDict]:
55
62
  return None
56
63
 
57
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
+
58
77
  class KonectyClient:
59
78
  def __init__(self, base_url: str, token: str = "") -> None:
60
79
  self.base_url = base_url
@@ -420,6 +439,145 @@ class KonectyClient:
420
439
  """Get login method flags for the namespace (GET /api/auth/login-options)."""
421
440
  return await self._auth.get_login_options()
422
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
+
423
581
  @property
424
582
  def _query(self) -> QueryService:
425
583
  if not hasattr(self, "_query_service"):
@@ -514,12 +672,18 @@ class KonectyClient:
514
672
  headers={"Authorization": self.headers["Authorization"]},
515
673
  ) as response,
516
674
  ):
517
- response.raise_for_status()
518
- 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}")
519
682
  if not result.get("success", False):
520
683
  errors = result.get("errors", [])
521
684
  logger.error(errors)
522
- raise KonectyAPIError(errors)
685
+ raise_for_konecty_errors(errors)
686
+ response.raise_for_status()
523
687
  data = result.get("data", [])
524
688
  return cast(List[KonectyDict], data)
525
689
 
@@ -569,12 +733,19 @@ class KonectyClient:
569
733
  params=params,
570
734
  headers={"Authorization": self.headers["Authorization"]},
571
735
  )
572
- response.raise_for_status()
573
- 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}")
574
744
  if not result.get("success", False):
575
745
  errors = result.get("errors", [])
576
746
  logger.error(errors)
577
- raise KonectyAPIError(errors)
747
+ raise_for_konecty_errors(errors)
748
+ response.raise_for_status()
578
749
  data = result.get("data", [])
579
750
  return cast(List[KonectyDict], data)
580
751
 
@@ -0,0 +1,93 @@
1
+ """Exceptions for the Konecty SDK."""
2
+
3
+
4
+ class KonectyError(Exception):
5
+ """Base exception for Konecty errors."""
6
+
7
+ pass
8
+
9
+
10
+ class KonectyAPIError(KonectyError):
11
+ """Raised when the API returns success=false or a non-2xx status."""
12
+
13
+ pass
14
+
15
+
16
+ class KonectyGoogleSessionError(KonectyAPIError):
17
+ """
18
+ Raised by exchange_google_code. Carries the machine-readable ``code`` from
19
+ ``errors[0].code`` so callers can branch or translate without parsing the
20
+ message; ``failed`` covers unreadable or unrecognised bodies.
21
+
22
+ Subclasses KonectyAPIError so existing ``except KonectyAPIError`` keeps working.
23
+
24
+ Mirrors KonectyGoogleSessionError in the TypeScript SDK — keep both in sync.
25
+ """
26
+
27
+ def __init__(self, code: str, message: str) -> None:
28
+ super().__init__(message)
29
+ self.code = code
30
+
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
+
83
+ class KonectyValidationError(KonectyError):
84
+ """Raised for validation errors."""
85
+
86
+ pass
87
+
88
+
89
+ class KonectySerializationError(KonectyError):
90
+ """Raised when a value is not serializable."""
91
+
92
+ def __init__(self) -> None:
93
+ super().__init__("Tipo não serializável")
@@ -74,8 +74,13 @@ async def request(
74
74
 
75
75
  if stream:
76
76
  session = aiohttp.ClientSession()
77
- response = await session.request(method, url, **kwargs)
78
- response.raise_for_status()
77
+ # Until StreamResponse owns the session, a failure here must close it or it leaks.
78
+ try:
79
+ response = await session.request(method, url, **kwargs)
80
+ response.raise_for_status()
81
+ except BaseException:
82
+ await session.close()
83
+ raise
79
84
  return StreamResponse(session, response)
80
85
 
81
86
  async with aiohttp.ClientSession() as session:
@@ -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
1
  Metadata-Version: 2.5
2
2
  Name: konecty_sdk_python
3
- Version: 2.1.3
3
+ Version: 2.1.5
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,6 +1,6 @@
1
1
  [project]
2
2
  name = "konecty_sdk_python"
3
- version = "2.1.3"
3
+ version = "2.1.5"
4
4
  description = "Konecty SDK Python"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1,42 +0,0 @@
1
- """Exceptions for the Konecty SDK."""
2
-
3
-
4
- class KonectyError(Exception):
5
- """Base exception for Konecty errors."""
6
-
7
- pass
8
-
9
-
10
- class KonectyAPIError(KonectyError):
11
- """Raised when the API returns success=false or a non-2xx status."""
12
-
13
- pass
14
-
15
-
16
- class KonectyGoogleSessionError(KonectyAPIError):
17
- """
18
- Raised by exchange_google_code. Carries the machine-readable ``code`` from
19
- ``errors[0].code`` so callers can branch or translate without parsing the
20
- message; ``failed`` covers unreadable or unrecognised bodies.
21
-
22
- Subclasses KonectyAPIError so existing ``except KonectyAPIError`` keeps working.
23
-
24
- Mirrors KonectyGoogleSessionError in the TypeScript SDK — keep both in sync.
25
- """
26
-
27
- def __init__(self, code: str, message: str) -> None:
28
- super().__init__(message)
29
- self.code = code
30
-
31
-
32
- class KonectyValidationError(KonectyError):
33
- """Raised for validation errors."""
34
-
35
- pass
36
-
37
-
38
- class KonectySerializationError(KonectyError):
39
- """Raised when a value is not serializable."""
40
-
41
- def __init__(self) -> None:
42
- super().__init__("Tipo não serializável")