v1
This commit is contained in:
136
google_sheets.py
136
google_sheets.py
@@ -5,127 +5,99 @@ from gspread.utils import rowcol_to_a1
|
||||
# Настройка логирования
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def log_exceptions(func):
|
||||
"""Декоратор для логирования исключений."""
|
||||
"""Декоратор для стандартизированного логирования исключений в методах класса."""
|
||||
def wrapper(self, *args, **kwargs):
|
||||
try:
|
||||
return func(self, *args, **kwargs)
|
||||
except gspread.exceptions.APIError as e:
|
||||
# Логируем специфичные ошибки API Google
|
||||
logger.error(f"Google API Error in {func.__name__}: {e.response.status_code} - {e.response.text}")
|
||||
raise # Перевыбрасываем, чтобы app.py мог обработать
|
||||
# Логируем специфичные ошибки API Google с деталями
|
||||
error_details = e.response.json().get('error', {})
|
||||
status = error_details.get('status')
|
||||
message = error_details.get('message')
|
||||
logger.error(
|
||||
f"Ошибка Google API в методе {func.__name__}: {status} - {message}. "
|
||||
f"Проверьте права доступа сервисного аккаунта ({self.client_email}) к таблице."
|
||||
)
|
||||
# Перевыбрасываем, чтобы вызывающий код мог ее обработать
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error in {func.__name__}: {e}", exc_info=True)
|
||||
logger.error(f"Непредвиденная ошибка в {func.__name__}: {e}", exc_info=True)
|
||||
raise
|
||||
return wrapper
|
||||
|
||||
|
||||
class GoogleSheets:
|
||||
def __init__(self, cred_file, sheet_url):
|
||||
"""Инициализация клиента Google Sheets."""
|
||||
"""
|
||||
Инициализация клиента для работы с Google Sheets.
|
||||
|
||||
Args:
|
||||
cred_file (str): Путь к JSON-файлу с учетными данными сервисного аккаунта.
|
||||
sheet_url (str): URL Google Таблицы.
|
||||
"""
|
||||
try:
|
||||
# Используем service_account для аутентификации
|
||||
self.client = gspread.service_account(filename=cred_file)
|
||||
self.sheet_url = sheet_url
|
||||
# Открываем таблицу по URL
|
||||
self.spreadsheet = self.client.open_by_url(sheet_url)
|
||||
logger.info(f"Successfully connected to Google Sheet: {self.spreadsheet.title}")
|
||||
logger.info(f"Успешное подключение к Google Sheet: '{self.spreadsheet.title}'")
|
||||
except gspread.exceptions.SpreadsheetNotFound:
|
||||
logger.error(f"Таблица по URL '{sheet_url}' не найдена. Проверьте URL и права доступа.")
|
||||
raise
|
||||
except FileNotFoundError:
|
||||
logger.error(f"Файл с учетными данными не найден по пути: {cred_file}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize GoogleSheets client or open sheet {sheet_url}: {e}", exc_info=True)
|
||||
logger.error(f"Ошибка инициализации клиента GoogleSheets или открытия таблицы {sheet_url}: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
@log_exceptions
|
||||
def get_sheet(self, sheet_name):
|
||||
"""Возвращает объект листа по имени."""
|
||||
"""Возвращает объект листа (worksheet) по его имени."""
|
||||
try:
|
||||
sheet = self.spreadsheet.worksheet(sheet_name)
|
||||
logger.debug(f"Retrieved worksheet object for '{sheet_name}'")
|
||||
logger.debug(f"Получен объект листа '{sheet_name}'")
|
||||
return sheet
|
||||
except gspread.exceptions.WorksheetNotFound:
|
||||
logger.error(f"Worksheet '{sheet_name}' not found in spreadsheet '{self.spreadsheet.title}'.")
|
||||
logger.error(f"Лист '{sheet_name}' не найден в таблице '{self.spreadsheet.title}'.")
|
||||
raise
|
||||
except Exception:
|
||||
raise
|
||||
|
||||
@log_exceptions
|
||||
def get_sheets(self):
|
||||
"""Получение списка листов в таблице."""
|
||||
"""Получает список всех листов в таблице."""
|
||||
sheets = self.spreadsheet.worksheets()
|
||||
# Собираем информацию о листах: название и ID
|
||||
sheet_data = [{"title": sheet.title, "id": sheet.id} for sheet in sheets]
|
||||
logger.debug(f"Retrieved {len(sheet_data)} sheets: {[s['title'] for s in sheet_data]}")
|
||||
logger.debug(f"Получено {len(sheet_data)} листов: {[s['title'] for s in sheet_data]}")
|
||||
return sheet_data
|
||||
|
||||
@log_exceptions
|
||||
def update_cell(self, sheet_name, cell, new_value):
|
||||
"""Обновляет значение ячейки с логированием старого значения."""
|
||||
sheet = self.get_sheet(sheet_name)
|
||||
# Используем try-except для получения старого значения, т.к. ячейка может быть пустой
|
||||
old_value = None
|
||||
try:
|
||||
old_value = sheet.acell(cell).value
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not get old value for cell {cell} in sheet {sheet_name}: {e}")
|
||||
|
||||
# gspread рекомендует использовать update для одиночных ячеек тоже
|
||||
sheet.update(cell, new_value, value_input_option='USER_ENTERED')
|
||||
# Логируем новое значение, т.к. оно могло быть преобразовано Google Sheets
|
||||
try:
|
||||
logged_new_value = sheet.acell(cell).value
|
||||
except Exception:
|
||||
logged_new_value = new_value # Fallback if reading back fails
|
||||
|
||||
logger.info(f"Cell {cell} in sheet '{sheet_name}' updated. Old: '{old_value}', New: '{logged_new_value}'")
|
||||
|
||||
@log_exceptions
|
||||
def clear_and_write_data(self, sheet_name, data, start_cell="A1"):
|
||||
def clear_and_write_data(self, sheet_name, data):
|
||||
"""
|
||||
Очищает ВЕСЬ указанный лист и записывает новые данные (список списков),
|
||||
начиная с ячейки start_cell.
|
||||
Очищает указанный лист и записывает на него новые данные.
|
||||
|
||||
Args:
|
||||
sheet_name (str): Имя листа для записи.
|
||||
data (list): Список списков с данными для записи (первый список - заголовки).
|
||||
"""
|
||||
if not isinstance(data, list):
|
||||
raise TypeError("Data must be a list of lists.")
|
||||
raise TypeError("Данные для записи должны быть списком списков.")
|
||||
|
||||
sheet = self.get_sheet(sheet_name)
|
||||
|
||||
logger.info(f"Clearing entire sheet '{sheet_name}'...")
|
||||
sheet.clear() # Очищаем весь лист
|
||||
logger.info(f"Sheet '{sheet_name}' cleared.")
|
||||
logger.info(f"Очистка листа '{sheet_name}'...")
|
||||
sheet.clear()
|
||||
logger.info(f"Лист '{sheet_name}' очищен.")
|
||||
|
||||
if not data or not data[0]: # Проверяем, есть ли вообще данные для записи
|
||||
logger.warning(f"No data provided to write to sheet '{sheet_name}' after clearing.")
|
||||
return # Ничего не записываем, если данных нет
|
||||
# Проверяем, есть ли данные для записи
|
||||
if not data:
|
||||
logger.warning(f"Нет данных для записи на лист '{sheet_name}' после очистки.")
|
||||
return # Завершаем, если список данных пуст
|
||||
|
||||
num_rows = len(data)
|
||||
num_cols = len(data[0]) # Предполагаем, что все строки имеют одинаковую длину
|
||||
num_cols = len(data[0]) if data and data[0] else 0
|
||||
|
||||
# Рассчитываем конечную ячейку на основе начальной и размеров данных
|
||||
try:
|
||||
start_row, start_col = gspread.utils.a1_to_rowcol(start_cell)
|
||||
end_row = start_row + num_rows - 1
|
||||
end_col = start_col + num_cols - 1
|
||||
end_cell = rowcol_to_a1(end_row, end_col)
|
||||
range_to_write = f"{start_cell}:{end_cell}"
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to calculate range from start_cell '{start_cell}' and data dimensions ({num_rows}x{num_cols}): {e}. Defaulting to A1 notation if possible.")
|
||||
# Фоллбэк на стандартный A1 диапазон, если расчет сломался
|
||||
end_cell_simple = rowcol_to_a1(num_rows, num_cols)
|
||||
range_to_write = f"A1:{end_cell_simple}"
|
||||
if start_cell != "A1":
|
||||
logger.warning(f"Using default range {range_to_write} as calculation from start_cell failed.")
|
||||
|
||||
|
||||
logger.info(f"Writing {num_rows} rows and {num_cols} columns to sheet '{sheet_name}' in range {range_to_write}...")
|
||||
# Используем update для записи всего диапазона одним запросом
|
||||
sheet.update(range_to_write, data, value_input_option='USER_ENTERED')
|
||||
logger.info(f"Successfully wrote data to sheet '{sheet_name}', range {range_to_write}.")
|
||||
|
||||
@log_exceptions
|
||||
def read_range(self, sheet_name, range_a1):
|
||||
"""Чтение значений из диапазона."""
|
||||
sheet = self.get_sheet(sheet_name)
|
||||
# batch_get возвращает список списков значений [[...], [...]]
|
||||
# Используем get() для более простого чтения диапазона
|
||||
values = sheet.get(range_a1)
|
||||
# values = sheet.batch_get([range_a1])[0] # batch_get возвращает [[values_for_range1], [values_for_range2], ...]
|
||||
logger.debug(f"Read {len(values)} rows from sheet '{sheet_name}', range {range_a1}.")
|
||||
# logger.debug(f"Значения из диапазона {range_a1}: {values}") # Может быть слишком много данных для лога
|
||||
return values
|
||||
logger.info(f"Запись {num_rows} строк и {num_cols} колонок на лист '{sheet_name}'...")
|
||||
# Используем метод update для записи всего диапазона одним API-вызовом
|
||||
sheet.update(data, value_input_option='USER_ENTERED')
|
||||
logger.info(f"Данные успешно записаны на лист '{sheet_name}'.")
|
||||
Reference in New Issue
Block a user