12 ноября 2023 г.
2860
4

Работа с AmoCRM на Python

Интеграция
В данной статье я расскажу как взаимодействовать с API AmoCRM на Python. Разберем создание интеграции, работу протокола OAUTH 2.0. Научимся получать сделки по id, добавлять сделки в раздел "Неразобранное" и определять задачи на ответственного менеджера.

В данной статье я расскажу как взаимодействовать с API AmoCRM на Python. Разберем создание интеграции, работу протокола OAUTH 2.0. Научимся получать сделки по id, добавлять сделки в раздел "Неразобранное" и определять задачи на ответственного менеджера.

AmoCRM - web программа для анализа продаж

Для кого статья

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

Вдохновение на написание этой статьи пришло не из воздуха, а было продиктовано реальными обстоятельствами. Когда мне пришлось разрабатывать интеграцию бэкенда веб-сайта с AmoCRM, я обнаружил скудность информационных ресурсов на Python, учитывая что официальная поддержка AmoCRM ограничивается библиотекой на PHP.

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

P. S.
Не судите строго, это моя первая статья.

Часть первая: Создание интеграции AmoCRM c сайтом на Python

Первым шагом необходимо перейти в Ваш аккаунт администратора AmoCRM и создать интеграцию:

1. Переходим в amoМаркет -> жмем на три точки-> Создать интеграцию

Страница AmoCRM: Создать интеграцию

2. Выбираем «Внешняя интеграция». Второй подход, по всей видимости, становится наиболее релевантным, если вашей целью является создание интеграции, которой смогут успешно пользоваться и другие пользователи.

Подключение внешней интеграции в AmoCRM

3. Давайте остановимся на полях, которые необходимо запомнить.

Подключение внешней интеграции в AmoCRM
    • Перенаправление по ссылке - это процесс, когда вы указываете свой домен, включая схему. Оптимальным будет указание домена, на котором планируется использовать интеграцию. Однако, AmoCRM не проводит строгую проверку этого, как подтвердило мое собственное тестирование интеграции на личном ноутбуке.
      Пример: https://justcode.kz
    • Ссылка для вебхука об отключении - на этом ресурсе вы можете указать адрес, который будет обрабатывать вебхук. Это необходимо, когда вы хотите получать уведомление, что ваша интеграции «отвалилась». Можно оставить пустым.
    • Предоставить доступ - здесь все зависит от функций, которые будет выполнять ваша интеграция. В моем случае, я выбрал опцию "Все".
    • Контроль дублей - это полезная функция, если вы хотите, чтобы AmoCRM проверяла наличие дубликатов в заявках (дополнительную информацию вы можете найти в документации AmoCRM). Однако, на практике я не заметил отличий в работе с включенным и выключенным этим параметром. Поэтому решение о его использовании оставляю на ваше усмотрение.
    • Множественные источники - как упоминается в документации, AmoCRM поддерживает использование нескольких источников. Это может быть полезно для массовых интеграций, которым требуется поддержка нескольких источников через один канал интеграции, например, несколько номеров WhatsApp. Если вы не планируете продавать вашу интеграцию, то галочку ставить не стоит.
    • Язык - выбор очевиден и, предположительно, не требует дополнительного объяснения.
    • Название и описание - заполняйте как угодно
Редактирование интеграции AmoCRM

4. Нажимаем заветную кнопку “Сохранить”.

5. Заходим в раздел установленные, и видим, что наша интеграция отключена. Давайте включим ее.

Установленные интеграции в AmoCRM

Для этого заходим в нашу интеграцию и нажимаем «Отключить» (Я уже нажал). У Вас появится чекбокс и кнопка “Установить”, принимаем условия и нажимам “Установить”.

Установка интеграции в AmoCRM

На этом первичная настройка завершена, у вас должно быть написано «Установлено», как на скриншоте ниже.

Завершение установки интеграции в AmoCRM

Часть вторая: «OAUTH 2.0»

Первым шагом в использовании API является процесс авторизации. В AmoCRM этот механизм выполнен в соответствии с протоколом OAUTH 2.0.

Если Вы ранее не сталкивались с данным протоколом, рекомендую ознакомиться со следующим материалом:
https://www.amocrm.ru/developers/content/oauth/oauth

