knowledge2 0.4.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.
Files changed (139) hide show
  1. knowledge2-0.4.0.dist-info/METADATA +556 -0
  2. knowledge2-0.4.0.dist-info/RECORD +139 -0
  3. knowledge2-0.4.0.dist-info/WHEEL +5 -0
  4. knowledge2-0.4.0.dist-info/top_level.txt +1 -0
  5. sdk/__init__.py +70 -0
  6. sdk/_async_base.py +525 -0
  7. sdk/_async_paging.py +57 -0
  8. sdk/_base.py +541 -0
  9. sdk/_logging.py +41 -0
  10. sdk/_paging.py +73 -0
  11. sdk/_preview.py +70 -0
  12. sdk/_raw_response.py +25 -0
  13. sdk/_request_options.py +51 -0
  14. sdk/_transport.py +144 -0
  15. sdk/_validation.py +25 -0
  16. sdk/_validation_response.py +36 -0
  17. sdk/_version.py +3 -0
  18. sdk/async_client.py +320 -0
  19. sdk/async_resources/__init__.py +45 -0
  20. sdk/async_resources/_mixin_base.py +42 -0
  21. sdk/async_resources/a2a.py +230 -0
  22. sdk/async_resources/agents.py +489 -0
  23. sdk/async_resources/audit.py +145 -0
  24. sdk/async_resources/auth.py +133 -0
  25. sdk/async_resources/console.py +409 -0
  26. sdk/async_resources/corpora.py +276 -0
  27. sdk/async_resources/deployments.py +106 -0
  28. sdk/async_resources/documents.py +592 -0
  29. sdk/async_resources/feeds.py +248 -0
  30. sdk/async_resources/indexes.py +208 -0
  31. sdk/async_resources/jobs.py +165 -0
  32. sdk/async_resources/metadata.py +48 -0
  33. sdk/async_resources/models.py +102 -0
  34. sdk/async_resources/onboarding.py +538 -0
  35. sdk/async_resources/orgs.py +37 -0
  36. sdk/async_resources/pipelines.py +523 -0
  37. sdk/async_resources/projects.py +90 -0
  38. sdk/async_resources/search.py +262 -0
  39. sdk/async_resources/training.py +357 -0
  40. sdk/async_resources/usage.py +91 -0
  41. sdk/client.py +417 -0
  42. sdk/config.py +182 -0
  43. sdk/errors.py +178 -0
  44. sdk/examples/auth_factory.py +34 -0
  45. sdk/examples/batch_operations.py +57 -0
  46. sdk/examples/document_upload.py +56 -0
  47. sdk/examples/e2e_lifecycle.py +213 -0
  48. sdk/examples/error_handling.py +61 -0
  49. sdk/examples/pagination.py +64 -0
  50. sdk/examples/quickstart.py +36 -0
  51. sdk/examples/request_options.py +44 -0
  52. sdk/examples/search.py +64 -0
  53. sdk/integrations/__init__.py +57 -0
  54. sdk/integrations/_client.py +101 -0
  55. sdk/integrations/langchain/__init__.py +6 -0
  56. sdk/integrations/langchain/retriever.py +166 -0
  57. sdk/integrations/langchain/tools.py +108 -0
  58. sdk/integrations/llamaindex/__init__.py +11 -0
  59. sdk/integrations/llamaindex/filters.py +78 -0
  60. sdk/integrations/llamaindex/retriever.py +162 -0
  61. sdk/integrations/llamaindex/tools.py +109 -0
  62. sdk/integrations/llamaindex/vector_store.py +320 -0
  63. sdk/models/__init__.py +18 -0
  64. sdk/models/_base.py +24 -0
  65. sdk/models/_registry.py +457 -0
  66. sdk/models/a2a.py +92 -0
  67. sdk/models/agents.py +109 -0
  68. sdk/models/audit.py +28 -0
  69. sdk/models/auth.py +49 -0
  70. sdk/models/chunks.py +20 -0
  71. sdk/models/common.py +14 -0
  72. sdk/models/console.py +103 -0
  73. sdk/models/corpora.py +48 -0
  74. sdk/models/deployments.py +13 -0
  75. sdk/models/documents.py +126 -0
  76. sdk/models/embeddings.py +24 -0
  77. sdk/models/evaluation.py +17 -0
  78. sdk/models/feedback.py +9 -0
  79. sdk/models/feeds.py +57 -0
  80. sdk/models/indexes.py +36 -0
  81. sdk/models/jobs.py +52 -0
  82. sdk/models/models.py +26 -0
  83. sdk/models/onboarding.py +323 -0
  84. sdk/models/orgs.py +11 -0
  85. sdk/models/pipelines.py +147 -0
  86. sdk/models/projects.py +19 -0
  87. sdk/models/search.py +149 -0
  88. sdk/models/training.py +57 -0
  89. sdk/models/usage.py +39 -0
  90. sdk/namespaces.py +386 -0
  91. sdk/py.typed +0 -0
  92. sdk/resources/__init__.py +45 -0
  93. sdk/resources/_mixin_base.py +40 -0
  94. sdk/resources/a2a.py +230 -0
  95. sdk/resources/agents.py +487 -0
  96. sdk/resources/audit.py +144 -0
  97. sdk/resources/auth.py +138 -0
  98. sdk/resources/console.py +411 -0
  99. sdk/resources/corpora.py +269 -0
  100. sdk/resources/deployments.py +105 -0
  101. sdk/resources/documents.py +597 -0
  102. sdk/resources/feeds.py +246 -0
  103. sdk/resources/indexes.py +210 -0
  104. sdk/resources/jobs.py +164 -0
  105. sdk/resources/metadata.py +53 -0
  106. sdk/resources/models.py +99 -0
  107. sdk/resources/onboarding.py +542 -0
  108. sdk/resources/orgs.py +35 -0
  109. sdk/resources/pipeline_builder.py +257 -0
  110. sdk/resources/pipelines.py +520 -0
  111. sdk/resources/projects.py +87 -0
  112. sdk/resources/search.py +277 -0
  113. sdk/resources/training.py +358 -0
  114. sdk/resources/usage.py +92 -0
  115. sdk/types/__init__.py +366 -0
  116. sdk/types/a2a.py +88 -0
  117. sdk/types/agents.py +133 -0
  118. sdk/types/audit.py +26 -0
  119. sdk/types/auth.py +45 -0
  120. sdk/types/chunks.py +18 -0
  121. sdk/types/common.py +10 -0
  122. sdk/types/console.py +99 -0
  123. sdk/types/corpora.py +42 -0
  124. sdk/types/deployments.py +11 -0
  125. sdk/types/documents.py +104 -0
  126. sdk/types/embeddings.py +22 -0
  127. sdk/types/evaluation.py +15 -0
  128. sdk/types/feedback.py +7 -0
  129. sdk/types/feeds.py +61 -0
  130. sdk/types/indexes.py +30 -0
  131. sdk/types/jobs.py +50 -0
  132. sdk/types/models.py +22 -0
  133. sdk/types/onboarding.py +395 -0
  134. sdk/types/orgs.py +9 -0
  135. sdk/types/pipelines.py +177 -0
  136. sdk/types/projects.py +14 -0
  137. sdk/types/search.py +116 -0
  138. sdk/types/training.py +55 -0
  139. sdk/types/usage.py +37 -0
