Недавно дали мне задачку - сделать скрипт, который будет вытаскивать данные из базы данных Notion по API и загружать их в хранилище S3 в формате Parquet (автоматическая работа скрипта была на заказчике), при этом:
нужен был определенный порядок выгрузки столбцов, которые будет задавать заказчик самостоятельно
нужно было ограничить выгрузку данных типами столбцов, которые заранее оговорили с заказчиком
Полную версию проекта можно посмотреть у меня на GitHub. Ну а в статье речь пойдет только про выгрузку из Notion.
Итак, первым делом надо создать интеграцию, получить токен и подключить эту интеграцию на странице с базой данных в Notion:
Создаем интеграцию → идем сюда https://www.notion.com/my-integrations → нажимаем + New integration → даем имя (например my_token_bd) → сохраняем токен
Идем на страницу Notion с базой данных, к которой нужен доступ по API → жмем “…” правом верхнем углу
→ крутим вниз до + Add Connections
→ выбираем созданную интеграцию (по имени)
Если нужно подробнее → смотри доку по Api Notion.
Запрашиваем данные из базы Notion:
def fetch_database_items(notion, database_id: str) -> List[Dict]:
"""
Запросить элементы базы данных из Notion.
:param database_id: Идентификатор базы данных в Notion.
:param notion: Экземпляр клиента для взаимодействия с API Notion.
:return: Список элементов базы данных.
"""
response = notion.databases.query(database_id=database_id)
return response['results']
Чтобы посмотреть что внутри я закинула их в json и пошла лицезреть, что там выгрузилось.
Я думала, что сразу выгрузится то, что мне нужно, то, что я вижу в самой базе в Notion.
Но нет...там была гигантская json - ина, в которой были указаны все параметры базы...и даже цвет текста...
Пример (из-за размера показан не весь):
[
{
"object": "page",
"id": "a1b2c3d4-e5f6-7g8h-9i0j-1234567890ab",
"created_time": "2024-01-01T09:00:00.000Z",
"last_edited_time": "2024-01-05T09:00:00.000Z",
"created_by": {
"object": "user",
"id": "1a2b3c4d-5e6f-7g8h-9i0j-1234567890ab"
},
"last_edited_by": {
"object": "user",
"id": "1a2b3c4d-5e6f-7g8h-9i0j-1234567890ab"
},
"cover": null,
"icon": null,
"parent": {
"type": "database_id",
"database_id": "abcdef12-3456-7890-abcd-ef1234567890"
},
"archived": false,
"in_trash": false,
"properties": {
"Last edited by": {
"id": "last_editor",
"type": "last_edited_by",
"last_edited_by": {
"object": "user",
"id": "1a2b3c4d-5e6f-7g8h-9i0j-1234567890ab"
}
},
"Multi-select": {
"id": "multi_select",
"type": "multi_select",
"multi_select": [
{
"id": "initiation",
"name": "Initiation",
"color": "gray"
},
{
"id": "planning",
"name": "Planning",
"color": "brown"
}
]
},
"Phone Number": {
"id": "phone_number",
"type": "phone_number",
"phone_number": "1234567890"
},
"Select": {
"id": "select",
"type": "select",
"select": {
"id": "design",
"name": "Design",
"color": "blue"
}
},
"Number": {
"id": "number",
"type": "number",
"number": 1
},
"Last edited time": {
"id": "last_edited_time",
"type": "last_edited_time",
"last_edited_time": "2024-01-05T09:00:00.000Z"
},
"Date": {
"id": "date",
"type": "date",
"date": {
"start": "2024-01-01",
"end": "2024-01-10",
"time_zone": null
}
},
"Created time": {
"id": "created_time",
"type": "created_time",
"created_time": "2024-01-01T09:00:00.000Z"
},
"Status": {
"id": "status",
"type": "status",
"status": {
"id": "not_started",
"name": "Not started",
"color": "default"
}
},
"Text": {
"id": "rich_text",
"type": "rich_text",
"rich_text": [
{
"type": "text",
"text": {
"content": "Overview of Project Alpha",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "Overview of Project Alpha",
"href": null
}
]
},
"Email": {
"id": "email",
"type": "email",
"email": "example@domain.com"
},
"Name": {
"id": "title",
"type": "title",
"title": [
{
"type": "text",
"text": {
"content": "Project Alpha",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "Project Alpha",
"href": null
}
]
}
},
"url": "https://www.notion.so/Project-Alpha-a1b2c3d4e5f67g8h9i0j1234567890ab",
"public_url": null
},
{
"object": "page",
"id": "b2c3d4e5-f6g7-h8i9-j0k1-2345678901bc",
"created_time": "2024-02-01T09:00:00.000Z",
"last_edited_time": "2024-02-05T09:00:00.000Z",
"created_by": {
"object": "user",
"id": "2b3c4d5e-6f7g-8h9i-0j1k-2345678901bc"
},
"last_edited_by": {
"object": "user",
"id": "2b3c4d5e-6f7g-8h9i-0j1k-2345678901bc"
},
"cover": null,
"icon": null,
"parent": {
"type": "database_id",
"database_id": "abcdef12-3456-7890-abcd-ef1234567890"
},
"archived": false,
"in_trash": false,
"properties": {
"Last edited by": {
"id": "last_editor",
"type": "last_edited_by",
"last_edited_by": {
"object": "user",
"id": "2b3c4d5e-6f7g-8h9i-0j1k-2345678901bc"
}
},
"Multi-select": {
"id": "multi_select",
"type": "multi_select",
"multi_select": [
{
"id": "execution",
"name": "Execution",
"color": "green"
},
и т.д.
Пошла искать, как можно просто вытащить нужное. Надеялась найти готовое решение, но увы. Писала в слак в сообщество разработчиков, которые в теме по Api Notion, ответ один:
"You need to format it yourself based on the different types of properties. ".
Получилось сделать так:
REQUIRED_PROPERTIES = [
'title', 'rich_text', 'date', 'select',
'multi_select', 'number', 'checkbox', 'url'
]
def extract_properties(item: Dict) -> Dict:
"""
Извлечь свойства элемента базы данных Notion.
:param item: Объект элемента базы данных Notion.
:return: Словарь с извлеченными свойствами элемента.
"""
properties = item['properties']
extracted_data = {'id': item['id']}
property_types = {}
for prop_name, prop_values in properties.items():
prop_type = prop_values['type']
if prop_type in REQUIRED_PROPERTIES:
if prop_type == 'title' or prop_type == 'rich_text':
extracted_data[prop_name] = get_text(prop_values[prop_type])
property_types[prop_name] = 'string'
elif prop_type == 'date':
start_date, end_date = get_date(prop_values.get('date', {}))
extracted_data[f'{prop_name}_start'] = start_date
extracted_data[f'{prop_name}_end'] = end_date
property_types[f'{prop_name}_start'] = 'string'
property_types[f'{prop_name}_end'] = 'string'
elif prop_type == 'select':
extracted_data[prop_name] = get_select(prop_values['select'])
property_types[prop_name] = 'string'
elif prop_type == 'multi_select':
extracted_data[prop_name] = get_multi_select(
prop_values['multi_select']
)
property_types[prop_name] = 'array'
elif prop_type == 'url':
extracted_data[prop_name] = prop_values.get(prop_type)
property_types[prop_name] = 'string'
elif prop_type == 'number':
extracted_data[prop_name] = prop_values.get(prop_type)
property_types[prop_name] = 'int64'
elif prop_type == 'checkbox':
extracted_data[prop_name] = prop_values.get(prop_type)
property_types[prop_name] = 'bool'
with open('property_types.json', 'w+') as f:
json.dump(property_types, f)
return extracted_data
* параметры начальной и конечной даты выгружаются отдельно, не в кортеж, такой был запрос заказчика
* property_types - словарь с типами свойств, нужен для создания схемы с нужным порядком
Ну и сами функции:
def get_text(text_object: List[Dict]) -> str:
"""
Получить текст из объекта текста базы данных Notion.
:param text_object: Объект текста из базы данных Notion.
:return: Объединенный текст.
"""
text = ''
for rt in text_object:
text += rt.get('plain_text')
return text
def get_date(date_object: Dict) -> tuple:
"""
Получить дату или диапазон дат из объекта даты базы данных Notion.
:param date_object: Объект даты из базы данных Notion.
:return: Одиночная дата или кортеж с начальной и конечной датами.
"""
start_date = date_object.get('start', None) if date_object else None
end_date = date_object.get('end', None) if date_object else None
return start_date, end_date
def get_select(select_object: Dict) -> Union[str, None]:
"""
Получить имя выбранного элемента из объекта выбора базы данных Notion.
:param select_object: Объект выбора из базы данных Notion.
:return: Имя выбранного элемента или None, если объект отсутствует.
"""
return select_object.get('name') if select_object else None
def get_multi_select(multi_select_object: List[Dict]) -> List[str]:
"""
Получить список строк, объединяющих элементы выбора из базы данных Notion.
:param multi_select_object: Список объектов выбора из базы данных Notion.
:return: Список строк, объединяющих элементы выбора.
"""
return [', '.join([item.get('name', '') for item in multi_select_object])]
После извлечения данные уже выглядели так (пример, выгружала в формат CSV):
id,Number,Name,Select,URL,id,Text,Multi-select,Date_start,Date_end
a1b2c3d4-e5f6-7g8h-9i0j-1234567890ab,1,Project Alpha,Design,www.example.com/alpha,a1b2c3d4-e5f6-7g8h-9i0j-1234567890ab,Overview of Project Alpha,"['Initiation, Planning']",2024-01-01,2024-01-10
b2c3d4e5-f6g7-h8i9-j0k1-2345678901bc,2,Project Beta,Development,www.example.com/beta,b2c3d4e5-f6g7-h8i9-j0k1-2345678901bc,Details of Project Beta,"['Execution, Monitoring']",2024-02-15,2024-03-01
c3d4e5f6-g7h8-i9j0-k1l2-3456789012cd,3,Project Gamma,Testing,www.example.com/gamma,c3d4e5f6-g7h8-i9j0-k1l2-3456789012cd,Testing Phase of Project Gamma,"['Testing, Validation']",2024-03-10,2024-03-20
d4e5f6g7-h8i9-j0k1-l2m3-4567890123de,4,Project Delta,Launch,www.example.com/delta,d4e5f6g7-h8i9-j0k1-l2m3-4567890123de,Launch Phase of Project Delta,"['Launch, Review']",2024-04-01,2024-04-05
e5f6g7h8-i9j0-k1l2-m3n4-5678901234ef,5,Project Epsilon,Maintenance,www.example.com/epsilon,e5f6g7h8-i9j0-k1l2-m3n4-5678901234ef,Maintenance of Project Epsilon,"['Maintenance, Support']",2024-05-15,2024-05-20
Таким образом можно выгружать различные свойства из базы данных Notion.
Пожалуйста, не судите строго – я только начинаю свой путь в разработке. Надеюсь, мой опыт кому-нибудь поможет) И вы делитесь своим, если есть что сказать по теме статьи)
Комментарии (2)
Andrey_Solomatin
12.08.2024 07:21+1Код очень сложный, мне не хватает мотивации его понять. Сдишком большая вложеннось.
Вот здесь явно присутствует дупликация. Можно упростить.elif prop_type == 'url': extracted_data[prop_name] = prop_values.get(prop_type) property_types[prop_name] = 'string' elif prop_type == 'number': extracted_data[prop_name] = prop_values.get(prop_type) property_types[prop_name] = 'int64' elif prop_type == 'checkbox': extracted_data[prop_name] = prop_values.get(prop_type) property_types[prop_name] = 'bool'
"checkbox": "bool", } if prop_type in props_mapping: property_types[prop_name] = props[prop_type] property_types[prop_name] = props_mapping[prop_type]
Andrey_Solomatin
Посмотрите на Pydantic. Там можно создать модель со вложенной структурой и вытащить только то, что надо.