Очевидно, что основа нашей дальнейшей работы будет строиться на использовании двух токенов - access_token и refresh_token.

Рассмотрим каждый из них более детально:

Access_token (токен доступа) - это основной токен в данной паре. Он используется для авторизации при взаимодействии с API AmoCRM. Этот токен действителен в течение 24 часов, после чего его необходимо обновить для дальнейшего использования API AmoCRM. В этом нам помогает refresh_token.

Refresh_token (токен обновления) - это уникальный токен, который выдается при каждом получении access_token. Другими словами, каждый access_token требует своего refresh_token. Этот токен действителен в течение 15 дней и необходим для обновления access_token по истечении его срока действия, то есть каждые 24 часа. Обратите внимание, что если вы не будете использовать интеграцию в течение 15 дней, refresh_token "истечет", и вы больше не сможете получить access_token.

Следующим вопросом, вероятно, будет - откуда взять эту пару токенов? Переходим к процессу получения этой "желанной пары".

Часть третья: «Первичное получение пары токенов»

Переходя к настройкам нашей интеграции в раздел "Ключи и доступы", мы обнаруживаем следующие элементы: секретный ключ, идентификатор интеграции и код авторизации. Все три составляющие необходимы для получения пары токенов (access_token и refresh_token), и их использование необходимо только в начале работы интеграции (этот шаг выполняется только один раз, или в случае, если прошло более 15 дней и вы не использовали интеграцию - см. примечание выше о последствиях 15 дней бездействия).

    1. 1
      Секретный ключ - это статический элемент, который вы можете обновить вручную в случае утечки ключа.
    2. 2
      Идентификатор (ID) - его функциональность, предположительно, очевидна и не требует дополнительного объяснения.
    3. 3
      Код авторизации - это критически важное поле. Обратите внимание, что AmoCRM предупреждает, что код действителен всего 20 минут. При написании кода может потребоваться зайти и скопировать новый код, если вы не успеете получить пару токенов в течение указанных 20 минут.
Ключи и доступы

На теории достаточно, переходим к коду.

1. Начнем с импортирования необходимых библиотек в проект. В случае, если какая-либо библиотека отсутствует, выполните ее установку с помощью pip. На данном этапе мы не будем подробно останавливаться.


import dotenv
import jwt
import requests
from datetime import datetime
import time
import logging
from requests.exceptions import JSONDecodeError
from justcode.settings import BASE_DIR
from dotenv import load_dotenv
import os
                            

2. Так как токены являются важной конфиденциальной информацией, их хранение должно происходить в соответствии с «best practices» безопасности. Я выбрал для себя вариант хранения в .env файле. Если вы ранее не сталкивались с .env, рекомендую ознакомиться с соответствующим материалом: https://pypi.org/project/python-dotenv/

3. Инициализируем .env


dotenv_path = os.path.join(“Ваш путь до файла”, ".env")
load_dotenv(dotenv_path=dotenv_path)
load_dotenv()
                            

4. Создаем сам файл .env, со следующей структурой


AMOCRM_SUBDOMAIN=вставляем «Ссылку для перенаправления» из настроек интеграции, вы задали это при создании интеграции
AMOCRM_CLIENT_ID=вставляем «ID интеграции» из настроек интеграции
AMOCRM_CLIENT_SECRET=вставляем «Секретный ключ» из настроек интеграции
AMOCRM_REDIRECT_URL=https://ваш url из настроек, которые вы зада.kz
AMOCRM_ACCESS_TOKEN='' 
AMOCRM_REFRESH_TOKEN=''
                            

Значения переменных AMOCRM_ACCESS_TOKEN и AMOCRM_REFRESH_TOKEN в .env файле остаются пустыми.

В основном файле, где вы работаете над интеграцией, добавляется следующий код:


subdomain = os.getenv("AMOCRM_SUBDOMAIN")
client_id = os.getenv("AMOCRM_CLIENT_ID")
client_secret = os.getenv("AMOCRM_CLIENT_SECRET")
redirect_uri = os.getenv("AMOCRM_REDIRECT_URL")
secret_code = "Сюда как раз вставляем Код авторизации из настроек, ключ который живет только 20 минут"
                            

