Недавно дали мне задачку - сделать скрипт, который будет вытаскивать данные из базы данных Notion по API и загружать их в хранилище S3 в формате Parquet (автоматическая работа скрипта была на заказчике), при этом:

  • нужен был определенный порядок выгрузки столбцов, которые будет задавать заказчик самостоятельно

  • нужно было ограничить выгрузку данных типами столбцов, которые заранее оговорили с заказчиком

Полную версию проекта можно посмотреть у меня на GitHub. Ну а в статье речь пойдет только про выгрузку из Notion.


Итак, первым делом надо создать интеграцию, получить токен и подключить эту интеграцию на странице с базой данных в Notion:

  1. Создаем интеграцию → идем сюда https://www.notion.com/my-integrations → нажимаем + New integration → даем имя (например my_token_bd) → сохраняем токен

  1. Идем на страницу 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)


  1. Andrey_Solomatin
    12.08.2024 07:21
    +2

    Посмотрите на Pydantic. Там можно создать модель со вложенной структурой и вытащить только то, что надо.

    from pydantic import BaseModel
    
    
    class Person(BaseModel):
        name: str
    
    
    class MyData(BaseModel):
        person: Person
    
    
    if __name__ == '__main__':
        print(
            MyData.parse_obj(
                {
                    "person": {"name": "John", "other": "gray"},
                    "other": "gray"
                }
            )
        )
    
    # person=Person(name='John')


  1. 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]