thorbit-sdk 0.2.0__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Thorbit
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,48 @@
1
+ Metadata-Version: 2.4
2
+ Name: thorbit-sdk
3
+ Version: 0.2.0
4
+ Summary: Typed Python client for the Thorbit API
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://thorbit.ai
7
+ Project-URL: Repository, https://github.com/VilovietaSEO/thorbit-sdk
8
+ Project-URL: Issues, https://github.com/VilovietaSEO/thorbit-sdk/issues
9
+ Requires-Python: >=3.11
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: httpx<1,>=0.27
13
+ Requires-Dist: pydantic<3,>=2.8
14
+ Dynamic: license-file
15
+
16
+ # Thorbit Python SDK
17
+
18
+ Typed Python client for the unified Thorbit API. Generated Pydantic models and methods cover all 79 Thorbit tools.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ python -m pip install thorbit-sdk
24
+ ```
25
+
26
+ Python 3.11 or newer is required.
27
+
28
+ ## Use
29
+
30
+ ```python
31
+ import os
32
+
33
+ from thorbit.generated_tools import GeneratedCallThorbitTools
34
+
35
+ with GeneratedCallThorbitTools(
36
+ {"api_key": os.environ["THORBIT_API_KEY"]}
37
+ ) as thorbit:
38
+ response = thorbit.thorbit_account_projects_list({"limit": 25})
39
+ print(response.result)
40
+ ```
41
+
42
+ The lower-level `CallThorbitTools.call_tool` method supports dynamic integrations. Catch the exported stable exception classes for validation, HTTP, timeout, response, and transport failures.
43
+
44
+ Source and the generated contract are public at [VilovietaSEO/thorbit-sdk](https://github.com/VilovietaSEO/thorbit-sdk).
45
+
46
+ ## License
47
+
48
+ MIT
@@ -0,0 +1,33 @@
1
+ # Thorbit Python SDK
2
+
3
+ Typed Python client for the unified Thorbit API. Generated Pydantic models and methods cover all 79 Thorbit tools.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ python -m pip install thorbit-sdk
9
+ ```
10
+
11
+ Python 3.11 or newer is required.
12
+
13
+ ## Use
14
+
15
+ ```python
16
+ import os
17
+
18
+ from thorbit.generated_tools import GeneratedCallThorbitTools
19
+
20
+ with GeneratedCallThorbitTools(
21
+ {"api_key": os.environ["THORBIT_API_KEY"]}
22
+ ) as thorbit:
23
+ response = thorbit.thorbit_account_projects_list({"limit": 25})
24
+ print(response.result)
25
+ ```
26
+
27
+ The lower-level `CallThorbitTools.call_tool` method supports dynamic integrations. Catch the exported stable exception classes for validation, HTTP, timeout, response, and transport failures.
28
+
29
+ Source and the generated contract are public at [VilovietaSEO/thorbit-sdk](https://github.com/VilovietaSEO/thorbit-sdk).
30
+
31
+ ## License
32
+
33
+ MIT
@@ -0,0 +1,24 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "thorbit-sdk"
7
+ version = "0.2.0"
8
+ description = "Typed Python client for the Thorbit API"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.11"
13
+ dependencies = [
14
+ "httpx>=0.27,<1",
15
+ "pydantic>=2.8,<3",
16
+ ]
17
+
18
+ [project.urls]
19
+ Homepage = "https://thorbit.ai"
20
+ Repository = "https://github.com/VilovietaSEO/thorbit-sdk"
21
+ Issues = "https://github.com/VilovietaSEO/thorbit-sdk/issues"
22
+
23
+ [tool.setuptools.packages.find]
24
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,27 @@
1
+ """Public Thorbit Python SDK surface."""
2
+
3
+ from .client import (
4
+ CallThorbitTools,
5
+ ThorbitClientOptions,
6
+ ThorbitClientProtocol,
7
+ ThorbitHttpError,
8
+ ThorbitRequestValidationError,
9
+ ThorbitResponseValidationError,
10
+ ThorbitResultModelError,
11
+ ThorbitSdkError,
12
+ ThorbitTimeoutError,
13
+ ThorbitTransportError,
14
+ )
15
+
16
+ __all__ = [
17
+ "CallThorbitTools",
18
+ "ThorbitClientOptions",
19
+ "ThorbitClientProtocol",
20
+ "ThorbitHttpError",
21
+ "ThorbitRequestValidationError",
22
+ "ThorbitResponseValidationError",
23
+ "ThorbitResultModelError",
24
+ "ThorbitSdkError",
25
+ "ThorbitTimeoutError",
26
+ "ThorbitTransportError",
27
+ ]
@@ -0,0 +1,299 @@
1
+ """Validated synchronous transport for the unified Thorbit tool API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from types import TracebackType
7
+ from typing import Protocol, TypeVar, cast, runtime_checkable
8
+ from urllib.parse import quote
9
+
10
+ import httpx
11
+ from pydantic import (
12
+ AnyHttpUrl,
13
+ BaseModel,
14
+ ConfigDict,
15
+ Field,
16
+ SecretStr,
17
+ TypeAdapter,
18
+ ValidationError,
19
+ field_validator,
20
+ )
21
+ from pydantic.errors import PydanticUserError
22
+
23
+ TResult = TypeVar("TResult")
24
+
25
+ DEFAULT_THORBIT_BASE_URL = "https://thorbit.ai"
26
+ DEFAULT_THORBIT_TIMEOUT_SECONDS = 120.0
27
+ THORBIT_TOOL_ROUTE = "/api/v1/mcp/thorbit"
28
+
29
+
30
+ class ThorbitClientOptions(BaseModel):
31
+ """Validated connection options whose representation redacts the API key."""
32
+
33
+ model_config = ConfigDict(
34
+ extra="forbid",
35
+ frozen=True,
36
+ str_strip_whitespace=True,
37
+ validate_default=True,
38
+ )
39
+
40
+ api_key: SecretStr
41
+ base_url: AnyHttpUrl = Field(default=DEFAULT_THORBIT_BASE_URL)
42
+ timeout_seconds: float = Field(
43
+ default=DEFAULT_THORBIT_TIMEOUT_SECONDS,
44
+ ge=1,
45
+ le=300,
46
+ )
47
+
48
+ @field_validator("api_key")
49
+ @classmethod
50
+ def validate_api_key(cls, api_key: SecretStr) -> SecretStr:
51
+ if not api_key.get_secret_value().strip():
52
+ raise ValueError("api_key must not be empty")
53
+ return api_key
54
+
55
+ @field_validator("base_url")
56
+ @classmethod
57
+ def validate_base_url(cls, base_url: AnyHttpUrl) -> AnyHttpUrl:
58
+ if base_url.username is not None or base_url.password is not None:
59
+ raise ValueError("base_url must not contain credentials")
60
+ if base_url.query is not None or base_url.fragment is not None:
61
+ raise ValueError("base_url must not contain a query or fragment")
62
+ return base_url
63
+
64
+
65
+ @runtime_checkable
66
+ class ThorbitClientProtocol(Protocol):
67
+ """Structural contract implemented by the generic and generated clients."""
68
+
69
+ def call_tool(
70
+ self,
71
+ tool_name: str,
72
+ input: Mapping[str, object],
73
+ result_model: type[TResult],
74
+ ) -> TResult:
75
+ """Call one exact MCP tool and validate its response model."""
76
+ ...
77
+
78
+
79
+ class ThorbitSdkError(Exception):
80
+ """Base class for stable, catchable Thorbit SDK failures."""
81
+
82
+
83
+ class ThorbitRequestValidationError(ThorbitSdkError):
84
+ """The local tool invocation could not be safely serialized."""
85
+
86
+
87
+ class ThorbitTransportError(ThorbitSdkError):
88
+ """The Thorbit HTTP exchange did not produce a response."""
89
+
90
+
91
+ class ThorbitTimeoutError(ThorbitTransportError):
92
+ """The Thorbit request exceeded the configured timeout."""
93
+
94
+ def __init__(self, timeout_seconds: float) -> None:
95
+ super().__init__(
96
+ f"Thorbit API request timed out after {timeout_seconds:g} seconds"
97
+ )
98
+ self.timeout_seconds = timeout_seconds
99
+
100
+
101
+ class ThorbitHttpError(ThorbitSdkError):
102
+ """A non-success HTTP response with canonical Thorbit error metadata."""
103
+
104
+ def __init__(
105
+ self,
106
+ status_code: int,
107
+ message: str,
108
+ *,
109
+ code: str | None = None,
110
+ request_id: str | None = None,
111
+ retryable: bool | None = None,
112
+ ) -> None:
113
+ super().__init__(message)
114
+ self.status_code = status_code
115
+ self.code = code
116
+ self.request_id = request_id
117
+ self.retryable = retryable
118
+
119
+
120
+ class ThorbitResponseValidationError(ThorbitSdkError):
121
+ """A success response did not match the caller-supplied Pydantic model."""
122
+
123
+ def __init__(
124
+ self,
125
+ message: str,
126
+ *,
127
+ status_code: int,
128
+ issues: tuple[dict[str, object], ...] = (),
129
+ ) -> None:
130
+ super().__init__(message)
131
+ self.status_code = status_code
132
+ self.issues = issues
133
+
134
+
135
+ class ThorbitResultModelError(ThorbitSdkError):
136
+ """The supplied result model cannot be used by Pydantic v2."""
137
+
138
+
139
+ def _optional_string(value: object) -> str | None:
140
+ return value if isinstance(value, str) and value else None
141
+
142
+
143
+ def _http_error(response: httpx.Response, payload: object) -> ThorbitHttpError:
144
+ code: str | None = None
145
+ message: str | None = None
146
+ request_id: str | None = None
147
+ retryable: bool | None = None
148
+
149
+ if isinstance(payload, Mapping):
150
+ request_id = _optional_string(payload.get("requestId"))
151
+ error = payload.get("error")
152
+ if isinstance(error, Mapping):
153
+ code = _optional_string(error.get("code"))
154
+ message = _optional_string(error.get("message"))
155
+ raw_retryable = error.get("retryable")
156
+ retryable = raw_retryable if isinstance(raw_retryable, bool) else None
157
+ else:
158
+ message = _optional_string(payload.get("message"))
159
+ code = _optional_string(error)
160
+
161
+ return ThorbitHttpError(
162
+ response.status_code,
163
+ message or f"Thorbit API request failed with HTTP {response.status_code}",
164
+ code=code,
165
+ request_id=request_id,
166
+ retryable=retryable,
167
+ )
168
+
169
+
170
+ def _validation_issues(error: ValidationError) -> tuple[dict[str, object], ...]:
171
+ return tuple(
172
+ {
173
+ "type": issue["type"],
174
+ "location": tuple(issue["loc"]),
175
+ "message": issue["msg"],
176
+ }
177
+ for issue in error.errors(include_url=False, include_input=False)
178
+ )
179
+
180
+
181
+ class CallThorbitTools(ThorbitClientProtocol):
182
+ """Call Thorbit MCP tools through the unified authenticated REST route."""
183
+
184
+ def __init__(
185
+ self,
186
+ options: ThorbitClientOptions | Mapping[str, object],
187
+ *,
188
+ http_client: httpx.Client | None = None,
189
+ ) -> None:
190
+ self._options = (
191
+ options
192
+ if isinstance(options, ThorbitClientOptions)
193
+ else ThorbitClientOptions.model_validate(options)
194
+ )
195
+ self._owns_http_client = http_client is None
196
+ self._http_client = http_client or httpx.Client()
197
+ self._base_url = str(self._options.base_url).rstrip("/")
198
+
199
+ @property
200
+ def options(self) -> ThorbitClientOptions:
201
+ """Return the immutable, secret-redacting connection options."""
202
+ return self._options
203
+
204
+ def call_tool(
205
+ self,
206
+ tool_name: str,
207
+ input: Mapping[str, object],
208
+ result_model: type[TResult],
209
+ ) -> TResult:
210
+ """Call one tool and return a Pydantic-validated result."""
211
+ if not isinstance(tool_name, str) or not tool_name.strip():
212
+ raise ThorbitRequestValidationError("tool_name must not be empty")
213
+ if not isinstance(input, Mapping):
214
+ raise ThorbitRequestValidationError("input must be a mapping")
215
+ if not isinstance(result_model, type):
216
+ raise ThorbitResultModelError("result_model must be a type")
217
+
218
+ encoded_tool_name = quote(tool_name, safe="")
219
+ url = f"{self._base_url}{THORBIT_TOOL_ROUTE}/{encoded_tool_name}"
220
+ headers = {
221
+ "Accept": "application/vnd.thorbit.tool-result+json",
222
+ "Content-Type": "application/json",
223
+ "x-thorbit-api-key": self._options.api_key.get_secret_value(),
224
+ }
225
+
226
+ try:
227
+ response = self._http_client.post(
228
+ url,
229
+ headers=headers,
230
+ json=dict(input),
231
+ timeout=self._options.timeout_seconds,
232
+ )
233
+ except httpx.TimeoutException as error:
234
+ raise ThorbitTimeoutError(self._options.timeout_seconds) from error
235
+ except httpx.HTTPError as error:
236
+ raise ThorbitTransportError("Thorbit API network request failed") from error
237
+ except (TypeError, ValueError, OverflowError) as error:
238
+ raise ThorbitRequestValidationError(
239
+ "Thorbit tool input could not be serialized as JSON"
240
+ ) from error
241
+
242
+ try:
243
+ payload: object = response.json()
244
+ except ValueError as error:
245
+ if not response.is_success:
246
+ raise _http_error(response, None) from error
247
+ raise ThorbitResponseValidationError(
248
+ "Thorbit API returned malformed JSON",
249
+ status_code=response.status_code,
250
+ ) from error
251
+
252
+ if not response.is_success:
253
+ raise _http_error(response, payload)
254
+
255
+ try:
256
+ adapter = TypeAdapter(result_model)
257
+ except (PydanticUserError, TypeError) as error:
258
+ raise ThorbitResultModelError(
259
+ "result_model must define a Pydantic-compatible schema"
260
+ ) from error
261
+
262
+ try:
263
+ return cast(TResult, adapter.validate_python(payload))
264
+ except ValidationError as error:
265
+ raise ThorbitResponseValidationError(
266
+ "Thorbit API response failed result-model validation",
267
+ status_code=response.status_code,
268
+ issues=_validation_issues(error),
269
+ ) from error
270
+
271
+ def close(self) -> None:
272
+ """Close the internally owned HTTP client, if any."""
273
+ if self._owns_http_client:
274
+ self._http_client.close()
275
+
276
+ def __enter__(self) -> CallThorbitTools:
277
+ return self
278
+
279
+ def __exit__(
280
+ self,
281
+ exc_type: type[BaseException] | None,
282
+ exc_value: BaseException | None,
283
+ traceback: TracebackType | None,
284
+ ) -> None:
285
+ self.close()
286
+
287
+
288
+ __all__ = [
289
+ "CallThorbitTools",
290
+ "ThorbitClientOptions",
291
+ "ThorbitClientProtocol",
292
+ "ThorbitHttpError",
293
+ "ThorbitRequestValidationError",
294
+ "ThorbitResponseValidationError",
295
+ "ThorbitResultModelError",
296
+ "ThorbitSdkError",
297
+ "ThorbitTimeoutError",
298
+ "ThorbitTransportError",
299
+ ]