Затем создаем четыре функции:

  1. 1
    _is_expire – функция, которая принимает access_token и проверяет его актуальность (действителен ли он или уже прошло более 24 часов).
  2. 2
    _save_tokens – функция, которая принимает два токена и сохраняет их в .env файл
  3. 3
    Два геттера _get_refresh_token и _get_access_token, для получения токенов из .env файла

def _is_expire(token: str):
    token_data = jwt.decode(token, options={"verify_signature": False})
    exp = datetime.utcfromtimestamp(token_data["exp"])
    now = datetime.utcnow()

    return now >= exp


def _save_tokens(access_token: str, refresh_token: str):
    # Записываем в ключи .env
    os.environ["AMOCRM_ACCESS_TOKEN"] = access_token
    os.environ["AMOCRM_REFRESH_TOKEN"] = refresh_token
    dotenv.set_key(dotenv_path, "AMOCRM_ACCESS_TOKEN", os.environ["AMOCRM_ACCESS_TOKEN"])
    dotenv.set_key(dotenv_path, "AMOCRM_REFRESH_TOKEN", os.environ["AMOCRM_REFRESH_TOKEN"])


def _get_refresh_token():
    return os.getenv("AMOCRM_REFRESH_TOKEN")


def _get_access_token():
    return os.getenv("AMOCRM_ACCESS_TOKEN")
                            

На этом этапе мы подготовили все для получения токенов.

Теперь создаем класс AmoCRMWrapper и в нем метод init_oauth2. Как было упомянуто ранее, получение токенов в обычной ситуации происходит только один раз, и для этой цели нам потребуется метод init_oauth2.

С этого момента мы начинаем работать с API, и все наши запросы будут отправлять JSON-объекты.

В переменную data добавляем client_id, client_secret, secret_code, redirect_uri, которые мы инициализировали ранее (см. пункт 5). Затем с помощью библиотеки requests отправляем запрос на сервер, получаем из ответа токены и с помощью функции _save_tokens сохраняем их.


class AmoCRMWrapper:
    def init_oauth2(self):
        data = {
            "client_id": client_id,
            "client_secret": client_secret,
            "grant_type": "authorization_code",
            "code": secret_code,
            "redirect_uri": redirect_uri
        }

        response = requests.post("https://{}.amocrm.ru/oauth2/access_token".format(subdomain), json=data).json()
        print(response)
        access_token = response["access_token"]
        refresh_token = response["refresh_token"]

        _save_tokens(access_token, refresh_token)
                        

Теперь нам нужно создать экземпляр класса AmoCRMWrapper и вызвать метод init_oauth2().


amocrm_wrapper_1 = AmoCRMWrapper() 

amocrm_wrapper_1.init_oauth2()
                        

Запустите ваш код. Теперь у нас есть токены, и мы готовы начать работу с API.

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

Ну а мы следуем дальше.

Часть четвертая: «Отправка запросов»

Для начала, создадим функцию, которая будет получать новую пару токенов при истечении срока действия access_token (то есть, если прошло более 24 часов с момента его получения) и сохранять их в .env файл.

Функция для получения пары ключей

Согласно документации, отправляем запрос на получение новой пары токенов.


def _get_new_tokens():
    data = {
            "client_id": client_id,
            "client_secret": client_secret,
            "grant_type": "refresh_token",
            "refresh_token": _get_refresh_token(),
            "redirect_uri": redirect_uri
    }
    response = requests.post("https://{}.amocrm.ru/oauth2/access_token".format(subdomain), json=data).json()
    access_token = response["access_token"]
    refresh_token = response["refresh_token"]

    _save_tokens(access_token, refresh_token)
                        

Теперь дополним наш класс AmoCRMWrapper методом _base_request. Этот метод будет использоваться для отправки запросов к API.


def _base_request(self, **kwargs):
    if _is_expire(_get_access_token()):
        _get_new_tokens()

    access_token = "Bearer " + _get_access_token()

    headers = {"Authorization": access_token}
    req_type = kwargs.get("type")
    response = ""
    if req_type == "get":
        try:
            response = requests.get("https://{}.amocrm.ru{}".format(
                subdomain, kwargs.get("endpoint")), headers=headers).json()
        except JSONDecodeError as e:
            logging.exception(e)

    elif req_type == "get_param":
        url = "https://{}.amocrm.ru{}?{}".format(
            subdomain,
            kwargs.get("endpoint"), kwargs.get("parameters"))
        response = requests.get(str(url), headers=headers).json()
    elif req_type == "post":
        response = requests.post("https://{}.amocrm.ru{}".format(
            subdomain,
            kwargs.get("endpoint")), headers=headers, json=kwargs.get("data")).json()
    return response
                        

