feat: Adobe Sign OAuth client and API wrapper
auth_adobe.py — one-time browser Auth Code Grant flow; saves access and refresh tokens to .env. Targets the EU2 shard. adobe_api.py — thin API client with auto token refresh on 401. Supports GET, POST (JSON and multipart), PUT, and binary download. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a1601009dc
commit
343955241d
|
|
@ -0,0 +1,133 @@
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
from dotenv import load_dotenv, set_key
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
SHARD = "eu2"
|
||||||
|
TOKEN_URL = f"https://api.{SHARD}.adobesign.com/oauth/v2/token"
|
||||||
|
REDIRECT_URI = "https://localhost:8080/callback"
|
||||||
|
ENV_FILE = os.path.join(os.path.dirname(__file__), "..", ".env")
|
||||||
|
|
||||||
|
|
||||||
|
def _refresh_access_token():
|
||||||
|
client_id = os.getenv("ADOBE_CLIENT_ID")
|
||||||
|
client_secret = os.getenv("ADOBE_CLIENT_SECRET")
|
||||||
|
refresh_token = os.getenv("ADOBE_REFRESH_TOKEN")
|
||||||
|
|
||||||
|
if not all([client_id, client_secret, refresh_token]):
|
||||||
|
raise RuntimeError("Missing credentials for token refresh. Run src/auth_adobe.py first.")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"grant_type": "refresh_token",
|
||||||
|
"refresh_token": refresh_token,
|
||||||
|
"client_id": client_id,
|
||||||
|
"client_secret": client_secret,
|
||||||
|
"redirect_uri": REDIRECT_URI,
|
||||||
|
}
|
||||||
|
resp = requests.post(TOKEN_URL, data=data)
|
||||||
|
resp.raise_for_status()
|
||||||
|
new_token = resp.json()["access_token"]
|
||||||
|
|
||||||
|
abs_env = os.path.abspath(ENV_FILE)
|
||||||
|
set_key(abs_env, "ADOBE_ACCESS_TOKEN", new_token)
|
||||||
|
os.environ["ADOBE_ACCESS_TOKEN"] = new_token
|
||||||
|
return new_token
|
||||||
|
|
||||||
|
|
||||||
|
def adobe_api_post_multipart(endpoint, files, data=None):
|
||||||
|
"""Upload a file via multipart/form-data (e.g. transient documents)."""
|
||||||
|
token = os.getenv("ADOBE_ACCESS_TOKEN")
|
||||||
|
base_url = os.getenv("ADOBE_SIGN_BASE_URL", f"https://api.{SHARD}.adobesign.com/api/rest/v6")
|
||||||
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
url = f"{base_url}/{endpoint}"
|
||||||
|
resp = requests.post(url, headers=headers, files=files, data=data or {})
|
||||||
|
if resp.status_code == 401:
|
||||||
|
token = _refresh_access_token()
|
||||||
|
headers["Authorization"] = f"Bearer {token}"
|
||||||
|
resp = requests.post(url, headers=headers, files=files, data=data or {})
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp.json()
|
||||||
|
|
||||||
|
|
||||||
|
def adobe_api_post_json(endpoint, body):
|
||||||
|
"""POST JSON body to an Adobe Sign endpoint."""
|
||||||
|
token = os.getenv("ADOBE_ACCESS_TOKEN")
|
||||||
|
base_url = os.getenv("ADOBE_SIGN_BASE_URL", f"https://api.{SHARD}.adobesign.com/api/rest/v6")
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
url = f"{base_url}/{endpoint}"
|
||||||
|
resp = requests.post(url, headers=headers, json=body)
|
||||||
|
if resp.status_code == 401:
|
||||||
|
token = _refresh_access_token()
|
||||||
|
headers["Authorization"] = f"Bearer {token}"
|
||||||
|
resp = requests.post(url, headers=headers, json=body)
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp.json()
|
||||||
|
|
||||||
|
|
||||||
|
def adobe_api_put_json(endpoint, body):
|
||||||
|
"""PUT JSON body to an Adobe Sign endpoint."""
|
||||||
|
token = os.getenv("ADOBE_ACCESS_TOKEN")
|
||||||
|
base_url = os.getenv("ADOBE_SIGN_BASE_URL", f"https://api.{SHARD}.adobesign.com/api/rest/v6")
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
url = f"{base_url}/{endpoint}"
|
||||||
|
resp = requests.put(url, headers=headers, json=body)
|
||||||
|
if resp.status_code == 401:
|
||||||
|
token = _refresh_access_token()
|
||||||
|
headers["Authorization"] = f"Bearer {token}"
|
||||||
|
resp = requests.put(url, headers=headers, json=body)
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp.json()
|
||||||
|
|
||||||
|
|
||||||
|
def adobe_api_get_bytes(endpoint):
|
||||||
|
"""Download binary content (e.g. PDF files) from the Adobe Sign API."""
|
||||||
|
token = os.getenv("ADOBE_ACCESS_TOKEN")
|
||||||
|
base_url = os.getenv("ADOBE_SIGN_BASE_URL", f"https://api.{SHARD}.adobesign.com/api/rest/v6")
|
||||||
|
|
||||||
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
url = f"{base_url}/{endpoint}"
|
||||||
|
resp = requests.get(url, headers=headers)
|
||||||
|
|
||||||
|
if resp.status_code == 401:
|
||||||
|
token = _refresh_access_token()
|
||||||
|
headers["Authorization"] = f"Bearer {token}"
|
||||||
|
resp = requests.get(url, headers=headers)
|
||||||
|
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp.content
|
||||||
|
|
||||||
|
|
||||||
|
def adobe_api_get(endpoint, params=None):
|
||||||
|
token = os.getenv("ADOBE_ACCESS_TOKEN")
|
||||||
|
base_url = os.getenv("ADOBE_SIGN_BASE_URL", f"https://api.{SHARD}.adobesign.com/api/rest/v6")
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
url = f"{base_url}/{endpoint}"
|
||||||
|
resp = requests.get(url, headers=headers, params=params)
|
||||||
|
|
||||||
|
if resp.status_code == 401:
|
||||||
|
# Token expired — refresh and retry once
|
||||||
|
token = _refresh_access_token()
|
||||||
|
headers["Authorization"] = f"Bearer {token}"
|
||||||
|
resp = requests.get(url, headers=headers, params=params)
|
||||||
|
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp.json()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
library_docs = adobe_api_get("libraryDocuments")
|
||||||
|
print("Library Documents:", library_docs)
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
"""
|
||||||
|
One-time Adobe Sign OAuth setup.
|
||||||
|
|
||||||
|
Run this script once to authorize the app and save tokens to .env:
|
||||||
|
python src/auth_adobe.py
|
||||||
|
|
||||||
|
Prerequisites:
|
||||||
|
- Set ADOBE_CLIENT_ID and ADOBE_CLIENT_SECRET in .env (or export them)
|
||||||
|
- Redirect URI in your Adobe Sign app must be set to: https://localhost
|
||||||
|
|
||||||
|
After authorizing in the browser, the page will fail to load (that's expected).
|
||||||
|
Copy the full URL from the address bar and paste it when prompted.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import webbrowser
|
||||||
|
from urllib.parse import urlencode, urlparse, parse_qs
|
||||||
|
|
||||||
|
from dotenv import load_dotenv, set_key
|
||||||
|
import requests
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
SHARD = "eu2"
|
||||||
|
AUTH_URL = f"https://secure.{SHARD}.adobesign.com/public/oauth/v2"
|
||||||
|
TOKEN_URL = f"https://api.{SHARD}.adobesign.com/oauth/v2/token"
|
||||||
|
REDIRECT_URI = "https://localhost:8080/callback"
|
||||||
|
SCOPES = "library_read:self library_write:self user_read:self"
|
||||||
|
ENV_FILE = os.path.join(os.path.dirname(__file__), "..", ".env")
|
||||||
|
|
||||||
|
|
||||||
|
def get_auth_url(client_id):
|
||||||
|
params = {
|
||||||
|
"redirect_uri": REDIRECT_URI,
|
||||||
|
"response_type": "code",
|
||||||
|
"client_id": client_id,
|
||||||
|
"scope": SCOPES,
|
||||||
|
}
|
||||||
|
return f"{AUTH_URL}?{urlencode(params)}"
|
||||||
|
|
||||||
|
|
||||||
|
def extract_code(redirected_url):
|
||||||
|
parsed = urlparse(redirected_url)
|
||||||
|
params = parse_qs(parsed.query)
|
||||||
|
if "code" not in params:
|
||||||
|
error = params.get("error_description", params.get("error", ["unknown"]))[0]
|
||||||
|
raise ValueError(f"No code in URL. Error: {error}")
|
||||||
|
return params["code"][0]
|
||||||
|
|
||||||
|
|
||||||
|
def exchange_code_for_tokens(code, client_id, client_secret):
|
||||||
|
data = {
|
||||||
|
"grant_type": "authorization_code",
|
||||||
|
"code": code,
|
||||||
|
"redirect_uri": REDIRECT_URI,
|
||||||
|
"client_id": client_id,
|
||||||
|
"client_secret": client_secret,
|
||||||
|
}
|
||||||
|
resp = requests.post(TOKEN_URL, data=data)
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp.json()
|
||||||
|
|
||||||
|
|
||||||
|
def save_tokens(tokens):
|
||||||
|
abs_env = os.path.abspath(ENV_FILE)
|
||||||
|
set_key(abs_env, "ADOBE_ACCESS_TOKEN", tokens["access_token"])
|
||||||
|
if "refresh_token" in tokens:
|
||||||
|
set_key(abs_env, "ADOBE_REFRESH_TOKEN", tokens["refresh_token"])
|
||||||
|
set_key(abs_env, "ADOBE_SIGN_BASE_URL", f"https://api.{SHARD}.adobesign.com/api/rest/v6")
|
||||||
|
print(f"Tokens saved to {abs_env}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
client_id = os.getenv("ADOBE_CLIENT_ID")
|
||||||
|
client_secret = os.getenv("ADOBE_CLIENT_SECRET")
|
||||||
|
|
||||||
|
if not client_id or not client_secret:
|
||||||
|
print("ERROR: ADOBE_CLIENT_ID and ADOBE_CLIENT_SECRET must be set in .env")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
url = get_auth_url(client_id)
|
||||||
|
print(f"\nOpening browser for authorization...")
|
||||||
|
print(f"\nIf the browser doesn't open, go to:\n{url}\n")
|
||||||
|
webbrowser.open(url)
|
||||||
|
|
||||||
|
print("After authorizing, the browser will land on a page that fails to load.")
|
||||||
|
print("That's expected — just copy the full URL from the address bar and paste it here.\n")
|
||||||
|
redirected_url = input("Paste the redirect URL: ").strip()
|
||||||
|
|
||||||
|
try:
|
||||||
|
code = extract_code(redirected_url)
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"ERROR: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("Exchanging code for tokens...")
|
||||||
|
tokens = exchange_code_for_tokens(code, client_id, client_secret)
|
||||||
|
save_tokens(tokens)
|
||||||
|
print("Done. You can now run the migrator.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
Reference in New Issue