hpde-analytics-cli 1.0.0__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.
- hpde_analytics_cli/__init__.py +4 -0
- hpde_analytics_cli/api/__init__.py +1 -0
- hpde_analytics_cli/api/client.py +325 -0
- hpde_analytics_cli/auth/__init__.py +1 -0
- hpde_analytics_cli/auth/credentials.py +256 -0
- hpde_analytics_cli/auth/oauth.py +529 -0
- hpde_analytics_cli/main.py +360 -0
- hpde_analytics_cli/utils/__init__.py +1 -0
- hpde_analytics_cli/utils/data_export.py +366 -0
- hpde_analytics_cli/utils/field_discovery.py +351 -0
- hpde_analytics_cli/utils/report_generator.py +459 -0
- hpde_analytics_cli-1.0.0.dist-info/METADATA +309 -0
- hpde_analytics_cli-1.0.0.dist-info/RECORD +17 -0
- hpde_analytics_cli-1.0.0.dist-info/WHEEL +5 -0
- hpde_analytics_cli-1.0.0.dist-info/entry_points.txt +2 -0
- hpde_analytics_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
- hpde_analytics_cli-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OAuth 1.0a authentication module for MotorsportsReg API.
|
|
3
|
+
|
|
4
|
+
Implements the three-legged OAuth flow:
|
|
5
|
+
1. Request token retrieval
|
|
6
|
+
2. User authorization
|
|
7
|
+
3. Access token exchange
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import socket
|
|
13
|
+
import threading
|
|
14
|
+
import webbrowser
|
|
15
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
18
|
+
from urllib.parse import parse_qs, urlparse
|
|
19
|
+
|
|
20
|
+
from requests_oauthlib import OAuth1Session
|
|
21
|
+
|
|
22
|
+
# Default callback port for local OAuth flow
|
|
23
|
+
DEFAULT_CALLBACK_PORT = 8089
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class OAuthCallbackHandler(BaseHTTPRequestHandler):
|
|
27
|
+
"""HTTP request handler that captures OAuth callback parameters."""
|
|
28
|
+
|
|
29
|
+
oauth_verifier = None
|
|
30
|
+
oauth_token = None
|
|
31
|
+
|
|
32
|
+
def do_GET(self):
|
|
33
|
+
"""Handle GET request from OAuth callback."""
|
|
34
|
+
# Parse the query string
|
|
35
|
+
parsed = urlparse(self.path)
|
|
36
|
+
params = parse_qs(parsed.query)
|
|
37
|
+
|
|
38
|
+
# Log what we received for debugging
|
|
39
|
+
print(f"\n [Callback] Received request: {self.path}")
|
|
40
|
+
|
|
41
|
+
# Ignore non-callback requests (favicon, root path, etc.)
|
|
42
|
+
if not parsed.path.startswith("/callback"):
|
|
43
|
+
self.send_response(200)
|
|
44
|
+
self.send_header("Content-type", "text/html")
|
|
45
|
+
self.end_headers()
|
|
46
|
+
response = """
|
|
47
|
+
<html>
|
|
48
|
+
<head><title>Waiting...</title></head>
|
|
49
|
+
<body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
|
|
50
|
+
<p>Waiting for MotorsportsReg authorization callback...</p>
|
|
51
|
+
<p>Please complete the authorization on motorsportreg.com</p>
|
|
52
|
+
</body>
|
|
53
|
+
</html>
|
|
54
|
+
"""
|
|
55
|
+
self.wfile.write(response.encode())
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
print(f" [Callback] OAuth parameters: {params}")
|
|
59
|
+
|
|
60
|
+
# Extract OAuth parameters
|
|
61
|
+
OAuthCallbackHandler.oauth_verifier = params.get("oauth_verifier", [None])[0]
|
|
62
|
+
OAuthCallbackHandler.oauth_token = params.get("oauth_token", [None])[0]
|
|
63
|
+
|
|
64
|
+
# Check for OAuth error response
|
|
65
|
+
oauth_error = params.get("error", [None])[0]
|
|
66
|
+
error_description = params.get("error_description", ["Unknown error"])[0]
|
|
67
|
+
|
|
68
|
+
# Send response to browser
|
|
69
|
+
self.send_response(200)
|
|
70
|
+
self.send_header("Content-type", "text/html")
|
|
71
|
+
self.end_headers()
|
|
72
|
+
|
|
73
|
+
if OAuthCallbackHandler.oauth_verifier:
|
|
74
|
+
response = """
|
|
75
|
+
<html>
|
|
76
|
+
<head><title>Authorization Successful</title></head>
|
|
77
|
+
<body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
|
|
78
|
+
<h1 style="color: green;">Authorization Successful!</h1>
|
|
79
|
+
<p>You can close this window and return to the terminal.</p>
|
|
80
|
+
<p>Verification code received. The script will continue automatically.</p>
|
|
81
|
+
</body>
|
|
82
|
+
</html>
|
|
83
|
+
"""
|
|
84
|
+
elif oauth_error:
|
|
85
|
+
response = f"""
|
|
86
|
+
<html>
|
|
87
|
+
<head><title>Authorization Denied</title></head>
|
|
88
|
+
<body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
|
|
89
|
+
<h1 style="color: red;">Authorization Denied</h1>
|
|
90
|
+
<p>Error: {oauth_error}</p>
|
|
91
|
+
<p>{error_description}</p>
|
|
92
|
+
</body>
|
|
93
|
+
</html>
|
|
94
|
+
"""
|
|
95
|
+
else:
|
|
96
|
+
response = f"""
|
|
97
|
+
<html>
|
|
98
|
+
<head><title>Authorization Issue</title></head>
|
|
99
|
+
<body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
|
|
100
|
+
<h1 style="color: orange;">Missing Verification Code</h1>
|
|
101
|
+
<p>Received callback but no oauth_verifier parameter.</p>
|
|
102
|
+
<p>Parameters received: {params}</p>
|
|
103
|
+
</body>
|
|
104
|
+
</html>
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
self.wfile.write(response.encode())
|
|
108
|
+
|
|
109
|
+
def log_message(self, format, *args):
|
|
110
|
+
"""Suppress HTTP server logging."""
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class MSROAuth:
|
|
115
|
+
"""Handles OAuth 1.0a authentication with MotorsportsReg API."""
|
|
116
|
+
|
|
117
|
+
REQUEST_TOKEN_URL = "/rest/tokens/request"
|
|
118
|
+
AUTHORIZE_URL = "/index.cfm/event/oauth" # MSR-specific OAuth authorization page
|
|
119
|
+
ACCESS_TOKEN_URL = "/rest/tokens/access"
|
|
120
|
+
ME_URL = "/rest/me.json" # Use .json suffix for JSON response
|
|
121
|
+
|
|
122
|
+
def __init__(
|
|
123
|
+
self,
|
|
124
|
+
consumer_key: str,
|
|
125
|
+
consumer_secret: str,
|
|
126
|
+
base_url: str = "https://api.motorsportreg.com",
|
|
127
|
+
callback_url: Optional[str] = None,
|
|
128
|
+
callback_port: int = DEFAULT_CALLBACK_PORT,
|
|
129
|
+
token_file: Optional[str] = None,
|
|
130
|
+
):
|
|
131
|
+
"""
|
|
132
|
+
Initialize the OAuth handler.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
consumer_key: OAuth consumer key from MotorsportsReg
|
|
136
|
+
consumer_secret: OAuth consumer secret from MotorsportsReg
|
|
137
|
+
base_url: API base URL
|
|
138
|
+
callback_url: OAuth callback URL (defaults to localhost)
|
|
139
|
+
callback_port: Port for local callback server
|
|
140
|
+
token_file: Path to store/load access tokens
|
|
141
|
+
"""
|
|
142
|
+
self.consumer_key = consumer_key
|
|
143
|
+
self.consumer_secret = consumer_secret
|
|
144
|
+
self.base_url = base_url.rstrip("/")
|
|
145
|
+
self.callback_port = callback_port
|
|
146
|
+
self.callback_url = callback_url or f"http://localhost:{callback_port}/callback"
|
|
147
|
+
|
|
148
|
+
# Token storage
|
|
149
|
+
if token_file is None:
|
|
150
|
+
project_root = Path(__file__).parent.parent.parent
|
|
151
|
+
self.token_file = project_root / "tokens" / "access_token.json"
|
|
152
|
+
else:
|
|
153
|
+
self.token_file = Path(token_file)
|
|
154
|
+
|
|
155
|
+
# OAuth tokens (populated during auth flow)
|
|
156
|
+
self.request_token = None
|
|
157
|
+
self.request_token_secret = None
|
|
158
|
+
self.access_token = None
|
|
159
|
+
self.access_token_secret = None
|
|
160
|
+
self.profile_id: Optional[str] = None
|
|
161
|
+
self.organizations: List[Dict[str, Any]] = []
|
|
162
|
+
|
|
163
|
+
# Try to load existing tokens
|
|
164
|
+
self._load_tokens()
|
|
165
|
+
|
|
166
|
+
def _load_tokens(self) -> bool:
|
|
167
|
+
"""
|
|
168
|
+
Load access tokens from file if they exist.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
True if tokens were loaded successfully, False otherwise
|
|
172
|
+
"""
|
|
173
|
+
if not self.token_file.exists():
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
with open(self.token_file, "r") as f:
|
|
178
|
+
data = json.load(f)
|
|
179
|
+
self.access_token = data.get("access_token")
|
|
180
|
+
self.access_token_secret = data.get("access_token_secret")
|
|
181
|
+
self.profile_id = data.get("profile_id")
|
|
182
|
+
self.organizations = data.get("organizations", [])
|
|
183
|
+
|
|
184
|
+
if self.access_token and self.access_token_secret:
|
|
185
|
+
print(f"Loaded existing access tokens from {self.token_file}")
|
|
186
|
+
return True
|
|
187
|
+
except (json.JSONDecodeError, IOError) as e:
|
|
188
|
+
print(f"Warning: Could not load tokens: {e}")
|
|
189
|
+
|
|
190
|
+
return False
|
|
191
|
+
|
|
192
|
+
def _save_tokens(self) -> None:
|
|
193
|
+
"""Save access tokens to file."""
|
|
194
|
+
self.token_file.parent.mkdir(parents=True, exist_ok=True)
|
|
195
|
+
|
|
196
|
+
data = {
|
|
197
|
+
"access_token": self.access_token,
|
|
198
|
+
"access_token_secret": self.access_token_secret,
|
|
199
|
+
"profile_id": self.profile_id,
|
|
200
|
+
"organizations": self.organizations,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
with open(self.token_file, "w") as f:
|
|
204
|
+
json.dump(data, f, indent=2)
|
|
205
|
+
|
|
206
|
+
print(f"Access tokens saved to {self.token_file}")
|
|
207
|
+
|
|
208
|
+
def has_valid_tokens(self) -> bool:
|
|
209
|
+
"""Check if we have access tokens that might be valid."""
|
|
210
|
+
return bool(self.access_token and self.access_token_secret)
|
|
211
|
+
|
|
212
|
+
def get_request_token(self) -> Tuple[str, str]:
|
|
213
|
+
"""
|
|
214
|
+
Step 1: Obtain a request token from MotorsportsReg.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Tuple of (request_token, request_token_secret)
|
|
218
|
+
|
|
219
|
+
Raises:
|
|
220
|
+
Exception: If request token retrieval fails
|
|
221
|
+
"""
|
|
222
|
+
print("\n[Step 1/3] Requesting temporary token...")
|
|
223
|
+
|
|
224
|
+
oauth = OAuth1Session(
|
|
225
|
+
self.consumer_key,
|
|
226
|
+
client_secret=self.consumer_secret,
|
|
227
|
+
callback_uri=self.callback_url,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
url = f"{self.base_url}{self.REQUEST_TOKEN_URL}"
|
|
231
|
+
response = oauth.fetch_request_token(url)
|
|
232
|
+
|
|
233
|
+
self.request_token = response.get("oauth_token")
|
|
234
|
+
self.request_token_secret = response.get("oauth_token_secret")
|
|
235
|
+
|
|
236
|
+
if not self.request_token:
|
|
237
|
+
raise Exception("Failed to obtain request token")
|
|
238
|
+
|
|
239
|
+
print(f" Request token obtained: {self.request_token[:20]}...")
|
|
240
|
+
return self.request_token, self.request_token_secret
|
|
241
|
+
|
|
242
|
+
def get_authorization_url(self) -> str:
|
|
243
|
+
"""
|
|
244
|
+
Step 2: Generate the authorization URL for user to visit.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
URL string for user to authorize the application
|
|
248
|
+
"""
|
|
249
|
+
if not self.request_token:
|
|
250
|
+
raise Exception("Must obtain request token first")
|
|
251
|
+
|
|
252
|
+
# MotorsportsReg uses the main site for authorization
|
|
253
|
+
auth_base = "https://www.motorsportreg.com"
|
|
254
|
+
auth_url = f"{auth_base}{self.AUTHORIZE_URL}?oauth_token={self.request_token}"
|
|
255
|
+
|
|
256
|
+
print("\n[Step 2/3] User authorization required")
|
|
257
|
+
print("-" * 60)
|
|
258
|
+
print("Please visit this URL to authorize the application:")
|
|
259
|
+
print(f"\n {auth_url}\n")
|
|
260
|
+
print("-" * 60)
|
|
261
|
+
|
|
262
|
+
return auth_url
|
|
263
|
+
|
|
264
|
+
def get_access_token(self, verifier: str) -> dict:
|
|
265
|
+
"""
|
|
266
|
+
Step 3: Exchange request token for access token.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
verifier: The oauth_verifier code from authorization
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
Dict containing access_token, access_token_secret, profile_id
|
|
273
|
+
|
|
274
|
+
Raises:
|
|
275
|
+
Exception: If access token exchange fails
|
|
276
|
+
"""
|
|
277
|
+
print("\n[Step 3/3] Exchanging for access token...")
|
|
278
|
+
|
|
279
|
+
oauth = OAuth1Session(
|
|
280
|
+
self.consumer_key,
|
|
281
|
+
client_secret=self.consumer_secret,
|
|
282
|
+
resource_owner_key=self.request_token,
|
|
283
|
+
resource_owner_secret=self.request_token_secret,
|
|
284
|
+
verifier=verifier,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
url = f"{self.base_url}{self.ACCESS_TOKEN_URL}"
|
|
288
|
+
response = oauth.fetch_access_token(url)
|
|
289
|
+
|
|
290
|
+
self.access_token = response.get("oauth_token")
|
|
291
|
+
self.access_token_secret = response.get("oauth_token_secret")
|
|
292
|
+
self.profile_id = response.get("userid") or response.get("profile_id")
|
|
293
|
+
|
|
294
|
+
if not self.access_token:
|
|
295
|
+
raise Exception("Failed to obtain access token")
|
|
296
|
+
|
|
297
|
+
print(f" Access token obtained: {self.access_token[:20]}...")
|
|
298
|
+
if self.profile_id:
|
|
299
|
+
print(f" Profile ID: {self.profile_id}")
|
|
300
|
+
|
|
301
|
+
# Save tokens for future use
|
|
302
|
+
self._save_tokens()
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
"access_token": self.access_token,
|
|
306
|
+
"access_token_secret": self.access_token_secret,
|
|
307
|
+
"profile_id": self.profile_id,
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
def get_oauth_session(self) -> OAuth1Session:
|
|
311
|
+
"""
|
|
312
|
+
Get an authenticated OAuth1Session for making API requests.
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
Configured OAuth1Session instance
|
|
316
|
+
|
|
317
|
+
Raises:
|
|
318
|
+
Exception: If no valid access tokens available
|
|
319
|
+
"""
|
|
320
|
+
if not self.has_valid_tokens():
|
|
321
|
+
raise Exception("No valid access tokens. Please authenticate first.")
|
|
322
|
+
|
|
323
|
+
session = OAuth1Session(
|
|
324
|
+
self.consumer_key,
|
|
325
|
+
client_secret=self.consumer_secret,
|
|
326
|
+
resource_owner_key=self.access_token,
|
|
327
|
+
resource_owner_secret=self.access_token_secret,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
# Set default headers to request JSON responses
|
|
331
|
+
session.headers.update(
|
|
332
|
+
{
|
|
333
|
+
"Accept": "application/json",
|
|
334
|
+
"Content-Type": "application/json",
|
|
335
|
+
}
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
return session
|
|
339
|
+
|
|
340
|
+
def validate_connection(self) -> Dict[str, Any]:
|
|
341
|
+
"""
|
|
342
|
+
Validate the OAuth connection by calling /rest/me endpoint.
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
User profile data including organizations
|
|
346
|
+
|
|
347
|
+
Raises:
|
|
348
|
+
Exception: If validation fails
|
|
349
|
+
"""
|
|
350
|
+
print("\nValidating connection...")
|
|
351
|
+
|
|
352
|
+
session = self.get_oauth_session()
|
|
353
|
+
url = f"{self.base_url}{self.ME_URL}"
|
|
354
|
+
|
|
355
|
+
response = session.get(url)
|
|
356
|
+
|
|
357
|
+
if response.status_code == 401:
|
|
358
|
+
# Tokens are invalid, clear them
|
|
359
|
+
self.access_token = None
|
|
360
|
+
self.access_token_secret = None
|
|
361
|
+
if self.token_file.exists():
|
|
362
|
+
self.token_file.unlink()
|
|
363
|
+
raise Exception("Authentication failed - tokens are invalid or expired")
|
|
364
|
+
|
|
365
|
+
response.raise_for_status()
|
|
366
|
+
|
|
367
|
+
data = response.json()
|
|
368
|
+
|
|
369
|
+
# MSR wraps responses in {"response": {"profile": {...}}}
|
|
370
|
+
if "response" in data:
|
|
371
|
+
data = data["response"]
|
|
372
|
+
if "profile" in data:
|
|
373
|
+
profile_data = data["profile"]
|
|
374
|
+
else:
|
|
375
|
+
profile_data = data
|
|
376
|
+
|
|
377
|
+
# Extract organization info
|
|
378
|
+
self.organizations = profile_data.get("organizations", [])
|
|
379
|
+
|
|
380
|
+
# Update saved tokens with organization info
|
|
381
|
+
self._save_tokens()
|
|
382
|
+
|
|
383
|
+
return profile_data
|
|
384
|
+
|
|
385
|
+
def _start_callback_server(self) -> HTTPServer:
|
|
386
|
+
"""Start a local HTTP server to receive the OAuth callback."""
|
|
387
|
+
# Reset handler state
|
|
388
|
+
OAuthCallbackHandler.oauth_verifier = None
|
|
389
|
+
OAuthCallbackHandler.oauth_token = None
|
|
390
|
+
|
|
391
|
+
server = HTTPServer(("localhost", self.callback_port), OAuthCallbackHandler)
|
|
392
|
+
server.timeout = 300 # 5 minute timeout
|
|
393
|
+
|
|
394
|
+
return server
|
|
395
|
+
|
|
396
|
+
def _wait_for_callback(self, server: HTTPServer, max_requests: int = 10) -> str:
|
|
397
|
+
"""Wait for the OAuth callback and return the verifier."""
|
|
398
|
+
print(f"\nWaiting for authorization callback on port {self.callback_port}...")
|
|
399
|
+
print("(Press Ctrl+C to cancel)\n")
|
|
400
|
+
|
|
401
|
+
# Handle multiple requests (browser may make favicon/other requests)
|
|
402
|
+
for _ in range(max_requests):
|
|
403
|
+
server.handle_request()
|
|
404
|
+
|
|
405
|
+
verifier = OAuthCallbackHandler.oauth_verifier
|
|
406
|
+
if verifier:
|
|
407
|
+
return verifier
|
|
408
|
+
|
|
409
|
+
raise Exception(
|
|
410
|
+
"No verification code received from callback. "
|
|
411
|
+
"Please ensure you complete the authorization on motorsportreg.com"
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
def run_auth_flow(self, auto_open_browser: bool = True) -> dict:
|
|
415
|
+
"""
|
|
416
|
+
Run the complete OAuth authentication flow interactively.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
auto_open_browser: Automatically open authorization URL in browser
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
User profile data from /rest/me
|
|
423
|
+
|
|
424
|
+
Raises:
|
|
425
|
+
Exception: If any step of the auth flow fails
|
|
426
|
+
"""
|
|
427
|
+
print("=" * 60)
|
|
428
|
+
print("MotorsportsReg OAuth 1.0a Authentication")
|
|
429
|
+
print("=" * 60)
|
|
430
|
+
|
|
431
|
+
# Check for existing valid tokens
|
|
432
|
+
if self.has_valid_tokens():
|
|
433
|
+
print("\nFound existing access tokens. Validating...")
|
|
434
|
+
try:
|
|
435
|
+
profile = self.validate_connection()
|
|
436
|
+
print("\n[OK] Connection validated successfully!")
|
|
437
|
+
return profile
|
|
438
|
+
except Exception as e:
|
|
439
|
+
print(f"\nExisting tokens invalid: {e}")
|
|
440
|
+
print("Starting fresh authentication flow...")
|
|
441
|
+
|
|
442
|
+
# Start callback server before requesting token
|
|
443
|
+
server = self._start_callback_server()
|
|
444
|
+
|
|
445
|
+
try:
|
|
446
|
+
# Step 1: Get request token
|
|
447
|
+
self.get_request_token()
|
|
448
|
+
|
|
449
|
+
# Step 2: Get authorization URL
|
|
450
|
+
auth_url = self.get_authorization_url()
|
|
451
|
+
|
|
452
|
+
# Open browser automatically if requested
|
|
453
|
+
if auto_open_browser:
|
|
454
|
+
print("\n" + "!" * 60)
|
|
455
|
+
print("IMPORTANT: Copy and paste this URL into your browser:")
|
|
456
|
+
print(f"\n {auth_url}\n")
|
|
457
|
+
print("!" * 60)
|
|
458
|
+
print("\nAttempting to open browser automatically...")
|
|
459
|
+
try:
|
|
460
|
+
webbrowser.open(auth_url)
|
|
461
|
+
except Exception as e:
|
|
462
|
+
print(f" Could not open browser automatically: {e}")
|
|
463
|
+
print("\nIf the wrong page opens, manually navigate to the URL above.")
|
|
464
|
+
else:
|
|
465
|
+
print("\nPlease open the URL above in your browser.")
|
|
466
|
+
|
|
467
|
+
# Step 3: Wait for callback with verifier
|
|
468
|
+
verifier = self._wait_for_callback(server)
|
|
469
|
+
print(f"\n Received verifier: {verifier[:20]}...")
|
|
470
|
+
|
|
471
|
+
# Step 4: Exchange for access token
|
|
472
|
+
self.get_access_token(verifier)
|
|
473
|
+
|
|
474
|
+
# Validate the new tokens
|
|
475
|
+
profile = self.validate_connection()
|
|
476
|
+
|
|
477
|
+
print("\n" + "=" * 60)
|
|
478
|
+
print("[OK] Authentication successful!")
|
|
479
|
+
print("=" * 60)
|
|
480
|
+
|
|
481
|
+
return profile
|
|
482
|
+
|
|
483
|
+
finally:
|
|
484
|
+
server.server_close()
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def create_oauth_from_env() -> MSROAuth:
|
|
488
|
+
"""
|
|
489
|
+
Create an MSROAuth instance using credentials from keyring or environment.
|
|
490
|
+
|
|
491
|
+
Credential sources (in priority order):
|
|
492
|
+
1. System keyring (if available and configured)
|
|
493
|
+
2. Environment variables (MSR_CONSUMER_KEY, MSR_CONSUMER_SECRET)
|
|
494
|
+
|
|
495
|
+
Other environment variables:
|
|
496
|
+
MSR_BASE_URL: API base URL (optional)
|
|
497
|
+
MSR_CALLBACK_URL: OAuth callback URL (optional, defaults to localhost)
|
|
498
|
+
MSR_CALLBACK_PORT: Port for callback server (optional, default 8089)
|
|
499
|
+
|
|
500
|
+
Returns:
|
|
501
|
+
Configured MSROAuth instance
|
|
502
|
+
|
|
503
|
+
Raises:
|
|
504
|
+
ValueError: If credentials cannot be found from any source
|
|
505
|
+
"""
|
|
506
|
+
# Import credential manager to get credentials from keyring or env
|
|
507
|
+
from hpde_analytics_cli.auth.credentials import CredentialManager
|
|
508
|
+
|
|
509
|
+
credential_manager = CredentialManager()
|
|
510
|
+
|
|
511
|
+
try:
|
|
512
|
+
consumer_key, consumer_secret = credential_manager.get_credentials()
|
|
513
|
+
except ValueError:
|
|
514
|
+
raise ValueError(
|
|
515
|
+
"No credentials found. Run with --configure to set up credentials, "
|
|
516
|
+
"or set MSR_CONSUMER_KEY and MSR_CONSUMER_SECRET environment variables."
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
base_url = os.getenv("MSR_BASE_URL", "https://api.motorsportreg.com")
|
|
520
|
+
callback_url = os.getenv("MSR_CALLBACK_URL") # None = use localhost default
|
|
521
|
+
callback_port = int(os.getenv("MSR_CALLBACK_PORT", str(DEFAULT_CALLBACK_PORT)))
|
|
522
|
+
|
|
523
|
+
return MSROAuth(
|
|
524
|
+
consumer_key=consumer_key,
|
|
525
|
+
consumer_secret=consumer_secret,
|
|
526
|
+
base_url=base_url,
|
|
527
|
+
callback_url=callback_url,
|
|
528
|
+
callback_port=callback_port,
|
|
529
|
+
)
|