Метод _base_request будет использоваться для отправки запросов к API.

В методе _base_request реализуем следующую логику: отправка GET-запросов, GET-запросов с параметрами или POST-запросов. Через **kwargs будем принимать следующие переменные:

  • type - для понимания, какой запрос нужно сделать GET, GET с параметрами или POST.
    Строки:
    req_type = kwargs.get("type")
  • Конечную точку API или URI.
    Строки:
    kwargs.get("endpoint")
  • Данные, которые будем отправлять на сервер в формате json.
    Строки:
    json=kwargs.get("data")

Также нам необходимо добавить к запросу заголовок "Authorization", в котором будем передавать access token.

Строки:
access_token = "Bearer " + _get_access_token() – сначала с помощью метода _get_access_token(), получаем наш access токен, формируем строку с указанием типа токена "Bearer".
"Bearer " – Означает использование, что токен не требует от предъявителя доказательства владения. То есть, имея токен, любое приложение может получить доступ к ресурсам.
headers = {"Authorization": access_token}

В конце возвращаем объект response - ответ от API в формате JSON.

На этом этапе мы написали все основные функции и методы для работы с API AmoCRM. Далее с помощью метода _base_request вы сможете отправить любой необходимый вам запрос к API.

Часть пятая: «Формируем запросы к API»

Для того чтобы выполнить API запросы к AmoCRM, вам нужно будет изучить документацию по API, чтобы определить endpoint-ы, типы запросов и данные, которые требуется отправить. Это можно найти здесь: https://www.amocrm.ru/developers/content/crm_platform/api-reference

Пример 1: Получение информации о сделке

В качестве примера, давайте рассмотрим процесс получения информации о сделке (Lead) по ее ID. Описание этого запроса можно найти здесь: https://www.amocrm.ru/developers/content/crm_platform/leads-api#lead-detail

Мы реализуем этот запрос в нашем классе AmoCRMWrapper добавлением метода get_lead_by_id. Этот метод будет принимать lead_id в качестве параметра, затем формировать URL, и отправлять запрос с помощью метода _base_request.


def get_lead_by_id(self, lead_id):
    url = "/api/v4/leads/" + str(lead_id)
    return self._base_request(endpoint=url, type="get")
                        

Попробуем протестировать наш новый метод. Создадим экземпляр класса и вызовем метод get_lead_by_id, указав ID сделки. Выведем результат в консоль.

Где взять ID-cделки?
Для этого перейдите к любой сделке в вашем аккаунте AmoCRM - ID сделки отображается в URL страницы сделки. AmoCRM Cкриншот что такое ID ниже:

id лида в AmoCRM

Пример кода:


if __name__ == "__main__":
    amocrm_wrapper_1 = AmoCRMWrapper()

    print(amocrm_wrapper_1.get_lead_by_id(12879113))
                        

Пример вывода:

Пример вывода при успешной интеграции

Пример 2: Постановка задачи для ответственного менеджера

Описание: https://www.amocrm.ru/developers/content/crm_platform/tasks-api#tasks-add

Мы можем реализовать этот запрос в нашем классе AmoCRMWrapper с помощью нового метода set_task.

Код функции:


def set_task(self, lead_id, responsible_user_id):
    data = [
        {
            "task_type_id": 1,
            "text": "Повторная заявка, необходимо связаться",
            "complete_till": 0,
            "entity_id": lead_id,
            "entity_type": "leads",
            "responsible_user_id": responsible_user_id,
        }
    ]

    response = self._base_request(endpoint="/api/v4/tasks", type="post", data=data)
    return response
                        

Давайте воспользуемся нашим методом:
1. В переменную lead сохраним объект “Лида” полученного от API с помощью метода get_lead_by_id.
2. Вызываем метод set_task с передачей ID-сделки и ID ответственного пользователя (в объекте lead можно обратиться по ключу responsible_user_id)

