algospaced-dsa / gdrive_sync.py
iamfebin's picture
Configure AlgoSpaced for Hugging Face Spaces deployment
aaa634c
Raw
History Blame Contribute Delete
11.5 kB
import re
import streamlit as st
from google.oauth2 import service_account
from googleapiclient.discovery import build
import db
def get_secret(key):
import os
# Try environment variables first
env_val = os.environ.get(key.upper()) or os.environ.get(key)
if env_val:
if key == "gcp_service_account" or (isinstance(env_val, str) and env_val.strip().startswith("{")):
import json
try:
return json.loads(env_val)
except Exception:
pass
return env_val
try:
# Try Streamlit secrets first
if hasattr(st, "secrets") and st.secrets and key in st.secrets:
return st.secrets[key]
except Exception:
pass
# Fallback to loading from toml directly
import toml
try:
secrets_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".streamlit", "secrets.toml")
if os.path.exists(secrets_path):
data = toml.load(secrets_path)
if key in data:
return data[key]
except Exception:
pass
return None
def get_credentials():
creds_dict = get_secret("gcp_service_account")
if not creds_dict:
raise KeyError("gcp_service_account not found in secrets")
return service_account.Credentials.from_service_account_info(
dict(creds_dict),
scopes=[
'https://www.googleapis.com/auth/drive.readonly',
'https://www.googleapis.com/auth/documents.readonly'
]
)
def extract_text_from_elements(elements):
text = ""
for value in elements:
if 'paragraph' in value:
for elem in value.get('paragraph', {}).get('elements', []):
if 'textRun' in elem:
text += elem.get('textRun', {}).get('content', '')
return text
def parse_problem_body(problem_name, body):
# Match headers like "Method 1:" or "Method 2 (Two Pointers):"
method_matches = list(re.finditer(r'Method\s*(\d+)(?:\s*\(([^)]+)\))?:', body, re.IGNORECASE))
difficulty = "Medium"
diff_match = re.search(r'^\s*(Easy|Medium|Hard)\b', body, re.IGNORECASE | re.MULTILINE)
if diff_match:
difficulty = diff_match.group(1).capitalize()
methods = []
# Extract description text: everything before the first Method match or Time Complexity line, excluding the difficulty word.
desc_raw = ""
if method_matches:
desc_raw = body[:method_matches[0].start()]
else:
time_match = re.search(r'Time\s+Complexity:\s*([^\n\r]+)', body, re.IGNORECASE)
if time_match:
desc_raw = body[:time_match.start()]
else:
desc_raw = body
description = re.sub(r'^\s*(Easy|Medium|Hard)\b', '', desc_raw, flags=re.IGNORECASE | re.MULTILINE).strip()
if not method_matches:
# Fallback for when there are no "Method X" headers (treat the whole body as one method)
time_match = re.search(r'Time\s+Complexity:\s*([^\n\r]+)', body, re.IGNORECASE)
space_match = re.search(r'Space\s+Complexity:\s*([^\n\r]+)', body, re.IGNORECASE)
optimal_time = time_match.group(1).strip() if time_match else "O(N)"
optimal_space = space_match.group(1).strip() if space_match else "O(N)"
code = ""
if space_match:
start_pos = space_match.end()
raw_code = body[start_pos:].strip()
# Improved regex to handle leading indentation/spaces/tabs
code_start_match = re.search(r'(?:^|\n)[ \t]*(?:def\s+|class\s+|import\s+|from\s+\S+\s+import|```[a-zA-Z]*)', raw_code)
if code_start_match:
raw_code = raw_code[code_start_match.start():].strip()
# Clean up: remove "Code:" or "Reference Code:" if explicitly present
raw_code = re.sub(r'^(Code|Reference Code):\s*', '', raw_code, flags=re.IGNORECASE)
# Clean up: strip markdown backticks if present
raw_code = re.sub(r'^```[a-zA-Z]*\s*', '', raw_code)
raw_code = re.sub(r'\s*```$', '', raw_code)
code = raw_code.strip()
methods.append({
"name": f"{problem_name} - Method 1 (Standard)",
"pattern": "Standard",
"difficulty": difficulty,
"optimal_time": optimal_time,
"optimal_space": optimal_space,
"code": code,
"description": description
})
else:
for j, m_match in enumerate(method_matches):
m_start = m_match.end()
m_end = method_matches[j+1].start() if j + 1 < len(method_matches) else len(body)
method_num = m_match.group(1).strip()
method_name = m_match.group(2).strip() if m_match.group(2) else f"Method {method_num}"
method_body = body[m_start:m_end]
# Complexities
time_match = re.search(r'Time\s+Complexity:\s*([^\n\r]+)', method_body, re.IGNORECASE)
space_match = re.search(r'Space\s+Complexity:\s*([^\n\r]+)', method_body, re.IGNORECASE)
optimal_time = time_match.group(1).strip() if time_match else "O(N)"
optimal_space = space_match.group(1).strip() if space_match else "O(N)"
# Extract code block: grab everything following the "Space Complexity" line
code = ""
if space_match:
start_pos = space_match.end()
raw_code = method_body[start_pos:].strip()
# Improved regex to handle leading indentation/spaces/tabs
code_start_match = re.search(r'(?:^|\n)[ \t]*(?:def\s+|class\s+|import\s+|from\s+\S+\s+import|```[a-zA-Z]*)', raw_code)
if code_start_match:
raw_code = raw_code[code_start_match.start():].strip()
# Clean up: remove "Code:" or "Reference Code:" if explicitly present
raw_code = re.sub(r'^(Code|Reference Code):\s*', '', raw_code, flags=re.IGNORECASE)
# Clean up: strip markdown backticks if present
raw_code = re.sub(r'^```[a-zA-Z]*\s*', '', raw_code)
raw_code = re.sub(r'\s*```$', '', raw_code)
code = raw_code.strip()
methods.append({
"name": f"{problem_name} - Method {method_num} ({method_name})",
"pattern": method_name,
"difficulty": difficulty,
"optimal_time": optimal_time,
"optimal_space": optimal_space,
"code": code,
"description": description
})
return methods
def parse_multi_problem_doc(text):
# Find all problem headers: e.g. "1. Two Sum"
matches = list(re.finditer(r'^\s*(\d+)\.\s*([^\n\r]+)', text, re.MULTILINE))
problems = []
for i, match in enumerate(matches):
start_idx = match.end()
end_idx = matches[i+1].start() if i + 1 < len(matches) else len(text)
problem_number = int(match.group(1).strip())
problem_name = match.group(2).strip()
problem_body = text[start_idx:end_idx]
parsed_methods = parse_problem_body(problem_name, problem_body)
for m in parsed_methods:
m['problem_number'] = problem_number
problems.extend(parsed_methods)
return problems
def sync_gdrive(client=None):
if client is None:
client = db.get_supabase_client()
try:
creds = get_credentials()
except KeyError:
return "GCP credentials not found in secrets.toml"
drive_service = build('drive', 'v3', credentials=creds)
docs_service = build('docs', 'v1', credentials=creds)
folder_id = get_secret("GDRIVE_FOLDER_ID")
if not folder_id:
return "GDRIVE_FOLDER_ID not found in secrets.toml"
query = f"'{folder_id}' in parents and mimeType='application/vnd.google-apps.document' and trashed=false"
results = drive_service.files().list(q=query, fields="nextPageToken, files(id, name)").execute()
items = results.get('files', [])
inserted_count = 0
updated_count = 0
for item in items:
doc_id = item['id']
doc_name = item['name']
db.log_trace(f"Sync: Processing Doc '{doc_name}' ({doc_id})")
# Fetch doc content
doc = docs_service.documents().get(documentId=doc_id).execute()
doc_content = doc.get('body').get('content')
full_text = extract_text_from_elements(doc_content)
parsed_problems = parse_multi_problem_doc(full_text)
db.log_trace(f"Sync: Parsed {len(parsed_problems)} methods from '{doc_name}'")
for p in parsed_problems:
# Check if this specific problem/method name exists in DB for this user
existing = db.get_problem_by_name(p['name'], client)
problem_data = {
"name": p['name'],
"problem_number": p.get('problem_number'),
"pattern": p['pattern'],
"difficulty": p['difficulty'],
"google_doc_url": f"https://docs.google.com/document/d/{doc_id}/edit",
"google_doc_id": doc_id,
"optimal_time": p['optimal_time'],
"optimal_space": p['optimal_space'],
"reference_code": p['code'],
"description": p.get('description', '')
}
db.log_trace(f"Sync: Method '{p['name']}' -> code length={len(p['code'])}")
if not existing:
# Insert problem
db.log_trace(f"Sync: Inserting new problem: '{p['name']}'")
prob_res = db.insert_problem(problem_data, client)
db.log_trace(f"Sync: Insert result data: {prob_res.data if prob_res else 'None'}")
# Insert review queue starting at Box 1
if prob_res and prob_res.data:
prob_id = prob_res.data[0]['id']
db.insert_review({
"problem_id": prob_id,
"box_level": 1
}, client)
inserted_count += 1
else:
# Update problem in case anything changed
db.log_trace(f"Sync: Updating existing problem: '{p['name']}' (ID={existing['id']})")
update_res = db.update_problem(existing['id'], {
"problem_number": p.get('problem_number'),
"pattern": p['pattern'],
"difficulty": p['difficulty'],
"optimal_time": p['optimal_time'],
"optimal_space": p['optimal_space'],
"reference_code": p['code'],
"description": p.get('description', '')
}, client)
db.log_trace(f"Sync: Update result data: {update_res.data if update_res else 'None'}")
updated_count += 1
if p.get('problem_number'):
db.insert_journey_progress(p['problem_number'], client)
return f"Sync complete. Created {inserted_count} new methods, updated {updated_count} existing methods."