Моя реализация не задействует goole sheet api напрямую, а использует gspread

95%
Готовность проекта
На момент написания статьи, основной функционал реализован в полной мере, поставленные задачи полностью автоматизированные и не требуют вмешательство пользователя. Однако есть некоторые идейные доработки которые я планирую реализовать, и возможно сделаю рефактор (англ. refactoring) кода в более компактный и удобный вид, попутно оптимизируя процессы генерации и обновление данных.

Задачи

Поскольку данный проект я делал по заказу своей жены) не буду описывать концептуальную идею а перейду сразу к непосредственным задачам которые я должен был автоматизировать. Прежде всего хочу обозначить суть проекта: существует таблица гугла, к ней привязана гугл форма в которую клиенты будут вводить данные о желаемой покупки. Это и фио клиента, краткое наименование товара, его артикл, необходимое ему количество и ещё некоторые другие данные не представляющие интерес в конкретном проекте. Итак что должен был делать скрипт:

  • Сортировка клиентов в кластеры* (по алфавиту), кластер должен завершатся пустой строкой разграничения в которую в последствии попадает сумма заказа (фактически самая сложная задача)
  • Актуализация данных путём парсинга по артиклу с целевого сайта магазина. Получение и запись данных (полное название товара, цена за единицу, наличие товара на складе, проверка на статус “товар партнёра” )
  • Расчёты кластера, нужно учитывать желаемое количество и цену за единицу товара, делать расчёт и конвертировать в бел.рубль с +8% орг сбора.
  • Косметические маркеры: в случае если статус товара относился к (нет в наличии) или (товар партнёра), необходимо было окрасить всю строку соответствующим цветом.
  • Возможность удалять строки с помощью специального запроса (при отправки данных через гугл форму, клиент может выбрать тип действия (добавляю/удаляю), если ФИО а артикул запроса находят дубликат в таблице, они оба стираются из неё.

*Кластер это: упорядоченны данные относящиеся к конкретному ФИО, то есть если один человек хочет заказать несколько товаров, то они должны хранится по порядку, строка за строкой рядом с друг-другом. После того как к примеру клиент уже имеющийся в таблице добавляет новую запись о желании купить товар, его данные попадают в конец списка. Программа должна удалить их снизу, и приплюсовать к уже имеющимся кластеру данных.

в данном проекте не будет демонтироваться исходный код, в связи с его коммерческой составляющие.

пролог

Почему не google api

Не буду делать долгих вступлений и признаю сразу, да я ленив, и лень моя двигатель собственного личностного прогресса (типа оправдался крч), суть в том что когда я начал разбираться с api от гугла (а это было ещё за три дня до начала написания кода), я натыкался на разные статьи, в том числе и на хабре, где в “понятной” и “доступной” форме демонтировалась работа с основными элементами взаимодействия. С первого взгляда я честно говоря немного ох**л, прежде всего от простыни кода обычных запросов по банальному обновлению ячейки, до сложных задач типа окраски ячейки или её модификации. Гугл предлагает использовать формат json для упаковки запросов к api, в целом я разделяю практический подход данного решения, однако почему было-бы не проще реализовать это уже под капотом библиотеки, а не заставлять разработчика вникать во все тонкости своеобразного синтаксиса и формы данных. В общем я правда пытался понять, делал три заходы что бы дочитать до конца, но не один не увенчался успехом. Тогда я решил что наверняка есть люди которые как и я решили не плыть по течению а поссать против него :3

Вариантов было не много, и путём не долгих мыслительных процессов (зашел на ютуб, увидел гайды, начал изучать), я принял решения использовать gspread. Его синтаксис безупречно прост, и максимально логичен, вызываем метод, передаём аргументы, профит. В общем всё как я хотел (ну правда не совсем всё, но об этом позже).

Далее я приведу пример ёмкости записи, и у вас не останется сомнений в том что данная библиотека это настоящая находка. Вообще в этой статье будут по большей степени вставки кода именно по работе с этой библиотекой, и немного моих странных парадоксов с которыми я столкнулся в ходе написания этого кода.

логическая яма

Трудности осознания

Самой большой проблемой для меня была естественно логика моего будущего скрипта. Самое сложное было придумать сортировку данных, ведь данные с таблицы я получал в виде списка словорей, где каждая строка это уникальный словарь, в общем говоря проще это был json. С одной стороны это жутко удобно, ведь у каждого поля есть своё уникальное имя, оно же ключ словаря по которому я могу получить или привязать данные. Мне повезло что сама библиотека вне зависимости от строки привязывает её данные к ключам из первого столбца заголовков, таким образом сэкономив мне ещё массу времени и нервов. Однако главным на повестке дня оставался вопрос, КАК мне отсортировать данные в таблице, что делать с пустыми строками, как кластеризовать людей в группы. В общем моя проблема заключалась в том что я сразу пытался представить себе всю программу в целом, пытая свой мозг нюансами реализации всего и вся, и естественно что проблему можно было решить только фрагментированием её на более мелкие. Скажу сразу что далось мне ох как не просто, я осознал что это пожалуй один из самых сложных этапов в разработке, выстроить чёткую линию невозможно, ведь ты не знаешь что ждёт тебя впереди, и как говорится дорогу осилит идущий. Мне сильно помогла визуализация задачи, я нарисовал на бумаги всю таблицу, и стал думать о том как мне хранить данные, как индефицировать их состояние и положение в таблице.

Умственная обёртка

В связи с отсутствием представления конечной реализации мне ничего не осталась как поэтапно пытаться решать проблему. Я разбил задачу условно на две части, хранение данных и их обработка. Вторая часть у меня уже была отработана на написании парсеров, по этому я даже не беспокоился. Сложным для меня было понять что делать первым, мозг пытался ухватиться за всё и сразу, но нужно было построить чёткую логику программы что бы иметь возможность масштабировать её в дальнейшем не переписывая всё и вся. По этому в мою голову пришло второе не по важности решение, раз уж я питонист, нужно использовать ООП, а это значит что строки должны быть экземплярами класса, иметь свои атрибуты равные параметрам полей, тогда и работать с ними одно удовольствие.

Сортировка

itemgetter

Спасибо великому гуглу что он навел меня на великолепный пример сортировки с помощью встроенной библиотеки operator а в частности метода itemgetter. Он позволил мне отсортировать весь массив данных полученных из таблицы, прямо по ключу внутри списка из словарей. И это действительно потрясающий метод) ведь я ломал над этой часть голову очень долго, а решалось всё одной строчкой кода:

sorted_list_new.sort(key=itemgetter('Фамилия и имя'))

На выходе я получал отсортированный массив словарей, осталось только придумать что с ним делать дальше) ах да, я же решился сделать из них экземпляры класса. В это был второй по сложности этап над которым я думал два дня, точнее сказать я его представлял себе, и решал важность момент формирования экземпляра класса. Я понял что мой экземпляр должен быть полностью боевой единицей, обладающей способностью определять своё будущие положение в таблице, осознающий своё состояние и актуальность, а главное – способный к трансформации.

Разделяй и властвуй

В начале было слово, и словом этим было что-то не хорошее)), к сожалению я пишу эту статью уже остыв немного после недельного процесса мозгового штурма. В процессе разработки я то и дело сталкивался с различными проблема вывода данных, я получал не то что ожидал от работы своего скрипта. К сожалению я ещё не в полной мере умею логировать действия программы, и тестирования с помощью принтов такое себе дело)) это стало поводом освоить дебагер в pycharm (крайне полезная штука). В общем то одной из логических проблем для меня стала работа со списком. Я всегда придерживался парадигмы о том что данные лучше оставить в списке, а не нужные из него удалить, но тут меня ждал большой и толстый облом. По какой-то неведом мне причине, питон воспринимал пустые строки по разному, в какому-то случае он принимал просто ” ” а в каких-то Null, и это создавало большие проблемы для меня, ведь пришлось изобретать велосипед описывающий трансформацию Null в пустую строку, ну а что было делать) я и так сидел несколько часов и втыкал ЗА ЧТООО))). Ну это сейчас мне смешно, а тогда было очень даже не смешно. Вишенкой на торте стала реализация упаковки для кластеров. Я долго думал, как же мне реализовать условие: если в списке есть три одинаковых ФИО, отложить их, и отделить условной чертой (пустой строкой). Решение мне пришлось почти во сне, когда я укладывал сына спать и в голове была звонка пустота, просто потому что мозги уже и так расплавились от работы. Я смог представить себе эту задачу, буквально визуализировал это, если бы у меня стояла задача сделать это руками. И решение было до безумия простым

