gurucloud-kb 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gurucloud_kb/__init__.py +90 -0
- gurucloud_kb/_async_http.py +112 -0
- gurucloud_kb/_http.py +112 -0
- gurucloud_kb/async_client.py +187 -0
- gurucloud_kb/async_kb.py +279 -0
- gurucloud_kb/client.py +196 -0
- gurucloud_kb/errors.py +61 -0
- gurucloud_kb/kb.py +290 -0
- gurucloud_kb/py.typed +0 -0
- gurucloud_kb/types.py +254 -0
- gurucloud_kb-0.1.0.dist-info/METADATA +184 -0
- gurucloud_kb-0.1.0.dist-info/RECORD +14 -0
- gurucloud_kb-0.1.0.dist-info/WHEEL +4 -0
- gurucloud_kb-0.1.0.dist-info/licenses/LICENSE +21 -0
gurucloud_kb/__init__.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""GuruCloud Knowledge Bank SDK.
|
|
2
|
+
|
|
3
|
+
Example::
|
|
4
|
+
|
|
5
|
+
from gurucloud_kb import GuruCloudClient
|
|
6
|
+
|
|
7
|
+
client = GuruCloudClient(api_key="kb_abc123...")
|
|
8
|
+
|
|
9
|
+
# List all KBs
|
|
10
|
+
kbs = client.list_kbs()
|
|
11
|
+
|
|
12
|
+
# Work with a specific KB
|
|
13
|
+
kb = client.get_kb("my-kb-uuid")
|
|
14
|
+
results = kb.search("how does auth work?")
|
|
15
|
+
|
|
16
|
+
# Get MCP server definition for agent injection
|
|
17
|
+
mcp_def = kb.get_mcp_server_definition()
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from gurucloud_kb.async_client import AsyncGuruCloudClient
|
|
21
|
+
from gurucloud_kb.async_kb import AsyncKnowledgeBank
|
|
22
|
+
from gurucloud_kb.client import GuruCloudClient
|
|
23
|
+
from gurucloud_kb.errors import (
|
|
24
|
+
APIError,
|
|
25
|
+
AuthenticationError,
|
|
26
|
+
ConnectionError,
|
|
27
|
+
GuruCloudError,
|
|
28
|
+
NotFoundError,
|
|
29
|
+
PermissionError,
|
|
30
|
+
RateLimitError,
|
|
31
|
+
)
|
|
32
|
+
from gurucloud_kb.kb import KnowledgeBank
|
|
33
|
+
from gurucloud_kb.types import (
|
|
34
|
+
APIKeyInfo,
|
|
35
|
+
BatchIngestResult,
|
|
36
|
+
CategoryConfig,
|
|
37
|
+
DeduplicationEvent,
|
|
38
|
+
DeduplicationEventList,
|
|
39
|
+
DeduplicationEventSummary,
|
|
40
|
+
DimensionConfig,
|
|
41
|
+
DimensionQuery,
|
|
42
|
+
DimensionSchema,
|
|
43
|
+
EntryEventLog,
|
|
44
|
+
EntryEventLogList,
|
|
45
|
+
EntryInput,
|
|
46
|
+
EntryResult,
|
|
47
|
+
KBInfo,
|
|
48
|
+
MCPServerDefinition,
|
|
49
|
+
SchemaWarning,
|
|
50
|
+
SearchRequest,
|
|
51
|
+
SearchResult,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
__all__ = [
|
|
55
|
+
# Sync client
|
|
56
|
+
"GuruCloudClient",
|
|
57
|
+
"KnowledgeBank",
|
|
58
|
+
# Async client
|
|
59
|
+
"AsyncGuruCloudClient",
|
|
60
|
+
"AsyncKnowledgeBank",
|
|
61
|
+
# Errors
|
|
62
|
+
"GuruCloudError",
|
|
63
|
+
"APIError",
|
|
64
|
+
"AuthenticationError",
|
|
65
|
+
"PermissionError",
|
|
66
|
+
"NotFoundError",
|
|
67
|
+
"RateLimitError",
|
|
68
|
+
"ConnectionError",
|
|
69
|
+
# Types
|
|
70
|
+
"KBInfo",
|
|
71
|
+
"DimensionConfig",
|
|
72
|
+
"CategoryConfig",
|
|
73
|
+
"DimensionSchema",
|
|
74
|
+
"SchemaWarning",
|
|
75
|
+
"EntryInput",
|
|
76
|
+
"EntryResult",
|
|
77
|
+
"DimensionQuery",
|
|
78
|
+
"SearchRequest",
|
|
79
|
+
"SearchResult",
|
|
80
|
+
"MCPServerDefinition",
|
|
81
|
+
"APIKeyInfo",
|
|
82
|
+
"BatchIngestResult",
|
|
83
|
+
"DeduplicationEvent",
|
|
84
|
+
"DeduplicationEventSummary",
|
|
85
|
+
"DeduplicationEventList",
|
|
86
|
+
"EntryEventLog",
|
|
87
|
+
"EntryEventLogList",
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Async HTTP transport for the GuruCloud KB SDK.
|
|
2
|
+
|
|
3
|
+
Wraps httpx.AsyncClient to provide:
|
|
4
|
+
- Automatic Bearer token injection
|
|
5
|
+
- Response envelope unwrapping (``{"data": ...}``)
|
|
6
|
+
- Typed error raising
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
from gurucloud_kb.errors import (
|
|
16
|
+
APIError,
|
|
17
|
+
AuthenticationError,
|
|
18
|
+
ConnectionError,
|
|
19
|
+
NotFoundError,
|
|
20
|
+
PermissionError,
|
|
21
|
+
RateLimitError,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
_DEFAULT_TIMEOUT = 30.0
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AsyncHTTPClient:
|
|
28
|
+
"""Async wrapper around httpx for the KB API."""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
base_url: str,
|
|
33
|
+
api_key: str,
|
|
34
|
+
timeout: float = _DEFAULT_TIMEOUT,
|
|
35
|
+
) -> None:
|
|
36
|
+
self._base_url = base_url.rstrip("/")
|
|
37
|
+
self._client = httpx.AsyncClient(
|
|
38
|
+
base_url=f"{self._base_url}/api/v1/kb",
|
|
39
|
+
headers={
|
|
40
|
+
"Authorization": f"Bearer {api_key}",
|
|
41
|
+
"Content-Type": "application/json",
|
|
42
|
+
},
|
|
43
|
+
timeout=timeout,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# ── public verbs ────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
async def get(self, path: str, params: dict[str, Any] | None = None) -> Any:
|
|
49
|
+
return await self._request("GET", path, params=params)
|
|
50
|
+
|
|
51
|
+
async def post(self, path: str, json: Any = None) -> Any:
|
|
52
|
+
return await self._request("POST", path, json=json)
|
|
53
|
+
|
|
54
|
+
async def put(self, path: str, json: Any = None) -> Any:
|
|
55
|
+
return await self._request("PUT", path, json=json)
|
|
56
|
+
|
|
57
|
+
async def patch(self, path: str, json: Any = None) -> Any:
|
|
58
|
+
return await self._request("PATCH", path, json=json)
|
|
59
|
+
|
|
60
|
+
async def delete(self, path: str) -> Any:
|
|
61
|
+
return await self._request("DELETE", path)
|
|
62
|
+
|
|
63
|
+
async def close(self) -> None:
|
|
64
|
+
await self._client.aclose()
|
|
65
|
+
|
|
66
|
+
# ── internals ───────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
async def _request(
|
|
69
|
+
self,
|
|
70
|
+
method: str,
|
|
71
|
+
path: str,
|
|
72
|
+
params: dict[str, Any] | None = None,
|
|
73
|
+
json: Any = None,
|
|
74
|
+
) -> Any:
|
|
75
|
+
try:
|
|
76
|
+
resp = await self._client.request(method, path, params=params, json=json)
|
|
77
|
+
except httpx.ConnectError as exc:
|
|
78
|
+
raise ConnectionError(f"Cannot reach {self._base_url}: {exc}") from exc
|
|
79
|
+
except httpx.TimeoutException as exc:
|
|
80
|
+
raise ConnectionError(f"Request timed out: {exc}") from exc
|
|
81
|
+
|
|
82
|
+
if resp.status_code >= 400:
|
|
83
|
+
self._raise_for_status(resp)
|
|
84
|
+
|
|
85
|
+
# The API wraps successful responses in {"data": ...}
|
|
86
|
+
body = resp.json()
|
|
87
|
+
if isinstance(body, dict) and "data" in body:
|
|
88
|
+
return body["data"]
|
|
89
|
+
return body
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def _raise_for_status(resp: httpx.Response) -> None:
|
|
93
|
+
"""Map HTTP error responses to typed SDK exceptions."""
|
|
94
|
+
try:
|
|
95
|
+
body = resp.json()
|
|
96
|
+
except Exception:
|
|
97
|
+
raise APIError(resp.status_code, "unknown", resp.text)
|
|
98
|
+
|
|
99
|
+
error = body.get("error", {})
|
|
100
|
+
code = error.get("code", "unknown") if isinstance(error, dict) else "unknown"
|
|
101
|
+
message = error.get("message", resp.text) if isinstance(error, dict) else str(error)
|
|
102
|
+
|
|
103
|
+
status = resp.status_code
|
|
104
|
+
if status == 401:
|
|
105
|
+
raise AuthenticationError(code, message)
|
|
106
|
+
if status == 403:
|
|
107
|
+
raise PermissionError(code, message)
|
|
108
|
+
if status == 404:
|
|
109
|
+
raise NotFoundError(code, message)
|
|
110
|
+
if status == 429:
|
|
111
|
+
raise RateLimitError(code, message)
|
|
112
|
+
raise APIError(status, code, message)
|
gurucloud_kb/_http.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Low-level HTTP transport for the GuruCloud KB SDK.
|
|
2
|
+
|
|
3
|
+
Wraps httpx to provide:
|
|
4
|
+
- Automatic Bearer token injection
|
|
5
|
+
- Response envelope unwrapping (``{"data": ...}``)
|
|
6
|
+
- Typed error raising
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
from gurucloud_kb.errors import (
|
|
16
|
+
APIError,
|
|
17
|
+
AuthenticationError,
|
|
18
|
+
ConnectionError,
|
|
19
|
+
NotFoundError,
|
|
20
|
+
PermissionError,
|
|
21
|
+
RateLimitError,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
_DEFAULT_TIMEOUT = 30.0
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class HTTPClient:
|
|
28
|
+
"""Thin wrapper around httpx for the KB API."""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
base_url: str,
|
|
33
|
+
api_key: str,
|
|
34
|
+
timeout: float = _DEFAULT_TIMEOUT,
|
|
35
|
+
) -> None:
|
|
36
|
+
self._base_url = base_url.rstrip("/")
|
|
37
|
+
self._client = httpx.Client(
|
|
38
|
+
base_url=f"{self._base_url}/api/v1/kb",
|
|
39
|
+
headers={
|
|
40
|
+
"Authorization": f"Bearer {api_key}",
|
|
41
|
+
"Content-Type": "application/json",
|
|
42
|
+
},
|
|
43
|
+
timeout=timeout,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# ── public verbs ────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
def get(self, path: str, params: dict[str, Any] | None = None) -> Any:
|
|
49
|
+
return self._request("GET", path, params=params)
|
|
50
|
+
|
|
51
|
+
def post(self, path: str, json: Any = None) -> Any:
|
|
52
|
+
return self._request("POST", path, json=json)
|
|
53
|
+
|
|
54
|
+
def put(self, path: str, json: Any = None) -> Any:
|
|
55
|
+
return self._request("PUT", path, json=json)
|
|
56
|
+
|
|
57
|
+
def patch(self, path: str, json: Any = None) -> Any:
|
|
58
|
+
return self._request("PATCH", path, json=json)
|
|
59
|
+
|
|
60
|
+
def delete(self, path: str) -> Any:
|
|
61
|
+
return self._request("DELETE", path)
|
|
62
|
+
|
|
63
|
+
def close(self) -> None:
|
|
64
|
+
self._client.close()
|
|
65
|
+
|
|
66
|
+
# ── internals ───────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
def _request(
|
|
69
|
+
self,
|
|
70
|
+
method: str,
|
|
71
|
+
path: str,
|
|
72
|
+
params: dict[str, Any] | None = None,
|
|
73
|
+
json: Any = None,
|
|
74
|
+
) -> Any:
|
|
75
|
+
try:
|
|
76
|
+
resp = self._client.request(method, path, params=params, json=json)
|
|
77
|
+
except httpx.ConnectError as exc:
|
|
78
|
+
raise ConnectionError(f"Cannot reach {self._base_url}: {exc}") from exc
|
|
79
|
+
except httpx.TimeoutException as exc:
|
|
80
|
+
raise ConnectionError(f"Request timed out: {exc}") from exc
|
|
81
|
+
|
|
82
|
+
if resp.status_code >= 400:
|
|
83
|
+
self._raise_for_status(resp)
|
|
84
|
+
|
|
85
|
+
# The API wraps successful responses in {"data": ...}
|
|
86
|
+
body = resp.json()
|
|
87
|
+
if isinstance(body, dict) and "data" in body:
|
|
88
|
+
return body["data"]
|
|
89
|
+
return body
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def _raise_for_status(resp: httpx.Response) -> None:
|
|
93
|
+
"""Map HTTP error responses to typed SDK exceptions."""
|
|
94
|
+
try:
|
|
95
|
+
body = resp.json()
|
|
96
|
+
except Exception:
|
|
97
|
+
raise APIError(resp.status_code, "unknown", resp.text)
|
|
98
|
+
|
|
99
|
+
error = body.get("error", {})
|
|
100
|
+
code = error.get("code", "unknown") if isinstance(error, dict) else "unknown"
|
|
101
|
+
message = error.get("message", resp.text) if isinstance(error, dict) else str(error)
|
|
102
|
+
|
|
103
|
+
status = resp.status_code
|
|
104
|
+
if status == 401:
|
|
105
|
+
raise AuthenticationError(code, message)
|
|
106
|
+
if status == 403:
|
|
107
|
+
raise PermissionError(code, message)
|
|
108
|
+
if status == 404:
|
|
109
|
+
raise NotFoundError(code, message)
|
|
110
|
+
if status == 429:
|
|
111
|
+
raise RateLimitError(code, message)
|
|
112
|
+
raise APIError(status, code, message)
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""AsyncGuruCloudClient — async entry point for the SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from gurucloud_kb._async_http import AsyncHTTPClient
|
|
8
|
+
from gurucloud_kb.async_kb import AsyncKnowledgeBank
|
|
9
|
+
from gurucloud_kb.types import (
|
|
10
|
+
APIKeyInfo,
|
|
11
|
+
DimensionSchema,
|
|
12
|
+
KBInfo,
|
|
13
|
+
MCPServerDefinition,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
_DEFAULT_BASE_URL = "https://www.gurucloudai.com"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AsyncGuruCloudClient:
|
|
20
|
+
"""Async client for the GuruCloud Knowledge Bank API.
|
|
21
|
+
|
|
22
|
+
Authenticate with a KB API key (``kb_...``).
|
|
23
|
+
|
|
24
|
+
Example::
|
|
25
|
+
|
|
26
|
+
from gurucloud_kb import AsyncGuruCloudClient
|
|
27
|
+
|
|
28
|
+
async with AsyncGuruCloudClient(api_key="kb_abc123...") as client:
|
|
29
|
+
kb = await client.get_kb("my-kb-uuid")
|
|
30
|
+
results = await kb.search("how does auth work?")
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
api_key: str,
|
|
36
|
+
*,
|
|
37
|
+
base_url: str = _DEFAULT_BASE_URL,
|
|
38
|
+
timeout: float = 30.0,
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Initialize the async client.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
api_key: KB API key (starts with ``kb_``).
|
|
44
|
+
base_url: GuruCloud API base URL.
|
|
45
|
+
timeout: Request timeout in seconds.
|
|
46
|
+
"""
|
|
47
|
+
if not api_key.startswith("kb_"):
|
|
48
|
+
raise ValueError("API key must start with 'kb_'")
|
|
49
|
+
|
|
50
|
+
self._http = AsyncHTTPClient(base_url=base_url, api_key=api_key, timeout=timeout)
|
|
51
|
+
|
|
52
|
+
# ── Knowledge Bank operations ───────────────────────────────
|
|
53
|
+
|
|
54
|
+
async def list_kbs(self) -> list[KBInfo]:
|
|
55
|
+
"""List all Knowledge Banks owned by the authenticated user."""
|
|
56
|
+
return await self._http.get("/banks")
|
|
57
|
+
|
|
58
|
+
async def get_kb(self, kb_id: str) -> AsyncKnowledgeBank:
|
|
59
|
+
"""Get a Knowledge Bank by ID, returning an :class:`AsyncKnowledgeBank` object.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
kb_id: Knowledge Bank UUID.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
An :class:`AsyncKnowledgeBank` instance.
|
|
66
|
+
"""
|
|
67
|
+
info: KBInfo = await self._http.get(f"/banks/{kb_id}")
|
|
68
|
+
return AsyncKnowledgeBank(self._http, info)
|
|
69
|
+
|
|
70
|
+
async def create_kb(
|
|
71
|
+
self,
|
|
72
|
+
name: str,
|
|
73
|
+
*,
|
|
74
|
+
description: str = "",
|
|
75
|
+
dimension_schema: DimensionSchema | None = None,
|
|
76
|
+
) -> AsyncKnowledgeBank:
|
|
77
|
+
"""Create a new Knowledge Bank.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
name: Human-readable KB name.
|
|
81
|
+
description: Optional description.
|
|
82
|
+
dimension_schema: Optional custom schema (uses default if omitted).
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
An :class:`AsyncKnowledgeBank` instance for the new KB.
|
|
86
|
+
"""
|
|
87
|
+
payload: dict[str, Any] = {"name": name, "description": description}
|
|
88
|
+
if dimension_schema is not None:
|
|
89
|
+
payload["dimension_schema"] = dimension_schema
|
|
90
|
+
|
|
91
|
+
info: KBInfo = await self._http.post("/banks", json=payload)
|
|
92
|
+
return AsyncKnowledgeBank(self._http, info)
|
|
93
|
+
|
|
94
|
+
async def update_kb(
|
|
95
|
+
self,
|
|
96
|
+
kb_id: str,
|
|
97
|
+
*,
|
|
98
|
+
name: str | None = None,
|
|
99
|
+
description: str | None = None,
|
|
100
|
+
) -> KBInfo:
|
|
101
|
+
"""Update a KB's name and/or description.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
kb_id: Knowledge Bank UUID.
|
|
105
|
+
name: New name (optional).
|
|
106
|
+
description: New description (optional).
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Updated KB info.
|
|
110
|
+
"""
|
|
111
|
+
updates: dict[str, str] = {}
|
|
112
|
+
if name is not None:
|
|
113
|
+
updates["name"] = name
|
|
114
|
+
if description is not None:
|
|
115
|
+
updates["description"] = description
|
|
116
|
+
return await self._http.patch(f"/banks/{kb_id}", json=updates)
|
|
117
|
+
|
|
118
|
+
async def delete_kb(self, kb_id: str) -> dict[str, Any]:
|
|
119
|
+
"""Delete a Knowledge Bank and all associated resources.
|
|
120
|
+
|
|
121
|
+
Requires ``admin`` scope on the API key.
|
|
122
|
+
"""
|
|
123
|
+
return await self._http.delete(f"/banks/{kb_id}")
|
|
124
|
+
|
|
125
|
+
# ── MCP server definition ───────────────────────────────────
|
|
126
|
+
|
|
127
|
+
async def get_mcp_server_definition(self, kb_id: str) -> MCPServerDefinition:
|
|
128
|
+
"""Get the MCP server definition for a KB, including a PAT.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
kb_id: Knowledge Bank UUID.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Full MCP server definition with token for agent injection.
|
|
135
|
+
"""
|
|
136
|
+
return await self._http.post(f"/banks/{kb_id}/mcp-server-definition")
|
|
137
|
+
|
|
138
|
+
# ── API key management ──────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
async def create_api_key(
|
|
141
|
+
self,
|
|
142
|
+
name: str,
|
|
143
|
+
*,
|
|
144
|
+
scopes: list[str] | None = None,
|
|
145
|
+
expires_at: str | None = None,
|
|
146
|
+
rate_limit_per_hour: int = 1000,
|
|
147
|
+
) -> APIKeyInfo:
|
|
148
|
+
"""Create a new API key.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
name: Human-readable label.
|
|
152
|
+
scopes: Granted scopes (default: ``["read", "write"]``).
|
|
153
|
+
expires_at: Optional ISO 8601 expiry timestamp.
|
|
154
|
+
rate_limit_per_hour: Rate limit (default: 1000).
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
API key info including the raw key.
|
|
158
|
+
"""
|
|
159
|
+
payload: dict[str, Any] = {"name": name, "rate_limit_per_hour": rate_limit_per_hour}
|
|
160
|
+
if scopes is not None:
|
|
161
|
+
payload["scopes"] = scopes
|
|
162
|
+
if expires_at is not None:
|
|
163
|
+
payload["expires_at"] = expires_at
|
|
164
|
+
return await self._http.post("/api-keys", json=payload)
|
|
165
|
+
|
|
166
|
+
async def list_api_keys(self) -> list[APIKeyInfo]:
|
|
167
|
+
"""List all API keys (keys are masked)."""
|
|
168
|
+
return await self._http.get("/api-keys")
|
|
169
|
+
|
|
170
|
+
async def delete_api_key(self, key_id: str) -> dict[str, Any]:
|
|
171
|
+
"""Delete an API key."""
|
|
172
|
+
return await self._http.delete(f"/api-keys/{key_id}")
|
|
173
|
+
|
|
174
|
+
# ── lifecycle ───────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
async def close(self) -> None:
|
|
177
|
+
"""Close the underlying HTTP connection pool."""
|
|
178
|
+
await self._http.close()
|
|
179
|
+
|
|
180
|
+
async def __aenter__(self) -> AsyncGuruCloudClient:
|
|
181
|
+
return self
|
|
182
|
+
|
|
183
|
+
async def __aexit__(self, *args: object) -> None:
|
|
184
|
+
await self.close()
|
|
185
|
+
|
|
186
|
+
def __repr__(self) -> str:
|
|
187
|
+
return f"AsyncGuruCloudClient(base_url={self._http._base_url!r})"
|