yania-n's picture
fix: full codebase audit - 18 bugs fixed across 9 files
386a178
Raw History Blame Contribute Delete
4.67 kB
"""Google Sheets integration — exports RACI and Risk Register as shareable spreadsheets."""
import asyncio
import os
from core.planner import ERROR_MESSAGES, ProjectPlan, RACIEntry
SCOPES = ["https://www.googleapis.com/auth/spreadsheets"]
def _build_service():
"""Build Google Sheets API service. Return None if credentials unavailable."""
try:
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
except ImportError:
return None
client_id = os.environ.get("GOOGLE_CLIENT_ID")
client_secret = os.environ.get("GOOGLE_CLIENT_SECRET")
if not client_id or not client_secret:
return None
try:
credentials = Credentials(
token=None,
client_id=client_id,
client_secret=client_secret,
scopes=SCOPES,
)
return build("sheets", "v4", credentials=credentials)
except Exception:
return None
def _raci_rows(plan: ProjectPlan) -> list[list]:
"""Build rows for RACI sheet using plan.raci entries."""
# Collect all unique teams in order of appearance
teams: list[str] = []
for entry in plan.raci:
for team in (entry.responsible + [entry.accountable] + entry.consulted + entry.informed):
if team and team not in teams:
teams.append(team)
header = ["Task"] + teams
rows = [header]
for entry in plan.raci:
row = [entry.task]
for team in teams:
roles: list[str] = []
if team in entry.responsible:
roles.append("R")
if team == entry.accountable:
roles.append("A")
if team in entry.consulted:
roles.append("C")
if team in entry.informed:
roles.append("I")
row.append("/".join(roles) if roles else "")
rows.append(row)
return rows
def _risks_rows(plan: ProjectPlan) -> list[list]:
"""Build rows for Risk Register using plan.risks entries."""
header = ["Phase", "Description", "Likelihood", "Impact", "Mitigation"]
rows = [header]
for risk in plan.risks:
rows.append([
risk.phase_id or "General",
risk.description,
risk.likelihood,
risk.impact,
risk.mitigation,
])
return rows
async def export_raci(plan: ProjectPlan) -> dict:
"""
Create a new Google Spreadsheet with RACI data.
Returns {"success": bool, "url": str | None, "error": str | None}
"""
service = _build_service()
if service is None:
return {"success": False, "url": None, "error": ERROR_MESSAGES["google_auth"]}
try:
rows = _raci_rows(plan)
def _create() -> str:
body = {"properties": {"title": "ProjectCraft — RACI Matrix"}}
spreadsheet = service.spreadsheets().create(body=body, fields="spreadsheetId").execute()
sheet_id = spreadsheet["spreadsheetId"]
service.spreadsheets().values().update(
spreadsheetId=sheet_id,
range="Sheet1!A1",
valueInputOption="RAW",
body={"values": rows},
).execute()
return f"https://docs.google.com/spreadsheets/d/{sheet_id}"
url = await asyncio.to_thread(_create)
return {"success": True, "url": url, "error": None}
except Exception as exc:
return {"success": False, "url": None, "error": str(exc)}
async def export_risks(plan: ProjectPlan) -> dict:
"""
Create a new Google Spreadsheet with Risk Register data.
Returns {"success": bool, "url": str | None, "error": str | None}
"""
service = _build_service()
if service is None:
return {"success": False, "url": None, "error": ERROR_MESSAGES["google_auth"]}
try:
rows = _risks_rows(plan)
def _create() -> str:
body = {"properties": {"title": "ProjectCraft — Risk Register"}}
spreadsheet = service.spreadsheets().create(body=body, fields="spreadsheetId").execute()
sheet_id = spreadsheet["spreadsheetId"]
service.spreadsheets().values().update(
spreadsheetId=sheet_id,
range="Sheet1!A1",
valueInputOption="RAW",
body={"values": rows},
).execute()
return f"https://docs.google.com/spreadsheets/d/{sheet_id}"
url = await asyncio.to_thread(_create)
return {"success": True, "url": url, "error": None}
except Exception as exc:
return {"success": False, "url": None, "error": str(exc)}