import http.server
import json
import os
import sys
import urllib.parse
import urllib.request
import webbrowser
from pathlib import Path
WAVE_AUTH_URL = "https://api.waveapps.com/oauth2/authorize/"
WAVE_TOKEN_URL = "https://api.waveapps.com/oauth2/token/"
REDIRECT_PORT = 3099
REDIRECT_URI = f"http://localhost:{REDIRECT_PORT}/callback"
ENV_PATH = Path(__file__).parent / ".env"
def load_env():
env = {}
for line in ENV_PATH.read_text().splitlines():
line = line.strip()
if line and "=" in line and not line.startswith("#"):
key, val = line.split("=", 1)
env[key.strip()] = val.strip()
return env
def save_tokens(access_token, refresh_token):
lines = ENV_PATH.read_text().splitlines()
new_lines = []
keys_written = set()
for line in lines:
stripped = line.strip()
if stripped.startswith("TOKEN="):
new_lines.append(f"TOKEN={access_token}")
keys_written.add("TOKEN")
elif stripped.startswith("REFRESH_TOKEN="):
new_lines.append(f"REFRESH_TOKEN={refresh_token}")
keys_written.add("REFRESH_TOKEN")
else:
new_lines.append(line)
if "TOKEN" not in keys_written:
new_lines.append(f"TOKEN={access_token}")
if "REFRESH_TOKEN" not in keys_written:
new_lines.append(f"REFRESH_TOKEN={refresh_token}")
ENV_PATH.write_text("\n".join(new_lines) + "\n")
def exchange_code(client_id, client_secret, code):
data = urllib.parse.urlencode({
"client_id": client_id,
"client_secret": client_secret,
"code": code,
"grant_type": "authorization_code",
"redirect_uri": REDIRECT_URI,
}).encode()
req = urllib.request.Request(WAVE_TOKEN_URL, data=data, headers={
"Content-Type": "application/x-www-form-urlencoded",
})
resp = urllib.request.urlopen(req)
return json.loads(resp.read())
class CallbackHandler(http.server.BaseHTTPRequestHandler):
auth_code = None
def do_GET(self):
parsed = urllib.parse.urlparse(self.path)
if parsed.path == "/callback":
params = urllib.parse.parse_qs(parsed.query)
if "code" in params:
CallbackHandler.auth_code = params["code"][0]
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(b"<h1>Authorization successful!</h1><p>You can close this tab.</p>")
else:
error = params.get("error", ["unknown"])[0]
self.send_response(400)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(f"<h1>Error: {error}</h1>".encode())
else:
self.send_response(404)
self.end_headers()
def log_message(self, format, *args):
pass
def main():
env = load_env()
client_id = env.get("CLIENT_ID")
client_secret = env.get("CLIENT_SECRET")
if not client_id or not client_secret:
print("ERROR: CLIENT_ID and CLIENT_SECRET must be set in .env")
sys.exit(1)
auth_params = urllib.parse.urlencode({
"client_id": client_id,
"response_type": "code",
"scope": "account:read business:read customer:read invoice:read product:read sales_tax:read transaction:* user:read vendor:read",
"redirect_uri": REDIRECT_URI,
})
auth_url = f"{WAVE_AUTH_URL}?{auth_params}"
print(f"Starting callback server on port {REDIRECT_PORT}...")
server = http.server.HTTPServer(("localhost", REDIRECT_PORT), CallbackHandler)
print(f"Opening browser for Wave authorization...")
print(f"\nIf browser doesn't open, visit:\n{auth_url}\n")
webbrowser.open(auth_url)
print("Waiting for authorization callback...")
while CallbackHandler.auth_code is None:
server.handle_request()
code = CallbackHandler.auth_code
server.server_close()
print(f"Got authorization code. Exchanging for tokens...")
try:
token_data = exchange_code(client_id, client_secret, code)
except urllib.error.HTTPError as e:
body = e.read().decode()
print(f"ERROR exchanging code: {e.code} {body}")
sys.exit(1)
access_token = token_data["access_token"]
refresh_token = token_data["refresh_token"]
expires_in = token_data.get("expires_in", "unknown")
scope = token_data.get("scope", "")
save_tokens(access_token, refresh_token)
print(f"\nSuccess! Tokens saved to .env")
print(f" Access token expires in: {expires_in}s")
print(f" Scopes: {scope}")
print(f" Refresh token saved for automatic renewal")
if __name__ == "__main__":
main()