orth 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.
- orth-0.1.0.dist-info/METADATA +244 -0
- orth-0.1.0.dist-info/RECORD +9 -0
- orth-0.1.0.dist-info/WHEEL +4 -0
- orth-0.1.0.dist-info/licenses/LICENSE +21 -0
- orthogonal/__init__.py +16 -0
- orthogonal/_client.py +224 -0
- orthogonal/_exceptions.py +5 -0
- orthogonal/_types.py +26 -0
- orthogonal/py.typed +1 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: orth
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK to call any API on the Orthogonal platform
|
|
5
|
+
Project-URL: Homepage, https://orthogonal.sh
|
|
6
|
+
Project-URL: Repository, https://github.com/orthogonal-sh/python
|
|
7
|
+
Project-URL: Issues, https://github.com/orthogonal-sh/python/issues
|
|
8
|
+
Author-email: Orthogonal <founders@orthogonal.sh>
|
|
9
|
+
License: MIT License
|
|
10
|
+
|
|
11
|
+
Copyright (c) 2026 Orthogonal
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
in the Software without restriction, including without limitation the rights
|
|
16
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
furnished to do so, subject to the following conditions:
|
|
19
|
+
|
|
20
|
+
The above copyright notice and this permission notice shall be included in all
|
|
21
|
+
copies or substantial portions of the Software.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
29
|
+
SOFTWARE.
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Keywords: ai-agents,api,orthogonal,payments,sdk,x402
|
|
32
|
+
Classifier: Development Status :: 3 - Alpha
|
|
33
|
+
Classifier: Intended Audience :: Developers
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Programming Language :: Python :: 3
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
41
|
+
Classifier: Typing :: Typed
|
|
42
|
+
Requires-Python: >=3.9
|
|
43
|
+
Requires-Dist: httpx<1,>=0.24
|
|
44
|
+
Provides-Extra: dev
|
|
45
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
46
|
+
Requires-Dist: mypy>=1.8; extra == 'dev'
|
|
47
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
48
|
+
Requires-Dist: pytest>=7.4; extra == 'dev'
|
|
49
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
50
|
+
Description-Content-Type: text/markdown
|
|
51
|
+
|
|
52
|
+
<div align="center">
|
|
53
|
+
|
|
54
|
+
# Orthogonal Python SDK
|
|
55
|
+
|
|
56
|
+
**Python SDK for calling APIs on the [Orthogonal](https://orthogonal.com) platform.**
|
|
57
|
+
|
|
58
|
+
Call any API on the Orthogonal platform through one client and one credit balance — authentication, routing, and billing are handled for you.
|
|
59
|
+
|
|
60
|
+
[](https://www.python.org)
|
|
61
|
+
[](./LICENSE)
|
|
62
|
+
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
## Table of Contents
|
|
66
|
+
|
|
67
|
+
- [Why Orthogonal](#why-orthogonal)
|
|
68
|
+
- [Installation](#installation)
|
|
69
|
+
- [Quick Start](#quick-start)
|
|
70
|
+
- [Authentication](#authentication)
|
|
71
|
+
- [Usage](#usage)
|
|
72
|
+
- [Async Usage](#async-usage)
|
|
73
|
+
- [Error Handling](#error-handling)
|
|
74
|
+
- [API Reference](#api-reference)
|
|
75
|
+
- [Related](#related)
|
|
76
|
+
- [License](#license)
|
|
77
|
+
|
|
78
|
+
## Why Orthogonal
|
|
79
|
+
|
|
80
|
+
Orthogonal puts a catalog of APIs behind one account and one balance:
|
|
81
|
+
|
|
82
|
+
- **One integration** — call any API on the platform through a single `run()` method.
|
|
83
|
+
- **Pay per use** — a single credit balance instead of juggling dozens of provider subscriptions.
|
|
84
|
+
- **Sync and async** — a blocking `Orthogonal` client and an `await`-able `AsyncOrthogonal`, both usable as context managers.
|
|
85
|
+
- **Typed** — `RunOptions` / `RunResponse` typed dicts and full type hints.
|
|
86
|
+
|
|
87
|
+
## Installation
|
|
88
|
+
|
|
89
|
+
Requires **Python 3.9+**.
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
pip install orth
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Quick Start
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
import os
|
|
99
|
+
from orthogonal import Orthogonal
|
|
100
|
+
|
|
101
|
+
with Orthogonal(api_key=os.environ["ORTHOGONAL_API_KEY"]) as orthogonal:
|
|
102
|
+
res = orthogonal.run(
|
|
103
|
+
api="tavily",
|
|
104
|
+
path="/search",
|
|
105
|
+
query={"query": "latest AI news"},
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
print(res["data"]) # the upstream API's response
|
|
109
|
+
print(res["price"]) # amount charged, e.g. "0.01"
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Get an API key from your [Orthogonal dashboard](https://orthogonal.com/dashboard).
|
|
113
|
+
|
|
114
|
+
## Authentication
|
|
115
|
+
|
|
116
|
+
Pass your key to the constructor (reading it from the environment keeps keys out of source):
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
orthogonal = Orthogonal(api_key=os.environ["ORTHOGONAL_API_KEY"])
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Usage
|
|
123
|
+
|
|
124
|
+
### GET request (query params)
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
res = orthogonal.run(
|
|
128
|
+
api="fantastic-jobs",
|
|
129
|
+
path="/v1/active-ats",
|
|
130
|
+
query={"time_frame": "1h", "limit": 10},
|
|
131
|
+
)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### POST request (JSON body)
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
res = orthogonal.run(
|
|
138
|
+
api="some-api",
|
|
139
|
+
path="/v1/generate",
|
|
140
|
+
body={"prompt": "a red bicycle"},
|
|
141
|
+
)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Options dict instead of keyword arguments
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
res = orthogonal.run({
|
|
148
|
+
"api": "tavily",
|
|
149
|
+
"path": "/search",
|
|
150
|
+
"query": {"query": "hello world"},
|
|
151
|
+
})
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Custom headers
|
|
155
|
+
|
|
156
|
+
Headers passed to the constructor are sent on every request:
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
orthogonal = Orthogonal(
|
|
160
|
+
api_key=os.environ["ORTHOGONAL_API_KEY"],
|
|
161
|
+
headers={"x-my-trace-id": "abc123"},
|
|
162
|
+
)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Async Usage
|
|
166
|
+
|
|
167
|
+
`AsyncOrthogonal` mirrors the sync client with `await` and `async with`:
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
import asyncio
|
|
171
|
+
from orthogonal import AsyncOrthogonal
|
|
172
|
+
|
|
173
|
+
async def main():
|
|
174
|
+
async with AsyncOrthogonal(api_key=os.environ["ORTHOGONAL_API_KEY"]) as orthogonal:
|
|
175
|
+
res = await orthogonal.run(
|
|
176
|
+
api="tavily",
|
|
177
|
+
path="/search",
|
|
178
|
+
query={"query": "orthogonal"},
|
|
179
|
+
)
|
|
180
|
+
print(res["data"])
|
|
181
|
+
|
|
182
|
+
asyncio.run(main())
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Error Handling
|
|
186
|
+
|
|
187
|
+
`run()` returns the response on success and raises **`OrthogonalError`** on any non-2xx response (invalid key, insufficient credits, or an upstream/validation error). The message describes what went wrong.
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
from orthogonal import Orthogonal, OrthogonalError
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
res = orthogonal.run(api="tavily", path="/search", query={"query": "x"})
|
|
194
|
+
except OrthogonalError as err:
|
|
195
|
+
print(f"request failed: {err}")
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## API Reference
|
|
199
|
+
|
|
200
|
+
### `Orthogonal(api_key, *, headers=None, base_url=...)`
|
|
201
|
+
|
|
202
|
+
| Argument | Type | Description |
|
|
203
|
+
| --- | --- | --- |
|
|
204
|
+
| `api_key` | `str` | **Required.** Your Orthogonal API key (`orth_live_…` / `orth_test_…`). |
|
|
205
|
+
| `headers` | `Mapping[str, str] \| None` | Optional headers sent on every request. |
|
|
206
|
+
| `base_url` | `str` | Override the API base URL (advanced). |
|
|
207
|
+
|
|
208
|
+
### `orthogonal.run(options=None, *, api, path, query=None, body=None)` → `RunResponse`
|
|
209
|
+
|
|
210
|
+
Call an endpoint. Pass **either** an `options` dict **or** keyword arguments (not both).
|
|
211
|
+
|
|
212
|
+
| Argument | Type | Description |
|
|
213
|
+
| --- | --- | --- |
|
|
214
|
+
| `api` | `str` | **Required.** The API slug (e.g. `"tavily"`). |
|
|
215
|
+
| `path` | `str` | **Required.** The endpoint path (e.g. `"/search"`). |
|
|
216
|
+
| `query` | `Mapping[str, JSONValue]` | Query parameters. |
|
|
217
|
+
| `body` | `Mapping[str, JSONValue]` | Request body for POST/PUT/PATCH. |
|
|
218
|
+
|
|
219
|
+
**`RunResponse`** (a `TypedDict`)
|
|
220
|
+
|
|
221
|
+
| Key | Type | Description |
|
|
222
|
+
| --- | --- | --- |
|
|
223
|
+
| `success` | `bool` | Whether the call succeeded. |
|
|
224
|
+
| `price` | `str` | Amount charged in USD (e.g. `"0.01"`). |
|
|
225
|
+
| `data` | `JSONValue` | The upstream API's response. |
|
|
226
|
+
|
|
227
|
+
### `AsyncOrthogonal(...)`
|
|
228
|
+
|
|
229
|
+
Same constructor and `run(...)` signature as `Orthogonal`, but `run()` is a coroutine (`await`) and the client supports `async with` / `await client.close()`.
|
|
230
|
+
|
|
231
|
+
### `OrthogonalError`
|
|
232
|
+
|
|
233
|
+
Raised by `run()` on a non-2xx response. Subclass of `Exception`; `str(err)` is a human-readable message.
|
|
234
|
+
|
|
235
|
+
Both clients are context managers — use `with` / `async with` (or call `close()`) so the underlying HTTP connection is released.
|
|
236
|
+
|
|
237
|
+
## Related
|
|
238
|
+
|
|
239
|
+
- **[`@orth/cli`](https://www.npmjs.com/package/@orth/cli)** — the Orthogonal command-line tool.
|
|
240
|
+
- **[`@orth/sdk`](https://www.npmjs.com/package/@orth/sdk)** — the official TypeScript/JavaScript SDK.
|
|
241
|
+
|
|
242
|
+
## License
|
|
243
|
+
|
|
244
|
+
[MIT](./LICENSE) © Orthogonal
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
orthogonal/__init__.py,sha256=OmB4k6rFi6N77peIvfUkqcb-4A_xihdUKLHNKN2ouRU,346
|
|
2
|
+
orthogonal/_client.py,sha256=PWJzg_y2tnOwcK4NEWuVyWDUL2-ZwRxlwxiv8Q634lc,7424
|
|
3
|
+
orthogonal/_exceptions.py,sha256=aAosaAev8MKGgsFQrUipXAV2U0jckb1uBRx9F0xHEys,143
|
|
4
|
+
orthogonal/_types.py,sha256=aZlTbysfTM14kxxn4iB2pc463tLvTrgBTmOIeMkBXcI,607
|
|
5
|
+
orthogonal/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
6
|
+
orth-0.1.0.dist-info/METADATA,sha256=uTFP1UUx_HwQAVI4Vk8-q_Z3LmgT3pH5w-NDsNp_3CY,7920
|
|
7
|
+
orth-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
8
|
+
orth-0.1.0.dist-info/licenses/LICENSE,sha256=lJVAB8xpPTFoafV6eUIwECKM7CAaR1uBX-iRQ4TigiA,1067
|
|
9
|
+
orth-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Orthogonal
|
|
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.
|
orthogonal/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Python client for the Orthogonal API."""
|
|
2
|
+
|
|
3
|
+
from ._client import AsyncOrthogonal, Orthogonal
|
|
4
|
+
from ._exceptions import OrthogonalError
|
|
5
|
+
from ._types import JSONValue, RunOptions, RunResponse
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"AsyncOrthogonal",
|
|
9
|
+
"JSONValue",
|
|
10
|
+
"Orthogonal",
|
|
11
|
+
"OrthogonalError",
|
|
12
|
+
"RunOptions",
|
|
13
|
+
"RunResponse",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.0"
|
orthogonal/_client.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""HTTP clients for the Orthogonal API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
from typing import Any, Dict, Mapping, Optional, Union, cast
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from ._exceptions import OrthogonalError
|
|
12
|
+
from ._types import JSONValue, RunOptions, RunResponse
|
|
13
|
+
|
|
14
|
+
VERSION = "0.1.0"
|
|
15
|
+
BASE_URL = "https://api.orth.sh"
|
|
16
|
+
_RUN_PATH = "/v1/run"
|
|
17
|
+
_INVALID_API_KEY = "Invalid API key. Visit https://orthogonal.sh to get one!"
|
|
18
|
+
_INSUFFICIENT_FUNDS = "Insufficient funds. Add USDC at https://orthogonal.sh"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _js_truthy(value: Any) -> bool:
|
|
22
|
+
"""Return JavaScript-like truthiness for values found in API responses."""
|
|
23
|
+
|
|
24
|
+
if value is None or value is False:
|
|
25
|
+
return False
|
|
26
|
+
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
|
27
|
+
return value != 0 and not (isinstance(value, float) and math.isnan(value))
|
|
28
|
+
if isinstance(value, str):
|
|
29
|
+
return bool(value)
|
|
30
|
+
return True
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _format_error(raw: Any, status: int) -> str:
|
|
34
|
+
fallback = f"Request failed with status {status}"
|
|
35
|
+
if raw is None:
|
|
36
|
+
return fallback
|
|
37
|
+
if isinstance(raw, str):
|
|
38
|
+
return raw or fallback
|
|
39
|
+
if isinstance(raw, Mapping):
|
|
40
|
+
message = raw.get("message")
|
|
41
|
+
if isinstance(message, str) and message:
|
|
42
|
+
return message
|
|
43
|
+
error = raw.get("error")
|
|
44
|
+
if isinstance(error, str) and error:
|
|
45
|
+
return error
|
|
46
|
+
if isinstance(raw, (Mapping, list)):
|
|
47
|
+
try:
|
|
48
|
+
encoded = json.dumps(raw, separators=(",", ":"), ensure_ascii=False)
|
|
49
|
+
except (TypeError, ValueError, OverflowError):
|
|
50
|
+
return fallback
|
|
51
|
+
return encoded if encoded not in {"{}", "[]", "null"} else fallback
|
|
52
|
+
if isinstance(raw, bool):
|
|
53
|
+
return "true" if raw else "false"
|
|
54
|
+
return str(raw)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _error_from_response(response: httpx.Response, data: Any) -> OrthogonalError:
|
|
58
|
+
if response.status_code == 401:
|
|
59
|
+
return OrthogonalError(_INVALID_API_KEY)
|
|
60
|
+
if response.status_code == 402:
|
|
61
|
+
return OrthogonalError(_INSUFFICIENT_FUNDS)
|
|
62
|
+
|
|
63
|
+
nested_error: Any = None
|
|
64
|
+
top_level_error: Any = None
|
|
65
|
+
if isinstance(data, Mapping):
|
|
66
|
+
nested = data.get("data")
|
|
67
|
+
if isinstance(nested, Mapping):
|
|
68
|
+
nested_error = nested.get("error")
|
|
69
|
+
top_level_error = data.get("error")
|
|
70
|
+
|
|
71
|
+
raw_error = nested_error if _js_truthy(nested_error) else top_level_error
|
|
72
|
+
return OrthogonalError(_format_error(raw_error, response.status_code))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _headers(api_key: str, custom_headers: Optional[Mapping[str, str]]) -> Dict[str, str]:
|
|
76
|
+
headers = {
|
|
77
|
+
"Authorization": f"Bearer {api_key}",
|
|
78
|
+
"Content-Type": "application/json",
|
|
79
|
+
"User-Agent": f"orthogonal-python/{VERSION}",
|
|
80
|
+
"X-Orthogonal-Source": "sdk",
|
|
81
|
+
}
|
|
82
|
+
if custom_headers:
|
|
83
|
+
headers.update(custom_headers)
|
|
84
|
+
return headers
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _payload(
|
|
88
|
+
api: str,
|
|
89
|
+
path: str,
|
|
90
|
+
query: Optional[Mapping[str, JSONValue]],
|
|
91
|
+
body: Optional[Mapping[str, JSONValue]],
|
|
92
|
+
) -> Dict[str, Any]:
|
|
93
|
+
payload: Dict[str, Any] = {"api": api, "path": path}
|
|
94
|
+
if query is not None:
|
|
95
|
+
payload["query"] = dict(query)
|
|
96
|
+
if body is not None:
|
|
97
|
+
payload["body"] = dict(body)
|
|
98
|
+
return payload
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class Orthogonal:
|
|
102
|
+
"""Synchronous client for calling APIs on the Orthogonal platform."""
|
|
103
|
+
|
|
104
|
+
def __init__(
|
|
105
|
+
self,
|
|
106
|
+
api_key: str,
|
|
107
|
+
*,
|
|
108
|
+
headers: Optional[Mapping[str, str]] = None,
|
|
109
|
+
base_url: str = BASE_URL,
|
|
110
|
+
timeout: Union[float, httpx.Timeout, None] = 30.0,
|
|
111
|
+
http_client: Optional[httpx.Client] = None,
|
|
112
|
+
) -> None:
|
|
113
|
+
if not api_key:
|
|
114
|
+
raise ValueError("Orthogonal API key is required")
|
|
115
|
+
self._owns_client = http_client is None
|
|
116
|
+
self._client = http_client or httpx.Client(timeout=timeout)
|
|
117
|
+
self._url = f"{base_url.rstrip('/')}{_RUN_PATH}"
|
|
118
|
+
self._headers = _headers(api_key, headers)
|
|
119
|
+
|
|
120
|
+
def run(
|
|
121
|
+
self,
|
|
122
|
+
options: Optional[RunOptions] = None,
|
|
123
|
+
*,
|
|
124
|
+
api: Optional[str] = None,
|
|
125
|
+
path: Optional[str] = None,
|
|
126
|
+
query: Optional[Mapping[str, JSONValue]] = None,
|
|
127
|
+
body: Optional[Mapping[str, JSONValue]] = None,
|
|
128
|
+
) -> RunResponse:
|
|
129
|
+
"""Call an API through Orthogonal."""
|
|
130
|
+
|
|
131
|
+
if options is not None:
|
|
132
|
+
if api is not None or path is not None or query is not None or body is not None:
|
|
133
|
+
raise TypeError("Pass either options or keyword arguments, not both")
|
|
134
|
+
api = options["api"]
|
|
135
|
+
path = options["path"]
|
|
136
|
+
query = options.get("query")
|
|
137
|
+
body = options.get("body")
|
|
138
|
+
if api is None or path is None:
|
|
139
|
+
raise TypeError("run() requires 'api' and 'path'")
|
|
140
|
+
|
|
141
|
+
response = self._client.post(
|
|
142
|
+
self._url,
|
|
143
|
+
headers=self._headers,
|
|
144
|
+
json=_payload(api, path, query, body),
|
|
145
|
+
)
|
|
146
|
+
data = response.json()
|
|
147
|
+
if not response.is_success:
|
|
148
|
+
raise _error_from_response(response, data)
|
|
149
|
+
return cast(RunResponse, data)
|
|
150
|
+
|
|
151
|
+
def close(self) -> None:
|
|
152
|
+
"""Close the underlying HTTP client when it is SDK-owned."""
|
|
153
|
+
|
|
154
|
+
if self._owns_client:
|
|
155
|
+
self._client.close()
|
|
156
|
+
|
|
157
|
+
def __enter__(self) -> Orthogonal:
|
|
158
|
+
return self
|
|
159
|
+
|
|
160
|
+
def __exit__(self, *args: object) -> None:
|
|
161
|
+
self.close()
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class AsyncOrthogonal:
|
|
165
|
+
"""Asynchronous client for calling APIs on the Orthogonal platform."""
|
|
166
|
+
|
|
167
|
+
def __init__(
|
|
168
|
+
self,
|
|
169
|
+
api_key: str,
|
|
170
|
+
*,
|
|
171
|
+
headers: Optional[Mapping[str, str]] = None,
|
|
172
|
+
base_url: str = BASE_URL,
|
|
173
|
+
timeout: Union[float, httpx.Timeout, None] = 30.0,
|
|
174
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
|
175
|
+
) -> None:
|
|
176
|
+
if not api_key:
|
|
177
|
+
raise ValueError("Orthogonal API key is required")
|
|
178
|
+
self._owns_client = http_client is None
|
|
179
|
+
self._client = http_client or httpx.AsyncClient(timeout=timeout)
|
|
180
|
+
self._url = f"{base_url.rstrip('/')}{_RUN_PATH}"
|
|
181
|
+
self._headers = _headers(api_key, headers)
|
|
182
|
+
|
|
183
|
+
async def run(
|
|
184
|
+
self,
|
|
185
|
+
options: Optional[RunOptions] = None,
|
|
186
|
+
*,
|
|
187
|
+
api: Optional[str] = None,
|
|
188
|
+
path: Optional[str] = None,
|
|
189
|
+
query: Optional[Mapping[str, JSONValue]] = None,
|
|
190
|
+
body: Optional[Mapping[str, JSONValue]] = None,
|
|
191
|
+
) -> RunResponse:
|
|
192
|
+
"""Call an API through Orthogonal."""
|
|
193
|
+
|
|
194
|
+
if options is not None:
|
|
195
|
+
if api is not None or path is not None or query is not None or body is not None:
|
|
196
|
+
raise TypeError("Pass either options or keyword arguments, not both")
|
|
197
|
+
api = options["api"]
|
|
198
|
+
path = options["path"]
|
|
199
|
+
query = options.get("query")
|
|
200
|
+
body = options.get("body")
|
|
201
|
+
if api is None or path is None:
|
|
202
|
+
raise TypeError("run() requires 'api' and 'path'")
|
|
203
|
+
|
|
204
|
+
response = await self._client.post(
|
|
205
|
+
self._url,
|
|
206
|
+
headers=self._headers,
|
|
207
|
+
json=_payload(api, path, query, body),
|
|
208
|
+
)
|
|
209
|
+
data = response.json()
|
|
210
|
+
if not response.is_success:
|
|
211
|
+
raise _error_from_response(response, data)
|
|
212
|
+
return cast(RunResponse, data)
|
|
213
|
+
|
|
214
|
+
async def close(self) -> None:
|
|
215
|
+
"""Close the underlying HTTP client when it is SDK-owned."""
|
|
216
|
+
|
|
217
|
+
if self._owns_client:
|
|
218
|
+
await self._client.aclose()
|
|
219
|
+
|
|
220
|
+
async def __aenter__(self) -> AsyncOrthogonal:
|
|
221
|
+
return self
|
|
222
|
+
|
|
223
|
+
async def __aexit__(self, *args: object) -> None:
|
|
224
|
+
await self.close()
|
orthogonal/_types.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Public type definitions for the Orthogonal SDK."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List, TypedDict, Union
|
|
4
|
+
|
|
5
|
+
JSONPrimitive = Union[str, int, float, bool, None]
|
|
6
|
+
JSONValue = Union[JSONPrimitive, List["JSONValue"], Dict[str, "JSONValue"]]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class _RequiredRunOptions(TypedDict):
|
|
10
|
+
api: str
|
|
11
|
+
path: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RunOptions(_RequiredRunOptions, total=False):
|
|
15
|
+
"""Options accepted by :meth:`Orthogonal.run`."""
|
|
16
|
+
|
|
17
|
+
query: Dict[str, JSONValue]
|
|
18
|
+
body: Dict[str, JSONValue]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class RunResponse(TypedDict):
|
|
22
|
+
"""Response returned by the Orthogonal API."""
|
|
23
|
+
|
|
24
|
+
success: bool
|
|
25
|
+
price: str
|
|
26
|
+
data: JSONValue
|
orthogonal/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|