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,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")