Упрощаем работу с Tableau через Телеграм

    image

    Всем привет. Предоставление доступа к готовым отчетам часто является отдельной проблемой. Вопрос удобства и оперативного доступа к результатам обработки данных для руководства во многом определяет судьбу их дальнейшего использования. Система Tableau (или по-простому Табло) не зря пользуется популярностью для подобных задач, позволяя быстро анализировать данные из многих источников, публиковать онлайн-отчеты на сервере, настраивать автоматические рассылки PDF-версий отчетов и многое другое.

    Однако даже когда все настроено, опубликовано и рассылается, коллеги сталкиваются с проблемами:

    • регулярные отчеты на почту теряются в потоке рабочих писем и найти нужное не всегда получается сразу.
    • как правило, онлайн-доступ к отчетам защищен корпоративным VPN. В некоторых ситуациях это доставляет проблем.
    • часто требуется получить отчет, не дожидаясь его регулярной рассылки. Например, план-факт по проекту за текущий месяц может потребоваться в любой день.
    • иногда пароль от своей учетки на сервере банально забывается или нужный отчет сложно отыскать среди других 100500 папок и отчетов.

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

    Будем исходить из следующих упрощений:

    • Клавиатура бота состоит их названий отчетов, которые он будет отправлять данному пользователю. Наличие вложенных меню в текущем варианте не подразумевается.
    • Бот обрабатывает все сообщения независимо друг от друга, т. е. не «запоминает» предыдущих сообщений пользователя.
    • Наличие или отсутствие доступа к отчету определяется по параметру chat_id, назначаемому каждому пользователю при начале работы с ботом (нажатии кнопки START).
    • У бота один админ, которому будут приходить заявки на доступ к отчетам.

    Для данной статьи использовалась версия Tableau Server 10.1.3, установленная на виртуалке Amazon Web Services: Windows Server 2016 64-bit, RAM 16GB (да, сервер Табло работает только на Windows Server). В качестве экспортируемых отчетов брались примеры из проекта Tableau Samples, по умолчанию устанавливаемых с сервером Табло. Например, такой:

    image

    Есть куча прекрасных примеров как в Телеграме создать бота, но можно просто воспользоваться документацией. Для работы с API Телеграма используем готовую питоновскую библиотеку. Используем Python 2.7 Anaconda.

    Б — Безопасность

    Использование Телеграма означает, что мы отправляем отчеты с корпоративного сервера куда-то вовне. Соответственно, нам необходимо вести реестр доступов, чтобы каждый получал только положенные ему отчеты. Причем надо бы добавлять пользователей/прав, не перезапуская скрипт. Выясним chat_id пользователя при начале работы с ботом и сразу дадим ему необходимые доступы.

    При начале работы пользователя с ботом нам необходимо выяснить его chat_id. Для этого в обработчике команды /start (функция start в коде) отправляем его chat_id админу бота, который сможет для данного пользователя (т. е. chat_id) составить клавиатуру с отчетами.

    При нажатии кнопки START пользователь Иван Иванов получит сообщение:
    Привет, в данный момент у вас нет доступа к отчетам. Ваша заявка принята, в ближайшее время доступ будет предоставлен.

    Админ бота получит сообщение:

    Заявка на доступ iivanov Иван Иванов 123456789

    Для такой процедуры заведем YAML-файл, в котором в разделе users для каждого пользователя составим «клавиатуру» с нужными отчетами. Такой формат потом довольно просто прочитать в скрипте и отправить в Телеграм в виде клавиатуры:

    Конфигурационный файл с доступами для пользователя
    users:
        123456789:
            keyboard:
                -
                      # название кнопки, которое увидит пользователь в чате
                    - buttonText: 'Global Temperatures'
                      # название файла, который будет выслан пользователю
                      reportName: 'Global Temperatures'
                      # адрес отчета на сервере Табло
                      report: '/views/Regional/GlobalTemperatures'
    
                      # название кнопки, которое увидит пользователь в чате
                    - buttonText: 'College Admissions'
                      # название файла, который будет выслан пользователю
                      reportName: 'College Admissions'
                      # адрес отчета на сервере Табло
                      report: '/views/Regional/College'
    
                -    
                      # название кнопки, которое увидит пользователь в чате
                    - buttonText: 'Regional FlightDelays'
                      # название файла, который будет выслан пользователю
                      reportName: 'Regional FlightDelays'
                      # адрес отчета на сервере Табло
                      report: '/views/Regional/FlightDelays'
    


    В данном варианте клавиатура будет содержать две строки:

    image

    Преобразовывать такую запись клавиатуры в формат Телеграма будет специальная функция:

    Формирование клавиатуры для Телеграма
    def makeCustomKeyboard( userKeyboard ):
        """
        Возвращает клавиатуру в формате Телеграма.
        На вход принимает элемент keyboard пользователя из файла privacy.yaml.
        """
        custom_keyboard = map( lambda z: [ telegram.KeyboardButton(x['buttonText']) for x in z ], userKeyboard )
        return telegram.ReplyKeyboardMarkup(custom_keyboard, resize_keyboard='True')
    


    Адрес конфигурационного файла в коде скрипта задается переменной CONFIG_FILE. Для обновления этих параметров в процессе работы бота используем функцию updateSettings().

    Функции обработки команды /start и сообщений будут каждый раз обновлять реестр доступов.

    def updateSettings():
      """
      Возвращает содержимое файла настроек
      """
      return load( file( CONFIG_FILE ) )
    

    Помимо файла с доступами для работы с сервером Табло нам потребуются следующие параметры:

    Токен бота и настройки сервера
    config:
        # токен бота
        botToken: '111222333:AAO4L0Ttt1tt01-1TtT1t_t22tT'
    
        # расположение файла tabcmd.exe на сервере
        tabcmdLocation: 'C:\Program Files\Tableau\Tableau Server\10.1\bin\'
    
        # куда сохраняем экспортируемые отчеты
        reportsLocation: 'C:\Users\Administrator\Documents\bot\reports\'
    
        # куда сохраняем логи
        logsLocation: 'C:\Users\Administrator\Documents\bot\logs\'
    
    admin:
        # на какой chat id отправляются запросы о предоставлении доступа
        id: 198765432
    


    Все готово, можно определить функцию обработки команды /start. Также нам пригодится функция logs, которая просто записывает в текстовый файл с chat_id пользователя его действия с ботом. Такой простой вариант в случае ошибки подскажет на какой строчке она произошла.

    Обработка команды /start
    def start(bot, update):
        """
        Обработка команды /start для бота.
        Проверяет есть ли пользователь в списке доступов (файл privacy.yaml, раздел users). 
        Если есть, то возвращает приветствие и клавиатуру с отчетами. В противном случае отправляет заявку на доступ админу
        """
        # chat_id написавшего
        chat_id = update.message.chat_id
        
        # текущая версия клавиатуры и отчетов
        config = updateSettings()
        
        # проверяем есть ли пользователь в списке доступов
        if chat_id in config['users']:
            logs(update, 'Received /start command from user in privacy.yaml')
            
            # формируем клавиатуру с отчетами для данного пользователя и отправляем приветствие
            reply_markup = makeCustomKeyboard(config['users'][int(chat_id)]['keyboard'])
            bot.sendMessage(chat_id=chat_id, text='Привет, вот доступный список отчетов', reply_markup=reply_markup)
            
            logs(update, 'List of available reports sent')
            
        # пользователя нет в файле с доступами
        else:
            logs(update, 'Received /start command from user NOT IN privacy.yaml')
            
            # отправляем сообщение пользователю
            bot.sendMessage(chat_id=chat_id, text='Привет, в данный момент у вас нет доступа к отчетам. ' + \
                            'Ваша заявка принята, в ближайшее время доступ будет предоставлен')
            
            # отправляем сообщение админу бота о запросе нового пользователя
            u = ast.literal_eval(str(update))
            bot.sendMessage(chat_id=config['admin']['id'], text='Заявка на доступ ' + 
                            str(u['message']['from']['username'].encode('utf-8')) + ' ' + 
                            str(u['message']['from']['first_name'].encode('utf-8')) + ' ' + 
                            str(u['message']['from']['last_name'].encode('utf-8')) + ' ' + 
                            str(chat_id))
            
            logs(update, 'Request for access sent')
    


    Ок, мы отправили пользователю клавиатуру с доступными ему отчетами. Теперь в нашем варианте надо просто сравнивать поступающие сообщения с названиями отчетов в файле доступов privacy.yaml для данного chat_id. Если полученное сообщение совпадает с одним из отчетов, то отправляем серверу Табло команду на экспорт этого отчета. И затем отправляем сгенерированный отчет пользователю.

    Команду на экспорт отчетов с сервера в PDF-формате можно отправлять через командную строку с помощью «get» или «export» (ссылка на документацию).

    Для этого сначала открываем сессию с сервером Табло (вместо localhost, admin и adminPassword укажите домен сервера Табло, свой логин и пароль):

    Перед экспортом «тяжелого» отчета неплохо бы предупредить пользователя:

    bot.sendMessage(chat_id=chat_id, text='Формирую отчет, пожалуйста подождите...', reply_markup=reply_markup)
    
    executeFile = os.path.join( tabcmdLocation, 'tabcmd.exe' )
    subprocess.call([ executeFile, 'login', '-s', 'localhost', '-u', 'admin', '-p', 'adminPassword '])
    

    Экспортируем отчет в папку на диске

    subprocess.call([ executeFile, 'get', reportAddress, '-f', reportsLocation + reportName + '.pdf'])
    

    Закрываем сессию на сервере

    subprocess.call([ executeFile, 'logout'])
    

    Осталось только отправить сгенерированный отчет пользователю

    bot.send_document(chat_id=chat_id, document=open(reportsLocation +
      reportName.encode('utf-8') + '.pdf', 'rb'))
    

    Проверим как это работает:

    image

    Экспортированный с сервера PDF-отчет:

    image

    Вот и все, надеюсь данный подход поможет вам упростить доступ к отчетам и повысить эффективность работы. Полная версия кода:

    Конфигурационный файл
    users:
        123456789:
            keyboard:
                -
                      # название кнопки, которое увидит пользователь в чате
                    - buttonText: 'Global Temperatures'
                      # название файла, который будет выслан пользователю
                      reportName: 'Global Temperatures'
                      # адрес отчета на сервере Табло
                      report: '/views/Regional/GlobalTemperatures'
    
                      # название кнопки, которое увидит пользователь в чате
                    - buttonText: 'College Admissions'
                      # название файла, который будет выслан пользователю
                      reportName: 'College Admissions'
                      # адрес отчета на сервере Табло
                      report: '/views/Regional/College'
    
                -    
                      # название кнопки, которое увидит пользователь в чате
                    - buttonText: 'Regional FlightDelays'
                      # название файла, который будет выслан пользователю
                      reportName: 'Regional FlightDelays'
                      # адрес отчета на сервере Табло
                      report: '/views/Regional/FlightDelays'
    
    config:
        # токен бота
        botToken: '111222333:AAO4L0Ttt1tt01-1TtT1t_t22tT'
    
        # расположение файла tabcmd.exe на сервере
        tabcmdLocation: 'C:\Program Files\Tableau\Tableau Server\10.1\bin\'
    
        # куда сохраняем экспортируемые отчеты
        reportsLocation: 'C:\Users\Administrator\Documents\bot\reports\'
    
        # куда сохраняем логи
        logsLocation: 'C:\Users\Administrator\Documents\bot\logs\'
    
    admin:
        # на какой chat id отправляются запросы о предоставлении доступа
        id: 198765432
    


    Код скрипта
    # -*- coding: utf-8 -*-
    
    CONFIG_FILE = r'C:\Users\Administrator\Documents\bot\privacy.yaml'
    
    # библиотека для работы с API Телеграма
    import telegram
    from telegram.ext import Updater
    from telegram.ext import MessageHandler, Filters
    from telegram.ext import CommandHandler
    
    import sys
    import subprocess
    from yaml import load
    import ast
    import os.path
    
    def updateSettings():
        """
        Возвращает содержимое файла настроек
        """
        return load( file( CONFIG_FILE ) )
    
    # читаем файл с установками
    config = updateSettings()
    
    # токен бота
    token = config['config']['botToken']
    
    # расположение файла tabcmd.exe на сервере
    tabcmdLocation = config['config']['tabcmdLocation']
    
    # куда сохраняем экспортируемые отчеты
    reportsLocation = config['config']['reportsLocation']
    
    # куда сохраняем логи
    logsLocation = config['config']['logsLocation']
    
    def makeCustomKeyboard( userKeyboard ):
        """
        Возвращает клавиатуру в формате Телеграма.
        На вход принимает элемент keyboard пользователя из файла privacy.yaml.
        """
        custom_keyboard = map( lambda z: [ telegram.KeyboardButton(x['buttonText']) for x in z ], userKeyboard )
        return telegram.ReplyKeyboardMarkup(custom_keyboard, resize_keyboard='True')
    
    def logs(update, comment):
        """
        Запись действий скрипта.
        Текст comment записывается в папку logs в файл, именованный по chat id пользователя.
        """
        u = ast.literal_eval(str(update))
        
        with open( os.path.join(logsLocation, str(update.message.chat_id) + '.txt'), 'a') as f:
            f.write( str(u['message']['from']['username'].encode('utf-8')) + '\t' +
                    str(u['message']['from']['first_name'].encode('utf-8')) + '\t' +
                    str(u['message']['from']['last_name'].encode('utf-8')) + '\t' +
                    str(u['message']['text'].encode('utf-8')) + '\t' +
                    str(comment) + '\n')
    
    def start(bot, update):
        """
        Обработка команды /start для бота.
        Проверяет есть ли пользователь в списке доступов (файл privacy.yaml, раздел users). 
        Если есть, то возвращает приветствие и клавиатуру с отчетами. В противном случае отправляет заявку на доступ админу
        """
        # chat_id написавшего
        chat_id = update.message.chat_id
        
        # текущая версия клавиатуры и отчетов
        config = updateSettings()
        
        # проверяем есть ли пользователь в списке доступов
        if chat_id in config['users']:
            logs(update, 'Received /start command from user in privacy.yaml')
            
            # формируем клавиатуру с отчетами для данного пользователя и отправляем приветствие
            reply_markup = makeCustomKeyboard(config['users'][int(chat_id)]['keyboard'])
            bot.sendMessage(chat_id=chat_id, text='Привет, вот доступный список отчетов', reply_markup=reply_markup)
            
            logs(update, 'List of available reports sent')
            
        # пользователя нет в файле с доступами
        else:
            logs(update, 'Received /start command from user NOT IN privacy.yaml')
            
            # отправляем сообщение пользователю
            bot.sendMessage(chat_id=chat_id, text='Привет, в данный момент у вас нет доступа к отчетам. ' + \
                            'Ваша заявка принята, в ближайшее время доступ будет предоставлен')
            
            # отправляем сообщение админу бота о запросе нового пользователя
            u = ast.literal_eval(str(update))
            bot.sendMessage(chat_id=config['admin']['id'], text='Заявка на доступ ' + 
                            str(u['message']['from']['username'].encode('utf-8')) + ' ' + 
                            str(u['message']['from']['first_name'].encode('utf-8')) + ' ' + 
                            str(u['message']['from']['last_name'].encode('utf-8')) + ' ' + 
                            str(chat_id))
            
            logs(update, 'Request for access sent')
    
    def echo(bot, update):
        """
        Обработка всех сообщений боту, кроме команд.
        Если пользователь есть в списке отчетов и текст сообщения совпал с кнопкой, то отправляем ему отчет
        """
        
        # chat_id текущего сообщения
        chat_id=update.message.chat_id
        
        # текст текущего сообщения
        response = update.message.text
        
        # обновляем версию клавиатуры и отчетов
        config = updateSettings()
        
        # разрешение на отправку отчетов (отправляется только в случае greenLight = True)
        greenLight = False
        
        # проходим по каждой линии клавиатуры для данного пользователя
        for line in config['users'][chat_id]['keyboard']:
            
            # проходим по каждой кнопке в линии клавиатуры
            for button in line:
                if button['buttonText'] == response:
                    logs(update, 'Menu found, generating current keyboard and reports')
                    
                    # формируем клавиатуру текущего отчета и генерим отчет
                    reply_markup = makeCustomKeyboard( config['users'][chat_id]['keyboard'] )
    
                    logs(update, 'Current keyboard generated')
    
                    # разрешение генерации отчета
                    greenLight = True
    
                    # задаем параметры отчета
                    reportName = button['reportName']
                    reportAddress = button['report'] + '?:refresh=yes'
    
        if greenLight:
            logs(update, 'Starting to generate report')
            
            # отправляем сообщение о начале генерации отчета (это может занять время)
            bot.sendMessage(chat_id=chat_id, text='Формирую отчет, пожалуйста подождите...', reply_markup=reply_markup)
            logs(update, 'Message about report generation sent')
            
            # задаем расположение служебного файла сервера Табло
            executeFile = os.path.join( tabcmdLocation, 'tabcmd.exe' )
            
            # открываем сессию на сервере
            subprocess.call([ executeFile, 'login',  '-s',  'localhost',  '-u',  'admin',  '-p',  'adminPassword'])
            
            # экспортируем отчет в формате PDF
            subprocess.call([ executeFile, 'get',  reportAddress,  '-f', reportsLocation + reportName + '.pdf'])
            
            # закрываем сессию
            subprocess.call([ executeFile, 'logout'])
            
            logs(update, 'Report generated, sending document')
            
            # отправляем отчет пользователю
            bot.send_document(chat_id=chat_id, document=open(reportsLocation +
                                                             reportName.encode('utf-8') + '.pdf', 'rb'))
            logs(update, 'Report sent')
            
        elif response == token:
        	updater.stop()
    
        # в случае, если сообщение не распознано	
        else:
            logs(update, 'Bad message, no access to this report')
            
            # формируем клавиатуру для данного пользователя и отправляем ему уведомление
            reply_markup = makeCustomKeyboard(config['users'][chat_id]['keyboard'])
            bot.sendMessage(chat_id=chat_id, text='Похоже у вас нет доступа к этому отчету. ' + \
                              'Пожалуйста, напишите admin@tableau.mycompany.ru запрос на доступ', reply_markup=reply_markup)
            
            logs(update, 'Main menu keyboard sent after bad message')
    
    # запускаем обработчик сообщений бота
    updater = Updater(token=token)
    dispatcher = updater.dispatcher
    updater.start_polling()
    
    # обработка команды /start
    start_handler = CommandHandler('start', start)
    dispatcher.add_handler(start_handler)
    
    # обработка текстовых сообщений
    echo_handler = MessageHandler([Filters.text], echo)
    dispatcher.add_handler(echo_handler)
    

    • +12
    • 6,2k
    • 1
    Поделиться публикацией
    Похожие публикации
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 1
    • +1
      Первая мысль: можно добавить к этому определение аномалий (e.g. «регистрации упали», «ошибки выросли») и рассылать сообщения о них сразу с отчётами и картинками.

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