if list_dict[i].get('Фамилия и имя') != list_dict[i + 1].get('Фамилия и имя'):

Грубо говоря, если следующие имя в списке не равно предыдущему, значит кластер нужно заканчивать, и подчеркнуть пустой строкой, в ином же случае происходит обычное наполнение списка. Сейчас я описал финальную Рабочую реализацию своей идеи, но пришел я к ней далеко не сразу. С начала были долгие идеологические мытарства по поводу подхода выборки данных. Мне казалось что правильнее было бы забирать данные из таблицы и переносить их в другую, удалив их из исходного списка. Но я столкнулся с парадоксом работы, даже при абсолютно правильной настройки алгоритма сортировки (как в примере выше), я всё равно получал совершенной иной результат нежели ожидалось изначально. Идея была простой, сделать список из списков, каждым элементом подсписка была бы строка. Какой бы подход я не использовал, у меня всё равно оставались бракованные данные, ну вот не получалось их удалить качественно, и методы различные пробовал, и подходы к фильтрации. И всё равно получал не то чего ожидал =( в конечно счёте я сдался и решил поменять свой взгляд на проблему, я решил что не буду ничего удалять, а просто стану забирать то что мне нужно, и как не удивительно это сработало)

интерактивное программирование

Самым занимательным открытием для меня во время работы над проектом, стало понимание того факта что я пишу его почти в слепую, подобно клубочку ниток которые я разматывал и осознавал трудности которые решал буквально на ходу. Я столкнулся с множеством трудностей, особенно нехваткой знаний о работе с дебагером (что послужило толчком к покупки аж трёх книг). Я понял что умею импровизировать и додумывать решение на ходу, честно говоря я вообще не был уверен что осилю эту задачу, но я не видел не одного серьёзного препятствия для того что не пойти до конца. В общем грубо говоря я решил задачу путём погружения в неё, слепого но смелого погружения), оглядевшись в пустоте я начал строить стены)) которые в конечном счете стали основой моего “прекрасного” кода. Итак перейдем непосредственно к демонстрации того как бы мой код мог бы выглядеть без замечательной библиотеки GSPREAD

Google Sheet API

Вот так можно заполнить данными пару прямоугольников на листе:

results = service.spreadsheets().values().batchUpdate(spreadsheetId = spreadsheet['spreadsheetId'], body = {
    "valueInputOption": "USER_ENTERED",
    "data": [
        {"range": "Сие есть название листа!B2:C3",
         "majorDimension": "ROWS",     # сначала заполнять ряды, затем столбцы (т.е. самые внутренние списки в values - это ряды)
         "values": [["This is B2", "This is C2"], ["This is B3", "This is C3"]]},

        {"range": "Сие есть название листа!D5:E6",
         "majorDimension": "COLUMNS",  # сначала заполнять столбцы, затем ряды (т.е. самые внутренние списки в values - это столбцы)
         "values": [["This is D5", "This is D6"], ["This is E5", "=5+5"]]}
    ]
}).execute()

Выглядит жутко не правда ли? И это ещё цветочки, ведь тут нет свойств и дополнительных параметров поля. Теперь взглянем на реализацию с помощью библиотеки gspread.

cell_list = worksheet.range('A1:C7')
# Заполняем все ячейки.
for cell in cell_list:
    cell.value = 'O_o'
# Обновляем заполненные данные.
worksheet.update_cells(cell_list)

Теперь надеюсь у вас не осталось вопросов почему я выбрал именно gspread. Хотя пример и банальный, не раскрывает всю полноту боли от работы с гугловский api, всё же это базовое действие которое будет использовать каждый при работе с таблицами. Если это не убедило вас в том что google sheet api действительно shit, тогда оцените масштаба проблемы на хабре, там человек запаралися и даже написал класс обёртку для упрощения работы, и в итоге получилась огромная простыня кода, методы которого нужно постоянно держать в голове что бы иметь возможность сполна реализовать потенциал api. Да не спорю, у прямой реализации есть неотъемлемые плюсы, это и гибкость в настройки, и функционал который в полной мере не был реализован не одной сторонней библиотекой. Но базовые функции такие (считывание/запись) в ячейки, gspread реализует максимально просто и понятно. Взглянем на ещё один пример базовой задачи, такой как создание новой таблицы. По сложившейся уже традиции начнём с гугловой реализации:

