返回 last30days-skill
test_device_auth.py
根目录 / skills / last30days / scripts / test_device_auth.py
1 #!/usr/bin/env python3
2 """Test ScrapeCreators GitHub device auth flow from the CLI.
3
4 Usage:
5 python3 scripts/test_device_auth.py
6
7 Flow:
8 1. Starts device code request
9 2. Shows user code + opens GitHub auth URL in browser
10 3. Polls for token until you complete auth
11 4. Fetches your profile and prints your API key
12 """
13
14 import json
15 import sys
16 import time
17 import webbrowser
18 from urllib.request import Request, urlopen
19 from urllib.error import HTTPError, URLError
20
21 BASE = "https://api.scrapecreators.com/v1/github/device"
22
23
24 def _post(url, data=None):
25 body = json.dumps(data).encode() if data else None
26 req = Request(url, data=body, method="POST")
27 req.add_header("Content-Type", "application/json")
28 with urlopen(req, timeout=15) as resp:
29 return json.loads(resp.read())
30
31
32 def _get(url, token):
33 req = Request(url)
34 req.add_header("Authorization", f"Bearer {token}")
35 with urlopen(req, timeout=15) as resp:
36 return json.loads(resp.read())
37
38
39 def main():
40 # Step 1: Start device flow
41 print("Starting ScrapeCreators GitHub device auth...\n")
42 try:
43 code_resp = _post(f"{BASE}/code")
44 except (HTTPError, URLError) as e:
45 print(f"Failed to start device flow: {e}")
46 sys.exit(1)
47
48 device_code = code_resp.get("device_code")
49 user_code = code_resp.get("user_code")
50 verification_uri = code_resp.get("verification_uri")
51 interval = code_resp.get("interval", 5)
52 expires_in = code_resp.get("expires_in", 900)
53
54 if not device_code or not user_code:
55 print(f"Unexpected response: {json.dumps(code_resp, indent=2)}")
56 sys.exit(1)
57
58 print(f"Your code: {user_code}")
59 print(f"Open: {verification_uri}")
60 print(f"Expires in: {expires_in}s\n")
61
62 # Open browser
63 if verification_uri:
64 webbrowser.open(verification_uri)
65 print("Opened browser. Enter the code above, then authorize.\n")
66
67 # Step 2: Poll for token
68 print("Waiting for authorization", end="", flush=True)
69 deadline = time.time() + expires_in
70 access_token = None
71
72 while time.time() < deadline:
73 time.sleep(interval)
74 print(".", end="", flush=True)
75 try:
76 token_resp = _post(f"{BASE}/token", {"device_code": device_code})
77 except HTTPError as e:
78 # Some APIs return 4xx while pending
79 if e.code in (400, 403, 428):
80 continue
81 print(f"\nPoll error: {e}")
82 sys.exit(1)
83 except URLError:
84 continue
85
86 if token_resp.get("access_token"):
87 access_token = token_resp["access_token"]
88 break
89
90 # Check for explicit error states
91 error = token_resp.get("error")
92 if error == "authorization_pending" or error == "slow_down":
93 if error == "slow_down":
94 interval = min(interval + 2, 30)
95 continue
96 if error in ("expired_token", "access_denied"):
97 print(f"\n\nAuth failed: {error}")
98 sys.exit(1)
99
100 if not access_token:
101 print("\n\nTimed out waiting for authorization.")
102 sys.exit(1)
103
104 print(f"\n\nAuthorized! Access token: {access_token[:12]}...\n")
105
106 # Step 3: Fetch profile
107 print("Fetching profile...")
108 try:
109 profile = _get(f"{BASE}/profile", access_token)
110 except (HTTPError, URLError) as e:
111 print(f"Failed to fetch profile: {e}")
112 print(f"(access_token was: {access_token})")
113 sys.exit(1)
114
115 print(f"\nProfile response:\n{json.dumps(profile, indent=2)}\n")
116
117 api_key = profile.get("api_key")
118 if api_key:
119 print("=" * 50)
120 print(f"Your ScrapeCreators API key: {api_key}")
121 print("=" * 50)
122 print(f"\nTo use it: echo 'SCRAPECREATORS_API_KEY={api_key}' >> ~/.config/last30days/.env")
123 else:
124 print("No api_key in profile response. Full response printed above.")
125
126
127 if __name__ == "__main__":
128 main()
129
129 lines PYTHON