""" upload_docusign_template.py --------------------------- Uploads a DocuSign template JSON file to DocuSign via the REST API. Authenticates using DocuSign OAuth tokens stored in .env. By default uses upsert: if a template with the same name already exists, the most recently modified one is updated (PUT). Use --force-create to always create a new template instead. Usage: python3 src/upload_docusign_template.py --file migration-output//docusign-template.json python3 src/upload_docusign_template.py --file --force-create First-time setup: python3 src/docusign_auth.py --authorize # authorize once python3 src/upload_docusign_template.py --file Required .env keys (see docusign_auth.py for full list): DOCUSIGN_CLIENT_ID, DOCUSIGN_CLIENT_SECRET, DOCUSIGN_ACCOUNT_ID, DOCUSIGN_AUTH_SERVER, DOCUSIGN_BASE_URL, DOCUSIGN_REDIRECT_URI """ import argparse import json import os import sys from typing import Optional import requests from dotenv import load_dotenv load_dotenv() sys.path.insert(0, os.path.dirname(__file__)) from docusign_auth import get_access_token def _make_headers(token: str) -> dict: return { "Authorization": f"Bearer {token}", "Content-Type": "application/json", "Accept": "application/json", } def _refresh_token_once(headers: dict) -> dict: """Clear cached token and return new headers with a fresh token.""" os.environ.pop("DOCUSIGN_ACCESS_TOKEN", None) os.environ.pop("DOCUSIGN_TOKEN_EXPIRY", None) return _make_headers(get_access_token()) def find_existing_template( name: str, account_id: str, base_url: str, headers: dict, ) -> Optional[str]: """ Search DocuSign for templates matching `name` exactly. Returns the templateId of the most recently modified match, or None. """ url = f"{base_url}/v2.1/accounts/{account_id}/templates" resp = requests.get(url, headers=headers, params={"search_text": name, "count": 100}) if resp.status_code == 401: headers.update(_refresh_token_once(headers)) resp = requests.get(url, headers=headers, params={"search_text": name, "count": 100}) if not resp.ok: return None data = resp.json() templates = data.get("envelopeTemplates") or data.get("templates") or [] # Exact name match only — search_text is a substring filter on DocuSign's side exact = [t for t in templates if t.get("name") == name] if not exact: return None # Most recently modified first exact.sort(key=lambda t: t.get("lastModified", ""), reverse=True) return exact[0]["templateId"] def upload_template(file_path: str, force_create: bool = False) -> str: """ Upsert a template JSON file to DocuSign. - If a template with the same name exists and force_create is False, the most recently modified one is updated (PUT). - Otherwise a new template is created (POST). Returns the templateId. """ if not os.path.exists(file_path): print(f"ERROR: File not found: {file_path}") sys.exit(1) with open(file_path) as f: template = json.load(f) account_id = os.getenv("DOCUSIGN_ACCOUNT_ID") base_url = os.getenv("DOCUSIGN_BASE_URL", "https://demo.docusign.net/restapi") if not account_id: print("ERROR: DOCUSIGN_ACCOUNT_ID must be set in .env") sys.exit(1) headers = _make_headers(get_access_token()) template_name = template.get("name", file_path) print(f"Uploading '{template_name}' to DocuSign...") existing_id: Optional[str] = None if not force_create: existing_id = find_existing_template(template_name, account_id, base_url, headers) if existing_id: # Update existing template url = f"{base_url}/v2.1/accounts/{account_id}/templates/{existing_id}" resp = requests.put(url, headers=headers, json=template) if resp.status_code == 401: headers = _refresh_token_once(headers) resp = requests.put(url, headers=headers, json=template) if not resp.ok: print(f"ERROR: Update failed ({resp.status_code})") print(resp.text) sys.exit(1) print(f"Template updated: {existing_id}") return existing_id else: # Create new template url = f"{base_url}/v2.1/accounts/{account_id}/templates" resp = requests.post(url, headers=headers, json=template) if resp.status_code == 401: headers = _refresh_token_once(headers) resp = requests.post(url, headers=headers, json=template) if not resp.ok: print(f"ERROR: Upload failed ({resp.status_code})") print(resp.text) sys.exit(1) result = resp.json() template_id = result.get("templateId") print(f"Template created: {template_id}") return template_id def main(): parser = argparse.ArgumentParser( description="Upload a DocuSign template JSON to your DocuSign account" ) parser.add_argument( "--file", required=True, help="Path to the docusign-template.json file to upload" ) parser.add_argument( "--force-create", action="store_true", help="Always create a new template instead of updating an existing one" ) args = parser.parse_args() upload_template(args.file, force_create=args.force_create) if __name__ == "__main__": main()