Spaces:
Sleeping
Sleeping
File size: 16,018 Bytes
c26f5b3 6a725a4 c26f5b3 6a725a4 c26f5b3 6a725a4 c26f5b3 6a725a4 c26f5b3 6a725a4 c26f5b3 6a725a4 c26f5b3 6a725a4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 | import os
import json
import pickle
from datetime import datetime
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google.oauth2 import service_account
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from googleapiclient.http import MediaFileUpload
# Google Drive API scopes
SCOPES = ['https://www.googleapis.com/auth/drive.file']
# ============================================================================
# Hard-coded Google Drive auth configuration
# Option 1: Service Account (recommended)
# Option 2: Refresh Token
# ============================================================================
# Option 1: Service Account JSON file (env-var friendly for Spaces)
SERVICE_ACCOUNT_FILE = os.getenv("GOOGLE_SERVICE_ACCOUNT_FILE", None) # e.g. "path/to/service-account-key.json"
# Option 2: Refresh Token (requires OAuth flow once)
REFRESH_TOKEN = os.getenv("GOOGLE_REFRESH_TOKEN", None)
CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID")
CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET")
# Fallback: interactive OAuth
USE_HARDCODED_AUTH = SERVICE_ACCOUNT_FILE is not None or REFRESH_TOKEN is not None
# Real OAuth 2.0 configuration - Public client credentials for this app
# These are safe to include in public code (they're meant to be public)
CLIENT_CONFIG = {
"installed": {
"client_id": CLIENT_ID,
"project_id": "patient-eval-app-439620",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_secret": CLIENT_SECRET,
"redirect_uris": ["http://localhost"]
}
}
class GoogleDriveSync:
def __init__(self, user_id="default"):
self.user_id = user_id
self.token_file = f'user_tokens/{user_id}_token.pickle'
self.service = None
self.folder_id = None
# Ensure user tokens directory exists
os.makedirs('user_tokens', exist_ok=True)
def start_oauth_flow(self):
"""Start OAuth flow for user authentication"""
try:
# Use Google's Flow from the client config
flow = InstalledAppFlow.from_client_config(CLIENT_CONFIG, SCOPES)
# Run the OAuth flow with port=0 to use any available port
creds = flow.run_local_server(port=0, open_browser=True)
# Save the credentials
with open(self.token_file, 'wb') as token:
pickle.dump(creds, token)
return True, f"Google authentication successful! You can now sync your evaluation data to your personal Google Drive."
except Exception as e:
return False, f"Authentication error: {str(e)}"
def authenticate(self):
"""Authenticate with hardcoded credentials, existing token, or start new flow"""
creds = None
# Option 1: Service Account (supports env JSON for Spaces)
service_account_json = os.getenv("GOOGLE_SERVICE_ACCOUNT_JSON")
if service_account_json:
try:
import tempfile
import json as json_lib
# Persist JSON string to a temporary file
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
json_lib.dump(json_lib.loads(service_account_json), f)
temp_file = f.name
creds = service_account.Credentials.from_service_account_file(
temp_file, scopes=SCOPES)
self.service = build('drive', 'v3', credentials=creds)
os.unlink(temp_file) # remove temp file
return True, "Authenticated with Service Account (from env)"
except Exception as e:
return False, f"Service Account authentication error: {str(e)}"
if SERVICE_ACCOUNT_FILE and os.path.exists(SERVICE_ACCOUNT_FILE):
try:
creds = service_account.Credentials.from_service_account_file(
SERVICE_ACCOUNT_FILE, scopes=SCOPES)
self.service = build('drive', 'v3', credentials=creds)
return True, "Authenticated with Service Account"
except Exception as e:
return False, f"Service Account authentication error: {str(e)}"
# Option 2: hard-coded refresh token
if REFRESH_TOKEN:
if not CLIENT_ID or not CLIENT_SECRET:
return False, "Refresh token provided but GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET are missing."
try:
creds = Credentials(
token=None,
refresh_token=REFRESH_TOKEN,
token_uri="https://oauth2.googleapis.com/token",
client_id=CLIENT_ID,
client_secret=CLIENT_SECRET
)
# Refresh to obtain a fresh access token
creds.refresh(Request())
self.service = build('drive', 'v3', credentials=creds)
return True, "Authenticated with Refresh Token"
except Exception as e:
return False, f"Refresh Token authentication error: {str(e)}"
# Option 3: previously saved token file
if os.path.exists(self.token_file):
try:
with open(self.token_file, 'rb') as token:
creds = pickle.load(token)
# Check if credentials are valid and refresh if needed
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
# Save refreshed credentials
with open(self.token_file, 'wb') as token:
pickle.dump(creds, token)
else:
return False, "Credentials expired. Please login again."
# Build the service
self.service = build('drive', 'v3', credentials=creds)
return True, f"Authenticated with your Google account"
except Exception as e:
# Token file corrupted, remove it
if os.path.exists(self.token_file):
os.remove(self.token_file)
return False, f"Authentication error: {str(e)}. Please login again."
return False, "Please authenticate with Google first by clicking 'Login to Google Drive'"
def create_or_get_folder(self, folder_name="Patient_Evaluations"):
"""Create or get the folder for storing evaluation data"""
if not self.service:
return None, "Not authenticated with Google Drive"
try:
# Search for existing folder
results = self.service.files().list(
q=f"name='{folder_name}' and mimeType='application/vnd.google-apps.folder' and trashed=false",
spaces='drive',
fields="files(id, name)"
).execute()
items = results.get('files', [])
if items:
self.folder_id = items[0]['id']
return self.folder_id, f"Found existing folder: {folder_name}"
# Create new folder
folder_metadata = {
'name': folder_name,
'mimeType': 'application/vnd.google-apps.folder'
}
folder = self.service.files().create(body=folder_metadata, fields='id').execute()
self.folder_id = folder.get('id')
return self.folder_id, f"Created new folder: {folder_name}"
except Exception as e:
return None, f"Error creating/finding folder: {str(e)}"
def upload_file(self, local_file_path, drive_file_name=None):
"""Upload a file to user's Google Drive"""
if not self.service or not self.folder_id:
return False, "Google Drive not properly initialized"
if not os.path.exists(local_file_path):
return False, f"Local file not found: {local_file_path}"
try:
if not drive_file_name:
drive_file_name = os.path.basename(local_file_path)
# Add timestamp to filename
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
name, ext = os.path.splitext(drive_file_name)
drive_file_name = f"{name}_{timestamp}{ext}"
file_metadata = {
'name': drive_file_name,
'parents': [self.folder_id]
}
media = MediaFileUpload(local_file_path, resumable=True)
file = self.service.files().create(
body=file_metadata,
media_body=media,
fields='id,name,webViewLink'
).execute()
# Make file shareable (optional)
try:
self.service.permissions().create(
fileId=file.get('id'),
body={'role': 'reader', 'type': 'anyone'}
).execute()
except:
pass # Permission setting failed, but upload succeeded
return True, {
'file_id': file.get('id'),
'name': file.get('name'),
'link': file.get('webViewLink'),
'message': f"File uploaded successfully: {drive_file_name}"
}
except Exception as e:
return False, f"Error uploading file: {str(e)}"
def upload_evaluation_data(self, csv_file_path, json_files_dir="patient_evaluations"):
"""Upload all evaluation data to user's Google Drive"""
results = []
# Upload CSV file
if os.path.exists(csv_file_path):
success, result = self.upload_file(csv_file_path, "patient_evaluations_master.csv")
results.append(('CSV Master File', success, result))
# Upload JSON files
if os.path.exists(json_files_dir):
json_files = [f for f in os.listdir(json_files_dir) if f.endswith('.json')]
for filename in json_files[:5]: # Limit to prevent too many uploads at once
file_path = os.path.join(json_files_dir, filename)
success, result = self.upload_file(file_path)
results.append(('Evaluation File', success, result))
return results
# Global instances per user
user_syncs = {}
def get_user_sync(user_id="default"):
"""Get or create GoogleDriveSync instance for user"""
if user_id not in user_syncs:
user_syncs[user_id] = GoogleDriveSync(user_id)
return user_syncs[user_id]
def start_google_login(user_id="default"):
"""Start Google OAuth flow for user"""
sync = get_user_sync(user_id)
return sync.start_oauth_flow()
def sync_to_google_drive(user_id="default"):
"""Main function to sync evaluation data to user's Google Drive"""
sync = get_user_sync(user_id)
# Check authentication
success, message = sync.authenticate()
if not success:
return False, message
# Create or get folder
folder_id, folder_message = sync.create_or_get_folder()
if not folder_id:
return False, folder_message
# Upload evaluation data
csv_path = "patient_evaluations/patient_evaluations_master.csv"
results = sync.upload_evaluation_data(csv_path)
success_count = sum(1 for _, success, _ in results if success)
total_count = len(results)
if success_count > 0:
links = []
for file_type, success, result in results:
if success and isinstance(result, dict) and 'link' in result:
links.append(f"β’ {file_type}: {result['link']}")
link_text = "\n" + "\n".join(links) if links else ""
return True, f"β
Synced {success_count}/{total_count} files to your Google Drive!{link_text}"
else:
return False, "β Failed to sync files. Please check your authentication and try again."
def get_drive_setup_instructions():
"""Return simplified instructions for users"""
return """
## π Save to Your Google Drive
Click "Login to Google Drive" below to connect your personal Google account.
### What happens:
1. **Secure Login**: A browser window will open for you to login to your Google account
2. **Permission Request**: You'll be asked to allow this app to save files to your Drive
3. **Automatic Backup**: Your evaluation data will be saved to a "Patient_Evaluations" folder in your Drive
4. **Your Data**: Only you can access your files - they're saved to YOUR personal Google Drive
### First Time Setup:
- Click "Login to Google Drive"
- Complete the Google authentication in your browser
- Return here and click "Sync to Google Drive" to backup your data
### Security:
- This app only saves evaluation files you create
- No access to your other Google Drive files
- You can revoke access anytime in your Google Account settings
"""
def check_user_authentication(user_id="default"):
"""Check if user is already authenticated"""
sync = get_user_sync(user_id)
success, message = sync.authenticate()
return success, message
def get_refresh_token():
"""
Helper: run once to fetch a refresh token for the hard-coded config.
Complete the OAuth flow, copy the printed token into REFRESH_TOKEN.
"""
try:
flow = InstalledAppFlow.from_client_config(CLIENT_CONFIG, SCOPES)
creds = flow.run_local_server(port=0, open_browser=True)
if creds and creds.refresh_token:
print("\n" + "="*60)
print("β
Refresh token retrieved!")
print("="*60)
print(f"\nPaste the following refresh_token into REFRESH_TOKEN:\n")
print(f"REFRESH_TOKEN = \"{creds.refresh_token}\"")
print("\n" + "="*60 + "\n")
return creds.refresh_token
else:
print("β Could not obtain refresh_token. Please retry.")
return None
except Exception as e:
print(f"β Error retrieving refresh_token: {str(e)}")
return None
def auto_upload_to_drive(file_path, user_id="default", folder_name="Chatbot_Data"):
"""
Automatically upload a file to Google Drive if user is authenticated.
Returns (success, message) tuple. If not authenticated, returns (False, None) silently.
This function fails silently to not interrupt normal operation.
"""
try:
sync = get_user_sync(user_id)
# Check authentication (uses hard-coded creds if configured)
success, message = sync.authenticate()
if not success:
# Log once when hard-coded auth was expected but failed
if USE_HARDCODED_AUTH:
print(f"β οΈ Google Drive authentication failed: {message}")
return False, None # Not authenticated, fail silently
# Create or get folder (use custom folder name for chatbot data)
folder_id, folder_message = sync.create_or_get_folder(folder_name)
if not folder_id:
return False, None # Fail silently if folder creation fails
# Upload file
success, result = sync.upload_file(file_path)
if success:
return True, f"β
Auto-uploaded to Google Drive: {result.get('name', 'file')}"
else:
return False, None # Fail silently on upload error
except Exception as e:
# Fail silently if there's an error (e.g., network issue, import error)
return False, None |