Сначала нужно инициализировать свои возможности (ключ, и ссылку на шлюз доступа), и создать переменную в которой будет храниться условно экземпляр доски.

import httplib2
import apiclient.discovery
from oauth2client.service_account import ServiceAccountCredentials
CREDENTIALS_FILE = 'test-proj-for-habr-article-1ab131d98a6b.json' 
# имя файла с закрытым ключом 
credentials = ServiceAccountCredentials.from_json_keyfile_name(CREDENTIALS_FILE, ['https://www.googleapis.com/auth/spreadsheets', 'https://www.googleapis.com/auth/drive']) httpAuth = credentials.authorize(httplib2.Http()) service = apiclient.discovery.build('sheets', 'v4', http = httpAuth)

Ну а теперь самое вкусное, создание непосредственно новой таблицы:


spreadsheet = service.spreadsheets().create(body = { 'properties': {'title': 'Сие есть название документа', 'locale': 'ru_RU'}, 'sheets': [{'properties': {'sheetType': 'GRID', 'sheetId': 0, 'title': 'Сие есть название листа', 'gridProperties': {'rowCount': 8, 'columnCount': 5}}}] }).execute()

На мой скромный взгляд, это просто омерзительно. Безусловно тут всё довольно наглядно, и данные представлены в json что упрощает работу с ними, но посмотрите сколько тут обязательных параметров, это я ещё опустил историю с предоставлением доступа (по умолчанию создаётся таблица закрытая вообще от всех, даже от хозяина самого ключа) только серверный аккаунт имеет доступ, и его после создания нужно неприменимо расшаривать. В общем сплошные траблы, а теперь забудем про ужасный shit api, и взглянем на перекисную реализацию абсолютного такого же процесса только с gspread:

Начинаем так же с инициализации

import gspread
gc = gspread.authorize(credentials)
sh = gc.create('A new spreadsheet')
# Расшариваем таблицу
sh.share('otto@example.com', perm_type='user', role='writer')

Тут думаю даже комментировать нечего, всё банально и очевидно. Помимо компактности записи, реализация не обременена дополнительными параметрами, правда это как плюс так и минус конечно, потому что задачи бывают разные. Вот тут мы и подошли к очередной проблеме с которой мне пришлось столкнулся.

границы библиотек

Как говорится “давайте решать проблемы по мере их поступления”, в моему случае это время наступило в самое неподходящий момент, когда я уже реализовал основной функционал автоматизации на базе gspread, я вдруг осознал что в документации нет каких описаний относительно модификации ячеек таблицы. Не формы, не цвета, не жирности и.т.д, вот тут я немного подпустил))) сразу начал на гитхабе зацепки, естественно в репозетории автора. И мне повезло что таким вопрос задавался не я один, и замечательный автор gspread (долгих лет ему, и отличного зрения), написал надстройку для своей библиотеки, называется она gspread_formatting синтаксис там уже немного сложнее, но и возможности куда шире. Таким образом я смог реализовать перекрашивание целыми строками, даже больше) при первом проходе моя программа вообще всю таблицу делает белой) что бы избежать конфликтов цветов разных слоёв, и реализовано это буквально 4мя строчками кода. Когда проект будет полностью готов я конечно же тут им поделюсь, но не сейчас 😉

эпилог

Прежде всего хочу сказать что данная статья писал для меня самого, это не из серии (делюсь опытом), потому что данная задача вообще стояла выше моего скила на момент её реализации. Да в целом задача плёвая, но на уровне алгоритмизации я не мог себе и представить как всё должно работать. По этому решение этой задачи стало для меня неким символов победы над страхом неизвестностости, это победа на прокрастинацией логики, и лени. Мне пришлось буквально расслоить своё мышление, поручив разным слоям разные задачи, которые в конечном счете самостоятельно смогли собраться в одно целое решение. В моей жизни пожалуй не бывало таких событий, что бы я чувствовал одновременно удовлетворение от проделанной работы, и восхищения над тем что мне это удалось). Ещё когда я только начинал свои эксперименты и писал блог на Flask, я буквально перепичатывал строчку за строчкой, и даже так умудрялся совершать ошибки) а потом тратил дни на их решения, однако то чувство восхищения от её решения живёт со мной и сейчас. Это маленькая победа над собой, и большой вклад в стимул идти дальше.