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.
@@ -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})"