Код функции:


if __name__ == "__main__":
    amocrm_wrapper_1 = AmoCRMWrapper()

    lead = amocrm_wrapper_1.get_lead_by_id(12879113)

    amocrm_wrapper_1.set_task(lead_id=12879113,
responsible_user_id=lead.get('responsible_user_id'))
                            

Заходим в сделку в AmoCRM и проверяем сохранилась ли задача. Должно выглядеть примерно так:

Постановка задачи в AmoCRM

Пример 3: Добавление “Лида” в несортированное

Описание:
https://www.amocrm.ru/developers/content/crm_platform/unsorted-api#unsorted-add-form

Давайте поподробнее остановимся, что такое custom field в AmoCRM
Ссылка на документацию: https://www.amocrm.ru/developers/content/crm_platform/custom-fields

Кастомные поля – это дополнительные поля, которые вы добавили при настройки AmoCRM в разделе сделок, контактов и прочего.

Скриншот моих полей для сделок ниже:

Карточка лида в AmoCRM

Чтобы корректно добавлять сделки, вам будет необходимо узнать ID кастомных полей. А вот, как это можно сделать без кода.

Вам потребуются браузер Firefox, так как он» из коробки» может структурированно отображать json объект.

В Firefox открываем, следующий url:
https://{ваш_сабдомен}.amocrm.ru/api/v4/leads/custom_fields

Иллюстрируя на примере с ID для пользовательского поля utm_content, следует учесть, что ID для каждого аккаунта уникальны. Это означает, что вам придется вручную собирать ID, которые вы планируете использовать при добавлении сделки. Я уверен, что вы справитесь с этой задачей без проблем.

Получение информации о лиде в AmoCRM

В нашем случае мы создаём метод add_unsorted_lead. Следует отметить, что все сильно зависит от пользовательских полей, которые вы добавили к вашим сделкам в AmoCRM.

В этом конкретном примере, функция через **kwargs получает данные, которые следует сохранить при создании сделки. В конечном итоге, метод возвращает ID созданной сделки.

Что касается конкретных полей в переменной data, их подробное описание можно найти в официальной документации по добавлению сделок в раздел "неразобранное". Ссылка на эту документацию была предоставлена ранее.


def add_unsorted_lead(self, **kwargs):
    data = [
        {
            "source_name": kwargs.get("lead_name"),
            "source_uid": "1",
            "_embedded": {
                "leads": [
                    {
                        "name": kwargs.get("form_lead_name"),
                        "custom_fields_values": [
                            {
                                # utm_content
                                "field_id": 92773,
                                "values": [
                                    {
                                        "value": kwargs.get("form_utm_content")
                                    }
                                ]
                            },
                            {
                                # utm_medium
                                "field_id": 92775,
                                "values": [
                                    {
                                        "value": kwargs.get("form_utm_medium")
                                    }
                                ]
                            },
                            {
                                # utm_campaign
                                "field_id": 92777,
                                "values": [
                                    {
                                        "value": kwargs.get("form_utm_campaign")
                                    }
                                ]
                            },
                            {
                                # utm_source
                                "field_id": 92779,
                                "values": [
                                    {
                                        "value": kwargs.get("form_utm_source")
                                    }
                                ]
                            },
                            {
                                # utm_term
                                "field_id": 92781,
                                "values": [
                                    {
                                        "value": kwargs.get("form_utm_term")
                                    }
                                ]
                            },
                            {
                                # _ym_uid
                                "field_id": 92801,
                                "values": [
                                    {
                                        "value": kwargs.get("form_ym_uid")
                                    }
                                ]
                            },
                            {
                                # _ym_counter
                                "field_id": 92803,
                                "values": [
                                    {
                                        "value": kwargs.get("form_ym_counter")
                                    }
                                ]
                            },
                            {
                                # Страница
                                "field_id": 982883,
                                "values": [
                                    {
                                        "value": kwargs.get("form_page_name")
                                    }
                                ]
                            },
                            {
                                # Форма
                                "field_id": 1923211,
                                "values": [
                                    {
                                        "value": kwargs.get("form_form_name")
                                    }
                                ]
                            },

                        ],
                        "_embedded": {
                            "tags": [
                                {
                                    "name": "Сайт"
                                }
                            ]
                        }
                    }
                ],
                "contacts": [
                    {
                        "name": kwargs.get("form_lead_name"),
                        # "first_name": "first_name justcode_v2",
                        # "last_name": "surname justcode_v2",
                        "custom_fields_values": [
                            {
                                "field_code": "PHONE",
                                "values": [
                                    {
                                        "value": kwargs.get("form_phone")
                                    }
                                ]
                            },
                            {
                                "field_id": 1925337,
                                "values": [
                                    {
                                        "value": kwargs.get("message")
                                    }
                                ]
                            }
                        ]
                    }
                ],
                # "companies": [
                #     # {
                #     #     "name": "ОАО Коспромсервис"
                #     # }
                # ]
            },
            "metadata": {
                "ip": kwargs.get("ip_address"),
                "form_id": "test",
                "form_sent_at": int(time.time()),
                "form_name": kwargs.get("form_form_name"),
                "form_page": kwargs.get("form_page_name"),
                "referer": "https://justcode.kz"
            }
        }
    ]

    response = self._base_request(endpoint="/api/v4/leads/unsorted/forms", type="post", data=data)

    lead_id = response.get('_embedded').get('unsorted')[0].get("_embedded").get("leads")[0].get("id")

    # Ну или получать вот так :)
    # for key, value in response.items():
    #     if key == "_embedded":
    # for item_key, item_value in test.items():
    #     print(item_key, " ", item_value, "\n")
    #     for k, v in item_value[0].items():
    #         if k == "_embedded":
    #             for x, y in v.items():
    #                 if x == "leads":
    #                     for a, b in y[0].items():
    #                         if a == "id":
    #                             return b
    return lead_id
                        

