Многим людям не хватает диаграмм в Notion. Поэтому я решил создать автоматическую вещь для их генерации.
Вот как это выглядит с моей стороны:
Кому интересно, как это реализовано, обращайтесь в кат.
Часть 1. Постановка задачи
Проблема, по сути, в том, что в Notion нет графиков, а информацию из таблицы нельзя просто так визуализировать (а хотелось бы).Соответственно, вам нужно создать что-то, что:
- Возьмем список страниц, где потенциально могут быть диаграммы.
- Соберу описание расписания с этих страниц.
Описания должны быть на страницах для наглядности читателю страницы + чтобы не залезать слишком часто в код для исправления.
- Добавит график сразу после его описания, удалив предыдущую версию графика.
- Сделает это автоматически (раз в час) и, желательно, бесплатно.
Часть 2. Раскладываем по полочкам
Мы напишем сервер на Django. А именно Django, потому что неофициальная библиотека Notion API написана на Python. Затем мы загружаем все это в Heroku. Из IFTTT мы будем с определенной частотой вытягивать свои вещи на Heroku.Часть 3. Разбираемся, что нам нужно написать
- Метод ответа на запрос от IFTTT
- Метод прочесывания страниц Notion и поиска описаний диаграмм.
- Метод получения данных для графика из таблицы
- Метод рисования графика и добавления его на страницу
Часть 4. Написание кода
Перейдите в Notion, нажмите Ctrl + Shift + J, перейдите в Application -> Cookies, скопируйте token_v2 и назовите его TOKEN. Это необходимо для того, чтобы библиотека могла как-то взаимодействовать с Notion API. Нам нужно как-то хранить список страниц, где потенциально могут встречаться описания для графиков.Мы сохраним этот вопрос просто:
Для того, чтобы как-то разобрать само описание, нам нужны ключевые слова для:PAGES = [ " https://www.notion.so/mixedself/Dashboard-40a3156030fd4d9cb1935993e1f2c7eb " ]
- Поля, данные которых будут находиться на нашей оси X
- Поля, данные которых будут находиться на нашей оси Y
- URL-адрес вывески
BASE_KEY = "Base:"
X_AXIS_KEY = "X axis:"
Y_AXIS_KEY = "Y axis:"
То есть наше пустое описание графика будет выглядеть примерно так:
def get_empty_object():
return {
"database": "",
"x": "",
"y": ""
}
Еще нам нужно как-то проверить, пусто ли описание.
Для этого напишем специальную функцию.
Если все поля не пусты, значит объект завершен и можно приступать к рисованию графика.
def is_not_empty(thing):
return thing != ""
def check_for_completeness(object):
return is_not_empty(object["database"]) and is_not_empty(object["x"]) and is_not_empty(object["y"])
Данные (по сути это просто текст) для формирования описания надо как-то очистить.
Напишем для этого пару функций.
Небольшое уточнение: Notion хранит жирный шрифт (как на картинке выше) __вот так__. def br_text(text):
return "__" + text + "__"
def clear_text(text):
return text.replace(br_text(BASE_KEY), "").
replace(BASE_KEY, "") \ .
replace(br_text(X_AXIS_KEY), "").
replace(X_AXIS_KEY, "") \ .
replace(br_text(Y_AXIS_KEY), "").
replace(Y_AXIS_KEY, "").
strip()
Теперь напишем, пожалуй, основную функцию для нашей штучки.
Ниже кода приведено объяснение того, что здесь происходит: def plot():
client = NotionClient(token_v2=TOKEN)
for page in PAGES:
blocks = client.get_block(page)
thing = get_empty_object()
for i in range(len(blocks.children)):
block = blocks.children[i]
print(block.type)
if block.type != "image":
title = block.title
if BASE_KEY in title:
thing["database"] = clear_text(title).
split("](")[0].
replace("[", "") elif X_AXIS_KEY in title: thing["x"] = clear_text(title) elif Y_AXIS_KEY in title: thing["y"] = clear_text(title) if check_for_completeness(thing): # not last block if i != len(blocks.children) - 1: next_block = blocks.children[i + 1] # if next block is picture, then it is previous # version of the plot, then we should remove it if blocks.children[i + 1].
type == "image":
next_block.remove()
draw_plot(client, thing, block, blocks)
thing = get_empty_object()
Подключаем нашу библиотеку к Notion. Затем мы просматриваем массив страниц, где нам потенциально могут понадобиться графики.
Проверяем каждую строку страницы: есть ли там один из наших ключей или нет. Если вдруг есть, чистим оттуда текст и помещаем его в объект. Как только объект заполнится, мы проверяем, был ли здесь уже сгенерированный график (если был, то удаляем его) и переходим к рисованию нового графика.
Далее мы напишем функцию для сбора данных из таблицы.
def get_lines_array(thing, client):
database = client.get_collection_view(thing["database"])
rows = database.default_query().
execute()
lines_array = []
for i in range(1, len(rows)):
previous_row = rows[i - 1]
current_row = rows[i]
line = [(get_point_from_row(thing, previous_row)), (get_point_from_row(thing, current_row))]
lines_array.append(line)
return lines_array
Здесь мы минируем базу, получаем все ее линии и проходим по всем линиям, образуя набор линий от точки к точке.
Что такое get_point_from_row? Дело в том, что если наш объект — дата (часто необходимо отобразить дату по оси X), то отобразить его просто невозможно, и нам необходимо его дополнительно обработать: def get_point_from_row(thing, row):
x_property = row.get_property(thing["x"])
y_property = row.get_property(thing["y"])
if thing["x"] == "date":
x_property = x_property.start
if thing["y"] == "date":
y_property = y_property.start
return x_property, y_property
Теперь мы готовы нарисовать наш график.
def draw_plot(client, thing, block, page):
photo = page.children.add_new(ImageBlock)
photo.move_to(block, "after")
array = get_lines_array(thing, client)
print(array)
for i in range(1, len(array)):
points = reparse_points(array[i - 1:i][0])
plt.plot(points[0], points[1], color="red")
if not path.exists("images"):
os.mkdir("images")
if thing["x"] == "date":
x_axis_dates()
filename = "images/" + random_string(15) + ".
png"
plt.savefig(filename)
print("Uploading " + filename)
photo.upload_file(filename)
Здесь добавляем новый блок (с фотографией) и перемещаем его под описание графика.
Затем мы повторно анализируем точки (подробнее об этом ниже), рисуем линии с помощью matplotlib, сохраняем полученное изображение со случайным именем файла и загружаем его в блок изображения.
Мы можем получить случайное имя файла следующим образом: def random_string(string_length=10):
letters = string.ascii_lowercase
return ''.
join(random.choice(letters) for i in range(string_length))
И нам нужно повторно проанализировать эти точки, потому что matplotlib принимает в качестве входных данных представление данных, отличное от того, как оно реализовано в настоящее время.
def reparse_points(points):
return [
[points[0][0], points[1][0]],
[points[0][1], points[1][1]],
]
Если присмотреться, метод также проверяет, являются ли данные, которые у нас есть на оси X, датой.
Если да, то нам просто нужно отобразить его правильно: def x_axis_dates(ax=None, fig=None):
if ax is None:
ax = plt.gca()
if fig is None:
fig = plt.gcf()
loc = mdates.AutoDateLocator()
fmt = mdates.AutoDateFormatter(loc)
ax.xaxis.set_major_locator(loc)
ax.xaxis.set_major_formatter(fmt)
fig.autofmt_xdate()
Теперь давайте напишем функцию, которая будет запускать новый поток при получении POST-запроса.
Почему ПОСТ? На всякий случай, чтобы если на вас вдруг кто-то посмотрит, скрипт не запустился.
Зачем новая тема? IFTTT, который мы будем использовать в качестве триггера для работы этой штуки, не любит очень долго ждать ответа от сервера (а в нашем случае ожидание может быть долгим), и через какое-то время может перестать запускаться вещь.
@csrf_exempt
def index(request):
if request.method == "POST":
thread = Thread(target=plot)
thread.start()
return HttpResponse("Hello, world.")
else:
return HttpResponse("Hello, world.")
Часть 5. IFTTT
Перейдите на вкладку создания апплета.Выберите триггер (в нашем случае это «Дата и время») и установите для него значение «каждый час».
Выбираем сработавший (то есть «тот») Webhook, указываем наш (пока) локальный адрес, чтобы его протестировать.
Вот и все.
Мы тестируем.
Загрузка в Хероку
Вы подумали, зачем мы заморачивались с этим триггером со стороны IFTTT — это чтобы не платить.Heroku предлагает бесплатный хостинг для нашей вещицы.
Главное, чтобы сервис спал не менее 6 часов.
И он обязательно будет спать, ведь мы зовем его на работу каждый час, а не каждую минуту.
Далее делаем следующее.
Пойдем в героку создать новый проект .
Далее установите его в свою операционную систему.
А дальше делаем все по инструкции, которая появится после создания приложения.
Загрузив все на героку, заходим в наш апплет и редактируем URL на новый.
Часть 5. IFTTT
Спасибо всем, кто дочитал до этого места.Надеюсь, эта статья вам в чем-то помогла.
Вы можете прочитать две другие мои статьи о Notion: Автоматически экспортируйте формы Google в Notion с помощью IFTTT и Django. Создание домашней библиотеки с помощью Notion и Python Теги: #python #программирование #облачные сервисы #Визуализация данных #ООП #django #graphics #matplotlib #notion #notion
-
Почему Онлайн-Новости — Отстой? 10 Причин
19 Oct, 24 -
Едем На Вечеринку Друзей Icamp В Этномир
19 Oct, 24 -
Социальная Сеть Для Пользователей Ubuntu
19 Oct, 24 -
Планирование Задач В Android
19 Oct, 24 -
Второе Знакомство С Ос Inferno
19 Oct, 24 -
Итоги Акции «Береги Свои Глаза С Малых Лет»
19 Oct, 24