Spaces:
Sleeping
Sleeping
Download src/storage/google_sheets.py from gouriikarus3d/Catalog-AI: direct link, hf CLI and curl.
- Browser
- Download file 21.9 kB
-
https://huggingface.co/spaces/gouriikarus3d/Catalog-AI/resolve/main/src/storage/google_sheets.py
- Command line
-
hf download hf://spaces/gouriikarus3d/Catalog-AI/src/storage/google_sheets.py
-
curl -L -o google_sheets.py https://huggingface.co/spaces/gouriikarus3d/Catalog-AI/resolve/main/src/storage/google_sheets.py
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 |