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 @@
|
|
|
1
|
+
"""API client module."""
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MotorsportsReg API client module.
|
|
3
|
+
|
|
4
|
+
Provides a high-level interface for accessing MotorsportsReg API endpoints.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
from typing import Any, Dict, Optional
|
|
9
|
+
|
|
10
|
+
from requests_oauthlib import OAuth1Session
|
|
11
|
+
|
|
12
|
+
from hpde_analytics_cli.auth.oauth import MSROAuth
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class APIError(Exception):
|
|
16
|
+
"""Exception raised for API errors."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self, message: str, status_code: Optional[int] = None, response_body: Optional[str] = None
|
|
20
|
+
):
|
|
21
|
+
super().__init__(message)
|
|
22
|
+
self.status_code = status_code
|
|
23
|
+
self.response_body = response_body
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class MSRClient:
|
|
27
|
+
"""Client for interacting with MotorsportsReg API endpoints."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, oauth: MSROAuth, organization_id: Optional[str] = None):
|
|
30
|
+
"""
|
|
31
|
+
Initialize the API client.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
oauth: Authenticated MSROAuth instance
|
|
35
|
+
organization_id: Organization ID for admin API access (optional)
|
|
36
|
+
"""
|
|
37
|
+
self.oauth = oauth
|
|
38
|
+
self.base_url = oauth.base_url
|
|
39
|
+
self.organization_id = organization_id
|
|
40
|
+
|
|
41
|
+
# Retry configuration
|
|
42
|
+
self.max_retries = 3
|
|
43
|
+
self.retry_delay = 1.0 # seconds
|
|
44
|
+
|
|
45
|
+
def _get_session(self) -> OAuth1Session:
|
|
46
|
+
"""Get an authenticated OAuth session."""
|
|
47
|
+
return self.oauth.get_oauth_session()
|
|
48
|
+
|
|
49
|
+
def _request(
|
|
50
|
+
self,
|
|
51
|
+
method: str,
|
|
52
|
+
endpoint: str,
|
|
53
|
+
params: Optional[Dict[str, Any]] = None,
|
|
54
|
+
include_org_header: bool = True,
|
|
55
|
+
retries: Optional[int] = None,
|
|
56
|
+
) -> Dict[str, Any]:
|
|
57
|
+
"""
|
|
58
|
+
Make an authenticated API request.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
method: HTTP method (GET, POST, etc.)
|
|
62
|
+
endpoint: API endpoint path (e.g., /rest/me.json)
|
|
63
|
+
params: Query parameters
|
|
64
|
+
include_org_header: Include X-Organization-Id header
|
|
65
|
+
retries: Number of retries (defaults to self.max_retries)
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Parsed JSON response (unwrapped from MSR response envelope)
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
APIError: If the request fails
|
|
72
|
+
"""
|
|
73
|
+
if retries is None:
|
|
74
|
+
retries = self.max_retries
|
|
75
|
+
|
|
76
|
+
session = self._get_session()
|
|
77
|
+
|
|
78
|
+
# Ensure endpoint has .json suffix for JSON response
|
|
79
|
+
if not endpoint.endswith(".json"):
|
|
80
|
+
endpoint = endpoint + ".json"
|
|
81
|
+
|
|
82
|
+
url = f"{self.base_url}{endpoint}"
|
|
83
|
+
|
|
84
|
+
headers = {}
|
|
85
|
+
if include_org_header and self.organization_id:
|
|
86
|
+
headers["X-Organization-Id"] = self.organization_id
|
|
87
|
+
|
|
88
|
+
last_error: APIError = APIError("Request failed with unknown error")
|
|
89
|
+
|
|
90
|
+
for attempt in range(retries + 1):
|
|
91
|
+
try:
|
|
92
|
+
if method.upper() == "GET":
|
|
93
|
+
response = session.get(url, params=params, headers=headers)
|
|
94
|
+
elif method.upper() == "POST":
|
|
95
|
+
response = session.post(url, data=params, headers=headers)
|
|
96
|
+
else:
|
|
97
|
+
raise APIError(f"Unsupported HTTP method: {method}")
|
|
98
|
+
|
|
99
|
+
# Handle response
|
|
100
|
+
if response.status_code == 200:
|
|
101
|
+
data = response.json()
|
|
102
|
+
# MSR wraps responses in {"response": {...}}
|
|
103
|
+
if isinstance(data, dict) and "response" in data:
|
|
104
|
+
data = data["response"]
|
|
105
|
+
return data
|
|
106
|
+
elif response.status_code == 401:
|
|
107
|
+
raise APIError(
|
|
108
|
+
"Authentication failed - tokens may be invalid or expired",
|
|
109
|
+
status_code=401,
|
|
110
|
+
response_body=response.text,
|
|
111
|
+
)
|
|
112
|
+
elif response.status_code == 403:
|
|
113
|
+
raise APIError(
|
|
114
|
+
"Access forbidden - insufficient permissions",
|
|
115
|
+
status_code=403,
|
|
116
|
+
response_body=response.text,
|
|
117
|
+
)
|
|
118
|
+
elif response.status_code == 404:
|
|
119
|
+
raise APIError(
|
|
120
|
+
f"Resource not found: {endpoint}",
|
|
121
|
+
status_code=404,
|
|
122
|
+
response_body=response.text,
|
|
123
|
+
)
|
|
124
|
+
elif response.status_code >= 500:
|
|
125
|
+
# Server error - retry
|
|
126
|
+
last_error = APIError(
|
|
127
|
+
f"Server error: {response.status_code}",
|
|
128
|
+
status_code=response.status_code,
|
|
129
|
+
response_body=response.text,
|
|
130
|
+
)
|
|
131
|
+
if attempt < retries:
|
|
132
|
+
time.sleep(self.retry_delay * (attempt + 1))
|
|
133
|
+
continue
|
|
134
|
+
raise last_error
|
|
135
|
+
else:
|
|
136
|
+
raise APIError(
|
|
137
|
+
f"Request failed with status {response.status_code}",
|
|
138
|
+
status_code=response.status_code,
|
|
139
|
+
response_body=response.text,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
except APIError:
|
|
143
|
+
raise
|
|
144
|
+
except Exception as e:
|
|
145
|
+
last_error = APIError(f"Request failed: {str(e)}")
|
|
146
|
+
if attempt < retries:
|
|
147
|
+
time.sleep(self.retry_delay * (attempt + 1))
|
|
148
|
+
continue
|
|
149
|
+
raise last_error
|
|
150
|
+
|
|
151
|
+
raise last_error
|
|
152
|
+
|
|
153
|
+
def get_me(self) -> Dict[str, Any]:
|
|
154
|
+
"""
|
|
155
|
+
Get the authenticated user's profile.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
User profile data including organizations
|
|
159
|
+
"""
|
|
160
|
+
return self._request("GET", "/rest/me", include_org_header=False)
|
|
161
|
+
|
|
162
|
+
def get_organization_calendar(self, organization_id: Optional[str] = None) -> Dict[str, Any]:
|
|
163
|
+
"""
|
|
164
|
+
Get the organization's event calendar.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
organization_id: Organization ID (uses default if not provided)
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Calendar data with list of events
|
|
171
|
+
|
|
172
|
+
Endpoint: GET /rest/calendars/organization/{organization_id}
|
|
173
|
+
"""
|
|
174
|
+
org_id = organization_id or self.organization_id
|
|
175
|
+
if not org_id:
|
|
176
|
+
raise ValueError("Organization ID is required")
|
|
177
|
+
|
|
178
|
+
return self._request("GET", f"/rest/calendars/organization/{org_id}")
|
|
179
|
+
|
|
180
|
+
def get_event_entrylist(self, event_id: str) -> Dict[str, Any]:
|
|
181
|
+
"""
|
|
182
|
+
Get the entry list for an event.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
event_id: Event ID
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Entry list data
|
|
189
|
+
|
|
190
|
+
Endpoint: GET /rest/events/{event_id}/entrylist
|
|
191
|
+
"""
|
|
192
|
+
if not event_id:
|
|
193
|
+
raise ValueError("Event ID is required")
|
|
194
|
+
|
|
195
|
+
return self._request("GET", f"/rest/events/{event_id}/entrylist")
|
|
196
|
+
|
|
197
|
+
def get_event_attendees(self, event_id: str) -> Dict[str, Any]:
|
|
198
|
+
"""
|
|
199
|
+
Get complete attendee list for an event.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
event_id: Event ID
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Attendee data
|
|
206
|
+
|
|
207
|
+
Endpoint: GET /rest/events/{event_id}/attendees
|
|
208
|
+
"""
|
|
209
|
+
if not event_id:
|
|
210
|
+
raise ValueError("Event ID is required")
|
|
211
|
+
|
|
212
|
+
return self._request("GET", f"/rest/events/{event_id}/attendees")
|
|
213
|
+
|
|
214
|
+
def get_event_assignments(self, event_id: str) -> Dict[str, Any]:
|
|
215
|
+
"""
|
|
216
|
+
Get event assignments/entries including vehicle information.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
event_id: Event ID
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Assignment data with vehicle details
|
|
223
|
+
|
|
224
|
+
Endpoint: GET /rest/events/{event_id}/assignments
|
|
225
|
+
"""
|
|
226
|
+
if not event_id:
|
|
227
|
+
raise ValueError("Event ID is required")
|
|
228
|
+
|
|
229
|
+
return self._request("GET", f"/rest/events/{event_id}/assignments")
|
|
230
|
+
|
|
231
|
+
def get_timing_feed(self, event_id: str) -> Dict[str, Any]:
|
|
232
|
+
"""
|
|
233
|
+
Get timing and scoring feed for an event.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
event_id: Event ID
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
Timing/scoring data
|
|
240
|
+
|
|
241
|
+
Endpoint: GET /rest/events/{event_id}/feeds/timing
|
|
242
|
+
"""
|
|
243
|
+
if not event_id:
|
|
244
|
+
raise ValueError("Event ID is required")
|
|
245
|
+
|
|
246
|
+
return self._request("GET", f"/rest/events/{event_id}/feeds/timing")
|
|
247
|
+
|
|
248
|
+
def get_all_endpoint_data(self, event_id: Optional[str] = None) -> Dict[str, Any]:
|
|
249
|
+
"""
|
|
250
|
+
Fetch data from all available endpoints for field discovery.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
event_id: Optional event ID for event-specific endpoints
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Dict with data from each endpoint, keyed by endpoint name
|
|
257
|
+
"""
|
|
258
|
+
results = {}
|
|
259
|
+
|
|
260
|
+
# Always fetch user profile
|
|
261
|
+
print(" Fetching /rest/me...")
|
|
262
|
+
try:
|
|
263
|
+
results["me"] = self.get_me()
|
|
264
|
+
print(" [OK] User profile retrieved")
|
|
265
|
+
except APIError as e:
|
|
266
|
+
print(f" [ERROR] {e}")
|
|
267
|
+
results["me"] = {"error": str(e)}
|
|
268
|
+
|
|
269
|
+
# Fetch organization calendar
|
|
270
|
+
if self.organization_id:
|
|
271
|
+
print(f" Fetching organization calendar (org: {self.organization_id})...")
|
|
272
|
+
try:
|
|
273
|
+
results["calendar"] = self.get_organization_calendar()
|
|
274
|
+
print(" [OK] Calendar retrieved")
|
|
275
|
+
|
|
276
|
+
# Get first event ID if not provided
|
|
277
|
+
if not event_id:
|
|
278
|
+
events = results["calendar"].get("events", [])
|
|
279
|
+
if events:
|
|
280
|
+
event_id = events[0].get("id")
|
|
281
|
+
print(f" Using first event from calendar: {event_id}")
|
|
282
|
+
except APIError as e:
|
|
283
|
+
print(f" [ERROR] {e}")
|
|
284
|
+
results["calendar"] = {"error": str(e)}
|
|
285
|
+
|
|
286
|
+
# Event-specific endpoints (require event_id)
|
|
287
|
+
if event_id:
|
|
288
|
+
event_endpoints = [
|
|
289
|
+
("entrylist", self.get_event_entrylist, "Entry list"),
|
|
290
|
+
("attendees", self.get_event_attendees, "Attendees"),
|
|
291
|
+
("assignments", self.get_event_assignments, "Assignments"),
|
|
292
|
+
("timing", self.get_timing_feed, "Timing feed"),
|
|
293
|
+
]
|
|
294
|
+
|
|
295
|
+
for key, method, description in event_endpoints:
|
|
296
|
+
print(f" Fetching {description} (event: {event_id})...")
|
|
297
|
+
try:
|
|
298
|
+
results[key] = method(event_id)
|
|
299
|
+
print(f" [OK] {description} retrieved")
|
|
300
|
+
except APIError as e:
|
|
301
|
+
print(f" [ERROR] {e}")
|
|
302
|
+
results[key] = {"error": str(e)}
|
|
303
|
+
else:
|
|
304
|
+
print(" [SKIP] No event ID available - skipping event-specific endpoints")
|
|
305
|
+
print(" Use --event-id to specify an event, or ensure calendar has events")
|
|
306
|
+
|
|
307
|
+
return results
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def create_client_from_oauth(oauth: MSROAuth, organization_id: Optional[str] = None) -> MSRClient:
|
|
311
|
+
"""
|
|
312
|
+
Create an API client from an authenticated OAuth handler.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
oauth: Authenticated MSROAuth instance
|
|
316
|
+
organization_id: Optional organization ID override
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
Configured MSRClient instance
|
|
320
|
+
"""
|
|
321
|
+
# Use provided org ID, or fall back to first organization from profile
|
|
322
|
+
if not organization_id and oauth.organizations:
|
|
323
|
+
organization_id = oauth.organizations[0].get("id")
|
|
324
|
+
|
|
325
|
+
return MSRClient(oauth=oauth, organization_id=organization_id)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""OAuth authentication module."""
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Credential Manager Module
|
|
3
|
+
|
|
4
|
+
Securely stores and retrieves OAuth credentials using the system keyring.
|
|
5
|
+
Falls back to environment variables/.env file if keyring is unavailable.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import getpass
|
|
9
|
+
import os
|
|
10
|
+
from typing import Optional, Tuple
|
|
11
|
+
|
|
12
|
+
# Application identifier for keyring storage
|
|
13
|
+
APP_NAME = "hpde-analytics-cli"
|
|
14
|
+
|
|
15
|
+
# Credential keys
|
|
16
|
+
KEY_CONSUMER_KEY = "msr_consumer_key"
|
|
17
|
+
KEY_CONSUMER_SECRET = "msr_consumer_secret"
|
|
18
|
+
|
|
19
|
+
# Try to import keyring, but don't fail if unavailable
|
|
20
|
+
try:
|
|
21
|
+
import keyring
|
|
22
|
+
|
|
23
|
+
KEYRING_AVAILABLE = True
|
|
24
|
+
except ImportError:
|
|
25
|
+
KEYRING_AVAILABLE = False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CredentialManager:
|
|
29
|
+
"""
|
|
30
|
+
Manages OAuth credentials with secure storage.
|
|
31
|
+
|
|
32
|
+
Priority order for credential retrieval:
|
|
33
|
+
1. System keyring (if available and credentials stored)
|
|
34
|
+
2. Environment variables
|
|
35
|
+
3. Interactive prompt (for --configure)
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, app_name: str = APP_NAME):
|
|
39
|
+
"""
|
|
40
|
+
Initialize the credential manager.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
app_name: Application identifier for keyring storage
|
|
44
|
+
"""
|
|
45
|
+
self.app_name = app_name
|
|
46
|
+
|
|
47
|
+
def keyring_available(self) -> bool:
|
|
48
|
+
"""Check if keyring is available and functional."""
|
|
49
|
+
if not KEYRING_AVAILABLE:
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
# Test if keyring backend is actually working
|
|
53
|
+
try:
|
|
54
|
+
# Try to get a non-existent key to test functionality
|
|
55
|
+
keyring.get_password(self.app_name, "__test__")
|
|
56
|
+
return True
|
|
57
|
+
except Exception:
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
def get_credentials_from_keyring(self) -> Tuple[Optional[str], Optional[str]]:
|
|
61
|
+
"""
|
|
62
|
+
Retrieve credentials from system keyring.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Tuple of (consumer_key, consumer_secret) or (None, None) if not found
|
|
66
|
+
"""
|
|
67
|
+
if not self.keyring_available():
|
|
68
|
+
return None, None
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
consumer_key = keyring.get_password(self.app_name, KEY_CONSUMER_KEY)
|
|
72
|
+
consumer_secret = keyring.get_password(self.app_name, KEY_CONSUMER_SECRET)
|
|
73
|
+
return consumer_key, consumer_secret
|
|
74
|
+
except Exception:
|
|
75
|
+
return None, None
|
|
76
|
+
|
|
77
|
+
def get_credentials_from_env(self) -> Tuple[Optional[str], Optional[str]]:
|
|
78
|
+
"""
|
|
79
|
+
Retrieve credentials from environment variables.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Tuple of (consumer_key, consumer_secret) or (None, None) if not found
|
|
83
|
+
"""
|
|
84
|
+
consumer_key = os.environ.get("MSR_CONSUMER_KEY")
|
|
85
|
+
consumer_secret = os.environ.get("MSR_CONSUMER_SECRET")
|
|
86
|
+
return consumer_key, consumer_secret
|
|
87
|
+
|
|
88
|
+
def get_credentials(self) -> Tuple[str, str]:
|
|
89
|
+
"""
|
|
90
|
+
Get credentials from the best available source.
|
|
91
|
+
|
|
92
|
+
Priority:
|
|
93
|
+
1. System keyring
|
|
94
|
+
2. Environment variables
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Tuple of (consumer_key, consumer_secret)
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
ValueError: If credentials cannot be found from any source
|
|
101
|
+
"""
|
|
102
|
+
# Try keyring first
|
|
103
|
+
consumer_key, consumer_secret = self.get_credentials_from_keyring()
|
|
104
|
+
if consumer_key and consumer_secret:
|
|
105
|
+
return consumer_key, consumer_secret
|
|
106
|
+
|
|
107
|
+
# Fall back to environment variables
|
|
108
|
+
consumer_key, consumer_secret = self.get_credentials_from_env()
|
|
109
|
+
if consumer_key and consumer_secret:
|
|
110
|
+
return consumer_key, consumer_secret
|
|
111
|
+
|
|
112
|
+
# No credentials found
|
|
113
|
+
raise ValueError(
|
|
114
|
+
"No credentials found. Run with --configure to set up credentials, "
|
|
115
|
+
"or set MSR_CONSUMER_KEY and MSR_CONSUMER_SECRET environment variables."
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def store_credentials(self, consumer_key: str, consumer_secret: str) -> bool:
|
|
119
|
+
"""
|
|
120
|
+
Store credentials in system keyring.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
consumer_key: OAuth consumer key
|
|
124
|
+
consumer_secret: OAuth consumer secret
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
True if stored successfully, False otherwise
|
|
128
|
+
"""
|
|
129
|
+
if not self.keyring_available():
|
|
130
|
+
print("Warning: System keyring not available. Cannot store credentials securely.")
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
keyring.set_password(self.app_name, KEY_CONSUMER_KEY, consumer_key)
|
|
135
|
+
keyring.set_password(self.app_name, KEY_CONSUMER_SECRET, consumer_secret)
|
|
136
|
+
return True
|
|
137
|
+
except Exception as e:
|
|
138
|
+
print(f"Warning: Failed to store credentials in keyring: {e}")
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
def delete_credentials(self) -> bool:
|
|
142
|
+
"""
|
|
143
|
+
Delete credentials from system keyring.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
True if deleted successfully, False otherwise
|
|
147
|
+
"""
|
|
148
|
+
if not self.keyring_available():
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
keyring.delete_password(self.app_name, KEY_CONSUMER_KEY)
|
|
153
|
+
keyring.delete_password(self.app_name, KEY_CONSUMER_SECRET)
|
|
154
|
+
return True
|
|
155
|
+
except Exception:
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
def has_stored_credentials(self) -> bool:
|
|
159
|
+
"""
|
|
160
|
+
Check if credentials are stored in keyring.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
True if credentials exist in keyring
|
|
164
|
+
"""
|
|
165
|
+
consumer_key, consumer_secret = self.get_credentials_from_keyring()
|
|
166
|
+
return bool(consumer_key and consumer_secret)
|
|
167
|
+
|
|
168
|
+
def configure_interactive(self) -> bool:
|
|
169
|
+
"""
|
|
170
|
+
Interactively configure credentials via command line prompts.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
True if configuration successful
|
|
174
|
+
"""
|
|
175
|
+
print("\n" + "=" * 60)
|
|
176
|
+
print("HPDE Analytics - Credential Configuration")
|
|
177
|
+
print("=" * 60)
|
|
178
|
+
|
|
179
|
+
if not self.keyring_available():
|
|
180
|
+
print("\nWarning: System keyring is not available.")
|
|
181
|
+
print("Credentials will need to be set via environment variables.")
|
|
182
|
+
print("\nTo use environment variables, add to your .env file:")
|
|
183
|
+
print(" MSR_CONSUMER_KEY=your_key_here")
|
|
184
|
+
print(" MSR_CONSUMER_SECRET=your_secret_here")
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
print("\nThis will store your MotorsportsReg OAuth credentials securely")
|
|
188
|
+
print("in your system's credential manager (Keychain/Credential Locker).")
|
|
189
|
+
|
|
190
|
+
# Check for existing credentials
|
|
191
|
+
if self.has_stored_credentials():
|
|
192
|
+
print("\nExisting credentials found in keyring.")
|
|
193
|
+
response = input("Do you want to replace them? (y/N): ").strip().lower()
|
|
194
|
+
if response != "y":
|
|
195
|
+
print("Configuration cancelled.")
|
|
196
|
+
return False
|
|
197
|
+
|
|
198
|
+
print("\nEnter your MotorsportsReg OAuth credentials:")
|
|
199
|
+
print("(These are provided by MotorsportsReg for API access)")
|
|
200
|
+
print()
|
|
201
|
+
|
|
202
|
+
# Get consumer key
|
|
203
|
+
consumer_key = input("Consumer Key: ").strip()
|
|
204
|
+
if not consumer_key:
|
|
205
|
+
print("Error: Consumer key cannot be empty.")
|
|
206
|
+
return False
|
|
207
|
+
|
|
208
|
+
# Get consumer secret (hidden input)
|
|
209
|
+
consumer_secret = getpass.getpass("Consumer Secret: ").strip()
|
|
210
|
+
if not consumer_secret:
|
|
211
|
+
print("Error: Consumer secret cannot be empty.")
|
|
212
|
+
return False
|
|
213
|
+
|
|
214
|
+
# Store credentials
|
|
215
|
+
if self.store_credentials(consumer_key, consumer_secret):
|
|
216
|
+
print("\n[OK] Credentials stored securely in system keyring.")
|
|
217
|
+
print(" You can now run the application without environment variables.")
|
|
218
|
+
return True
|
|
219
|
+
else:
|
|
220
|
+
print("\n[ERROR] Failed to store credentials.")
|
|
221
|
+
return False
|
|
222
|
+
|
|
223
|
+
def show_status(self) -> None:
|
|
224
|
+
"""Display current credential configuration status."""
|
|
225
|
+
print("\n" + "=" * 60)
|
|
226
|
+
print("Credential Status")
|
|
227
|
+
print("=" * 60)
|
|
228
|
+
|
|
229
|
+
# Check keyring availability
|
|
230
|
+
print(f"\nSystem keyring available: {'Yes' if self.keyring_available() else 'No'}")
|
|
231
|
+
|
|
232
|
+
# Check keyring credentials
|
|
233
|
+
keyring_key, keyring_secret = self.get_credentials_from_keyring()
|
|
234
|
+
has_keyring = bool(keyring_key and keyring_secret)
|
|
235
|
+
print(f"Credentials in keyring: {'Yes' if has_keyring else 'No'}")
|
|
236
|
+
|
|
237
|
+
# Check environment variables
|
|
238
|
+
env_key, env_secret = self.get_credentials_from_env()
|
|
239
|
+
has_env = bool(env_key and env_secret)
|
|
240
|
+
print(f"Credentials in environment: {'Yes' if has_env else 'No'}")
|
|
241
|
+
|
|
242
|
+
# Show which source will be used
|
|
243
|
+
if has_keyring:
|
|
244
|
+
print("\n[Active] Using credentials from system keyring")
|
|
245
|
+
elif has_env:
|
|
246
|
+
print("\n[Active] Using credentials from environment variables")
|
|
247
|
+
else:
|
|
248
|
+
print("\n[Warning] No credentials configured")
|
|
249
|
+
print(" Run with --configure to set up credentials")
|
|
250
|
+
|
|
251
|
+
print()
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def get_credential_manager() -> CredentialManager:
|
|
255
|
+
"""Get a configured credential manager instance."""
|
|
256
|
+
return CredentialManager()
|