Spaces:
Sleeping
Sleeping
| 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." | |