Заключение

В этой статье мы подробно изучили принципы работы протокола OAUTH 2 и его реализацию в рамках API amocrm. На практике мы реализовали механизм, который позволяет автоматически работать с OAUTH 2 для получения пары токенов: access и refresh.

На практических примерах разобрали процесс добавления сделок в раздел "Неразобранное", узнали, как получать детальную информацию по сделкам и научились ставить задачи для ответственного менеджера.

Цель статьи - помочь вам углубить понимание и получить практические навыки работы с протоколом OAUTH 2 и API amocrm.

Сташин Фёдор
CEO

Комментарии (4)

Нажимая на кнопку, я соглашаюсь на сбор и обработку персональных данных
  • Комментодин
    14 ноября 2023 г. 19:18

    comment1 comment1 comment1 comment1comment1 comment1comment1 comment1comment1 comment1comment1 comment1comment1 comment1comment1 comment1

    Ответ пользователю Комментодин
    Нажимая на кнопку, я соглашаюсь на сбор и обработку персональных данных
    • В
      30 декабря 2023 г. 14:55

      Комментодин, В

      Ответ пользователю В
      Нажимая на кнопку, я соглашаюсь на сбор и обработку персональных данных
    • Ответнакоммент
      14 ноября 2023 г. 19:19

      Комментодин, ответнакоммент1 ответнакоммент1 ответнакоммент1 ответнакоммент1 ответнакоммент1 ответнакоммент1 ответнакоммент1 ответнакоммент1 ответнакоммент1 ответнакоммент ответнакоммент ответнакоммент

      Ответ пользователю Ответнакоммент
      Нажимая на кнопку, я соглашаюсь на сбор и обработку персональных данных
    • Ответнаответна
      14 ноября 2023 г. 19:20

      Ответнакоммент, ответнаответна ответнаответна ответнаответна ответнаответна ответнаответна ответнаответна ответнаответна ответнаответна ответнаответна ответнаответна ответнаответна ответнаответна

      Ответ пользователю Ответнаответна
      Нажимая на кнопку, я соглашаюсь на сбор и обработку персональных данных
Скидка до 27% на все курсы:
Выбрать курс

Оставьте заявку на консультацию в течение

и получите персональную скидку

Мы поможем выбрать курс, объясним программу, расскажем о процессе обучения и ответим на любые ваши вопросы о курсах

Ваша заявка отправлена

Менеджер в ближайшее время свяжется с Вами для консультации

Ваша заявка отправлена

Менеджер в ближайшее время свяжется с Вами для консультации