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,451 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TurboSign Module - Digital signature operations
|
|
3
|
+
|
|
4
|
+
Provides single-step signature operations:
|
|
5
|
+
- create_signature_review_link
|
|
6
|
+
- send_signature
|
|
7
|
+
- get_status
|
|
8
|
+
- download
|
|
9
|
+
- void_document
|
|
10
|
+
- resend_email
|
|
11
|
+
- get_audit_trail
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
from typing import Any, Dict, List, Optional, Union
|
|
16
|
+
|
|
17
|
+
import httpx
|
|
18
|
+
|
|
19
|
+
from ..http import HttpClient, NetworkError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TurboSign:
|
|
23
|
+
"""TurboSign module for digital signature operations"""
|
|
24
|
+
|
|
25
|
+
_client: Optional[HttpClient] = None
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def configure(
|
|
29
|
+
cls,
|
|
30
|
+
api_key: Optional[str] = None,
|
|
31
|
+
access_token: Optional[str] = None,
|
|
32
|
+
base_url: str = "https://api.turbodocx.com",
|
|
33
|
+
org_id: Optional[str] = None,
|
|
34
|
+
sender_email: Optional[str] = None,
|
|
35
|
+
sender_name: Optional[str] = None
|
|
36
|
+
) -> None:
|
|
37
|
+
"""
|
|
38
|
+
Configure the TurboSign module with API credentials
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
api_key: TurboDocx API key (required)
|
|
42
|
+
access_token: OAuth2 access token (alternative to API key)
|
|
43
|
+
base_url: Base URL for the API (optional, defaults to https://api.turbodocx.com)
|
|
44
|
+
org_id: Organization ID (required)
|
|
45
|
+
sender_email: Reply-to email address for signature requests (required).
|
|
46
|
+
This email will be used as the reply-to address when sending
|
|
47
|
+
signature request emails. Without it, emails will default to
|
|
48
|
+
"API Service User via TurboSign".
|
|
49
|
+
sender_name: Sender name for signature requests (optional but strongly recommended).
|
|
50
|
+
This name will appear in signature request emails. Without this,
|
|
51
|
+
the sender will appear as "API Service User".
|
|
52
|
+
|
|
53
|
+
Example:
|
|
54
|
+
>>> TurboSign.configure(
|
|
55
|
+
... api_key=os.environ.get("TURBODOCX_API_KEY"),
|
|
56
|
+
... org_id=os.environ.get("TURBODOCX_ORG_ID"),
|
|
57
|
+
... sender_email="support@yourcompany.com",
|
|
58
|
+
... sender_name="Your Company Name" # Strongly recommended
|
|
59
|
+
... )
|
|
60
|
+
"""
|
|
61
|
+
cls._client = HttpClient(
|
|
62
|
+
api_key=api_key,
|
|
63
|
+
access_token=access_token,
|
|
64
|
+
base_url=base_url,
|
|
65
|
+
org_id=org_id,
|
|
66
|
+
sender_email=sender_email,
|
|
67
|
+
sender_name=sender_name
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def _get_client(cls) -> HttpClient:
|
|
72
|
+
"""Get the HTTP client instance, raising error if not configured"""
|
|
73
|
+
if cls._client is None:
|
|
74
|
+
raise RuntimeError(
|
|
75
|
+
"TurboSign not configured. Call TurboSign.configure(api_key='...', org_id='...') first."
|
|
76
|
+
)
|
|
77
|
+
return cls._client
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
async def create_signature_review_link(
|
|
81
|
+
cls,
|
|
82
|
+
recipients: List[Dict[str, Any]],
|
|
83
|
+
fields: List[Dict[str, Any]],
|
|
84
|
+
*,
|
|
85
|
+
file: Optional[bytes] = None,
|
|
86
|
+
file_name: Optional[str] = None,
|
|
87
|
+
file_link: Optional[str] = None,
|
|
88
|
+
deliverable_id: Optional[str] = None,
|
|
89
|
+
template_id: Optional[str] = None,
|
|
90
|
+
document_name: Optional[str] = None,
|
|
91
|
+
document_description: Optional[str] = None,
|
|
92
|
+
sender_name: Optional[str] = None,
|
|
93
|
+
sender_email: Optional[str] = None,
|
|
94
|
+
cc_emails: Optional[List[str]] = None
|
|
95
|
+
) -> Dict[str, Any]:
|
|
96
|
+
"""
|
|
97
|
+
Create signature review link without sending emails
|
|
98
|
+
|
|
99
|
+
This method uploads a document with signature fields and recipients,
|
|
100
|
+
but does NOT send signature request emails. Use this to preview
|
|
101
|
+
field placement before sending.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
recipients: List of recipients who will sign
|
|
105
|
+
Each recipient should have: name, email, signingOrder
|
|
106
|
+
fields: Signature fields configuration
|
|
107
|
+
Each field should have: type, recipientEmail, and positioning info
|
|
108
|
+
file: PDF file content as bytes
|
|
109
|
+
file_name: Original filename
|
|
110
|
+
file_link: URL to document file
|
|
111
|
+
deliverable_id: TurboDocx deliverable ID
|
|
112
|
+
template_id: TurboDocx template ID
|
|
113
|
+
document_name: Document name
|
|
114
|
+
document_description: Document description
|
|
115
|
+
sender_name: Sender name
|
|
116
|
+
sender_email: Sender email
|
|
117
|
+
cc_emails: List of CC email addresses
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Response with documentId, status, previewUrl, and recipients
|
|
121
|
+
|
|
122
|
+
Example:
|
|
123
|
+
>>> result = await TurboSign.create_signature_review_link(
|
|
124
|
+
... file=pdf_bytes,
|
|
125
|
+
... recipients=[{"name": "John Doe", "email": "john@example.com", "signingOrder": 1}],
|
|
126
|
+
... fields=[{"type": "signature", "page": 1, "x": 100, "y": 500, "width": 200, "height": 50, "recipientEmail": "john@example.com"}]
|
|
127
|
+
... )
|
|
128
|
+
"""
|
|
129
|
+
client = cls._get_client()
|
|
130
|
+
|
|
131
|
+
# Get sender config from client
|
|
132
|
+
sender_config = client.get_sender_config()
|
|
133
|
+
|
|
134
|
+
# Handle different file input methods
|
|
135
|
+
if file:
|
|
136
|
+
# For file upload, use form data with JSON strings
|
|
137
|
+
form_data: Dict[str, Any] = {
|
|
138
|
+
"recipients": json.dumps(recipients),
|
|
139
|
+
"fields": json.dumps(fields),
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# Add optional fields
|
|
143
|
+
if document_name:
|
|
144
|
+
form_data["documentName"] = document_name
|
|
145
|
+
if document_description:
|
|
146
|
+
form_data["documentDescription"] = document_description
|
|
147
|
+
|
|
148
|
+
# Use request senderEmail/senderName if provided, otherwise fall back to configured values
|
|
149
|
+
form_data["senderEmail"] = sender_email or sender_config["sender_email"]
|
|
150
|
+
if sender_name or sender_config["sender_name"]:
|
|
151
|
+
form_data["senderName"] = sender_name or sender_config["sender_name"]
|
|
152
|
+
|
|
153
|
+
if cc_emails:
|
|
154
|
+
form_data["ccEmails"] = json.dumps(cc_emails)
|
|
155
|
+
|
|
156
|
+
return await client.upload_file(
|
|
157
|
+
"/turbosign/single/prepare-for-review",
|
|
158
|
+
file=file,
|
|
159
|
+
file_name=file_name or None,
|
|
160
|
+
additional_data=form_data
|
|
161
|
+
)
|
|
162
|
+
else:
|
|
163
|
+
# For JSON body (template_id, file_link, deliverable_id)
|
|
164
|
+
# Backend expects recipients/fields as JSON strings (same as form-data)
|
|
165
|
+
json_body: Dict[str, Any] = {
|
|
166
|
+
"recipients": json.dumps(recipients),
|
|
167
|
+
"fields": json.dumps(fields),
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
# Add optional fields
|
|
171
|
+
if document_name:
|
|
172
|
+
json_body["documentName"] = document_name
|
|
173
|
+
if document_description:
|
|
174
|
+
json_body["documentDescription"] = document_description
|
|
175
|
+
|
|
176
|
+
# Use request senderEmail/senderName if provided, otherwise fall back to configured values
|
|
177
|
+
json_body["senderEmail"] = sender_email or sender_config["sender_email"]
|
|
178
|
+
if sender_name or sender_config["sender_name"]:
|
|
179
|
+
json_body["senderName"] = sender_name or sender_config["sender_name"]
|
|
180
|
+
|
|
181
|
+
if cc_emails:
|
|
182
|
+
json_body["ccEmails"] = json.dumps(cc_emails)
|
|
183
|
+
|
|
184
|
+
# URL, deliverable, or template
|
|
185
|
+
if file_link:
|
|
186
|
+
json_body["fileLink"] = file_link
|
|
187
|
+
if deliverable_id:
|
|
188
|
+
json_body["deliverableId"] = deliverable_id
|
|
189
|
+
if template_id:
|
|
190
|
+
json_body["templateId"] = template_id
|
|
191
|
+
|
|
192
|
+
return await client.post(
|
|
193
|
+
"/turbosign/single/prepare-for-review",
|
|
194
|
+
data=json_body
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
@classmethod
|
|
198
|
+
async def send_signature(
|
|
199
|
+
cls,
|
|
200
|
+
recipients: List[Dict[str, Any]],
|
|
201
|
+
fields: List[Dict[str, Any]],
|
|
202
|
+
*,
|
|
203
|
+
file: Optional[bytes] = None,
|
|
204
|
+
file_name: Optional[str] = None,
|
|
205
|
+
file_link: Optional[str] = None,
|
|
206
|
+
deliverable_id: Optional[str] = None,
|
|
207
|
+
template_id: Optional[str] = None,
|
|
208
|
+
document_name: Optional[str] = None,
|
|
209
|
+
document_description: Optional[str] = None,
|
|
210
|
+
sender_name: Optional[str] = None,
|
|
211
|
+
sender_email: Optional[str] = None,
|
|
212
|
+
cc_emails: Optional[List[str]] = None
|
|
213
|
+
) -> Dict[str, Any]:
|
|
214
|
+
"""
|
|
215
|
+
Send signature request and immediately send emails
|
|
216
|
+
|
|
217
|
+
This method uploads a document with signature fields and recipients,
|
|
218
|
+
then immediately sends signature request emails to all recipients.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
recipients: List of recipients who will sign
|
|
222
|
+
Each recipient should have: name, email, signingOrder
|
|
223
|
+
fields: Signature fields configuration
|
|
224
|
+
Each field should have: type, recipientEmail, and positioning info
|
|
225
|
+
file: PDF file content as bytes
|
|
226
|
+
file_name: Original filename
|
|
227
|
+
file_link: URL to document file
|
|
228
|
+
deliverable_id: TurboDocx deliverable ID
|
|
229
|
+
template_id: TurboDocx template ID
|
|
230
|
+
document_name: Document name
|
|
231
|
+
document_description: Document description
|
|
232
|
+
sender_name: Sender name
|
|
233
|
+
sender_email: Sender email
|
|
234
|
+
cc_emails: List of CC email addresses
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Response with success, documentId, and message
|
|
238
|
+
|
|
239
|
+
Example:
|
|
240
|
+
>>> result = await TurboSign.send_signature(
|
|
241
|
+
... file=pdf_bytes,
|
|
242
|
+
... recipients=[{"name": "John Doe", "email": "john@example.com", "signingOrder": 1}],
|
|
243
|
+
... fields=[{"type": "signature", "page": 1, "x": 100, "y": 500, "width": 200, "height": 50, "recipientEmail": "john@example.com"}]
|
|
244
|
+
... )
|
|
245
|
+
"""
|
|
246
|
+
client = cls._get_client()
|
|
247
|
+
|
|
248
|
+
# Get sender config from client
|
|
249
|
+
sender_config = client.get_sender_config()
|
|
250
|
+
|
|
251
|
+
# Handle different file input methods
|
|
252
|
+
if file:
|
|
253
|
+
# For file upload, use form data with JSON strings
|
|
254
|
+
form_data: Dict[str, Any] = {
|
|
255
|
+
"recipients": json.dumps(recipients),
|
|
256
|
+
"fields": json.dumps(fields),
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
# Add optional fields
|
|
260
|
+
if document_name:
|
|
261
|
+
form_data["documentName"] = document_name
|
|
262
|
+
if document_description:
|
|
263
|
+
form_data["documentDescription"] = document_description
|
|
264
|
+
|
|
265
|
+
# Use request senderEmail/senderName if provided, otherwise fall back to configured values
|
|
266
|
+
form_data["senderEmail"] = sender_email or sender_config["sender_email"]
|
|
267
|
+
if sender_name or sender_config["sender_name"]:
|
|
268
|
+
form_data["senderName"] = sender_name or sender_config["sender_name"]
|
|
269
|
+
|
|
270
|
+
if cc_emails:
|
|
271
|
+
form_data["ccEmails"] = json.dumps(cc_emails)
|
|
272
|
+
|
|
273
|
+
return await client.upload_file(
|
|
274
|
+
"/turbosign/single/prepare-for-signing",
|
|
275
|
+
file=file,
|
|
276
|
+
file_name=file_name or None,
|
|
277
|
+
additional_data=form_data
|
|
278
|
+
)
|
|
279
|
+
else:
|
|
280
|
+
# For JSON body (template_id, file_link, deliverable_id)
|
|
281
|
+
# Backend expects recipients/fields as JSON strings (same as form-data)
|
|
282
|
+
json_body: Dict[str, Any] = {
|
|
283
|
+
"recipients": json.dumps(recipients),
|
|
284
|
+
"fields": json.dumps(fields),
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
# Add optional fields
|
|
288
|
+
if document_name:
|
|
289
|
+
json_body["documentName"] = document_name
|
|
290
|
+
if document_description:
|
|
291
|
+
json_body["documentDescription"] = document_description
|
|
292
|
+
|
|
293
|
+
# Use request senderEmail/senderName if provided, otherwise fall back to configured values
|
|
294
|
+
json_body["senderEmail"] = sender_email or sender_config["sender_email"]
|
|
295
|
+
if sender_name or sender_config["sender_name"]:
|
|
296
|
+
json_body["senderName"] = sender_name or sender_config["sender_name"]
|
|
297
|
+
|
|
298
|
+
if cc_emails:
|
|
299
|
+
json_body["ccEmails"] = json.dumps(cc_emails)
|
|
300
|
+
|
|
301
|
+
# URL, deliverable, or template
|
|
302
|
+
if file_link:
|
|
303
|
+
json_body["fileLink"] = file_link
|
|
304
|
+
if deliverable_id:
|
|
305
|
+
json_body["deliverableId"] = deliverable_id
|
|
306
|
+
if template_id:
|
|
307
|
+
json_body["templateId"] = template_id
|
|
308
|
+
|
|
309
|
+
return await client.post(
|
|
310
|
+
"/turbosign/single/prepare-for-signing",
|
|
311
|
+
data=json_body
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
@classmethod
|
|
315
|
+
async def get_status(cls, document_id: str) -> Dict[str, Any]:
|
|
316
|
+
"""
|
|
317
|
+
Get the status of a document
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
document_id: ID of the document
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
Dict with status field:
|
|
324
|
+
- status: Document status (e.g., 'under_review', 'completed', 'voided')
|
|
325
|
+
|
|
326
|
+
Example:
|
|
327
|
+
>>> status = await TurboSign.get_status("doc-123")
|
|
328
|
+
>>> print(status["status"]) # 'under_review', 'completed', etc.
|
|
329
|
+
"""
|
|
330
|
+
client = cls._get_client()
|
|
331
|
+
return await client.get(f"/turbosign/documents/{document_id}/status")
|
|
332
|
+
|
|
333
|
+
@classmethod
|
|
334
|
+
async def download(cls, document_id: str) -> bytes:
|
|
335
|
+
"""
|
|
336
|
+
Download the signed document
|
|
337
|
+
|
|
338
|
+
The backend returns a presigned S3 URL. This method fetches
|
|
339
|
+
that URL and then downloads the actual file from S3.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
document_id: ID of the document
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
PDF file content as bytes
|
|
346
|
+
|
|
347
|
+
Example:
|
|
348
|
+
>>> pdf_content = await TurboSign.download("doc-123")
|
|
349
|
+
>>> with open("signed.pdf", "wb") as f:
|
|
350
|
+
... f.write(pdf_content)
|
|
351
|
+
"""
|
|
352
|
+
client = cls._get_client()
|
|
353
|
+
|
|
354
|
+
# Get presigned URL from API
|
|
355
|
+
response = await client.get(f"/turbosign/documents/{document_id}/download")
|
|
356
|
+
|
|
357
|
+
# Response contains downloadUrl
|
|
358
|
+
download_url = response.get("downloadUrl")
|
|
359
|
+
if not download_url:
|
|
360
|
+
raise ValueError("No download URL in response")
|
|
361
|
+
|
|
362
|
+
# Fetch actual file from S3
|
|
363
|
+
async with httpx.AsyncClient() as http_client:
|
|
364
|
+
try:
|
|
365
|
+
file_response = await http_client.get(download_url)
|
|
366
|
+
if not file_response.is_success:
|
|
367
|
+
raise NetworkError(f"Failed to download file: {file_response.status_code}")
|
|
368
|
+
return file_response.content
|
|
369
|
+
except (httpx.NetworkError, httpx.TimeoutException) as e:
|
|
370
|
+
raise NetworkError(f"Failed to download file: {e}")
|
|
371
|
+
|
|
372
|
+
@classmethod
|
|
373
|
+
async def void_document(cls, document_id: str, reason: str) -> Dict[str, Any]:
|
|
374
|
+
"""
|
|
375
|
+
Void a document (cancel signature request)
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
document_id: ID of the document to void
|
|
379
|
+
reason: Reason for voiding the document
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
Dict with:
|
|
383
|
+
- id: Document ID (str)
|
|
384
|
+
- name: Document name (str)
|
|
385
|
+
- status: Document status, should be 'voided' (str)
|
|
386
|
+
- voidReason: Reason for voiding (str, optional)
|
|
387
|
+
- voidedAt: ISO timestamp when voided (str, optional)
|
|
388
|
+
|
|
389
|
+
Example:
|
|
390
|
+
>>> result = await TurboSign.void_document("doc-123", "Document needs revision")
|
|
391
|
+
>>> print(result["status"]) # "voided"
|
|
392
|
+
>>> print(result["voidedAt"]) # "2025-01-26T12:00:00.000Z"
|
|
393
|
+
"""
|
|
394
|
+
client = cls._get_client()
|
|
395
|
+
return await client.post(
|
|
396
|
+
f"/turbosign/documents/{document_id}/void",
|
|
397
|
+
data={"reason": reason}
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
@classmethod
|
|
401
|
+
async def resend_email(
|
|
402
|
+
cls,
|
|
403
|
+
document_id: str,
|
|
404
|
+
recipient_ids: List[str]
|
|
405
|
+
) -> Dict[str, Any]:
|
|
406
|
+
"""
|
|
407
|
+
Resend signature request email to recipients
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
document_id: ID of the document
|
|
411
|
+
recipient_ids: List of recipient IDs to resend emails to
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
Dict with:
|
|
415
|
+
- success: Whether the resend was successful (bool)
|
|
416
|
+
- recipientCount: Number of recipients who received email (int)
|
|
417
|
+
|
|
418
|
+
Example:
|
|
419
|
+
>>> result = await TurboSign.resend_email("doc-123", ["rec-1", "rec-2"])
|
|
420
|
+
>>> print(result["recipientCount"]) # 2
|
|
421
|
+
"""
|
|
422
|
+
client = cls._get_client()
|
|
423
|
+
return await client.post(
|
|
424
|
+
f"/turbosign/documents/{document_id}/resend-email",
|
|
425
|
+
data={"recipientIds": recipient_ids}
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
@classmethod
|
|
429
|
+
async def get_audit_trail(cls, document_id: str) -> Dict[str, Any]:
|
|
430
|
+
"""
|
|
431
|
+
Get audit trail for a document
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
document_id: ID of the document
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
Dict with:
|
|
438
|
+
- document: Dict with id and name
|
|
439
|
+
- auditTrail: List of audit entries, each with:
|
|
440
|
+
- id, documentId, actionType, timestamp
|
|
441
|
+
- previousHash, currentHash, createdOn
|
|
442
|
+
- details (optional), user (optional), recipient (optional)
|
|
443
|
+
|
|
444
|
+
Example:
|
|
445
|
+
>>> audit = await TurboSign.get_audit_trail("doc-123")
|
|
446
|
+
>>> print(audit["document"]["name"])
|
|
447
|
+
>>> for entry in audit["auditTrail"]:
|
|
448
|
+
... print(f"{entry['actionType']} - {entry['timestamp']}")
|
|
449
|
+
"""
|
|
450
|
+
client = cls._get_client()
|
|
451
|
+
return await client.get(f"/turbosign/documents/{document_id}/audit-trail")
|