clawops 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.
- clawops/__init__.py +62 -0
- clawops/_base_client.py +342 -0
- clawops/_client.py +171 -0
- clawops/_constants.py +13 -0
- clawops/_exceptions.py +147 -0
- clawops/_models.py +21 -0
- clawops/_resource.py +36 -0
- clawops/_utils.py +27 -0
- clawops/_version.py +1 -0
- clawops/pagination.py +104 -0
- clawops/py.typed +0 -0
- clawops/resources/__init__.py +10 -0
- clawops/resources/accounts.py +57 -0
- clawops/resources/calls.py +219 -0
- clawops/resources/numbers.py +157 -0
- clawops/resources/sip/__init__.py +22 -0
- clawops/resources/sip/credentials.py +134 -0
- clawops/types/__init__.py +24 -0
- clawops/types/call.py +46 -0
- clawops/types/call_params.py +46 -0
- clawops/types/number.py +50 -0
- clawops/types/number_params.py +30 -0
- clawops/types/shared.py +17 -0
- clawops/types/sip/__init__.py +3 -0
- clawops/types/sip/credential.py +48 -0
- clawops/types/sip/credential_params.py +14 -0
- clawops/webhooks.py +61 -0
- clawops-0.1.0.dist-info/METADATA +244 -0
- clawops-0.1.0.dist-info/RECORD +30 -0
- clawops-0.1.0.dist-info/WHEEL +4 -0
clawops/__init__.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""ClawOps Voice API의 공식 Python SDK.
|
|
2
|
+
|
|
3
|
+
Example::
|
|
4
|
+
|
|
5
|
+
from clawops import ClawOps
|
|
6
|
+
|
|
7
|
+
client = ClawOps(api_key="sk_...", account_id="AC1a2b3c4d")
|
|
8
|
+
call = client.calls.create(
|
|
9
|
+
to="01012345678", from_="07052358010",
|
|
10
|
+
url="https://my-app.com/twiml",
|
|
11
|
+
)
|
|
12
|
+
print(call.call_id)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from ._client import AsyncClawOps, ClawOps
|
|
16
|
+
from ._exceptions import (
|
|
17
|
+
APIConnectionError,
|
|
18
|
+
APIError,
|
|
19
|
+
APIResponseValidationError,
|
|
20
|
+
APIStatusError,
|
|
21
|
+
APITimeoutError,
|
|
22
|
+
AuthenticationError,
|
|
23
|
+
BadRequestError,
|
|
24
|
+
ClawOpsError,
|
|
25
|
+
ConflictError,
|
|
26
|
+
InternalServerError,
|
|
27
|
+
NotFoundError,
|
|
28
|
+
PermissionDeniedError,
|
|
29
|
+
ServiceUnavailableError,
|
|
30
|
+
UnprocessableEntityError,
|
|
31
|
+
)
|
|
32
|
+
from ._version import __version__
|
|
33
|
+
from .webhooks import WebhookVerificationError
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"ClawOps",
|
|
37
|
+
"AsyncClawOps",
|
|
38
|
+
"ClawOpsError",
|
|
39
|
+
"APIError",
|
|
40
|
+
"APIStatusError",
|
|
41
|
+
"APIConnectionError",
|
|
42
|
+
"APITimeoutError",
|
|
43
|
+
"APIResponseValidationError",
|
|
44
|
+
"BadRequestError",
|
|
45
|
+
"AuthenticationError",
|
|
46
|
+
"PermissionDeniedError",
|
|
47
|
+
"NotFoundError",
|
|
48
|
+
"ConflictError",
|
|
49
|
+
"UnprocessableEntityError",
|
|
50
|
+
"InternalServerError",
|
|
51
|
+
"ServiceUnavailableError",
|
|
52
|
+
"WebhookVerificationError",
|
|
53
|
+
"__version__",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
_locals = locals()
|
|
57
|
+
for _name in __all__:
|
|
58
|
+
if not _name.startswith("__"):
|
|
59
|
+
try:
|
|
60
|
+
_locals[_name].__module__ = "clawops"
|
|
61
|
+
except (TypeError, AttributeError):
|
|
62
|
+
pass
|
clawops/_base_client.py
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from random import random
|
|
5
|
+
from typing import Any, TypeVar
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
import pydantic
|
|
9
|
+
|
|
10
|
+
from ._constants import (
|
|
11
|
+
DEFAULT_BASE_URL,
|
|
12
|
+
DEFAULT_CONNECTION_LIMITS,
|
|
13
|
+
DEFAULT_MAX_RETRIES,
|
|
14
|
+
DEFAULT_TIMEOUT,
|
|
15
|
+
INITIAL_RETRY_DELAY,
|
|
16
|
+
MAX_RETRY_DELAY,
|
|
17
|
+
)
|
|
18
|
+
from ._exceptions import (
|
|
19
|
+
APIConnectionError,
|
|
20
|
+
APIResponseValidationError,
|
|
21
|
+
APITimeoutError,
|
|
22
|
+
_make_status_error,
|
|
23
|
+
)
|
|
24
|
+
from ._version import __version__
|
|
25
|
+
|
|
26
|
+
_T = TypeVar("_T", bound=pydantic.BaseModel)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SyncAPIClient:
|
|
30
|
+
"""동기 HTTP 클라이언트 베이스. httpx.Client를 래핑하며 인증, 재시도, 타임아웃, 에러 매핑을 처리합니다."""
|
|
31
|
+
|
|
32
|
+
_client: httpx.Client
|
|
33
|
+
_api_key: str
|
|
34
|
+
_base_url: str
|
|
35
|
+
_max_retries: int
|
|
36
|
+
_timeout: httpx.Timeout
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
*,
|
|
41
|
+
api_key: str,
|
|
42
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
43
|
+
timeout: float | httpx.Timeout = DEFAULT_TIMEOUT,
|
|
44
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
45
|
+
http_client: httpx.Client | None = None,
|
|
46
|
+
default_headers: dict[str, str] | None = None,
|
|
47
|
+
) -> None:
|
|
48
|
+
self._api_key = api_key
|
|
49
|
+
self._base_url = base_url.rstrip("/")
|
|
50
|
+
self._max_retries = max_retries
|
|
51
|
+
|
|
52
|
+
if isinstance(timeout, (int, float)):
|
|
53
|
+
self._timeout = httpx.Timeout(timeout)
|
|
54
|
+
else:
|
|
55
|
+
self._timeout = timeout
|
|
56
|
+
|
|
57
|
+
if http_client is not None:
|
|
58
|
+
self._client = http_client
|
|
59
|
+
else:
|
|
60
|
+
self._client = httpx.Client(
|
|
61
|
+
base_url=self._base_url,
|
|
62
|
+
timeout=self._timeout,
|
|
63
|
+
limits=DEFAULT_CONNECTION_LIMITS,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def _build_headers(self, extra_headers: dict[str, str] | None = None) -> dict[str, str]:
|
|
67
|
+
headers = {
|
|
68
|
+
"Authorization": f"Bearer {self._api_key}",
|
|
69
|
+
"Content-Type": "application/json",
|
|
70
|
+
"Accept": "application/json",
|
|
71
|
+
"User-Agent": f"clawops-python/{__version__}",
|
|
72
|
+
}
|
|
73
|
+
if extra_headers:
|
|
74
|
+
headers.update(extra_headers)
|
|
75
|
+
return headers
|
|
76
|
+
|
|
77
|
+
def _request(
|
|
78
|
+
self,
|
|
79
|
+
method: str,
|
|
80
|
+
path: str,
|
|
81
|
+
*,
|
|
82
|
+
body: dict[str, Any] | None = None,
|
|
83
|
+
query: dict[str, Any] | None = None,
|
|
84
|
+
cast_to: type[_T] | None = None,
|
|
85
|
+
extra_headers: dict[str, str] | None = None,
|
|
86
|
+
extra_query: dict[str, object] | None = None,
|
|
87
|
+
timeout: float | httpx.Timeout | None = None,
|
|
88
|
+
) -> _T | None:
|
|
89
|
+
headers = self._build_headers(extra_headers)
|
|
90
|
+
|
|
91
|
+
params = query.copy() if query else {}
|
|
92
|
+
if extra_query:
|
|
93
|
+
params.update(extra_query)
|
|
94
|
+
|
|
95
|
+
req_timeout = timeout if timeout is not None else self._timeout
|
|
96
|
+
if isinstance(req_timeout, (int, float)):
|
|
97
|
+
req_timeout = httpx.Timeout(req_timeout)
|
|
98
|
+
|
|
99
|
+
retries_left = self._max_retries
|
|
100
|
+
|
|
101
|
+
while True:
|
|
102
|
+
try:
|
|
103
|
+
response = self._client.request(
|
|
104
|
+
method=method,
|
|
105
|
+
url=path,
|
|
106
|
+
json=body,
|
|
107
|
+
params=params if params else None,
|
|
108
|
+
headers=headers,
|
|
109
|
+
timeout=req_timeout,
|
|
110
|
+
)
|
|
111
|
+
except httpx.TimeoutException as e:
|
|
112
|
+
if retries_left > 0:
|
|
113
|
+
retries_left -= 1
|
|
114
|
+
time.sleep(self._retry_delay(self._max_retries - retries_left))
|
|
115
|
+
continue
|
|
116
|
+
raise APITimeoutError(request=httpx.Request(method, self._base_url + path)) from e
|
|
117
|
+
except httpx.ConnectError as e:
|
|
118
|
+
if retries_left > 0:
|
|
119
|
+
retries_left -= 1
|
|
120
|
+
time.sleep(self._retry_delay(self._max_retries - retries_left))
|
|
121
|
+
continue
|
|
122
|
+
raise APIConnectionError(request=httpx.Request(method, self._base_url + path)) from e
|
|
123
|
+
|
|
124
|
+
if response.is_success:
|
|
125
|
+
if response.status_code == 204 or cast_to is None:
|
|
126
|
+
return None
|
|
127
|
+
try:
|
|
128
|
+
return cast_to.model_validate(response.json())
|
|
129
|
+
except pydantic.ValidationError as e:
|
|
130
|
+
raise APIResponseValidationError(response=response) from e
|
|
131
|
+
|
|
132
|
+
if retries_left > 0 and self._should_retry(response):
|
|
133
|
+
retries_left -= 1
|
|
134
|
+
time.sleep(self._retry_delay(self._max_retries - retries_left))
|
|
135
|
+
continue
|
|
136
|
+
|
|
137
|
+
raise _make_status_error(response=response)
|
|
138
|
+
|
|
139
|
+
def _should_retry(self, response: httpx.Response) -> bool:
|
|
140
|
+
if response.status_code in (408, 409, 429):
|
|
141
|
+
return True
|
|
142
|
+
if response.status_code >= 500:
|
|
143
|
+
return True
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
def _retry_delay(self, retries_taken: int) -> float:
|
|
147
|
+
delay = min(INITIAL_RETRY_DELAY * (2 ** retries_taken), MAX_RETRY_DELAY)
|
|
148
|
+
return delay * (1 + random())
|
|
149
|
+
|
|
150
|
+
def _get(self, path: str, *, cast_to: type[_T], query: dict[str, Any] | None = None,
|
|
151
|
+
extra_headers: dict[str, str] | None = None, extra_query: dict[str, object] | None = None,
|
|
152
|
+
timeout: float | httpx.Timeout | None = None) -> _T:
|
|
153
|
+
result = self._request("GET", path, cast_to=cast_to, query=query,
|
|
154
|
+
extra_headers=extra_headers, extra_query=extra_query, timeout=timeout)
|
|
155
|
+
assert result is not None
|
|
156
|
+
return result
|
|
157
|
+
|
|
158
|
+
def _post(self, path: str, *, body: dict[str, Any] | None = None, cast_to: type[_T],
|
|
159
|
+
extra_headers: dict[str, str] | None = None, extra_query: dict[str, object] | None = None,
|
|
160
|
+
timeout: float | httpx.Timeout | None = None) -> _T:
|
|
161
|
+
result = self._request("POST", path, body=body, cast_to=cast_to,
|
|
162
|
+
extra_headers=extra_headers, extra_query=extra_query, timeout=timeout)
|
|
163
|
+
assert result is not None
|
|
164
|
+
return result
|
|
165
|
+
|
|
166
|
+
def _put(self, path: str, *, body: dict[str, Any] | None = None, cast_to: type[_T],
|
|
167
|
+
extra_headers: dict[str, str] | None = None, extra_query: dict[str, object] | None = None,
|
|
168
|
+
timeout: float | httpx.Timeout | None = None) -> _T:
|
|
169
|
+
result = self._request("PUT", path, body=body, cast_to=cast_to,
|
|
170
|
+
extra_headers=extra_headers, extra_query=extra_query, timeout=timeout)
|
|
171
|
+
assert result is not None
|
|
172
|
+
return result
|
|
173
|
+
|
|
174
|
+
def _delete(self, path: str, *, extra_headers: dict[str, str] | None = None,
|
|
175
|
+
timeout: float | httpx.Timeout | None = None) -> None:
|
|
176
|
+
self._request("DELETE", path, cast_to=None, extra_headers=extra_headers, timeout=timeout)
|
|
177
|
+
|
|
178
|
+
def close(self) -> None:
|
|
179
|
+
self._client.close()
|
|
180
|
+
|
|
181
|
+
def __enter__(self) -> SyncAPIClient:
|
|
182
|
+
return self
|
|
183
|
+
|
|
184
|
+
def __exit__(self, *args: Any) -> None:
|
|
185
|
+
self.close()
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class AsyncAPIClient:
|
|
189
|
+
"""비동기 HTTP 클라이언트 베이스. SyncAPIClient의 async 버전."""
|
|
190
|
+
|
|
191
|
+
_client: httpx.AsyncClient
|
|
192
|
+
_api_key: str
|
|
193
|
+
_base_url: str
|
|
194
|
+
_max_retries: int
|
|
195
|
+
_timeout: httpx.Timeout
|
|
196
|
+
|
|
197
|
+
def __init__(
|
|
198
|
+
self,
|
|
199
|
+
*,
|
|
200
|
+
api_key: str,
|
|
201
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
202
|
+
timeout: float | httpx.Timeout = DEFAULT_TIMEOUT,
|
|
203
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
204
|
+
http_client: httpx.AsyncClient | None = None,
|
|
205
|
+
default_headers: dict[str, str] | None = None,
|
|
206
|
+
) -> None:
|
|
207
|
+
self._api_key = api_key
|
|
208
|
+
self._base_url = base_url.rstrip("/")
|
|
209
|
+
self._max_retries = max_retries
|
|
210
|
+
|
|
211
|
+
if isinstance(timeout, (int, float)):
|
|
212
|
+
self._timeout = httpx.Timeout(timeout)
|
|
213
|
+
else:
|
|
214
|
+
self._timeout = timeout
|
|
215
|
+
|
|
216
|
+
if http_client is not None:
|
|
217
|
+
self._client = http_client
|
|
218
|
+
else:
|
|
219
|
+
self._client = httpx.AsyncClient(
|
|
220
|
+
base_url=self._base_url,
|
|
221
|
+
timeout=self._timeout,
|
|
222
|
+
limits=DEFAULT_CONNECTION_LIMITS,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
def _build_headers(self, extra_headers: dict[str, str] | None = None) -> dict[str, str]:
|
|
226
|
+
headers = {
|
|
227
|
+
"Authorization": f"Bearer {self._api_key}",
|
|
228
|
+
"Content-Type": "application/json",
|
|
229
|
+
"Accept": "application/json",
|
|
230
|
+
"User-Agent": f"clawops-python/{__version__}",
|
|
231
|
+
}
|
|
232
|
+
if extra_headers:
|
|
233
|
+
headers.update(extra_headers)
|
|
234
|
+
return headers
|
|
235
|
+
|
|
236
|
+
async def _request(
|
|
237
|
+
self,
|
|
238
|
+
method: str,
|
|
239
|
+
path: str,
|
|
240
|
+
*,
|
|
241
|
+
body: dict[str, Any] | None = None,
|
|
242
|
+
query: dict[str, Any] | None = None,
|
|
243
|
+
cast_to: type[_T] | None = None,
|
|
244
|
+
extra_headers: dict[str, str] | None = None,
|
|
245
|
+
extra_query: dict[str, object] | None = None,
|
|
246
|
+
timeout: float | httpx.Timeout | None = None,
|
|
247
|
+
) -> _T | None:
|
|
248
|
+
import asyncio
|
|
249
|
+
|
|
250
|
+
headers = self._build_headers(extra_headers)
|
|
251
|
+
params = query.copy() if query else {}
|
|
252
|
+
if extra_query:
|
|
253
|
+
params.update(extra_query)
|
|
254
|
+
|
|
255
|
+
req_timeout = timeout if timeout is not None else self._timeout
|
|
256
|
+
if isinstance(req_timeout, (int, float)):
|
|
257
|
+
req_timeout = httpx.Timeout(req_timeout)
|
|
258
|
+
|
|
259
|
+
retries_left = self._max_retries
|
|
260
|
+
|
|
261
|
+
while True:
|
|
262
|
+
try:
|
|
263
|
+
response = await self._client.request(
|
|
264
|
+
method=method, url=path, json=body,
|
|
265
|
+
params=params if params else None,
|
|
266
|
+
headers=headers, timeout=req_timeout,
|
|
267
|
+
)
|
|
268
|
+
except httpx.TimeoutException as e:
|
|
269
|
+
if retries_left > 0:
|
|
270
|
+
retries_left -= 1
|
|
271
|
+
await asyncio.sleep(self._retry_delay(self._max_retries - retries_left))
|
|
272
|
+
continue
|
|
273
|
+
raise APITimeoutError(request=httpx.Request(method, self._base_url + path)) from e
|
|
274
|
+
except httpx.ConnectError as e:
|
|
275
|
+
if retries_left > 0:
|
|
276
|
+
retries_left -= 1
|
|
277
|
+
await asyncio.sleep(self._retry_delay(self._max_retries - retries_left))
|
|
278
|
+
continue
|
|
279
|
+
raise APIConnectionError(request=httpx.Request(method, self._base_url + path)) from e
|
|
280
|
+
|
|
281
|
+
if response.is_success:
|
|
282
|
+
if response.status_code == 204 or cast_to is None:
|
|
283
|
+
return None
|
|
284
|
+
try:
|
|
285
|
+
return cast_to.model_validate(response.json())
|
|
286
|
+
except pydantic.ValidationError as e:
|
|
287
|
+
raise APIResponseValidationError(response=response) from e
|
|
288
|
+
|
|
289
|
+
if retries_left > 0 and self._should_retry(response):
|
|
290
|
+
retries_left -= 1
|
|
291
|
+
await asyncio.sleep(self._retry_delay(self._max_retries - retries_left))
|
|
292
|
+
continue
|
|
293
|
+
|
|
294
|
+
raise _make_status_error(response=response)
|
|
295
|
+
|
|
296
|
+
def _should_retry(self, response: httpx.Response) -> bool:
|
|
297
|
+
if response.status_code in (408, 409, 429):
|
|
298
|
+
return True
|
|
299
|
+
if response.status_code >= 500:
|
|
300
|
+
return True
|
|
301
|
+
return False
|
|
302
|
+
|
|
303
|
+
def _retry_delay(self, retries_taken: int) -> float:
|
|
304
|
+
delay = min(INITIAL_RETRY_DELAY * (2 ** retries_taken), MAX_RETRY_DELAY)
|
|
305
|
+
return delay * (1 + random())
|
|
306
|
+
|
|
307
|
+
async def _get(self, path: str, *, cast_to: type[_T], query: dict[str, Any] | None = None,
|
|
308
|
+
extra_headers: dict[str, str] | None = None, extra_query: dict[str, object] | None = None,
|
|
309
|
+
timeout: float | httpx.Timeout | None = None) -> _T:
|
|
310
|
+
result = await self._request("GET", path, cast_to=cast_to, query=query,
|
|
311
|
+
extra_headers=extra_headers, extra_query=extra_query, timeout=timeout)
|
|
312
|
+
assert result is not None
|
|
313
|
+
return result
|
|
314
|
+
|
|
315
|
+
async def _post(self, path: str, *, body: dict[str, Any] | None = None, cast_to: type[_T],
|
|
316
|
+
extra_headers: dict[str, str] | None = None, extra_query: dict[str, object] | None = None,
|
|
317
|
+
timeout: float | httpx.Timeout | None = None) -> _T:
|
|
318
|
+
result = await self._request("POST", path, body=body, cast_to=cast_to,
|
|
319
|
+
extra_headers=extra_headers, extra_query=extra_query, timeout=timeout)
|
|
320
|
+
assert result is not None
|
|
321
|
+
return result
|
|
322
|
+
|
|
323
|
+
async def _put(self, path: str, *, body: dict[str, Any] | None = None, cast_to: type[_T],
|
|
324
|
+
extra_headers: dict[str, str] | None = None, extra_query: dict[str, object] | None = None,
|
|
325
|
+
timeout: float | httpx.Timeout | None = None) -> _T:
|
|
326
|
+
result = await self._request("PUT", path, body=body, cast_to=cast_to,
|
|
327
|
+
extra_headers=extra_headers, extra_query=extra_query, timeout=timeout)
|
|
328
|
+
assert result is not None
|
|
329
|
+
return result
|
|
330
|
+
|
|
331
|
+
async def _delete(self, path: str, *, extra_headers: dict[str, str] | None = None,
|
|
332
|
+
timeout: float | httpx.Timeout | None = None) -> None:
|
|
333
|
+
await self._request("DELETE", path, cast_to=None, extra_headers=extra_headers, timeout=timeout)
|
|
334
|
+
|
|
335
|
+
async def close(self) -> None:
|
|
336
|
+
await self._client.aclose()
|
|
337
|
+
|
|
338
|
+
async def __aenter__(self) -> AsyncAPIClient:
|
|
339
|
+
return self
|
|
340
|
+
|
|
341
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
342
|
+
await self.close()
|
clawops/_client.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from ._base_client import AsyncAPIClient, SyncAPIClient
|
|
8
|
+
from ._constants import DEFAULT_BASE_URL, DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT
|
|
9
|
+
from ._exceptions import ClawOpsError
|
|
10
|
+
from .resources.accounts import AccountContext, AsyncAccountContext
|
|
11
|
+
from .resources.calls import AsyncCalls, Calls
|
|
12
|
+
from .resources.numbers import AsyncNumbers, Numbers
|
|
13
|
+
from .resources.sip import AsyncSip, Sip
|
|
14
|
+
from .webhooks import Webhooks
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ClawOps(SyncAPIClient):
|
|
18
|
+
"""ClawOps Voice API의 동기 클라이언트.
|
|
19
|
+
|
|
20
|
+
Example::
|
|
21
|
+
|
|
22
|
+
from clawops import ClawOps
|
|
23
|
+
|
|
24
|
+
client = ClawOps(api_key="sk_...", account_id="AC1a2b3c4d")
|
|
25
|
+
|
|
26
|
+
call = client.calls.create(
|
|
27
|
+
to="01012345678", from_="07052358010",
|
|
28
|
+
url="https://my-app.com/twiml",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
other = client.accounts("AC_other")
|
|
32
|
+
other.calls.list()
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
api_key: API 키 (sk_...). 생략 시 CLAWOPS_API_KEY 환경변수 사용.
|
|
36
|
+
account_id: 기본 계정 ID (AC...). 생략 시 CLAWOPS_ACCOUNT_ID 환경변수 사용.
|
|
37
|
+
base_url: API 기본 URL. 기본값: https://api.claw-ops.com
|
|
38
|
+
timeout: 요청 타임아웃 (초 또는 httpx.Timeout). 기본값: 600초.
|
|
39
|
+
max_retries: 최대 재시도 횟수. 기본값: 2.
|
|
40
|
+
http_client: 커스텀 httpx.Client 인스턴스.
|
|
41
|
+
default_headers: 모든 요청에 포함할 기본 HTTP 헤더.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
_default_account_id: str
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
*,
|
|
49
|
+
api_key: str | None = None,
|
|
50
|
+
account_id: str | None = None,
|
|
51
|
+
base_url: str | None = None,
|
|
52
|
+
timeout: float | httpx.Timeout = DEFAULT_TIMEOUT,
|
|
53
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
54
|
+
http_client: httpx.Client | None = None,
|
|
55
|
+
default_headers: dict[str, str] | None = None,
|
|
56
|
+
) -> None:
|
|
57
|
+
if api_key is None:
|
|
58
|
+
api_key = os.environ.get("CLAWOPS_API_KEY")
|
|
59
|
+
if api_key is None:
|
|
60
|
+
raise ClawOpsError("api_key를 지정하거나 CLAWOPS_API_KEY 환경변수를 설정하세요.")
|
|
61
|
+
|
|
62
|
+
if account_id is None:
|
|
63
|
+
account_id = os.environ.get("CLAWOPS_ACCOUNT_ID")
|
|
64
|
+
if account_id is None:
|
|
65
|
+
raise ClawOpsError("account_id를 지정하거나 CLAWOPS_ACCOUNT_ID 환경변수를 설정하세요.")
|
|
66
|
+
|
|
67
|
+
if base_url is None:
|
|
68
|
+
base_url = os.environ.get("CLAWOPS_BASE_URL", DEFAULT_BASE_URL)
|
|
69
|
+
|
|
70
|
+
self._default_account_id = account_id
|
|
71
|
+
|
|
72
|
+
super().__init__(
|
|
73
|
+
api_key=api_key, base_url=base_url, timeout=timeout,
|
|
74
|
+
max_retries=max_retries, http_client=http_client, default_headers=default_headers,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def calls(self) -> Calls:
|
|
79
|
+
"""통화(Calls) 리소스에 접근합니다."""
|
|
80
|
+
return Calls(client=self, account_id=self._default_account_id)
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def numbers(self) -> Numbers:
|
|
84
|
+
"""전화번호(Numbers) 리소스에 접근합니다."""
|
|
85
|
+
return Numbers(client=self, account_id=self._default_account_id)
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def sip(self) -> Sip:
|
|
89
|
+
"""SIP 리소스에 접근합니다."""
|
|
90
|
+
return Sip(client=self, account_id=self._default_account_id)
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def webhooks(self) -> Webhooks:
|
|
94
|
+
"""Webhook 서명 검증 유틸리티."""
|
|
95
|
+
return Webhooks()
|
|
96
|
+
|
|
97
|
+
def accounts(self, account_id: str) -> AccountContext:
|
|
98
|
+
"""다른 계정의 리소스에 접근합니다.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
account_id: 접근할 계정 ID.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
해당 계정에 바인딩된 AccountContext 객체.
|
|
105
|
+
"""
|
|
106
|
+
return AccountContext(client=self, account_id=account_id)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class AsyncClawOps(AsyncAPIClient):
|
|
110
|
+
"""ClawOps Voice API의 비동기 클라이언트.
|
|
111
|
+
|
|
112
|
+
Example::
|
|
113
|
+
|
|
114
|
+
async with AsyncClawOps(api_key="sk_...", account_id="AC...") as client:
|
|
115
|
+
call = await client.calls.create(
|
|
116
|
+
to="01012345678", from_="07052358010",
|
|
117
|
+
url="https://my-app.com/twiml",
|
|
118
|
+
)
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
_default_account_id: str
|
|
122
|
+
|
|
123
|
+
def __init__(
|
|
124
|
+
self,
|
|
125
|
+
*,
|
|
126
|
+
api_key: str | None = None,
|
|
127
|
+
account_id: str | None = None,
|
|
128
|
+
base_url: str | None = None,
|
|
129
|
+
timeout: float | httpx.Timeout = DEFAULT_TIMEOUT,
|
|
130
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
131
|
+
http_client: httpx.AsyncClient | None = None,
|
|
132
|
+
default_headers: dict[str, str] | None = None,
|
|
133
|
+
) -> None:
|
|
134
|
+
if api_key is None:
|
|
135
|
+
api_key = os.environ.get("CLAWOPS_API_KEY")
|
|
136
|
+
if api_key is None:
|
|
137
|
+
raise ClawOpsError("api_key를 지정하거나 CLAWOPS_API_KEY 환경변수를 설정하세요.")
|
|
138
|
+
|
|
139
|
+
if account_id is None:
|
|
140
|
+
account_id = os.environ.get("CLAWOPS_ACCOUNT_ID")
|
|
141
|
+
if account_id is None:
|
|
142
|
+
raise ClawOpsError("account_id를 지정하거나 CLAWOPS_ACCOUNT_ID 환경변수를 설정하세요.")
|
|
143
|
+
|
|
144
|
+
if base_url is None:
|
|
145
|
+
base_url = os.environ.get("CLAWOPS_BASE_URL", DEFAULT_BASE_URL)
|
|
146
|
+
|
|
147
|
+
self._default_account_id = account_id
|
|
148
|
+
|
|
149
|
+
super().__init__(
|
|
150
|
+
api_key=api_key, base_url=base_url, timeout=timeout,
|
|
151
|
+
max_retries=max_retries, http_client=http_client, default_headers=default_headers,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def calls(self) -> AsyncCalls:
|
|
156
|
+
return AsyncCalls(client=self, account_id=self._default_account_id)
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def numbers(self) -> AsyncNumbers:
|
|
160
|
+
return AsyncNumbers(client=self, account_id=self._default_account_id)
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def sip(self) -> AsyncSip:
|
|
164
|
+
return AsyncSip(client=self, account_id=self._default_account_id)
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def webhooks(self) -> Webhooks:
|
|
168
|
+
return Webhooks()
|
|
169
|
+
|
|
170
|
+
def accounts(self, account_id: str) -> AsyncAccountContext:
|
|
171
|
+
return AsyncAccountContext(client=self, account_id=account_id)
|
clawops/_constants.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
DEFAULT_TIMEOUT = httpx.Timeout(timeout=600.0, connect=5.0)
|
|
6
|
+
DEFAULT_MAX_RETRIES = 2
|
|
7
|
+
DEFAULT_BASE_URL = "https://api.claw-ops.com"
|
|
8
|
+
INITIAL_RETRY_DELAY = 0.5
|
|
9
|
+
MAX_RETRY_DELAY = 8.0
|
|
10
|
+
DEFAULT_CONNECTION_LIMITS = httpx.Limits(
|
|
11
|
+
max_connections=1000,
|
|
12
|
+
max_keepalive_connections=100,
|
|
13
|
+
)
|