Catalog-AI / src /storage /google_sheets.py
gouri
prod
46ea0ef
Raw History Blame Contribute Delete
21.9 kB
"""Google Sheets integration for catalog data."""
import os
import json
from typing import Dict, List, Optional
from datetime import datetime
from google.oauth2.service_account import Credentials
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
class GoogleSheetsStorage:
"""Upload catalog data to Google Sheets."""
SCOPES = ['https://www.googleapis.com/auth/spreadsheets']
def __init__(self, credentials_file: str = 'credentials.json'):
"""
Initialize Google Sheets client.
Args:
credentials_file: Path to service account credentials JSON
"""
self.credentials_file = credentials_file
self.service = None
self._authenticate()
def _authenticate(self):
"""Authenticate with Google Sheets API using file or environment variable."""
try:
# First, try to load from environment variable (for HuggingFace Spaces)
creds_json_str = os.getenv('GOOGLE_SHEETS_CREDS_JSON')
if creds_json_str:
# Parse JSON string from environment
try:
creds_dict = json.loads(creds_json_str)
creds = Credentials.from_service_account_info(
creds_dict,
scopes=self.SCOPES
)
self.service = build('sheets', 'v4', credentials=creds)
print("βœ“ Google Sheets authenticated from environment variable")
return
except json.JSONDecodeError as e:
print(f"⚠ Failed to parse GOOGLE_SHEETS_CREDS_JSON: {e}")
# Fallback to credentials file
if os.path.exists(self.credentials_file):
creds = Credentials.from_service_account_file(
self.credentials_file,
scopes=self.SCOPES
)
self.service = build('sheets', 'v4', credentials=creds)
print(f"βœ“ Google Sheets authenticated from {self.credentials_file}")
return
# No credentials found
print(f"⚠ Google Sheets credentials not found")
print(" Set GOOGLE_SHEETS_CREDS_JSON environment variable or")
print(f" Provide {self.credentials_file} file")
self.service = None
except Exception as e:
print(f"⚠ Failed to authenticate with Google Sheets: {e}")
self.service = None
def _create_new_sheet(self, spreadsheet_id: str, sheet_name: str) -> Optional[int]:
"""
Create a new sheet tab in the spreadsheet.
Args:
spreadsheet_id: The spreadsheet ID
sheet_name: Name for the new sheet tab
Returns:
Sheet ID or None if failed
"""
if not self.service:
return None
try:
# Check if sheet already exists
spreadsheet = self.service.spreadsheets().get(
spreadsheetId=spreadsheet_id
).execute()
for sheet in spreadsheet.get('sheets', []):
if sheet['properties']['title'] == sheet_name:
print(f"β„Ή Sheet '{sheet_name}' already exists, will overwrite data")
return sheet['properties']['sheetId']
# Create new sheet
request_body = {
'requests': [{
'addSheet': {
'properties': {
'title': sheet_name
}
}
}]
}
response = self.service.spreadsheets().batchUpdate(
spreadsheetId=spreadsheet_id,
body=request_body
).execute()
sheet_id = response['replies'][0]['addSheet']['properties']['sheetId']
print(f"βœ“ Created new sheet tab: '{sheet_name}'")
return sheet_id
except HttpError as e:
print(f"⚠ Failed to create new sheet: {e}")
return None
def create_spreadsheet(self, title: str) -> Optional[str]:
"""
Create a new Google Spreadsheet.
Args:
title: Title for the spreadsheet
Returns:
Spreadsheet ID or None if failed
"""
if not self.service:
print("⚠ Google Sheets service not available")
return None
try:
spreadsheet = {
'properties': {
'title': title
}
}
spreadsheet = self.service.spreadsheets().create(
body=spreadsheet,
fields='spreadsheetId'
).execute()
spreadsheet_id = spreadsheet.get('spreadsheetId')
print(f"βœ“ Created spreadsheet: {spreadsheet_id}")
print(f" URL: https://docs.google.com/spreadsheets/d/{spreadsheet_id}")
return spreadsheet_id
except HttpError as e:
print(f"βœ— Failed to create spreadsheet: {e}")
return None
def upload_data(
self,
spreadsheet_id: str,
data: List[List],
sheet_name: str = 'Product Catalog',
header_row: Optional[List[str]] = None
) -> bool:
"""
Upload data to a Google Sheet.
Args:
spreadsheet_id: The spreadsheet ID
data: List of rows to upload
sheet_name: Name of the sheet tab
header_row: Optional header row
Returns:
True if successful, False otherwise
"""
if not self.service:
print("⚠ Google Sheets service not available")
return False
try:
# Prepare data with header
all_data = []
if header_row:
all_data.append(header_row)
all_data.extend(data)
# Create a new sheet tab with the specified name
self._create_new_sheet(spreadsheet_id, sheet_name)
# Upload new data to the new sheet
body = {
'values': all_data
}
result = self.service.spreadsheets().values().update(
spreadsheetId=spreadsheet_id,
range=f'{sheet_name}!A1',
valueInputOption='RAW',
body=body
).execute()
print(f"βœ“ Uploaded {result.get('updatedCells')} cells to Google Sheets")
return True
except HttpError as e:
print(f"βœ— Failed to upload data: {e}")
return False
def format_sheet(self, spreadsheet_id: str, sheet_name: str = 'Sheet1'):
"""
Apply formatting to the sheet (bold headers, freeze rows, etc.).
Args:
spreadsheet_id: The spreadsheet ID
sheet_name: Name of the sheet tab
"""
if not self.service:
return
try:
# Get sheet ID
spreadsheet = self.service.spreadsheets().get(
spreadsheetId=spreadsheet_id
).execute()
sheet_id = None
for sheet in spreadsheet['sheets']:
if sheet['properties']['title'] == sheet_name:
sheet_id = sheet['properties']['sheetId']
break
if sheet_id is None:
# Default to first sheet
sheet_id = spreadsheet['sheets'][0]['properties']['sheetId']
# Format requests
requests = [
# Freeze header row
{
'updateSheetProperties': {
'properties': {
'sheetId': sheet_id,
'gridProperties': {
'frozenRowCount': 1
}
},
'fields': 'gridProperties.frozenRowCount'
}
},
# Bold header row
{
'repeatCell': {
'range': {
'sheetId': sheet_id,
'startRowIndex': 0,
'endRowIndex': 1
},
'cell': {
'userEnteredFormat': {
'textFormat': {
'bold': True
},
'backgroundColor': {
'red': 0.9,
'green': 0.9,
'blue': 0.9
}
}
},
'fields': 'userEnteredFormat(textFormat,backgroundColor)'
}
},
# Auto-resize columns
{
'autoResizeDimensions': {
'dimensions': {
'sheetId': sheet_id,
'dimension': 'COLUMNS',
'startIndex': 0,
'endIndex': 10
}
}
}
]
body = {
'requests': requests
}
self.service.spreadsheets().batchUpdate(
spreadsheetId=spreadsheet_id,
body=body
).execute()
print("βœ“ Applied formatting to sheet")
except HttpError as e:
print(f"⚠ Failed to format sheet: {e}")
def apply_color_formatting(
self,
spreadsheet_id: str,
sheet_name: str,
color_cell_map: List[tuple]
):
"""
Apply background colors to specific cells based on hex codes.
Args:
spreadsheet_id: The spreadsheet ID
sheet_name: Name of the sheet tab
color_cell_map: List of tuples (row_index, col_index, hex_color)
"""
if not self.service or not color_cell_map:
return
try:
# Get sheet ID
spreadsheet = self.service.spreadsheets().get(
spreadsheetId=spreadsheet_id
).execute()
sheet_id = None
for sheet in spreadsheet['sheets']:
if sheet['properties']['title'] == sheet_name:
sheet_id = sheet['properties']['sheetId']
break
if sheet_id is None:
return
# Build format requests for each color cell
requests = []
for row_idx, col_idx, hex_color in color_cell_map:
# Parse hex color (e.g., "#FF5733")
hex_color = hex_color.lstrip('#')
if len(hex_color) != 6:
continue
try:
r = int(hex_color[0:2], 16) / 255.0
g = int(hex_color[2:4], 16) / 255.0
b = int(hex_color[4:6], 16) / 255.0
except:
continue
# Determine text color based on background brightness
brightness = (r * 299 + g * 587 + b * 114) / 1000
text_color = {
'red': 0.0 if brightness > 0.5 else 1.0,
'green': 0.0 if brightness > 0.5 else 1.0,
'blue': 0.0 if brightness > 0.5 else 1.0
}
requests.append({
'repeatCell': {
'range': {
'sheetId': sheet_id,
'startRowIndex': row_idx,
'endRowIndex': row_idx + 1,
'startColumnIndex': col_idx,
'endColumnIndex': col_idx + 1
},
'cell': {
'userEnteredFormat': {
'backgroundColor': {
'red': r,
'green': g,
'blue': b
},
'textFormat': text_color
}
},
'fields': 'userEnteredFormat(backgroundColor,textFormat)'
}
})
if requests:
body = {'requests': requests}
self.service.spreadsheets().batchUpdate(
spreadsheetId=spreadsheet_id,
body=body
).execute()
print(f"βœ“ Applied color formatting to {len(requests)} cells")
except HttpError as e:
print(f"⚠ Failed to apply color formatting: {e}")
def save_catalog(
self,
catalog: Dict,
spreadsheet_id: Optional[str] = None,
title: str = "Product Catalog",
sheet_name: Optional[str] = None,
include_prices: bool = True
) -> Optional[str]:
"""
Save catalog to Google Sheets.
Args:
catalog: The product catalog
spreadsheet_id: Existing spreadsheet ID (creates new if None, or uses default from env)
title: Title for new spreadsheet
sheet_name: Name for the sheet tab (auto-generated with timestamp if None)
include_prices: Whether to include prices column
Returns:
Spreadsheet ID or None if failed
"""
if not self.service:
print("⚠ Cannot save to Google Sheets - service not available")
return None
# Use default spreadsheet ID from environment if not provided
if not spreadsheet_id:
spreadsheet_id = os.getenv('GOOGLE_SPREADSHEET_ID')
if spreadsheet_id:
print(f"β„Ή Using default spreadsheet ID from environment: {spreadsheet_id}")
# Create spreadsheet if still no ID
if not spreadsheet_id:
spreadsheet_id = self.create_spreadsheet(title)
if not spreadsheet_id:
return None
# Generate sheet name with timestamp if not provided
if not sheet_name:
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
sheet_name = f"Catalog {timestamp}"
# Prepare data
rows = []
for product_id, product_data in catalog.items():
product_name = product_data.get('product_name', 'Unknown Product')
base_price = product_data.get('base_price') or 'N/A'
product_url = product_data.get('url', '')
specifications = product_data.get('specifications', {})
# Product header
if include_prices:
rows.append([
f"Base Model({product_name})",
product_name,
base_price,
'',
'',
product_url
])
else:
rows.append([
f"Base Model({product_name})",
product_name,
'',
product_url
])
# Specifications section
if specifications:
rows.append(['', '', '', '', '', '']) # Empty row for spacing
rows.append(['Specifications', '', '', '', '', ''])
for spec_key, spec_value in specifications.items():
if include_prices:
rows.append([
'',
f"{spec_key}: {spec_value}",
'',
'',
'Specification',
''
])
else:
rows.append([
'',
f"{spec_key}: {spec_value}",
'',
'Specification'
])
rows.append(['', '', '', '', '', '']) # Empty row after specs
# Customization categories
customizations = product_data.get('customizations', {})
color_cell_map = [] # Track which cells need color formatting: [(row, col, hex_color)]
# Group options by model (if model field exists)
model_grouped = {}
for category, options in customizations.items():
if not options:
continue
for option in options:
if isinstance(option, dict):
model_key = option.get('model', 'General') # Group by model or 'General' if no model
else:
model_key = 'General'
if model_key not in model_grouped:
model_grouped[model_key] = {}
if category not in model_grouped[model_key]:
model_grouped[model_key][category] = []
model_grouped[model_key][category].append(option)
# Output grouped by model
for model_key in sorted(model_grouped.keys()):
# Add model header if there are multiple models
if len(model_grouped) > 1:
rows.append(['', '', '', '', '', '']) # Spacing
if include_prices:
rows.append([
f"═══ {model_key} ═══",
'',
'',
'',
f'Model Configuration',
''
])
else:
rows.append([
f"═══ {model_key} ═══",
'',
'',
f'Model Configuration'
])
# Output categories for this model
for category, options in model_grouped[model_key].items():
if not options:
continue
for i, option in enumerate(options):
# Handle both dict and string option types
if isinstance(option, dict):
label = option.get('label', str(option))
price = option.get('price', '')
image = option.get('image', '')
hex_color = option.get('hex_color', '')
else:
label = str(option)
price = ''
image = ''
hex_color = ''
current_row = len(rows) + 1 # +1 for header row
if include_prices:
rows.append([
category if i == 0 else '',
label,
price,
image,
f"Category: {category}",
''
])
# Track color cell (column B = index 1, 0-indexed)
if hex_color:
color_cell_map.append((current_row, 1, hex_color))
else:
rows.append([
category if i == 0 else '',
label,
image,
''
])
if hex_color:
color_cell_map.append((current_row, 1, hex_color))
# Header row
header = ['Categories', 'Component', 'Price', 'Image', 'Notes', 'References'] if include_prices else ['Categories', 'Component', 'Image', 'References']
# Upload data
success = self.upload_data(spreadsheet_id, rows, sheet_name=sheet_name, header_row=header)
if success:
# Apply formatting
self.format_sheet(spreadsheet_id, sheet_name=sheet_name)
# Apply color formatting to color cells
if color_cell_map:
self.apply_color_formatting(spreadsheet_id, sheet_name, color_cell_map)
print(f"\nβœ“ Catalog uploaded to Google Sheets")
print(f" Sheet: '{sheet_name}'")
print(f" URL: https://docs.google.com/spreadsheets/d/{spreadsheet_id}\n")
return spreadsheet_id
return None