@@ -0,0 +1,91 @@
1
+ """Async usage metrics resource mixin for the Knowledge2 SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from sdk._request_options import RequestOptions
8
+ from sdk.async_resources._mixin_base import AsyncRequesterMixin
9
+ from sdk.types import UsageByCorpusResponse, UsageByKeyResponse, UsageSummaryResponse
10
+
11
+
12
+ class AsyncUsageMixin(AsyncRequesterMixin):
13
+ async def usage_summary(
14
+ self,
15
+ *,
16
+ range_value: str = "7d",
17
+ corpus_id: str | None = None,
18
+ request_options: RequestOptions | None = None,
19
+ ) -> UsageSummaryResponse:
20
+ """Retrieve an aggregate usage summary.
21
+
22
+ .. note:: Requires **bearer token** auth, not an API key.
23
+
24
+ Args:
25
+ range_value: Time range for the summary (e.g. ``"7d"``,
26
+ ``"30d"``).
27
+ corpus_id: Optional corpus to scope the summary to.
28
+
29
+ Returns:
30
+ Aggregated usage statistics for the requested period.
31
+
32
+ Raises:
33
+ AuthenticationError: If called with an API key instead of a
34
+ bearer token.
35
+ Knowledge2Error: If the API request fails.
36
+ """
37
+ params: dict[str, Any] = {"range": range_value}
38
+ if corpus_id:
39
+ params["corpus_id"] = corpus_id
40
+ data = await self._request(
41
+ "GET", "/v1/usage/summary", params=params, request_options=request_options
42
+ )
43
+ return self._maybe_validate(data, "UsageSummaryResponse")
44
+
45
+ async def usage_by_corpus(
46
+ self,
47
+ *,
48
+ range_value: str = "7d",
49
+ request_options: RequestOptions | None = None,
50
+ ) -> UsageByCorpusResponse:
51
+ """Retrieve usage metrics broken down by corpus.
52
+
53
+ .. note:: Requires **bearer token** auth, not an API key.
54
+
55
+ Args:
56
+ range_value: Time range for the breakdown (e.g. ``"7d"``,
57
+ ``"30d"``).
58
+
59
+ Returns:
60
+ Per-corpus usage statistics for the requested period.
61
+
62
+ Raises:
63
+ AuthenticationError: If called with an API key instead of a
64
+ bearer token.
65
+ Knowledge2Error: If the API request fails.
66
+ """
67
+ data = await self._request(
68
+ "GET",
69
+ "/v1/usage/by_corpus",
70
+ params={"range": range_value},
71
+ request_options=request_options,
72
+ )
73
+ return self._maybe_validate(data, "UsageByCorpusResponse")
74
+
75
+ async def usage_by_key(
76
+ self, *, request_options: RequestOptions | None = None
77
+ ) -> UsageByKeyResponse:
78
+ """Retrieve usage metrics broken down by API key.
79
+
80
+ .. note:: Requires **bearer token** auth, not an API key.
81
+
82
+ Returns:
83
+ Per-key usage statistics.
84
+
85
+ Raises:
86
+ AuthenticationError: If called with an API key instead of a
87
+ bearer token.
88
+ Knowledge2Error: If the API request fails.
89
+ """
90
+ data = await self._request("GET", "/v1/usage/by_key", request_options=request_options)
91
+ return self._maybe_validate(data, "UsageByKeyResponse")
sdk/client.py ADDED
@@ -0,0 +1,417 @@
1
+ """Knowledge2 API client.
2
+
3
+ Usage::
4
+
5
+ from sdk import Knowledge2
6
+
7
+ client = Knowledge2(api_key="k2_...")
8
+ results = client.search(corpus_id, "my query")
9
+
10
+ # With validated config object:
11
+ from sdk import K2Config
12
+ config = K2Config(api_key="k2_...", max_retries=5)
13
+ client = Knowledge2(config=config)
14
+
15
+ # From environment variables:
16
+ client = Knowledge2.from_env()
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import functools
22
+ import threading
23
+ from collections.abc import Callable
24
+ from functools import cached_property
25
+ from pathlib import Path
26
+ from typing import TYPE_CHECKING, Any
27
+
28
+ import httpx
29
+
30
+ from sdk._base import BaseClient, ClientLimits, ClientTimeouts
31
+ from sdk._logging import set_debug as _set_debug
32
+ from sdk.resources import (
33
+ A2AMixin,
34
+ AgentsMixin,
35
+ AuditMixin,
36
+ AuthMixin,
37
+ ConsoleMixin,
38
+ CorporaMixin,
39
+ DeploymentsMixin,
40
+ DocumentsMixin,
41
+ FeedsMixin,
42
+ IndexesMixin,
43
+ JobsMixin,
44
+ MetadataMixin,
45
+ ModelsMixin,
46
+ OnboardingMixin,
47
+ OrgsMixin,
48
+ PipelinesMixin,
49
+ ProjectsMixin,
50
+ SearchMixin,
51
+ TrainingMixin,
52
+ UsageMixin,
53
+ )
54
+
55
+ if TYPE_CHECKING:
56
+ from sdk.config import K2Config
57
+ from sdk.namespaces import (
58
+ AuthNamespace,
59
+ CorporaNamespace,
60
+ DeploymentsNamespace,
61
+ DocumentsNamespace,
62
+ JobsNamespace,
63
+ ModelsNamespace,
64
+ SearchNamespace,
65
+ TrainingNamespace,
66
+ )
67
+
68
+ DEFAULT_API_HOST = "https://api.knowledge2.ai"
69
+
70
+ _SENTINEL = object()
71
+
72
+
73
+ class Knowledge2(
74
+ BaseClient,
75
+ OrgsMixin,
76
+ AuthMixin,
77
+ ProjectsMixin,
78
+ CorporaMixin,
79
+ ModelsMixin,
80
+ DocumentsMixin,
81
+ IndexesMixin,
82
+ SearchMixin,
83
+ MetadataMixin,
84
+ TrainingMixin,
85
+ DeploymentsMixin,
86
+ JobsMixin,
87
+ AuditMixin,
88
+ UsageMixin,
89
+ ConsoleMixin,
90
+ OnboardingMixin,
91
+ AgentsMixin,
92
+ FeedsMixin,
93
+ PipelinesMixin,
94
+ A2AMixin,
95
+ ):
96
+ """Knowledge2 API client.
97
+
98
+ The SDK is intentionally self-contained so it can be published directly from
99
+ the ``sdk/`` directory.
100
+
101
+ Args:
102
+ config: Optional :class:`~sdk.config.K2Config` instance. When
103
+ provided, its values are used as defaults that can be
104
+ overridden by explicit keyword arguments.
105
+ api_host: Base URL of the Knowledge2 API.
106
+ api_key: API key for authentication (``X-API-Key`` header).
107
+ org_id: Organisation ID. Auto-detected from the API key when
108
+ omitted via a ``GET /v1/auth/whoami`` call during construction.
109
+ Pass explicitly to skip this network call.
110
+ bearer_token: Bearer token for console / Auth0 authentication.
111
+ bearer_token_factory: Callable returning a bearer token string.
112
+ Called lazily on first request and cached for
113
+ ``token_cache_ttl`` seconds. Mutually exclusive with
114
+ ``bearer_token``.
115
+ token_cache_ttl: Seconds to cache the factory-produced token
116
+ (default 300). Set to ``0`` to disable caching.
117
+ admin_token: Admin token (``X-Admin-Token`` header).
118
+ headers: Extra default headers sent with every request.
119
+ user_agent: Custom ``User-Agent`` header value.
120
+ timeout: Request timeout in seconds (or an ``httpx.Timeout``).
121
+ limits: Connection pool limits.
122
+ max_retries: Maximum number of automatic retries for transient
123
+ errors (5xx, 429, connection failures, timeouts). Set to
124
+ ``0`` to disable.
125
+ http_client: Pre-configured ``httpx.Client`` to use for HTTP
126
+ transport. When supplied, the SDK does **not** close it —
127
+ the caller retains ownership. ``timeout`` and ``limits``
128
+ are ignored (configure them on the client directly).
129
+ lazy: When ``True``, skip the automatic ``GET /v1/auth/whoami``
130
+ call during construction. ``org_id`` will remain ``None``
131
+ unless provided explicitly or resolved later via
132
+ :meth:`get_whoami`. Defaults to ``False``.
133
+ """
134
+
135
+ def __init__(
136
+ self,
137
+ *,
138
+ config: K2Config | None = None,
139
+ api_host: str | object = _SENTINEL,
140
+ api_key: str | None | object = _SENTINEL,
141
+ org_id: str | None | object = _SENTINEL,
142
+ bearer_token: str | None | object = _SENTINEL,
143
+ bearer_token_factory: Callable[[], str] | None = None,
144
+ token_cache_ttl: float = 300.0,
145
+ admin_token: str | None | object = _SENTINEL,
146
+ headers: dict[str, str] | None = None,
147
+ user_agent: str | None = None,
148
+ timeout: float | ClientTimeouts | httpx.Timeout | None = None,
149
+ limits: ClientLimits | None = None,
150
+ max_retries: int | object = _SENTINEL,
151
+ validate_responses: bool | object = _SENTINEL,
152
+ http_client: httpx.Client | None = None,
153
+ lazy: bool = False,
154
+ ) -> None:
155
+ # Resolve config vs explicit kwargs (explicit kwargs win)
156
+ resolved_api_host: str = DEFAULT_API_HOST
157
+ resolved_api_key: str | None = None
158
+ resolved_org_id: str | None = None
159
+ resolved_bearer_token: str | None = None
160
+ resolved_admin_token: str | None = None
161
+ resolved_max_retries: int = 2
162
+ resolved_validate_responses: bool = False
163
+
164
+ if config is not None:
165
+ resolved_api_host = str(config.api_host)
166
+ resolved_api_key = config.api_key
167
+ resolved_org_id = config.org_id
168
+ resolved_bearer_token = config.bearer_token
169
+ resolved_admin_token = config.admin_token
170
+ resolved_max_retries = config.max_retries
171
+ resolved_validate_responses = getattr(config, "validate_responses", False)
172
+ # Build timeout from config if no explicit timeout given
173
+ if timeout is None:
174
+ timeout = ClientTimeouts(
175
+ connect=config.timeout_connect,
176
+ read=config.timeout_read,
177
+ write=config.timeout_write,
178
+ )
179
+ # Build limits from config if no explicit limits given
180
+ if limits is None:
181
+ limits = ClientLimits(
182
+ max_connections=config.max_connections,
183
+ max_keepalive_connections=config.max_keepalive_connections,
184
+ )
185
+
186
+ # Explicit kwargs override config
187
+ if api_host is not _SENTINEL:
188
+ resolved_api_host = api_host # type: ignore[assignment]
189
+ if api_key is not _SENTINEL:
190
+ resolved_api_key = api_key # type: ignore[assignment]
191
+ if org_id is not _SENTINEL:
192
+ resolved_org_id = org_id # type: ignore[assignment]
193
+ if bearer_token is not _SENTINEL:
194
+ resolved_bearer_token = bearer_token # type: ignore[assignment]
195
+ if admin_token is not _SENTINEL:
196
+ resolved_admin_token = admin_token # type: ignore[assignment]
197
+ if max_retries is not _SENTINEL:
198
+ resolved_max_retries = max_retries # type: ignore[assignment]
199
+ if validate_responses is not _SENTINEL:
200
+ resolved_validate_responses = validate_responses # type: ignore[assignment]
201
+
202
+ super().__init__(
203
+ resolved_api_host,
204
+ resolved_api_key,
205
+ bearer_token=resolved_bearer_token,
206
+ bearer_token_factory=bearer_token_factory,
207
+ token_cache_ttl=token_cache_ttl,
208
+ admin_token=resolved_admin_token,
209
+ headers=headers,
210
+ user_agent=user_agent,
211
+ timeout=timeout,
212
+ limits=limits,
213
+ max_retries=resolved_max_retries,
214
+ validate_responses=resolved_validate_responses,
215
+ http_client=http_client,
216
+ )
217
+ self.org_id = resolved_org_id
218
+ if not lazy and self.org_id is None and resolved_api_key is not None:
219
+ whoami = self.get_whoami()
220
+ self.org_id = whoami["org_id"] if isinstance(whoami, dict) else whoami.org_id # type: ignore[attr-defined]
221
+
222
+ # ------------------------------------------------------------------
223
+ # Factory classmethods
224
+ # ------------------------------------------------------------------
225
+
226
+ @classmethod
227
+ def from_env(cls, **overrides: Any) -> Knowledge2:
228
+ """Create a client from ``K2_*`` environment variables.
229
+
230
+ Equivalent to ``Knowledge2(config=K2Config())``, with any
231
+ *overrides* passed as constructor kwargs.
232
+
233
+ Requires ``pydantic-settings`` — install via
234
+ ``pip install knowledge2[config]``.
235
+ """
236
+ from sdk.config import K2Config
237
+
238
+ config = K2Config()
239
+ return cls(config=config, **overrides)
240
+
241
+ @classmethod
242
+ def from_file(cls, path: str | Path, **overrides: Any) -> Knowledge2:
243
+ """Create a client from a JSON or YAML config file.
244
+
245
+ Args:
246
+ path: Path to the configuration file.
247
+ **overrides: K2Config field values that override file
248
+ values (e.g. ``api_key="k2_..."``).
249
+ """
250
+ from sdk.config import K2Config
251
+
252
+ config = K2Config.from_file(path, **overrides)
253
+ return cls(config=config)
254
+
255
+ @classmethod
256
+ def from_profile(
257
+ cls,
258
+ name: str,
259
+ path: str | Path | None = None,
260
+ **overrides: Any,
261
+ ) -> Knowledge2:
262
+ """Create a client from a named profile in a config file.
263
+
264
+ Args:
265
+ name: Profile key to load (e.g. ``"staging"``).
266
+ path: Config file path. Defaults to ``~/.k2/config.yaml``.
267
+ **overrides: K2Config field values that override profile
268
+ values (e.g. ``api_key="k2_..."``).
269
+ """
270
+ from sdk.config import K2Config
271
+
272
+ config = K2Config.from_profile(name, path=path, **overrides)
273
+ return cls(config=config)
274
+
275
+ # ------------------------------------------------------------------
276
+ # Auth & debug helpers
277
+ # ------------------------------------------------------------------
278
+
279
+ def is_authenticated(self) -> bool:
280
+ """Return ``True`` if any authentication credential is configured."""
281
+ return bool(
282
+ self.api_key or self.bearer_token or self.admin_token or self._bearer_token_factory
283
+ )
284
+
285
+ @staticmethod
286
+ def set_debug(enabled: bool = True) -> None:
287
+ """Enable or disable SDK debug logging.
288
+
289
+ When enabled, request method, URL, response status, and retry
290
+ attempts are logged to stderr. Authentication headers are
291
+ redacted.
292
+
293
+ Args:
294
+ enabled: ``True`` to turn on debug logging, ``False`` to
295
+ turn it off.
296
+ """
297
+ _set_debug(enabled)
298
+
299
+ # ------------------------------------------------------------------
300
+ # Namespace accessors
301
+ # ------------------------------------------------------------------
302
+
303
+ @cached_property
304
+ def with_raw_response(self) -> WithRawResponse:
305
+ """Return an adapter that wraps every method to return :class:`RawResponse`.
306
+
307
+ Usage::
308
+
309
+ raw = client.with_raw_response.list_corpora()
310
+ raw.status_code # 200
311
+ raw.headers # {"content-type": "application/json", ...}
312
+ raw.parsed # Page[dict] — same typed result as normal call
313
+ """
314
+ return WithRawResponse(self)
315
+
316
+ @cached_property
317
+ def documents(self) -> DocumentsNamespace:
318
+ """Document operations namespace: ``client.documents.*``"""
319
+ from sdk.namespaces import DocumentsNamespace
320
+
321
+ return DocumentsNamespace(self)
322
+
323
+ @cached_property
324
+ def corpora(self) -> CorporaNamespace:
325
+ """Corpus operations namespace: ``client.corpora.*``"""
326
+ from sdk.namespaces import CorporaNamespace
327
+
328
+ return CorporaNamespace(self)
329
+
330
+ @cached_property
331
+ def search_ns(self) -> SearchNamespace:
332
+ """Search operations namespace: ``client.search_ns.*``
333
+
334
+ Named ``search_ns`` to avoid shadowing the :meth:`search` method.
335
+ """
336
+ from sdk.namespaces import SearchNamespace
337
+
338
+ return SearchNamespace(self)
339
+
340
+ @cached_property
341
+ def models_ns(self) -> ModelsNamespace:
342
+ """Model operations namespace: ``client.models_ns.*``
343
+
344
+ Named ``models_ns`` to avoid shadowing the ``models`` attribute
345
+ used by some serialization libraries.
346
+ """
347
+ from sdk.namespaces import ModelsNamespace
348
+
349
+ return ModelsNamespace(self)
350
+
351
+ @cached_property
352
+ def jobs(self) -> JobsNamespace:
353
+ """Job operations namespace: ``client.jobs.*``"""
354
+ from sdk.namespaces import JobsNamespace
355
+
356
+ return JobsNamespace(self)
357
+
358
+ @cached_property
359
+ def training_ns(self) -> TrainingNamespace:
360
+ """Training operations namespace: ``client.training_ns.*``"""
361
+ from sdk.namespaces import TrainingNamespace
362
+
363
+ return TrainingNamespace(self)
364
+
365
+ @cached_property
366
+ def deployments(self) -> DeploymentsNamespace:
367
+ """Deployment operations namespace: ``client.deployments.*``"""
368
+ from sdk.namespaces import DeploymentsNamespace
369
+
370
+ return DeploymentsNamespace(self)
371
+
372
+ @cached_property
373
+ def auth(self) -> AuthNamespace:
374
+ """Auth operations namespace: ``client.auth.*``"""
375
+ from sdk.namespaces import AuthNamespace
376
+
377
+ return AuthNamespace(self)
378
+
379
+
380
+ class WithRawResponse:
381
+ """Adapter that wraps a :class:`Knowledge2` client so that every
382
+ public method returns a :class:`~sdk._raw_response.RawResponse`
383
+ exposing HTTP status code, headers, and the parsed body.
384
+
385
+ Thread-safe: uses a ``threading.local`` flag so concurrent threads
386
+ do not interfere with each other.
387
+ """
388
+
389
+ def __init__(self, client: Knowledge2) -> None:
390
+ self._client = client
391
+
392
+ def __getattr__(self, name: str) -> Any:
393
+ attr = getattr(self._client, name)
394
+ if not callable(attr) or name.startswith("_"):
395
+ raise AttributeError(name)
396
+
397
+ @functools.wraps(attr)
398
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
399
+ self._client._raw_response_flag.enabled = True
400
+ try:
401
+ return attr(*args, **kwargs)
402
+ finally:
403
+ self._client._raw_response_flag.enabled = False
404
+
405
+ return wrapper
406
+
407
+
408
+ class Knowledge2Validated(Knowledge2):
409
+ """Knowledge2 client with Pydantic response validation enabled by default.
410
+
411
+ Equivalent to ``Knowledge2(validate_responses=True)``.
412
+ Requires: ``pip install knowledge2[pydantic]``
413
+ """
414
+
415
+ def __init__(self, **kwargs: Any) -> None:
416
+ kwargs.setdefault("validate_responses", True)
417
+ super().__init__(**kwargs)
sdk/config.py ADDED
@@ -0,0 +1,182 @@
1
+ """Typed, validated, immutable configuration for the Knowledge2 SDK.
2
+
3
+ Requires the ``pydantic-settings`` package::
4
+
5
+ pip install knowledge2[config]
6
+
7
+ Usage::
8
+
9
+ from sdk import Knowledge2, K2Config
10
+
11
+ # From environment variables (K2_API_KEY, K2_API_HOST, etc.)
12
+ config = K2Config()
13
+ client = Knowledge2(config=config)
14
+
15
+ # Explicit values — invalid config raises at construction
16
+ config = K2Config(api_key="k2_...", max_retries=5)
17
+ client = Knowledge2(config=config)
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ from pathlib import Path
24
+ from typing import Any
25
+
26
+ try:
27
+ from pydantic import Field, PositiveFloat, PositiveInt
28
+ from pydantic_settings import BaseSettings, SettingsConfigDict
29
+ except ImportError as _exc:
30
+ raise ImportError(
31
+ "K2Config requires 'pydantic-settings'. Install it with: pip install knowledge2[config]"
32
+ ) from _exc
33
+
34
+
35
+ class K2Config(BaseSettings):
36
+ """Frozen, validated configuration for :class:`~sdk.client.Knowledge2`.
37
+
38
+ Populated from environment variables with the ``K2_`` prefix
39
+ (e.g. ``K2_API_KEY``, ``K2_API_HOST``), or passed directly.
40
+
41
+ Invalid values (negative timeouts, unknown keys) raise a
42
+ :class:`pydantic.ValidationError` at construction time — not at
43
+ first request.
44
+
45
+ The object is **frozen** after creation; mutating fields raises an
46
+ error.
47
+ """
48
+
49
+ api_host: str = Field(default="https://api.knowledge2.ai")
50
+ api_key: str | None = Field(default=None, repr=False)
51
+ org_id: str | None = Field(default=None)
52
+ bearer_token: str | None = Field(default=None, repr=False)
53
+ admin_token: str | None = Field(default=None, repr=False)
54
+ max_retries: PositiveInt = Field(default=2)
55
+ timeout_connect: PositiveFloat | None = Field(default=5.0)
56
+ timeout_read: PositiveFloat | None = Field(default=60.0)
57
+ timeout_write: PositiveFloat | None = Field(default=30.0)
58
+ max_connections: PositiveInt = Field(default=20)
59
+ max_keepalive_connections: PositiveInt = Field(default=10)
60
+
61
+ model_config = SettingsConfigDict(
62
+ env_prefix="K2_",
63
+ frozen=True,
64
+ extra="forbid",
65
+ )
66
+
67
+ @classmethod
68
+ def from_file(cls, path: str | Path, **overrides: Any) -> K2Config:
69
+ """Load configuration from a JSON or YAML file.
70
+
71
+ YAML support requires ``PyYAML`` (install via
72
+ ``pip install knowledge2[yaml]``).
73
+
74
+ Explicit *overrides* take precedence over file values.
75
+
76
+ Args:
77
+ path: Path to a JSON or YAML configuration file.
78
+ **overrides: K2Config field values that override the file
79
+ contents (e.g. ``api_key="k2_..."``). Only config
80
+ field names are accepted.
81
+
82
+ Returns:
83
+ A frozen :class:`K2Config` instance.
84
+ """
85
+ p = Path(path).expanduser()
86
+ raw = p.read_text(encoding="utf-8")
87
+
88
+ suffix = p.suffix.lower()
89
+ if suffix in {".yaml", ".yml"}:
90
+ try:
91
+ import yaml # type: ignore[import-untyped]
92
+ except ImportError as exc:
93
+ raise ImportError(
94
+ "YAML config requires 'PyYAML'. Install it with: pip install knowledge2[yaml]"
95
+ ) from exc
96
+ data: dict[str, Any] = yaml.safe_load(raw) or {}
97
+ else:
98
+ data = json.loads(raw)
99
+
100
+ if not isinstance(data, dict):
101
+ raise TypeError(
102
+ f"Config file must contain a JSON/YAML mapping, got {type(data).__name__}"
103
+ )
104
+ data.update(overrides)
105
+ return cls(**data)
106
+
107
+ @classmethod
108
+ def from_profile(
109
+ cls,
110
+ name: str,
111
+ path: str | Path | None = None,
112
+ **overrides: Any,
113
+ ) -> K2Config:
114
+ """Load a named profile from a multi-profile config file.
115
+
116
+ The file must contain a top-level mapping where each key is a
117
+ profile name and each value is a config dict::
118
+
119
+ # ~/.k2/config.yaml
120
+ default:
121
+ api_key: k2_prod_...
122
+ staging:
123
+ api_host: https://staging.api.knowledge2.ai
124
+ api_key: k2_stg_...
125
+
126
+ Args:
127
+ name: The profile key to load.
128
+ path: Config file path. Defaults to ``~/.k2/config.yaml``
129
+ (falls back to ``~/.k2/config.json``).
130
+ **overrides: K2Config field values that override the
131
+ profile contents. Only config field names are accepted.
132
+
133
+ Returns:
134
+ A frozen :class:`K2Config` instance.
135
+
136
+ Raises:
137
+ FileNotFoundError: If no config file is found.
138
+ KeyError: If the profile name is not in the file.
139
+ """
140
+ if path is not None:
141
+ resolved = Path(path).expanduser()
142
+ else:
143
+ base = Path.home() / ".k2"
144
+ yaml_path = base / "config.yaml"
145
+ yml_path = base / "config.yml"
146
+ json_path = base / "config.json"
147
+ if yaml_path.exists():
148
+ resolved = yaml_path
149
+ elif yml_path.exists():
150
+ resolved = yml_path
151
+ elif json_path.exists():
152
+ resolved = json_path
153
+ else:
154
+ raise FileNotFoundError(
155
+ f"No config file found at {yaml_path}, {yml_path}, or {json_path}"
156
+ )
157
+
158
+ raw = resolved.read_text(encoding="utf-8")
159
+ suffix = resolved.suffix.lower()
160
+ if suffix in {".yaml", ".yml"}:
161
+ try:
162
+ import yaml
163
+ except ImportError as exc:
164
+ raise ImportError(
165
+ "YAML config requires 'PyYAML'. Install it with: pip install knowledge2[yaml]"
166
+ ) from exc
167
+ all_profiles: dict[str, Any] = yaml.safe_load(raw) or {}
168
+ else:
169
+ all_profiles = json.loads(raw)
170
+
171
+ if not isinstance(all_profiles, dict):
172
+ raise TypeError(
173
+ f"Config file must contain a mapping of profiles, got {type(all_profiles).__name__}"
174
+ )
175
+ if name not in all_profiles:
176
+ raise KeyError(
177
+ f"Profile '{name}' not found. Available profiles: {', '.join(all_profiles.keys())}"
178
+ )
179
+
180
+ data = dict(all_profiles[name])
181
+ data.update(overrides)
182
+ return cls(**data)