turbodocx-sdk 0.1.2__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.
- turbodocx_sdk/__init__.py +65 -0
- turbodocx_sdk/http.py +325 -0
- turbodocx_sdk/modules/__init__.py +4 -0
- turbodocx_sdk/modules/sign.py +451 -0
- turbodocx_sdk-0.1.2.dist-info/METADATA +530 -0
- turbodocx_sdk-0.1.2.dist-info/RECORD +8 -0
- turbodocx_sdk-0.1.2.dist-info/WHEEL +4 -0
- turbodocx_sdk-0.1.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TurboDocx Python SDK
|
|
3
|
+
|
|
4
|
+
Official SDK for TurboDocx API - Digital signatures, document generation,
|
|
5
|
+
and AI-powered workflows.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from .modules.sign import TurboSign
|
|
13
|
+
from .http import (
|
|
14
|
+
HttpClient,
|
|
15
|
+
TurboDocxError,
|
|
16
|
+
AuthenticationError,
|
|
17
|
+
ValidationError,
|
|
18
|
+
NotFoundError,
|
|
19
|
+
RateLimitError,
|
|
20
|
+
NetworkError
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TurboDocxClient:
|
|
25
|
+
"""Main client for interacting with TurboDocx API"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
api_key: str,
|
|
30
|
+
org_id: str,
|
|
31
|
+
base_url: str = "https://api.turbodocx.com"
|
|
32
|
+
):
|
|
33
|
+
"""
|
|
34
|
+
Initialize TurboDocx client
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
api_key: Your TurboDocx API key
|
|
38
|
+
org_id: Your Organization ID (required for authentication)
|
|
39
|
+
base_url: Base URL for the API (default: https://api.turbodocx.com)
|
|
40
|
+
"""
|
|
41
|
+
self.api_key = api_key
|
|
42
|
+
self.org_id = org_id
|
|
43
|
+
self.base_url = base_url
|
|
44
|
+
|
|
45
|
+
# Configure TurboSign module
|
|
46
|
+
TurboSign.configure(api_key=api_key, org_id=org_id, base_url=base_url)
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def sign(self) -> type:
|
|
50
|
+
"""Access TurboSign module for digital signature operations"""
|
|
51
|
+
return TurboSign
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
__all__ = [
|
|
55
|
+
"TurboDocxClient",
|
|
56
|
+
"TurboSign",
|
|
57
|
+
"HttpClient",
|
|
58
|
+
"TurboDocxError",
|
|
59
|
+
"AuthenticationError",
|
|
60
|
+
"ValidationError",
|
|
61
|
+
"NotFoundError",
|
|
62
|
+
"RateLimitError",
|
|
63
|
+
"NetworkError",
|
|
64
|
+
"__version__",
|
|
65
|
+
]
|
turbodocx_sdk/http.py
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HTTP client for TurboDocx API
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any, Dict, Optional, Tuple, Union
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def detect_file_type(file_bytes: bytes) -> Tuple[str, str]:
|
|
12
|
+
"""
|
|
13
|
+
Detect file type from magic bytes.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
file_bytes: File content as bytes
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Tuple of (mimetype, extension)
|
|
20
|
+
"""
|
|
21
|
+
if len(file_bytes) < 4:
|
|
22
|
+
return ("application/octet-stream", "bin")
|
|
23
|
+
|
|
24
|
+
# PDF: %PDF (0x25 0x50 0x44 0x46)
|
|
25
|
+
if file_bytes[0:4] == b'%PDF':
|
|
26
|
+
return ("application/pdf", "pdf")
|
|
27
|
+
|
|
28
|
+
# ZIP-based formats (DOCX, PPTX): starts with PK (0x50 0x4B)
|
|
29
|
+
if file_bytes[0:2] == b'PK':
|
|
30
|
+
# Check first 2000 bytes for internal markers
|
|
31
|
+
header = file_bytes[:min(len(file_bytes), 2000)]
|
|
32
|
+
header_str = header.decode('utf-8', errors='ignore')
|
|
33
|
+
|
|
34
|
+
# PPTX contains 'ppt/' in the ZIP structure
|
|
35
|
+
if 'ppt/' in header_str:
|
|
36
|
+
return (
|
|
37
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
38
|
+
"pptx"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# DOCX contains 'word/' in the ZIP structure
|
|
42
|
+
if 'word/' in header_str:
|
|
43
|
+
return (
|
|
44
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
45
|
+
"docx"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Default to DOCX for unknown ZIP
|
|
49
|
+
return (
|
|
50
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
51
|
+
"docx"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Unknown file type
|
|
55
|
+
return ("application/octet-stream", "bin")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class TurboDocxError(Exception):
|
|
59
|
+
"""Base exception for TurboDocx API errors"""
|
|
60
|
+
|
|
61
|
+
def __init__(self, message: str, status_code: Optional[int] = None, code: Optional[str] = None):
|
|
62
|
+
super().__init__(message)
|
|
63
|
+
self.status_code = status_code
|
|
64
|
+
self.code = code
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class AuthenticationError(TurboDocxError):
|
|
68
|
+
"""Raised when authentication fails (HTTP 401)"""
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class ValidationError(TurboDocxError):
|
|
73
|
+
"""Raised when validation fails (HTTP 400)"""
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class NotFoundError(TurboDocxError):
|
|
78
|
+
"""Raised when resource is not found (HTTP 404)"""
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class RateLimitError(TurboDocxError):
|
|
83
|
+
"""Raised when rate limit is exceeded (HTTP 429)"""
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class NetworkError(TurboDocxError):
|
|
88
|
+
"""Raised when network request fails"""
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class HttpClient:
|
|
93
|
+
"""HTTP client for TurboDocx API"""
|
|
94
|
+
|
|
95
|
+
def __init__(
|
|
96
|
+
self,
|
|
97
|
+
api_key: Optional[str] = None,
|
|
98
|
+
access_token: Optional[str] = None,
|
|
99
|
+
base_url: Optional[str] = None,
|
|
100
|
+
org_id: Optional[str] = None,
|
|
101
|
+
sender_email: Optional[str] = None,
|
|
102
|
+
sender_name: Optional[str] = None
|
|
103
|
+
):
|
|
104
|
+
"""
|
|
105
|
+
Initialize HTTP client
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
api_key: TurboDocx API key (required)
|
|
109
|
+
access_token: OAuth2 access token (alternative to API key)
|
|
110
|
+
base_url: Base URL for the API (optional, defaults to https://api.turbodocx.com)
|
|
111
|
+
org_id: Organization ID (required)
|
|
112
|
+
sender_email: Reply-to email address for signature requests (required).
|
|
113
|
+
This email will be used as the reply-to address when sending
|
|
114
|
+
signature request emails. Without it, emails will default to
|
|
115
|
+
"API Service User via TurboSign".
|
|
116
|
+
sender_name: Sender name for signature requests (optional but strongly recommended).
|
|
117
|
+
This name will appear in signature request emails. Without this,
|
|
118
|
+
the sender will appear as "API Service User".
|
|
119
|
+
"""
|
|
120
|
+
self.api_key = api_key or os.environ.get("TURBODOCX_API_KEY")
|
|
121
|
+
self.access_token = access_token
|
|
122
|
+
self.base_url = base_url or os.environ.get("TURBODOCX_BASE_URL", "https://api.turbodocx.com")
|
|
123
|
+
self.org_id = org_id or os.environ.get("TURBODOCX_ORG_ID")
|
|
124
|
+
self.sender_email = sender_email or os.environ.get("TURBODOCX_SENDER_EMAIL")
|
|
125
|
+
self.sender_name = sender_name or os.environ.get("TURBODOCX_SENDER_NAME")
|
|
126
|
+
|
|
127
|
+
if not self.api_key and not self.access_token:
|
|
128
|
+
raise AuthenticationError("API key or access token is required")
|
|
129
|
+
|
|
130
|
+
if not self.org_id:
|
|
131
|
+
raise AuthenticationError("Organization ID (org_id) is required for authentication")
|
|
132
|
+
|
|
133
|
+
if not self.sender_email:
|
|
134
|
+
raise ValidationError(
|
|
135
|
+
"sender_email is required. This email will be used as the reply-to address "
|
|
136
|
+
"for signature requests. Without it, emails will default to "
|
|
137
|
+
'"API Service User via TurboSign".'
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def get_sender_config(self) -> Dict[str, Optional[str]]:
|
|
141
|
+
"""
|
|
142
|
+
Get sender email and name configuration
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Dictionary with sender_email and sender_name
|
|
146
|
+
"""
|
|
147
|
+
return {
|
|
148
|
+
"sender_email": self.sender_email,
|
|
149
|
+
"sender_name": self.sender_name,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
def _get_headers(self, include_content_type: bool = True) -> Dict[str, str]:
|
|
153
|
+
"""Get default headers for requests"""
|
|
154
|
+
headers: Dict[str, str] = {}
|
|
155
|
+
|
|
156
|
+
if include_content_type:
|
|
157
|
+
headers["Content-Type"] = "application/json"
|
|
158
|
+
|
|
159
|
+
# API key is sent as Bearer token (backend expects Authorization header)
|
|
160
|
+
if self.access_token:
|
|
161
|
+
headers["Authorization"] = f"Bearer {self.access_token}"
|
|
162
|
+
elif self.api_key:
|
|
163
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
164
|
+
|
|
165
|
+
# Organization ID header (required by backend)
|
|
166
|
+
if self.org_id:
|
|
167
|
+
headers["x-rapiddocx-org-id"] = self.org_id
|
|
168
|
+
|
|
169
|
+
return headers
|
|
170
|
+
|
|
171
|
+
def _smart_unwrap(self, data: Any) -> Any:
|
|
172
|
+
"""
|
|
173
|
+
Smart unwrap response data.
|
|
174
|
+
If response has ONLY "data" key, extract it.
|
|
175
|
+
This handles backend responses that wrap data in { "data": { ... } }
|
|
176
|
+
"""
|
|
177
|
+
if isinstance(data, dict) and list(data.keys()) == ["data"]:
|
|
178
|
+
return data["data"]
|
|
179
|
+
return data
|
|
180
|
+
|
|
181
|
+
async def _handle_error_response(self, response: httpx.Response) -> None:
|
|
182
|
+
"""Handle error response from API"""
|
|
183
|
+
error_message = f"HTTP {response.status_code}: {response.reason_phrase}"
|
|
184
|
+
error_code: Optional[str] = None
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
error_data = response.json()
|
|
188
|
+
error_message = error_data.get("message") or error_data.get("error") or error_message
|
|
189
|
+
error_code = error_data.get("code")
|
|
190
|
+
except Exception:
|
|
191
|
+
pass
|
|
192
|
+
|
|
193
|
+
if response.status_code == 400:
|
|
194
|
+
raise ValidationError(error_message, response.status_code, error_code)
|
|
195
|
+
if response.status_code == 401:
|
|
196
|
+
raise AuthenticationError(error_message, response.status_code, error_code)
|
|
197
|
+
if response.status_code == 404:
|
|
198
|
+
raise NotFoundError(error_message, response.status_code, error_code)
|
|
199
|
+
if response.status_code == 429:
|
|
200
|
+
raise RateLimitError(error_message, response.status_code, error_code)
|
|
201
|
+
|
|
202
|
+
raise TurboDocxError(error_message, response.status_code, error_code)
|
|
203
|
+
|
|
204
|
+
async def get(self, path: str) -> Any:
|
|
205
|
+
"""
|
|
206
|
+
Make GET request to API
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
path: API endpoint path
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
Response data
|
|
213
|
+
"""
|
|
214
|
+
url = f"{self.base_url}{path}"
|
|
215
|
+
headers = self._get_headers()
|
|
216
|
+
|
|
217
|
+
async with httpx.AsyncClient(timeout=60.0) as client:
|
|
218
|
+
try:
|
|
219
|
+
response = await client.get(url, headers=headers)
|
|
220
|
+
|
|
221
|
+
if not response.is_success:
|
|
222
|
+
await self._handle_error_response(response)
|
|
223
|
+
|
|
224
|
+
content_type = response.headers.get("content-type", "")
|
|
225
|
+
if "application/json" in content_type:
|
|
226
|
+
return self._smart_unwrap(response.json())
|
|
227
|
+
|
|
228
|
+
return response.content
|
|
229
|
+
except httpx.TimeoutException as e:
|
|
230
|
+
raise NetworkError(f"Request timed out after 60 seconds: {str(e) or 'Timeout'}")
|
|
231
|
+
except httpx.NetworkError as e:
|
|
232
|
+
raise NetworkError(f"Network request failed: {str(e) or 'Connection error'}")
|
|
233
|
+
except TurboDocxError:
|
|
234
|
+
raise
|
|
235
|
+
except Exception as e:
|
|
236
|
+
raise NetworkError(f"Request failed: {str(e) or 'Unknown error'}")
|
|
237
|
+
|
|
238
|
+
async def post(self, path: str, data: Optional[Dict[str, Any]] = None) -> Any:
|
|
239
|
+
"""
|
|
240
|
+
Make POST request to API
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
path: API endpoint path
|
|
244
|
+
data: Request body data (will be sent as JSON)
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
Response data
|
|
248
|
+
"""
|
|
249
|
+
url = f"{self.base_url}{path}"
|
|
250
|
+
headers = self._get_headers()
|
|
251
|
+
|
|
252
|
+
async with httpx.AsyncClient(timeout=120.0) as client:
|
|
253
|
+
try:
|
|
254
|
+
response = await client.post(url, headers=headers, json=data)
|
|
255
|
+
|
|
256
|
+
if not response.is_success:
|
|
257
|
+
await self._handle_error_response(response)
|
|
258
|
+
|
|
259
|
+
return self._smart_unwrap(response.json())
|
|
260
|
+
except httpx.TimeoutException as e:
|
|
261
|
+
raise NetworkError(f"Request timed out after 120 seconds: {str(e) or 'Timeout'}")
|
|
262
|
+
except httpx.NetworkError as e:
|
|
263
|
+
raise NetworkError(f"Network request failed: {str(e) or 'Connection error'}")
|
|
264
|
+
except TurboDocxError:
|
|
265
|
+
raise
|
|
266
|
+
except Exception as e:
|
|
267
|
+
raise NetworkError(f"Request failed: {str(e) or 'Unknown error'}")
|
|
268
|
+
|
|
269
|
+
async def upload_file(
|
|
270
|
+
self,
|
|
271
|
+
path: str,
|
|
272
|
+
file: Union[str, bytes],
|
|
273
|
+
file_name: Optional[str] = None,
|
|
274
|
+
field_name: str = "file",
|
|
275
|
+
additional_data: Optional[Dict[str, Any]] = None
|
|
276
|
+
) -> Any:
|
|
277
|
+
"""
|
|
278
|
+
Upload file to API
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
path: API endpoint path
|
|
282
|
+
file: File path (str) or file content (bytes)
|
|
283
|
+
file_name: Name of the file (auto-detected for file paths)
|
|
284
|
+
field_name: Form field name for file
|
|
285
|
+
additional_data: Additional form data
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
Response data
|
|
289
|
+
"""
|
|
290
|
+
url = f"{self.base_url}{path}"
|
|
291
|
+
headers = self._get_headers(include_content_type=False)
|
|
292
|
+
|
|
293
|
+
# Handle file path vs bytes
|
|
294
|
+
if isinstance(file, str):
|
|
295
|
+
# File path - read from disk
|
|
296
|
+
with open(file, 'rb') as f:
|
|
297
|
+
file_bytes = f.read()
|
|
298
|
+
if file_name is None:
|
|
299
|
+
file_name = os.path.basename(file)
|
|
300
|
+
else:
|
|
301
|
+
# Bytes - use directly
|
|
302
|
+
file_bytes = file
|
|
303
|
+
if file_name is None:
|
|
304
|
+
# Detect extension from content
|
|
305
|
+
_, ext = detect_file_type(file_bytes)
|
|
306
|
+
file_name = f"document.{ext}"
|
|
307
|
+
|
|
308
|
+
# Detect MIME type from content
|
|
309
|
+
mime_type, _ = detect_file_type(file_bytes)
|
|
310
|
+
|
|
311
|
+
files = {field_name: (file_name, file_bytes, mime_type)}
|
|
312
|
+
data = additional_data or {}
|
|
313
|
+
|
|
314
|
+
async with httpx.AsyncClient(timeout=60.0) as client:
|
|
315
|
+
try:
|
|
316
|
+
response = await client.post(url, headers=headers, files=files, data=data)
|
|
317
|
+
|
|
318
|
+
if not response.is_success:
|
|
319
|
+
await self._handle_error_response(response)
|
|
320
|
+
|
|
321
|
+
return self._smart_unwrap(response.json())
|
|
322
|
+
except (httpx.NetworkError, httpx.TimeoutException) as e:
|
|
323
|
+
raise NetworkError(f"File upload failed: {str(e) or 'Connection error'}")
|
|
324
|
+
except Exception as e:
|
|
325
|
+
raise NetworkError(f"File upload failed: {str(e) or 'Unknown error'}")
|