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.
@@ -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'}")
@@ -0,0 +1,4 @@
1
+ # Modules package
2
+ from .sign import TurboSign
3
+
4
+ __all__ = ["TurboSign"]