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
sdk/_preview.py ADDED
@@ -0,0 +1,70 @@
1
+ """Decorator for marking SDK resource classes as preview features."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import functools
6
+ import inspect
7
+ import warnings
8
+ from typing import Any, TypeVar
9
+
10
+ _warned: set[str] = set()
11
+
12
+ _T = TypeVar("_T", bound=type)
13
+
14
+
15
+ def preview_resource(cls: _T) -> _T:
16
+ """Class decorator that wraps public methods to emit a one-shot RuntimeWarning.
17
+
18
+ The warning fires once per method name per process, alerting callers
19
+ that the resource requires a server-side feature flag.
20
+
21
+ Properties and private/dunder methods are left untouched.
22
+ """
23
+ for name, attr in list(vars(cls).items()):
24
+ if name.startswith("_"):
25
+ continue
26
+ if isinstance(attr, (property, classmethod, staticmethod)):
27
+ continue
28
+ if not callable(attr):
29
+ continue
30
+
31
+ fn = attr
32
+
33
+ if inspect.iscoroutinefunction(fn):
34
+
35
+ @functools.wraps(fn)
36
+ async def async_wrapper(
37
+ *args: Any, _fn: Any = fn, _name: str = name, **kwargs: Any
38
+ ) -> Any:
39
+ key = f"{_fn.__qualname__}"
40
+ if key not in _warned:
41
+ _warned.add(key)
42
+ warnings.warn(
43
+ f"{_name}() is a preview feature and may not be available "
44
+ f"in all environments. The underlying API requires a "
45
+ f"feature flag to be enabled.",
46
+ RuntimeWarning,
47
+ stacklevel=2,
48
+ )
49
+ return await _fn(*args, **kwargs)
50
+
51
+ setattr(cls, name, async_wrapper)
52
+ else:
53
+
54
+ @functools.wraps(fn)
55
+ def sync_wrapper(*args: Any, _fn: Any = fn, _name: str = name, **kwargs: Any) -> Any:
56
+ key = f"{_fn.__qualname__}"
57
+ if key not in _warned:
58
+ _warned.add(key)
59
+ warnings.warn(
60
+ f"{_name}() is a preview feature and may not be available "
61
+ f"in all environments. The underlying API requires a "
62
+ f"feature flag to be enabled.",
63
+ RuntimeWarning,
64
+ stacklevel=2,
65
+ )
66
+ return _fn(*args, **kwargs)
67
+
68
+ setattr(cls, name, sync_wrapper)
69
+
70
+ return cls
sdk/_raw_response.py ADDED
@@ -0,0 +1,25 @@
1
+ """Raw HTTP response wrapper for the Knowledge2 SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Generic, TypeVar
7
+
8
+ T = TypeVar("T")
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class RawResponse(Generic[T]):
13
+ """Typed wrapper exposing HTTP-level details alongside the parsed body.
14
+
15
+ Returned by ``client.with_raw_response.<method>(...)``.
16
+
17
+ Attributes:
18
+ status_code: HTTP status code (e.g. 200, 201).
19
+ headers: Response headers as a plain dict.
20
+ parsed: The same typed result the normal method would return.
21
+ """
22
+
23
+ status_code: int
24
+ headers: dict[str, str]
25
+ parsed: T
@@ -0,0 +1,51 @@
1
+ """Per-call request options for the Knowledge2 SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from sdk._base import ClientTimeouts
8
+
9
+ _BLOCKED_HEADERS = frozenset(
10
+ {
11
+ "authorization",
12
+ "content-type",
13
+ "accept",
14
+ "content-length",
15
+ "x-api-key",
16
+ "x-admin-token",
17
+ "user-agent",
18
+ }
19
+ )
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class RequestOptions:
24
+ """Per-call overrides for timeout, retry, and passthrough headers.
25
+
26
+ Any field left as ``None`` inherits the client-level default.
27
+
28
+ Args:
29
+ timeout: Override the client-level :class:`ClientTimeouts` for
30
+ this single call.
31
+ max_retries: Override the client-level ``max_retries`` for this
32
+ single call.
33
+ passthrough_headers: Non-semantic headers (e.g. ``X-Request-ID``,
34
+ ``X-Correlation-ID``) forwarded verbatim. SDK-managed headers
35
+ (``Authorization``, ``Content-Type``, ``X-API-Key``, etc.)
36
+ raise :class:`ValueError` if supplied.
37
+ """
38
+
39
+ timeout: ClientTimeouts | None = None
40
+ max_retries: int | None = None
41
+ passthrough_headers: dict[str, str] | None = None
42
+
43
+ def __post_init__(self) -> None:
44
+ if self.passthrough_headers:
45
+ for name in self.passthrough_headers:
46
+ if name.lower() in _BLOCKED_HEADERS:
47
+ raise ValueError(
48
+ f"Header {name!r} is managed by the SDK and cannot be "
49
+ f"set via passthrough_headers. "
50
+ f"Blocked headers: {sorted(_BLOCKED_HEADERS)}"
51
+ )
sdk/_transport.py ADDED
@@ -0,0 +1,144 @@
1
+ """Shared transport helpers for sync and async HTTP clients.
2
+
3
+ All functions are stateless and side-effect-free (except logging).
4
+ They are extracted from :class:`BaseClient` so that
5
+ :class:`AsyncBaseClient` can reuse the same logic without duplication.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import contextlib
11
+ import random
12
+ from typing import Any
13
+
14
+ import httpx
15
+
16
+ from sdk.errors import (
17
+ APIError,
18
+ AuthenticationError,
19
+ BadRequestError,
20
+ ConflictError,
21
+ Knowledge2Error,
22
+ NotFoundError,
23
+ PermissionDeniedError,
24
+ RateLimitError,
25
+ ServerError,
26
+ ValidationError,
27
+ )
28
+
29
+ # Status codes that map to specific error subclasses.
30
+ _STATUS_ERROR_MAP: dict[int, type[APIError]] = {
31
+ 400: BadRequestError,
32
+ 401: AuthenticationError,
33
+ 403: PermissionDeniedError,
34
+ 404: NotFoundError,
35
+ 409: ConflictError,
36
+ 422: ValidationError,
37
+ 429: RateLimitError,
38
+ 500: ServerError,
39
+ 502: ServerError,
40
+ 503: ServerError,
41
+ 504: ServerError,
42
+ }
43
+
44
+
45
+ def error_from_response(response: httpx.Response) -> APIError:
46
+ """Parse an error response into the appropriate APIError subclass."""
47
+ request_id = response.headers.get("X-Request-Id")
48
+ code: str | None = None
49
+ details: Any = None
50
+ message = response.text or response.reason_phrase or "Unknown error"
51
+ try:
52
+ payload = response.json()
53
+ except ValueError:
54
+ payload = None
55
+ if isinstance(payload, dict):
56
+ error = payload.get("error")
57
+ if isinstance(error, dict):
58
+ code = error.get("code")
59
+ details = error.get("details")
60
+ request_id = error.get("request_id") or request_id
61
+ message = error.get("message") or message
62
+ elif "detail" in payload:
63
+ detail = payload.get("detail")
64
+ if isinstance(detail, str):
65
+ message = detail
66
+ else:
67
+ details = detail
68
+ if request_id:
69
+ message = f"{message} (request_id={request_id})"
70
+
71
+ status = response.status_code
72
+ error_cls = _STATUS_ERROR_MAP.get(status)
73
+ if error_cls is None:
74
+ error_cls = ServerError if 500 <= status < 600 else APIError
75
+
76
+ if error_cls is RateLimitError:
77
+ retry_after_raw = response.headers.get("Retry-After")
78
+ retry_after: float | None = None
79
+ if retry_after_raw is not None:
80
+ with contextlib.suppress(ValueError, TypeError):
81
+ retry_after = float(retry_after_raw)
82
+ return RateLimitError(
83
+ message,
84
+ status_code=status,
85
+ retry_after=retry_after,
86
+ code=code,
87
+ details=details,
88
+ request_id=request_id,
89
+ )
90
+
91
+ return error_cls(
92
+ message,
93
+ status_code=status,
94
+ code=code,
95
+ details=details,
96
+ request_id=request_id,
97
+ )
98
+
99
+
100
+ def calculate_backoff(
101
+ attempt: int,
102
+ error: Knowledge2Error | None,
103
+ *,
104
+ backoff_factor: float = 0.5,
105
+ backoff_max: float = 8.0,
106
+ ) -> float:
107
+ """Calculate exponential backoff delay with jitter."""
108
+ if isinstance(error, RateLimitError) and error.retry_after is not None:
109
+ return error.retry_after
110
+ base = backoff_factor * (2**attempt)
111
+ jitter = random.random() * 0.25 * base
112
+ return min(base + jitter, backoff_max)
113
+
114
+
115
+ def build_auth_headers(
116
+ *,
117
+ api_key: str | None,
118
+ bearer_token: str | None,
119
+ admin_token: str | None,
120
+ user_agent: str,
121
+ default_headers: dict[str, str],
122
+ extra: dict[str, str] | None = None,
123
+ ) -> dict[str, str]:
124
+ """Assemble request headers with auth credentials."""
125
+ headers = dict(default_headers)
126
+ extra_headers = extra if extra is not None else {}
127
+ if api_key:
128
+ headers["X-API-Key"] = api_key
129
+ if "X-API-Key" in extra_headers:
130
+ extra_headers["X-API-Key"] = api_key
131
+ if bearer_token:
132
+ bearer_value = f"Bearer {bearer_token}"
133
+ headers["Authorization"] = bearer_value
134
+ if "Authorization" in extra_headers:
135
+ extra_headers["Authorization"] = bearer_value
136
+ if admin_token:
137
+ headers["X-Admin-Token"] = admin_token
138
+ if "X-Admin-Token" in extra_headers:
139
+ extra_headers["X-Admin-Token"] = admin_token
140
+ if user_agent and "User-Agent" not in headers and "User-Agent" not in extra_headers:
141
+ headers["User-Agent"] = user_agent
142
+ if extra_headers:
143
+ headers.update(extra_headers)
144
+ return headers
sdk/_validation.py ADDED
@@ -0,0 +1,25 @@
1
+ """Parameter validation utilities for the Knowledge2 SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ def require_str(value: str, name: str) -> str:
7
+ """Validate that a required string parameter is non-empty.
8
+
9
+ Strips whitespace and raises :class:`ValueError` if the result is
10
+ empty. Returns the stripped value for use as the canonical form.
11
+
12
+ Args:
13
+ value: The parameter value to validate.
14
+ name: The parameter name (used in the error message).
15
+
16
+ Returns:
17
+ The stripped, non-empty string.
18
+
19
+ Raises:
20
+ ValueError: If *value* is not a string or is empty/whitespace-only.
21
+ """
22
+ stripped = value.strip() if isinstance(value, str) else ""
23
+ if not stripped:
24
+ raise ValueError(f"'{name}' must be a non-empty string, got {value!r}")
25
+ return stripped
@@ -0,0 +1,36 @@
1
+ """Shared response validation logic for sync and async clients."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ def maybe_validate(data: Any, model_name: str, *, validate: bool, raw_response_cls: type) -> Any:
9
+ """Validate response data through its Pydantic model if validation is enabled.
10
+
11
+ Args:
12
+ data: The raw dict from ``_request()``.
13
+ model_name: The TypedDict class name (e.g. ``"SearchResponse"``).
14
+ validate: Whether response validation is enabled.
15
+ raw_response_cls: The RawResponse class to check against.
16
+
17
+ Returns:
18
+ The validated Pydantic model instance if validation is on,
19
+ otherwise the raw dict unchanged.
20
+ """
21
+ if isinstance(data, raw_response_cls):
22
+ return data
23
+ if not validate or data is None:
24
+ return data
25
+ try:
26
+ from sdk.models._registry import get_model_for_type
27
+ except ImportError as exc:
28
+ raise ImportError(
29
+ "Response validation requires the optional 'pydantic' dependency. "
30
+ "Install with: pip install 'knowledge2[pydantic]'"
31
+ ) from exc
32
+
33
+ model_cls = get_model_for_type(model_name)
34
+ if model_cls is None:
35
+ return data
36
+ return model_cls.model_validate(data) # type: ignore[attr-defined]
sdk/_version.py ADDED
@@ -0,0 +1,3 @@
1
+ """Single source of truth for the SDK version string."""
2
+
3
+ __version__ = "0.4.0"
sdk/async_client.py ADDED
@@ -0,0 +1,320 @@
1
+ """Async Knowledge2 API client.
2
+
3
+ Usage::
4
+
5
+ from sdk import AsyncKnowledge2
6
+
7
+ async with AsyncKnowledge2(api_key="k2_...") as client:
8
+ results = await client.search(corpus_id, "my query")
9
+
10
+ # Or use the async factory to auto-detect org_id:
11
+ client = await AsyncKnowledge2.create(api_key="k2_...")
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import contextvars
17
+ import functools
18
+ from collections.abc import Callable
19
+ from pathlib import Path
20
+ from typing import TYPE_CHECKING, Any
21
+
22
+ import httpx
23
+
24
+ from sdk._async_base import AsyncBaseClient
25
+ from sdk._base import ClientLimits, ClientTimeouts
26
+ from sdk._logging import set_debug as _set_debug
27
+ from sdk.async_resources import (
28
+ AsyncA2AMixin,
29
+ AsyncAgentsMixin,
30
+ AsyncAuditMixin,
31
+ AsyncAuthMixin,
32
+ AsyncConsoleMixin,
33
+ AsyncCorporaMixin,
34
+ AsyncDeploymentsMixin,
35
+ AsyncDocumentsMixin,
36
+ AsyncFeedsMixin,
37
+ AsyncIndexesMixin,
38
+ AsyncJobsMixin,
39
+ AsyncMetadataMixin,
40
+ AsyncModelsMixin,
41
+ AsyncOnboardingMixin,
42
+ AsyncOrgsMixin,
43
+ AsyncPipelinesMixin,
44
+ AsyncProjectsMixin,
45
+ AsyncSearchMixin,
46
+ AsyncTrainingMixin,
47
+ AsyncUsageMixin,
48
+ )
49
+
50
+ if TYPE_CHECKING:
51
+ from sdk.config import K2Config
52
+
53
+ DEFAULT_API_HOST = "https://api.knowledge2.ai"
54
+
55
+ _SENTINEL = object()
56
+
57
+
58
+ class AsyncKnowledge2(
59
+ AsyncBaseClient,
60
+ AsyncOrgsMixin,
61
+ AsyncAuthMixin,
62
+ AsyncProjectsMixin,
63
+ AsyncCorporaMixin,
64
+ AsyncModelsMixin,
65
+ AsyncDocumentsMixin,
66
+ AsyncIndexesMixin,
67
+ AsyncSearchMixin,
68
+ AsyncMetadataMixin,
69
+ AsyncTrainingMixin,
70
+ AsyncDeploymentsMixin,
71
+ AsyncJobsMixin,
72
+ AsyncAuditMixin,
73
+ AsyncUsageMixin,
74
+ AsyncConsoleMixin,
75
+ AsyncOnboardingMixin,
76
+ AsyncAgentsMixin,
77
+ AsyncFeedsMixin,
78
+ AsyncPipelinesMixin,
79
+ AsyncA2AMixin,
80
+ ):
81
+ """Async Knowledge2 API client.
82
+
83
+ The async variant of :class:`~sdk.client.Knowledge2`. Supports
84
+ ``async with`` context management and ``await``-based API calls.
85
+
86
+ Note: ``org_id`` auto-detection requires an async network call, so
87
+ it cannot happen in ``__init__``. Use :meth:`create` for automatic
88
+ ``org_id`` resolution.
89
+
90
+ Args:
91
+ config: Optional :class:`~sdk.config.K2Config` instance.
92
+ api_host: Base URL of the Knowledge2 API.
93
+ api_key: API key for authentication (``X-API-Key`` header).
94
+ org_id: Organisation ID. Use :meth:`create` for auto-detection.
95
+ bearer_token: Bearer token for console / Auth0 authentication.
96
+ bearer_token_factory: Callable returning a bearer token string.
97
+ token_cache_ttl: Seconds to cache the factory-produced token.
98
+ admin_token: Admin token (``X-Admin-Token`` header).
99
+ headers: Extra default headers sent with every request.
100
+ user_agent: Custom ``User-Agent`` header value.
101
+ timeout: Request timeout in seconds (or an ``httpx.Timeout``).
102
+ limits: Connection pool limits.
103
+ max_retries: Maximum number of automatic retries.
104
+ validate_responses: Enable Pydantic response validation.
105
+ http_client: Pre-configured ``httpx.AsyncClient``. When
106
+ supplied the SDK does **not** close it — the caller retains
107
+ ownership.
108
+ """
109
+
110
+ def __init__(
111
+ self,
112
+ *,
113
+ config: K2Config | None = None,
114
+ api_host: str | object = _SENTINEL,
115
+ api_key: str | None | object = _SENTINEL,
116
+ org_id: str | None | object = _SENTINEL,
117
+ bearer_token: str | None | object = _SENTINEL,
118
+ bearer_token_factory: Callable[[], str] | None = None,
119
+ token_cache_ttl: float = 300.0,
120
+ admin_token: str | None | object = _SENTINEL,
121
+ headers: dict[str, str] | None = None,
122
+ user_agent: str | None = None,
123
+ timeout: float | ClientTimeouts | httpx.Timeout | None = None,
124
+ limits: ClientLimits | None = None,
125
+ max_retries: int | object = _SENTINEL,
126
+ validate_responses: bool | object = _SENTINEL,
127
+ http_client: httpx.AsyncClient | None = None,
128
+ ) -> None:
129
+ # Resolve config vs explicit kwargs (explicit kwargs win)
130
+ resolved_api_host: str = DEFAULT_API_HOST
131
+ resolved_api_key: str | None = None
132
+ resolved_org_id: str | None = None
133
+ resolved_bearer_token: str | None = None
134
+ resolved_admin_token: str | None = None
135
+ resolved_max_retries: int = 2
136
+ resolved_validate_responses: bool = False
137
+
138
+ if config is not None:
139
+ resolved_api_host = str(config.api_host)
140
+ resolved_api_key = config.api_key
141
+ resolved_org_id = config.org_id
142
+ resolved_bearer_token = config.bearer_token
143
+ resolved_admin_token = config.admin_token
144
+ resolved_max_retries = config.max_retries
145
+ resolved_validate_responses = getattr(config, "validate_responses", False)
146
+ # Build timeout from config if no explicit timeout given
147
+ if timeout is None:
148
+ timeout = ClientTimeouts(
149
+ connect=config.timeout_connect,
150
+ read=config.timeout_read,
151
+ write=config.timeout_write,
152
+ )
153
+ # Build limits from config if no explicit limits given
154
+ if limits is None:
155
+ limits = ClientLimits(
156
+ max_connections=config.max_connections,
157
+ max_keepalive_connections=config.max_keepalive_connections,
158
+ )
159
+
160
+ # Explicit kwargs override config
161
+ if api_host is not _SENTINEL:
162
+ resolved_api_host = api_host # type: ignore[assignment]
163
+ if api_key is not _SENTINEL:
164
+ resolved_api_key = api_key # type: ignore[assignment]
165
+ if org_id is not _SENTINEL:
166
+ resolved_org_id = org_id # type: ignore[assignment]
167
+ if bearer_token is not _SENTINEL:
168
+ resolved_bearer_token = bearer_token # type: ignore[assignment]
169
+ if admin_token is not _SENTINEL:
170
+ resolved_admin_token = admin_token # type: ignore[assignment]
171
+ if max_retries is not _SENTINEL:
172
+ resolved_max_retries = max_retries # type: ignore[assignment]
173
+ if validate_responses is not _SENTINEL:
174
+ resolved_validate_responses = validate_responses # type: ignore[assignment]
175
+
176
+ super().__init__(
177
+ resolved_api_host,
178
+ resolved_api_key,
179
+ bearer_token=resolved_bearer_token,
180
+ bearer_token_factory=bearer_token_factory,
181
+ token_cache_ttl=token_cache_ttl,
182
+ admin_token=resolved_admin_token,
183
+ headers=headers,
184
+ user_agent=user_agent,
185
+ timeout=timeout,
186
+ limits=limits,
187
+ max_retries=resolved_max_retries,
188
+ validate_responses=resolved_validate_responses,
189
+ http_client=http_client,
190
+ )
191
+ self.org_id = resolved_org_id
192
+
193
+ # ------------------------------------------------------------------
194
+ # Async factory
195
+ # ------------------------------------------------------------------
196
+
197
+ @classmethod
198
+ async def create(cls, *, lazy: bool = False, **kwargs: Any) -> AsyncKnowledge2:
199
+ """Create an async client with automatic org_id detection.
200
+
201
+ Equivalent to constructing the client and then calling
202
+ ``get_whoami()`` to resolve the org_id when an API key is
203
+ provided but no org_id is given.
204
+
205
+ All keyword arguments are forwarded to :class:`AsyncKnowledge2`.
206
+
207
+ Args:
208
+ lazy: When ``True``, skip the automatic
209
+ ``GET /v1/auth/whoami`` call. ``org_id`` will remain
210
+ ``None`` unless provided explicitly or resolved later
211
+ via :meth:`get_whoami`. Defaults to ``False``.
212
+ **kwargs: Forwarded to :class:`AsyncKnowledge2`.
213
+ """
214
+ client = cls(**kwargs)
215
+ if not lazy and client.org_id is None and client.api_key is not None:
216
+ whoami = await client.get_whoami()
217
+ client.org_id = whoami["org_id"] if isinstance(whoami, dict) else whoami.org_id # type: ignore[attr-defined]
218
+ return client
219
+
220
+ # ------------------------------------------------------------------
221
+ # Factory classmethods
222
+ # ------------------------------------------------------------------
223
+
224
+ @classmethod
225
+ async def from_env(cls, **overrides: Any) -> AsyncKnowledge2:
226
+ """Create a client from ``K2_*`` environment variables.
227
+
228
+ Equivalent to ``AsyncKnowledge2.create(config=K2Config())``.
229
+ """
230
+ from sdk.config import K2Config
231
+
232
+ config = K2Config()
233
+ return await cls.create(config=config, **overrides)
234
+
235
+ @classmethod
236
+ async def from_file(cls, path: str | Path, **overrides: Any) -> AsyncKnowledge2:
237
+ """Create a client from a JSON or YAML config file.
238
+
239
+ Args:
240
+ path: Path to the configuration file.
241
+ **overrides: K2Config field values that override file values.
242
+ """
243
+ from sdk.config import K2Config
244
+
245
+ config = K2Config.from_file(path, **overrides)
246
+ return await cls.create(config=config)
247
+
248
+ @classmethod
249
+ async def from_profile(
250
+ cls,
251
+ name: str,
252
+ path: str | Path | None = None,
253
+ **overrides: Any,
254
+ ) -> AsyncKnowledge2:
255
+ """Create a client from a named profile in a config file.
256
+
257
+ Args:
258
+ name: Profile key to load (e.g. ``"staging"``).
259
+ path: Config file path. Defaults to ``~/.k2/config.yaml``.
260
+ **overrides: K2Config field values that override profile values.
261
+ """
262
+ from sdk.config import K2Config
263
+
264
+ config = K2Config.from_profile(name, path=path, **overrides)
265
+ return await cls.create(config=config)
266
+
267
+ # ------------------------------------------------------------------
268
+ # Auth & debug helpers
269
+ # ------------------------------------------------------------------
270
+
271
+ def is_authenticated(self) -> bool:
272
+ """Return ``True`` if any authentication credential is configured."""
273
+ return bool(
274
+ self.api_key or self.bearer_token or self.admin_token or self._bearer_token_factory
275
+ )
276
+
277
+ @staticmethod
278
+ def set_debug(enabled: bool = True) -> None:
279
+ """Enable or disable SDK debug logging."""
280
+ _set_debug(enabled)
281
+
282
+ @functools.cached_property
283
+ def with_raw_response(self) -> AsyncWithRawResponse:
284
+ """Return an adapter that wraps every method to return :class:`RawResponse`.
285
+
286
+ Usage::
287
+
288
+ raw = await async_client.with_raw_response.list_corpora()
289
+ raw.status_code # 200
290
+ raw.headers # {"content-type": "application/json", ...}
291
+ raw.parsed # Page[dict] — same typed result as normal call
292
+ """
293
+ return AsyncWithRawResponse(self)
294
+
295
+
296
+ class AsyncWithRawResponse:
297
+ """Adapter that wraps an :class:`AsyncKnowledge2` client so that every
298
+ public method returns a :class:`~sdk._raw_response.RawResponse`.
299
+
300
+ Task-safe: uses a ``contextvars.ContextVar`` so concurrent tasks
301
+ do not interfere with each other.
302
+ """
303
+
304
+ def __init__(self, client: AsyncKnowledge2) -> None:
305
+ self._client = client
306
+
307
+ def __getattr__(self, name: str) -> Any:
308
+ attr = getattr(self._client, name)
309
+ if not callable(attr) or name.startswith("_"):
310
+ raise AttributeError(name)
311
+
312
+ @functools.wraps(attr)
313
+ async def wrapper(*args: Any, **kwargs: Any) -> Any:
314
+ token = self._client._raw_response_flag.set(True)
315
+ try:
316
+ return await attr(*args, **kwargs)
317
+ finally:
318
+ self._client._raw_response_flag.reset(token)
319
+
320
+ return wrapper