#!/usr/bin/python3
# -*- coding: utf-8 -*-
""" Модуль с бизнес-сценариями """
import os
import re
import time
import ast
import datetime
import requests
import traceback
import pandas as pd
from itertools import count
from typing import List, Dict, Tuple, Any, Union

from . import error_handler
from . import executor
from . import manager_commands
from . import helper
from . import olap_commands
from . import authorization
from . import precondition

# -------------------------------------------------------------------------------
# logging
from autologging import traced, logged
import logging
from logging import NullHandler

logger = logging.getLogger(__name__)
logger.addHandler(NullHandler())

logger.info(" ---------------------------------------------------------------------------------------------- ")
logger.info(" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ SCRIPT STARTED ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ")
# -------------------------------------------------------------------------------


OPERANDS = ["=", "+", "-", "*", "/", "<", ">", "!=", "<=", ">="]
ALL_PERMISSIONS = 31
MONTHS = ["Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь",
          "Ноябрь", "Декабрь"]
WEEK_DAYS = ["Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота", "Воскресенье"]
PERIOD = {"Ежедневно": 1, "Еженедельно": 2, "Ежемесячно": 3}
WEEK = {"понедельник": 0, "вторник": 1, "среда": 2, "четверг": 3, "пятница": 4, "суббота": 5, "воскресенье": 6}
UPDATES = ["ручное", "по расписанию", "интервальное", "инкрементальное"]


def timing(f):
    """
    Profiling business logic funcs
    :param f: func to decorate
    :return:
    """

    def wrap(self, *args, **kwargs):
        self.func_name = f.__name__
        time1 = time.time()
        ret = f(self, *args, **kwargs)
        time2 = time.time()
        self.func_timing = '{:s} func exec time: {:.2f} sec'.format(f.__name__, (time2 - time1))
        return ret

    return wrap


@traced
@logged
class BusinessLogic:
    """
    Класс BusinessLogic.

    Используемые переменные класса:
    # язык интерфейса. Задается при авторизации. Возможно задать значения: "ru", "en", "de" или "fr"
    self.language = "ru"

    # базовый URL для работы
    self.url = url

    # словарь команд и состояний server-codes.json
    self.server_codes

    # id сессии
    self.session_id

    # uuid, который возвращается после авторизации
    self.authorization_uuid

    # список слоев сессии
    self.layers_list

    # id мультисферы
    self.cube_id

    # используемый id слоя
    self.active_layer_id

    # данные мультисферы в формате словаря {"dimensions": "", "facts": "", "data": ""}
    self.multisphere_data

    # для хранения названия мультисферы
    self.cube_name = ""

    # helper class
    self.h = helper.Helper(self)

    # общее количество строк текущей рабочей области
    self.total_row = 0

    # для измерения вреимени работы функций бизнес-логики
    self.func_timing = 0

    # записать имя функции для избежания конфликтов с декоратором
    self.func_name = ""

    :param login: логин пользователя Полиматика
    :param url: URL стенда Полиматика
    :param password: (необязательный) пароль пользователя Полиматика
    :param session_id: (необязательный) id сессии
    :param authorization_id: (необязательный) id авторизации
    """

    def __init__(self, login: str, url: str, password: str = None, session_id: str = None,
                 authorization_id: str = None, timeout: float = 60.0, jupiter: bool = False):
        logger.info("INIT CLASS BusinessLogic")

        """
        Инициализация класса BusinessLogic

        :param login: логин пользователя Полиматика
        :param url: URL стенда Полиматика
        :param password: (необязательный) пароль пользователя Полиматика
        :param session_id: (необязательный) id сессии
        :param authorization_id: (необязательный) id авторизации
        :param timeout: таймауты. по умолчанию = 60.0
        :param jupiter: (bool) запускается ли скрипт из Jupiter Notebook.
                По умолчанию jupiter = False (stderr stdout пишется в лог)
        """
        self.language = "ru"
        self.url = url
        self.server_codes = precondition.Preconditions(url).get_server_codes()

        # Флаг работы в Jupiter Notebook
        self.jupiter = jupiter
        # значение присвается в случае аварийного завершения работы
        # может быть удобно при работе в Jupiter Notebook
        self.current_exception = None

        # параметры аутентификации
        self.login = login

        # для измерения вреимени работы функций бизнес-логики
        self.func_timing = 0

        if session_id is None:
            try:
                self.session_id, self.authorization_uuid, self.func_timing = authorization.Authorization(
                    self.server_codes
                ).login(
                    user_name=login,
                    password=password,
                    url=url,
                    language=self.language
                )
            except BaseException as e:
                logger.exception(e)
                logger.exception("APPLICATION STOPPED")
                self.current_exception = str(e)
                if self.jupiter:
                    print("EXCEPTION!!! %s" % e)
                    return
                raise
        else:
            self.session_id, self.authorization_uuid = session_id, authorization_id

        # инициализация модуля Manager
        self.manager_command = manager_commands.ManagerCommands(
            self.session_id, self.authorization_uuid, url, self.server_codes, self.jupiter)
        # класс выполняющий команды
        self.exec_request = executor.Executor(self.session_id, self.authorization_uuid, url, timeout)
        # id модуля мультисферы, значение присываивается после создания куба и получения данных о кубе
        self.multisphere_module_id = ""
        # инициализация модуля Olap. ВАЖНО! Перед использованием получить self.multisphere_module_id
        self.olap_command = olap_commands.OlapCommands(self.session_id, self.multisphere_module_id,
                                                       url, self.server_codes, self.jupiter)
        self.layers_list = []
        self.cube_id = ""
        self.active_layer_id = ""
        self.multisphere_data = {}
        # для хранения названия мультисферы
        self.cube_name = ""

        # helper class
        self.h = helper.Helper(self)

        # общее количество строк текущей рабочей области
        self.total_row = 0

        # записать имя функции для избежания конфликтов с декоратором
        self.func_name = ""

        # DataFrame content, DataFrame columns
        self.df, self.df_cols = "", ""

    def update_total_row(self):
        result = self.execute_olap_command(
            command_name="view",
            state="get_2",
            from_row=0,
            from_col=0,
            num_row=1,
            num_col=1
        )
        self.total_row = self.h.parse_result(result, "total_row")
        return self.total_row

    @timing
    def get_cube(self, cube_name: str, num_row: int = 100, num_col: int = 100) -> str:
        """
        Получить id куба по его имени и открыть мультисферу
        :param cube_name: (str) имя куба (мультисферы)
        :param num_row: (int) количество строк, которые будут выведены
        :param num_col: (int) количество колонок, которые будут выведены
        :return: id куба
        """
        self.cube_name = cube_name
        # получение списка описаний мультисфер
        result = self.execute_manager_command(command_name="user_cube", state="list_request")
        if self.jupiter:
            if "ERROR" in str(result):
                return result
        # try:
        cubes_list = self.h.parse_result(result=result, key="cubes")
        if self.jupiter:
            if "ERROR" in str(cubes_list):
                return cubes_list

        # получить cube_id из списка мультисфер
        try:
            self.cube_id = self.h.get_cube_id(cubes_list, cube_name)
        except ValueError as e:
            logger.exception("EXCEPTION!!! %s", e)
            logger.exception("APPLICATION STOPPED")
            self.current_exception = str(e)
            if self.jupiter:
                return self.current_exception
            raise

        self.multisphere_data = self.create_multisphere_module(num_row=num_row, num_col=num_col)
        self.update_total_row()

        return self.cube_id

    def get_multisphere_data(self, num_row: int = 100, num_col: int = 100) -> [Dict, str]:
        """
        Получить данные мультисферы
        :param self: экземпляр класса BusinessLogic
        :param num_row: количество отображаемых строк
        :param num_col: количество отображаемых столбцов
        :return: (Dict) multisphere data, format: {"dimensions": "", "facts": "", "data": ""}
        """
        # Получить список слоев сессии
        result = self.execute_manager_command(command_name="user_layer", state="get_session_layers")
        # список слоев
        layers_list = self.h.parse_result(result=result, key="layers")
        if self.jupiter:
            if "ERROR" in str(layers_list):
                return layers_list
        try:
            # получить layer id
            self.layer_id = layers_list[0]["uuid"]
        except KeyError as e:
            logger.exception("EXCEPTION!!! %s\n%s", e, traceback.format_exc())
            logger.exception("APPLICATION STOPPED")
            self.current_exception = str(e)
            if self.jupiter:
                return self.current_exception
            raise
        except IndexError as e:
            logger.exception("EXCEPTION!!! %s\n%s", e, traceback.format_exc())
            logger.exception("APPLICATION STOPPED")
            self.current_exception = str(e)
            if self.jupiter:
                return self.current_exception
            raise

        # инициализация модуля Olap
        self.olap_command = olap_commands.OlapCommands(self.session_id, self.multisphere_module_id, self.url,
                                                       self.server_codes, self.jupiter)

        # рабочая область прямоугольника
        view_params = {
            "from_row": 0,
            "from_col": 0,
            "num_row": num_row,
            "num_col": num_col
        }

        # получить список размерностей и фактов, а также текущее состояние таблицы со значениями
        # (рабочая область модуля мультисферы)
        query = self.olap_command.multisphere_data(self.multisphere_module_id, view_params)
        if self.jupiter:
            if "EXCEPTION" in str(query):
                return query
        try:
            result = self.exec_request.execute_request(query)
        except BaseException as e:
            logger.exception(e)
            logger.exception("APPLICATION STOPPED")
            self.current_exception = str(e)
            if self.jupiter:
                return self.current_exception
            raise

        # multisphere data
        self.multisphere_data = {"dimensions": "", "facts": "", "data": ""}
        for item, index in [("dimensions", 0), ("facts", 1), ("data", 2)]:
            self.multisphere_data[item] = result["queries"][index]["command"][item]
        logger.info("Multisphere data successfully received: %s" % self.multisphere_data)
        return self.multisphere_data

    def get_cube_without_creating_module(self, cube_name: str) -> str:
        """
        Получить id куба по его имени, без создания модуля мультисферы
        :param cube_name: (str) имя куба (мультисферы)
        :return: id куба
        """
        self.cube_name = cube_name
        result = self.execute_manager_command(command_name="user_cube", state="list_request")

        # получение списка описаний мультисфер
        cubes_list = self.h.parse_result(result=result, key="cubes")
        if self.jupiter:
            if "ERROR" in str(cubes_list):
                return cubes_list

        # получить cube_id из списка мультисфер
        try:
            self.cube_id = self.h.get_cube_id(cubes_list, cube_name)
        except ValueError:
            return "Cube '%s' not found" % cube_name
            # logger.exception("APPLICATION STOPPED")
            # self.current_exception = "Cube '%s' not found" % cube_name
            # if self.jupiter:
            #     return self.current_exception
            # raise
        return self.cube_id

    @timing
    def move_dimension(self, dim_name: str, position: str, level: int) -> [Dict, str]:
        """
        Вынести размерность
        :param dim_name: (str) Название размерности
        :param position: (str) "left" / "up" (выносит размерность влево, либо вверх)
        :param level: (int) 0, 1, 2, ... (считается слева-направо для левой позиции,
                      сверху - вниз для верхней размерности)
        :return: (Dict) результат команды Olap "dimension", состояние "move"
        """
        # проверки
        try:
            position = error_handler.checks(self, self.func_name, position)
        except BaseException as e:
            logger.exception(e)
            logger.exception("APPLICATION STOPPED")
            self.current_exception = str(e)
            if self.jupiter:
                return self.current_exception
            raise

        # получение id размерности
        self.multisphere_data = self.get_multisphere_data()
        dim_id = self.h.get_dim_id(self.multisphere_data, dim_name, self.cube_name)
        if self.jupiter:
            if "ERROR" in str(dim_id):
                return dim_id

        self.update_total_row()

        return self.execute_olap_command(command_name="dimension",
                                         state="move",
                                         position=position,
                                         id=dim_id,
                                         level=level)

    @timing
    def get_measure_id(self, measure_name: str) -> [str, bool]:
        """
        Получить id факта
        :param measure_name: (str) название факта
        :return: (str) id факта
        """
        # получить словарь с размаерностями, фактами и данными
        self.get_multisphere_data()

        # id факта
        m_id = self.h.get_measure_id(self.multisphere_data, measure_name, self.cube_name)
        if self.jupiter:
            if "ERROR" in str(m_id):
                return m_id
        return m_id

    @timing
    def get_dim_id(self, dim_name: str) -> [str, bool]:
        """
        Получить id размерности
        :param dim_name: (str) название размерности
        :return: (str) id факта
        """
        # получить словарь с размаерностями, фактами и данными
        self.get_multisphere_data()

        # id размерности
        dim_id = self.h.get_dim_id(self.multisphere_data, dim_name, self.cube_name)
        if self.jupiter:
            if "ERROR" in str(dim_id):
                return dim_id
        return dim_id

    @timing
    def get_measure_name(self, measure_id: str) -> str:
        """
        Получить название факта
        :param measure_id: (str) id факта
        :return: (str) название факта
        """
        # проверки
        try:
            error_handler.checks(self, func_name=self.func_name)
        except BaseException as e:
            logger.exception(e)
            logger.exception("APPLICATION STOPPED")
            self.current_exception = str(e)
            if self.jupiter:
                return self.current_exception
            raise

        # получить словать с размаерностями, фактами и данными
        self.get_multisphere_data()

        measure_data = self.multisphere_data["facts"]
        logger.info("Full multisphere measure data: %s", measure_data)
        for i in measure_data:
            if i["id"] == measure_id:
                logger.info("Fact id: %s; its name: %s", measure_id, i["name"])
                return i["name"]
        logger.info("No measure %s in the multisphere!", measure_id)
        return "No measure id %s in the multisphere!" % measure_id

    @timing
    def get_dim_name(self, dim_id: str) -> str:
        """
        Получить ID размерности
        :param dim_id: (str) id размерности
        :return: (str) название размерности
        """
        # проверки
        try:
            error_handler.checks(self, func_name=self.func_name)
        except BaseException as e:
            logger.exception(e)
            logger.exception("APPLICATION STOPPED")
            self.current_exception = str(e)
            if self.jupiter:
                return self.current_exception
            raise

        # получить словать с размаерностями, фактами и данными
        self.get_multisphere_data()

        dim_data = self.multisphere_data["dimensions"]
        logger.info("Full multisphere dimension data: %s ", dim_id)
        for i in dim_data:
            if i["id"] == dim_id:
                logger.info("Retrieved dimension id: %s; its name: %s", dim_id, i["name"])
                return i["name"]
        logger.info("No dimension id %s in the multisphere!", dim_id)
        return "No dimension id %s in the multisphere!" % dim_id

    @timing
    def delete_dim_filter(self, dim_name: str, filter_name: str, num_row: int = 100) -> [Dict, str]:
        """
        Убрать выбранный фильтр размерности
        :param dim_name: (str) Название размерности
        :param filter_name: (str) Название метки/фильтра
        :param num_row: (int) Количество строк, которые будут отображаться в мультисфере
        :return: (Dict) команда Olap "filter", state: "apply_data"
        """
        # получить словать с размаерностями, фактами и данными
        self.get_multisphere_data()

        # id размерности
        dim_id = self.h.get_measure_or_dim_id(self.multisphere_data, "dimensions", dim_name)
        if self.jupiter:
            if "ERROR" in str(dim_id):
                return dim_id

        # Наложить фильтр на размерность (в неактивной области)
        # получение списка активных и неактивных фильтров
        result = self.execute_olap_command(command_name="filter",
                                           state="pattern_change",
                                           dimension=dim_id,
                                           pattern="",
                                           # кол-во значений отображается на экране, после скролла их становится больше:
                                           # num=30
                                           num=num_row)

        filters_list = self.h.parse_result(result=result, key="data")
        if self.jupiter:
            if "ERROR" in str(filters_list):
                return filters_list
        filters_values = self.h.parse_result(result=result, key="marks")
        if self.jupiter:
            if "ERROR" in str(filters_list):
                return filters_list

        # Снять метку по его интерфейсному названию
        for elem in filters_list:
            if elem == filter_name:
                ind = filters_list.index(filter_name)
                filters_values[ind] = 0
                break

        # 2. нажать применить
        command1 = self.olap_command.collect_command("olap", "filter", "apply_data", dimension=dim_id,
                                                     marks=filters_values)
        command2 = self.olap_command.collect_command("olap", "filter", "set", dimension=dim_id)
        if self.jupiter:
            if "EXCEPTION" in str(command1):
                return command1
            if "EXCEPTION" in str(command2):
                return command2
        query = self.olap_command.collect_request(command1, command2)

        try:
            result = self.exec_request.execute_request(query)
        except BaseException as e:
            logger.exception(e)
            logger.exception("APPLICATION STOPPED")
            self.current_exception = str(e)
            if self.jupiter:
                return self.current_exception
            raise

        self.update_total_row()

        return result

    @timing
    def clear_all_dim_filters(self, dim_name: str, num_row: int = 100) -> [Dict, bool]:
        """
        Очистить все фильтры размерности
        :param dim_name: (str) Название размерности
        :param num_row: (int) Количество строк, которые будут отображаться в мультисфере
        :return: (Dict) команда Olap "filter", state: "apply_data"
        """
        # получить словать с размаерностями, фактами и данными
        self.get_multisphere_data(num_row=num_row)

        # получение id размерности
        dim_id = self.h.get_measure_or_dim_id(self.multisphere_data, "dimensions", dim_name)
        if self.jupiter:
            if "ERROR" in str(dim_id):
                return dim_id

        # Наложить фильтр на размерность (в неактивной области)
        # получение списка активных и неактивных фильтров
        result = self.execute_olap_command(command_name="filter",
                                           state="pattern_change",
                                           dimension=dim_id,
                                           pattern="",
                                           # кол-во значений отображается на экране, после скролла их становится больше:
                                           # num=30
                                           num=num_row)

        filters_values = self.h.parse_result(result=result, key="marks")  # получить список on/off [0,0,...,0]
        if self.jupiter:
            if "ERROR" in str(filters_values):
                return filters_values

        # подготовить список для снятия меток: [0,0,..,0]
        length = len(filters_values)
        for i in range(length):
            filters_values[i] = 0

        # 1. сначала снять все отметки
        self.execute_olap_command(command_name="filter", state="filter_all_flag", dimension=dim_id)

        # 2. нажать применить
        command1 = self.olap_command.collect_command("olap", "filter", "apply_data", dimension=dim_id,
                                                     marks=filters_values)
        command2 = self.olap_command.collect_command("olap", "filter", "set", dimension=dim_id)
        if self.jupiter:
            if "EXCEPTION" in str(command1):
                return command1
            if "EXCEPTION" in str(command2):
                return command2
        query = self.olap_command.collect_request(command1, command2)

        try:
            result = self.exec_request.execute_request(query)
        except BaseException as e:
            logger.exception(e)
            logger.exception("APPLICATION STOPPED")
            self.current_exception = str(e)
            if self.jupiter:
                return self.current_exception
            raise

        self.update_total_row()

        return result

    @timing
    def put_dim_filter(self, dim_name: str, filter_name: Union[str, List] = None, start_date: Union[int, str] = None,
                       end_date: Union[int, str] = None) -> [Dict, str]:
        """
        Сделать выбранный фильтр активным

        Если в фильтрах используются месяцы, то использовать хначения (регистр важен!):
            ["Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь",
            "Ноябрь", "Декабрь"]

         Дни недели (регистр важен!): ["Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота",
            "Воскресенье"]
        :param dim_name: (str) Название размерности
        :param filter_name: (str) Название фильтра. None - если нужно указать интервал дат.
        :param start_date: (int, datetime.datetime) Начальная дата
        :param end_date: (int, datetime.datetime) Конечная дата
        :return: (Dict) команда Olap "filter", state: "apply_data"
        """
        # много проверок...
        # Заполнение списка dates_list в зависимости от содержания параметров filter_name, start_date, end_date
        try:
            dates_list = error_handler.checks(self,
                                              self.func_name,
                                              filter_name,
                                              start_date,
                                              end_date,
                                              MONTHS,
                                              WEEK_DAYS)
        except BaseException as e:
            logger.exception(e)
            logger.exception("APPLICATION STOPPED")
            self.current_exception = str(e)
            if self.jupiter:
                return self.current_exception
            raise

        # получение id размерности
        dim_id = self.get_dim_id(dim_name)

        # Наложить фильтр на размерность (в неактивной области)
        # получение списка активных и неактивных фильтров
        result = self.h.get_filter_rows(dim_id)

        filters_list = self.h.parse_result(result=result, key="data")  # получить названия фильтров
        if self.jupiter:
            if "ERROR" in str(filters_list):
                return filters_list
        filters_values = self.h.parse_result(result=result, key="marks")  # получить список on/off [0,0,...,0]
        if self.jupiter:
            if "ERROR" in str(filters_values):
                return filters_values

        try:
            if (filter_name is not None) and (filter_name not in filters_list):
                if isinstance(filter_name, List):
                    for elem in filter_name:
                        if elem not in filters_list:
                            raise ValueError("No filter '%s' in dimension '%s'" % (elem, dim_name))
                else:
                    raise ValueError("No filter '%s' in dimension '%s'" % (filter_name, dim_name))
        except ValueError as e:
            logger.exception(e)
            logger.exception("APPLICATION STOPPED")
            self.current_exception = str(e)
            if self.jupiter:
                return self.current_exception
            raise

        # подготовить список для снятия меток: [0,0,..,0]
        length = len(filters_values)
        for i in range(length):
            filters_values[i] = 0

        # сначала снять все отметки
        self.execute_olap_command(command_name="filter",
                                  state="filter_all_flag",
                                  dimension=dim_id)

        # ******************************************************************************************************

        # подготовить список фильтров с выбранными отмеченной меткой
        for idx, elem in enumerate(filters_list):
            if isinstance(filter_name, List):
                if elem in filter_name:
                    filters_values[idx] = 1
            # если фильтр по интервалу дат:
            elif filter_name is None:
                if elem in dates_list:
                    filters_values[idx] = 1
            # если фильтр выставлен по одному значению:
            elif elem == filter_name:
                ind = filters_list.index(filter_name)
                filters_values[ind] = 1
                break

        # 2. нажать применить
        command1 = self.olap_command.collect_command("olap", "filter", "apply_data", dimension=dim_id,
                                                     marks=filters_values)
        command2 = self.olap_command.collect_command("olap", "filter", "set", dimension=dim_id)
        if self.jupiter:
            if "EXCEPTION" in str(command1):
                return command1
            if "EXCEPTION" in str(command2):
                return command2
        query = self.olap_command.collect_request(command1, command2)

        try:
            result = self.exec_request.execute_request(query)
        except BaseException as e:
            logger.exception(e)
            logger.exception("APPLICATION STOPPED")
            self.current_exception = str(e)
            if self.jupiter:
                return self.current_exception
            raise

        self.update_total_row()

        return result

    @timing
    def create_consistent_dim(self, formula: str, separator: str, dimension_list: List) -> [Dict, str]:
        """
        Создать составную размерность
        :param formula: (str) формат [Размерность1]*[Размерность2]
        :param separator: (str) "*" / "_" / "-", ","
        :param dimension_list: (List) ["Размерность1", "Размерность2"]
        :return: (Dict) команда модуля Olap "dimension", состояние: "create_union",
        """
        # получить словать с размаерностями, фактами и данными
        self.get_multisphere_data()

        # подготовка списка с id размерностей
        dim_ids = []
        for i in dimension_list:
            dim_id = self.h.get_measure_or_dim_id(self.multisphere_data, "dimensions", i)
            if self.jupiter:
                if "ERROR" in str(dim_id):
                    return dim_id
            dim_ids.append(dim_id)

        # заполнение списка параметров единицами (1)
        visibillity_list = [1] * len(dim_ids)

        return self.execute_olap_command(command_name="dimension",
                                         state="create_union",
                                         name=formula,
                                         separator=separator,
                                         dim_ids=dim_ids,
                                         union_dims_visibility=visibillity_list)

    @timing
    def switch_unactive_dims_filter(self) -> [Dict, str]:
        """
        Переключить фильтр по неактивным размерностям
        :return: (Dict) команда модуля Olap "dimension", состояние "set_filter_mode"
        """
        result = self.execute_olap_command(command_name="dimension", state="set_filter_mode")
        self.update_total_row()
        return result

    @timing
    def copy_measure(self, measure_name: str) -> str:
        """
        Копировать факт
        :param measure_name: (str) имя факта
        :return: id копии факта
        """
        # получить словать с размаерностями, фактами и данными
        self.get_multisphere_data()

        # Получить id факта
        measure_id = self.h.get_measure_or_dim_id(self.multisphere_data, "facts", measure_name)
        if self.jupiter:
            if "ERROR" in str(measure_id):
                return measure_id

        result = self.execute_olap_command(command_name="fact", state="create_copy", fact=measure_id)

        new_measure_id = self.h.parse_result(result=result, key="create_id")

        return new_measure_id

    @timing
    def rename_measure(self, measure_name: str, new_measure_name: str) -> [Dict, str]:
        """
        Переименовать факт
        :param measure_name: (str) имя факта
        :param new_measure_name: (str) имя факта
        :return: (Dict) ответ после выполнения команды модуля Olap "fact", состояние: "rename"
        """
        # получить словать с размаерностями, фактами и данными
        self.get_multisphere_data()

        # получить id факта
        measure_id = self.h.get_measure_or_dim_id(self.multisphere_data, "facts", measure_name)
        if self.jupiter:
            if "ERROR" in str(measure_id):
                return measure_id

        return self.execute_olap_command(command_name="fact", state="rename", fact=measure_id, name=new_measure_name)

    @timing
    def rename_dimension(self, dim_name: str, new_name: str) -> [Dict, str]:
        """
        Переименовать размерность
        :param dim_name: (str) наименование размерности
        :param new_name: (str) наименование копии размерности
        :return: (Dict) ответ после выполнения command_name="dimension", state="rename"
        """
        # проверки
        try:
            error_handler.checks(self, self.func_name, new_name)
        except BaseException as e:
            logger.exception(e)
            logger.exception("APPLICATION STOPPED")
            self.current_exception = str(e)
            if self.jupiter:
                return self.current_exception
            raise

        # получить id размерности
        dim_id = self.get_dim_id(dim_name)

        # скопировать размерность
        result = self.execute_olap_command(command_name="dimension", state="create_copy", id=dim_id)
        copied_id = self.h.parse_result(result, "ndim_id")
        if self.jupiter:
            if "ERROR" in str(copied_id):
                return copied_id

        # переименовать скопированную размерность
        return self.execute_olap_command(command_name="dimension", state="rename", id=copied_id, name=new_name)

    @timing
    def change_measure_type(self, measure_id: str, type_name: str) -> [Dict, str]:
        """
        Поменять Вид факта
        :param measure_id: (str) id факта
        :param type_name: (str) название Вида факта (как в интерфейсе):
            "Значение"
            "Процент"
            "Ранг"
            "Количество уникальных"
            "Среднее"
            "Отклонение"
            "Минимум"
            "Максимум"
            "Изменение"
            "Изменение в %"
            "Нарастающее"
            "ABC"
            "Медиана"
            "Количество"
            "UNKNOWN"
        :return: (Dict) команда модуля Olap "fact", состояние: "set_type"
        """
        # Получить Вид факта (id)
        measure_type = self.h.get_measure_type(type_name)
        if self.jupiter:
            if "ERROR" in str(measure_type):
                return measure_type

        # выбрать Вид факта:
        return self.execute_olap_command(command_name="fact", state="set_type", fact=measure_id, type=measure_type)

    @timing
    def export(self, path: str, file_format: str) -> [Tuple, str]:
        """
        Экспортировать файл
        :param path: (str) путь, по которому файл будет сохранен
        :param file_format: (str) формат сохраненного файла: "csv", "xls", "json"
        :return (Tuple): file_name, path
        """
        # проверки
        try:
            error_handler.checks(self, self.func_name, file_format)
        except BaseException as e:
            logger.exception(e)
            logger.exception("APPLICATION STOPPED")
            self.current_exception = str(e)
            if self.jupiter:
                return self.current_exception
            raise

        # Экспортировать полученный результат
        self.execute_olap_command(
            command_name="xls_export",
            state="start",
            export_format=file_format,
            export_destination_type="local"
        )

        logger.info("Waiting for file name...")
        time.sleep(10)
        result = self.execute_olap_command(command_name="xls_export", state="check")
        # progress doesn't work
        # progress = result["queries"][0]["command"]["progress"]
        # print("Progress: %s" % progress)

        file_name = self.h.parse_result(result=result, key="file_name")
        if self.jupiter:
            if "ERROR" in str(file_name):
                return file_name

        logger.info("File name: %s", file_name)

        # URL, по которому лежит файл экспортируемый файл: базовый URL/resources/файл
        file_url = self.url + "/" + "resources" + "/" + file_name

        # выполнить запрос
        try:
            r = self.exec_request.execute_request(params=file_url, method="GET")
        except BaseException as e:
            logger.exception(e)
            logger.exception("APPLICATION STOPPED")
            self.current_exception = str(e)
            if self.jupiter:
                return self.current_exception
            raise

        # сохранить файл по указанному пути
        file_name = file_name[:-8].replace(":", "_")
        filePath = path + "//" + file_name

        # запись файла в указанную директорию
        try:
            with open(filePath, 'wb') as f:
                f.write(r.content)
        except IOError as e:
            logger.exception("EXCEPTION!!! %s", e)
            logger.exception("Creating path recursively...")
            os.makedirs(path, exist_ok=True)
            with open(filePath, 'wb') as f:
                f.write(r.content)

        # проверка что файл скачался после экспорта
        filesList = os.listdir(path)
        assert file_name in filesList, "File %s not in path %s" % (file_name, path)
        return file_name, path

    @timing
    def create_calculated_measure(self, new_name: str, formula: str) -> [Dict, str]:
        """
        Создать вычислимый факт. Элементы формулы должный быть разделеный ПРОБЕЛОМ!
        Список используемых операндов: ["=", "+", "-", "*", "/", "<", ">", "!=", "<=", ">="]

        Примеры формул:
        top([Сумма долга];1000)
        100 + [Больницы] / [Количество вызовов врача] * 2 + corr([Количество вызовов врача];[Больницы])

        :param new_name: (str) Имя нового факта
        :param formula: (str) формула. Элементы формулы должный быть разделеный ПРОБЕЛОМ!
        :return: (Dict) команда модуля Olap "fact", состояние: "create_calc"
        """
        # получить данные мультисферы
        self.get_multisphere_data()

        # преобразовать строковую формулу в список
        formula_lst = formula.split()
        # количество фактов == кол-во итераций
        join_iterations = formula.count("[")
        # если в названии фактов есть пробелы, склеивает их обратно
        formula_lst = self.h.join_splited_measures(formula_lst, join_iterations)

        # параметра formula
        output = ""
        opening_brackets = 0
        closing_brackets = 0
        try:
            for i in formula_lst:
                if i == "(":
                    output += "("
                    opening_brackets += 1
                    continue
                elif i == ")":
                    output += ")"
                    closing_brackets += 1
                    continue
                elif i == "not":
                    output += "not"
                    continue
                elif i == "and":
                    output += "and"
                    continue
                elif i == "or":
                    output += "or"
                    continue
                elif "total(" in i:
                    m = re.search('\[(.*?)\]', i)
                    total_content = m.group(0)
                    measure_id = self.h.get_measure_or_dim_id(self.multisphere_data, "facts", total_content[1:-1])
                    if self.jupiter:
                        if "ERROR" in measure_id:
                            return measure_id
                    output += "total(%s)" % measure_id
                    continue
                elif "top(" in i:
                    # top([из чего];сколько)
                    m = re.search('\[(.*?)\]', i)
                    measure_name = m.group(0)[1:-1]
                    measure_id = self.h.get_measure_or_dim_id(self.multisphere_data, "facts", measure_name)
                    if self.jupiter:
                        if "ERROR" in measure_id:
                            return measure_id

                    m = re.search('\d+', i)
                    int_value = m.group(0)

                    output += "top( fact(%s) ;%s)" % (measure_id, int_value)
                    continue
                elif "if(" in i:
                    message = "if(;;) не реализовано!"
                    logger.warning(message)
                    logger.exception("APPLICATION STOPPED")
                    self.current_exception = message
                    if self.jupiter:
                        return self.current_exception
                    raise
                elif "corr(" in i:
                    m = re.search('\((.*?)\)', i)
                    measures = m.group(1).split(";")
                    measure1 = measures[0]
                    measure2 = measures[1]
                    measure1 = self.h.get_measure_or_dim_id(self.multisphere_data, "facts", measure1[1:-1])
                    if self.jupiter:
                        if "ERROR" in measure1:
                            return measure1
                    measure2 = self.h.get_measure_or_dim_id(self.multisphere_data, "facts", measure2[1:-1])
                    if self.jupiter:
                        if "ERROR" in measure2:
                            return measure2
                    output += "corr( fact(%s) ; fact(%s) )" % (measure1, measure2)
                    continue
                elif i[0] == "[":
                    # если пользователь ввел факт в формте [2019,Больница]
                    # где 2019 - элемент самой верхней размерности, Больница - название факта
                    if "," in i:
                        measure_content = i[1:-1].split(",")
                        elem = measure_content[0]
                        measure_name = measure_content[1]

                        # сформировать словарь {"элемент верхней размерности": индекс_элемнета}
                        result = self.execute_olap_command(command_name="view",
                                                           state="get_2",
                                                           from_row=0,
                                                           from_col=0,
                                                           num_row=1,
                                                           num_col=1)

                        top_dims = self.h.parse_result(result=result, key="top_dims")
                        if self.jupiter:
                            if "ERROR" in str(top_dims):
                                return top_dims
                        result = self.execute_olap_command(command_name="dim_element_list_data", state="pattern_change",
                                                           dimension=top_dims[0], pattern="", num=30)

                        top_dim_values = self.h.parse_result(result=result, key="data")
                        if self.jupiter:
                            if "ERROR" in str(top_dim_values):
                                return top_dim_values
                        top_dim_indexes = self.h.parse_result(result=result, key="indexes")
                        if self.jupiter:
                            if "ERROR" in str(top_dim_indexes):
                                return top_dim_indexes
                        top_dim_dict = dict(zip(top_dim_values, top_dim_indexes))

                        measure_id = self.h.get_measure_or_dim_id(self.multisphere_data, "facts", measure_name)
                        if self.jupiter:
                            if "ERROR" in str(measure_id):
                                return measure_id

                        output += " fact(%s; %s) " % (measure_id, top_dim_dict[elem])
                        continue
                    measure_name = i[1:-1]
                    measure_id = self.h.get_measure_or_dim_id(self.multisphere_data, "facts", measure_name)
                    if self.jupiter:
                        if "ERROR" in str(measure_id):
                            return measure_id
                    output += " fact(%s) " % measure_id
                    continue
                elif i in OPERANDS:
                    output += i
                    continue
                elif i.isnumeric():
                    output += i
                    continue
                else:
                    raise ValueError("Unknown element in formula: %s " % i)

            if opening_brackets != closing_brackets:
                raise ValueError("Неправильный баланс скобочек в формуле!\nОткрывающих скобочек: %s \n"
                                 "Закрывающих скобочек: %s" % (opening_brackets, closing_brackets))
        except BaseException as e:
            logger.exception("EXCEPTION!!! %s\n%s", e, traceback.format_exc())
            logger.exception("APPLICATION STOPPED!!!")
            self.current_exception = str(e)
            if self.jupiter:
                return self.current_exception
            raise

        result = self.execute_olap_command(command_name="fact",
                                           state="create_calc",
                                           name=new_name,
                                           formula=output,
                                           uformula=formula)

        return result

    @timing
    def run_scenario(self, scenario_id: Union[str, None] = None, scenario_name: Union[str, None] = None,
                     timeout: int = 60) -> [Dict, str, bool]:
        """
        Запустить сценарий и дождаться его загрузки. В параметрах нужно указать id сценария или имя сценария
        :param scenario_id: uuid сценария
        :param scenario_name: название сценария
        :param timeout: (int) таймаут
        :return: (Dict) результат выполнения команды модуля Manager: command_name="user_iface", state="save_settings",
                 module_id, settings
        """
        # проверки
        try:
            error_handler.checks(self, self.func_name, scenario_id, scenario_name)
        except BaseException as e:
            logger.exception(e)
            logger.exception("APPLICATION STOPPED")
            self.current_exception = str(e)
            if self.jupiter:
                return self.current_exception
            raise

        # Получить список слоев сессии
        result = self.execute_manager_command(command_name="user_layer", state="get_session_layers")
        layers = set()

        session_layers_lst = self.h.parse_result(result=result, key="layers")
        if self.jupiter:
            if "ERROR" in str(session_layers_lst):
                return session_layers_lst

        for i in session_layers_lst:
            layers.add(i["uuid"])

        # Получить данные по всем сценариям
        script_data = self.execute_manager_command(command_name="script", state="list")

        request_queries = script_data.get("queries")
        request_queries = next(iter(request_queries))
        script_desc = request_queries.get("command", {}).get("script_descs")

        if (scenario_name is not None) and (scenario_id is not None):
            script_id = self.h.get_scenario_data(script_data, scenario_name)
            if self.jupiter:
                if "ERROR" in script_id:
                    return script_id
            if script_id != scenario_id:
                # raise ValueError("ID или имя сценария некорректно!")
                message = "ERROR!!! ID или имя сценария некорректно!"
                logger.error(message)
                logger.error("APPLICATION STOPPED!!!")
                self.current_exception = message
                if self.jupiter:
                    return self.current_exception
                raise
            # Запустить сценарий
            self.execute_manager_command(command_name="script", state="run", script_id=scenario_id)

        # если пользователь ввел имя сценария:
        elif (scenario_name is not None) and (scenario_id is None):
            # Получить id сценария
            script_id = self.h.get_scenario_data(script_data, scenario_name)
            if self.jupiter:
                if "ERROR" in script_id:
                    return script_id

            # Запустить сценарий
            self.execute_manager_command(command_name="script", state="run", script_id=script_id)

        # если пользователь ввел ID сценария:
        elif (scenario_id is not None) and (scenario_name is None):
            # проверка корректности ID сценария
            uuids = []
            for script in script_desc:
                uuids.append(script["uuid"])
            if scenario_id not in uuids:
                # raise ValueError("No such scenario!")
                message = "ERROR!!! No such scenario!"
                logger.error(message)
                logger.error("APPLICATION STOPPED!!!")
                self.current_exception = message
                if self.jupiter:
                    return self.current_exception
                raise

            # Запустить сценарий
            self.execute_manager_command(command_name="script", state="run", script_id=scenario_id)

        # Сценарий должен создать новый слой и запуститься на нем
        # Получить список слоев сессии
        result = self.execute_manager_command(command_name="user_layer", state="get_session_layers")

        # Получить новый список слоев сессии
        new_layers = set()

        session_layers_lst = self.h.parse_result(result=result, key="layers")
        if self.jupiter:
            if "ERROR" in session_layers_lst:
                return session_layers_lst

        for i in session_layers_lst:
            new_layers.add(i["uuid"])
        self.layers_list = list(new_layers)

        # получить id слоя, на котором запущен наш сценарий
        target_layer = new_layers - layers
        sc_layer = next(iter(target_layer))

        # ожидание загрузки сценария на слое
        output = self.h.wait_scenario_layer_loaded(sc_layer, timeout)
        if self.jupiter:
            if "EXCEPTION" in str(output):
                return output

        # параметр settings, для запроса, который делает слой активным
        settings = {"Profile": {
            "geometry": {"height": None, "width": 300, "x": 540.3125,
                         "y": "center", "z": 780}}, "cubes": {
            "geometry": {"height": 450, "width": 700, "x": "center",
                         "y": "center", "z": 813}}, "users": {
            "geometry": {"height": 450, "width": 700, "x": "center",
                         "y": "center", "z": 788}},
            "wm_layers2": {"lids": list(new_layers),
                           "active": sc_layer}}

        session_layers = self.execute_manager_command(command_name="user_layer", state="get_session_layers")

        user_layer_progress = self.h.parse_result(result=session_layers, key="layers")
        if self.jupiter:
            if "ERROR" in str(user_layer_progress):
                return user_layer_progress

        # проверка, что слой не в статусе Running
        # список module_descs должен заполнится, только если слой находится в статусе Stopped
        for _ in count(0):
            start = time.time()
            for i in user_layer_progress:
                if (i["uuid"] == sc_layer) and (i["script_run_status"]["message"] == "Running"):
                    session_layers = self.execute_manager_command(command_name="user_layer", state="get_session_layers")
                    user_layer_progress = self.h.parse_result(result=session_layers, key="layers")
                    if self.jupiter:
                        if "ERROR" in str(user_layer_progress):
                            return user_layer_progress
                    time.sleep(5)
                end = time.time()
                exec_time = end - start
                if exec_time > 60.0:
                    logger.error("ERROR!!! Waiting script_run_status is too long! Layer info: %s", i)
                    logger.error("APPLICATION STOPPED!!!")
                    self.current_exception = "ERROR!!! Waiting script_run_status is too long! Layer info: %s" % i
                    if self.jupiter:
                        return self.current_exception
                    raise
            break

        # обновить get_session_layers
        result = self.execute_manager_command(command_name="user_layer", state="get_session_layers")

        user_layers = self.h.parse_result(result=result, key="layers")
        if self.jupiter:
            if "ERROR" in str(user_layers):
                return user_layers

        for i in user_layers:
            if i["uuid"] == sc_layer:
                # для случаев, когда "module_descs" - пустой список (пустой сценарий) - вернуть False
                if not i["module_descs"]:
                    return False
                try:
                    self.multisphere_module_id = i["module_descs"][0]["uuid"]
                except IndexError:
                    logger.exception("No module_descs for layer id %s\nlayer data: %s", sc_layer, i)
                    logger.exception("APPLICATION STOPPED!!!")
                    self.current_exception = "No module_descs for layer id %s\nlayer data: %s" % (sc_layer, i)
                    if self.jupiter:
                        return self.current_exception
                    raise

                self.active_layer_id = i["uuid"]
                # инициализация модуля Olap (на случай, если нужно будет выполнять команды для работы с мультисферой)
                self.olap_command = olap_commands.OlapCommands(self.session_id, self.multisphere_module_id,
                                                               self.url, self.server_codes, self.jupiter)

                # Выбрать слой с запущенным скриптом
                self.execute_manager_command(command_name="user_layer", state="set_active_layer", layer_id=i["uuid"])

                self.execute_manager_command(command_name="user_layer", state="init_layer", layer_id=i["uuid"])

                result = self.execute_manager_command(command_name="user_iface", state="save_settings",
                                                      module_id=self.authorization_uuid, settings=settings)

                self.update_total_row()

                return result

    @timing
    def run_scenario_by_id(self, sc_id) -> Dict:
        """
        Запустить сценарий по id
        :param sc_id: id сценария
        :return: (Dict) command_name="script", state="run"
        """
        return self.execute_manager_command(command_name="script", state="run", script_id=sc_id)

    @timing
    def run_scenario_by_user(self, scenario_name: str, user_name: str, units: int = 500, timeout: int = 60) -> [Dict,
                                                                                                                str,
                                                                                                                bool]:
        """
        Запуск сценария от имени заданного пользователя.
        Внутри данного метода будет создана новая сессия (указанного пользователя). После выполнения сценария сессия будет убита.
        В параметрах нужно указать название сценария и имя используемого пользователя.
        :param scenario_name: название сценария
        :param user_name: имя пользователя, под которым запускается сценарий
        :param units: число строк мультисферы, из которых потом данные будут выгружены в данные мультисферы (df), данные о колонках (df_cols)
        :param timeout: (int) таймаут
        :return: (Tuple)  данные мультисферы df, данные о колонках мультсферы df_cols
        """
        sc = BusinessLogic(login=user_name, url=self.url)

        # Получить список слоев сессии
        result = sc.execute_manager_command(command_name="user_layer", state="get_session_layers")
        layers = set()

        session_layers_lst = sc.h.parse_result(result=result, key="layers")
        if self.jupiter:
            if "ERROR" in str(session_layers_lst):
                return session_layers_lst

        for i in session_layers_lst:
            layers.add(i["uuid"])

        # Получить данные по всем сценариям
        script_data = sc.execute_manager_command(command_name="script", state="list")

        # Получить id сценария
        script_id = sc.h.get_scenario_data(script_data, scenario_name)
        if self.jupiter:
            if "ERROR" in script_id:
                return script_id

        # Запустить сценарий
        sc.execute_manager_command(command_name="script", state="run", script_id=script_id)

        # Сценарий должен создать новый слой и запуститься на нем
        # Получить список слоев сессии
        result = sc.execute_manager_command(command_name="user_layer", state="get_session_layers")

        # Получить новый список слоев сессии
        new_layers = set()

        session_layers_lst = sc.h.parse_result(result=result, key="layers")
        if self.jupiter:
            if "ERROR" in session_layers_lst:
                return session_layers_lst

        for i in session_layers_lst:
            new_layers.add(i["uuid"])
        sc.layers_list = list(new_layers)

        # получить id слоя, на котором запущен наш сценарий
        target_layer = new_layers - layers
        sc_layer = next(iter(target_layer))

        # ожидание загрузки сценария на слое
        output = sc.h.wait_scenario_layer_loaded(sc_layer, timeout)
        if self.jupiter:
            if "EXCEPTION" in str(output):
                return output

        # параметр settings, для запроса, который делает слой активным
        settings = {"Profile": {
            "geometry": {"height": None, "width": 300, "x": 540.3125,
                         "y": "center", "z": 780}}, "cubes": {
            "geometry": {"height": 450, "width": 700, "x": "center",
                         "y": "center", "z": 813}}, "users": {
            "geometry": {"height": 450, "width": 700, "x": "center",
                         "y": "center", "z": 788}},
            "wm_layers2": {"lids": list(new_layers),
                           "active": sc_layer}}

        session_layers = sc.execute_manager_command(command_name="user_layer", state="get_session_layers")

        user_layer_progress = sc.h.parse_result(result=session_layers, key="layers")
        if self.jupiter:
            if "ERROR" in str(user_layer_progress):
                return user_layer_progress

        # проверка, что слой не в статусе Running
        # список module_descs должен заполнится, только если слой находится в статусе Stopped
        for _ in count(0):
            start = time.time()
            for i in user_layer_progress:
                if (i["uuid"] == sc_layer) and (i["script_run_status"]["message"] == "Running"):
                    session_layers = sc.execute_manager_command(command_name="user_layer", state="get_session_layers")
                    user_layer_progress = sc.h.parse_result(result=session_layers, key="layers")
                    if self.jupiter:
                        if "ERROR" in str(user_layer_progress):
                            return user_layer_progress
                    time.sleep(5)
                end = time.time()
                exec_time = end - start
                if exec_time > 60.0:
                    logger.error("ERROR!!! Waiting script_run_status is too long! Layer info: %s", i)
                    logger.error("APPLICATION STOPPED!!!")
                    self.current_exception = "ERROR!!! Waiting script_run_status is too long! Layer info: %s" % i
                    if self.jupiter:
                        return self.current_exception
                    raise
            break

        # обновить get_session_layers
        result = sc.execute_manager_command(command_name="user_layer", state="get_session_layers")

        user_layers = sc.h.parse_result(result=result, key="layers")
        if self.jupiter:
            if "ERROR" in str(user_layers):
                return user_layers

        for i in user_layers:
            if i["uuid"] == sc_layer:
                # для случаев, когда "module_descs" - пустой список (пустой сценарий) - вернуть False
                if not i["module_descs"]:
                    return False
                try:
                    sc.multisphere_module_id = i["module_descs"][0]["uuid"]
                except IndexError:
                    logger.exception("No module_descs for layer id %s\nlayer data: %s", sc_layer, i)
                    logger.exception("APPLICATION STOPPED!!!")
                    self.current_exception = "No module_descs for layer id %s\nlayer data: %s" % (sc_layer, i)
                    if self.jupiter:
                        return self.current_exception
                    raise

                sc.active_layer_id = i["uuid"]
                # инициализация модуля Olap (на случай, если нужно будет выполнять команды для работы с мультисферой)
                sc.olap_command = olap_commands.OlapCommands(sc.session_id, sc.multisphere_module_id,
                                                             sc.url, sc.server_codes, sc.jupiter)

                # Выбрать слой с запущенным скриптом
                sc.execute_manager_command(command_name="user_layer", state="set_active_layer", layer_id=i["uuid"])

                sc.execute_manager_command(command_name="user_layer", state="init_layer", layer_id=i["uuid"])

                sc.execute_manager_command(command_name="user_iface", state="save_settings",
                                           module_id=self.authorization_uuid, settings=settings)

                sc.update_total_row()
                gen = sc.get_data_frame(units=units)
                self.df, self.df_cols = next(gen)
                sc.logout()

                return self.df, self.df_cols

    @timing
    def get_data_frame(self, units: int = 100):
        """
        Подгрузка мультисферы постранично, порциями строк.
        Приверы использования:
        I.
        gen = sc.get_data_frame()
        df, df_cols = next(gen)
        print(df)
        print(df_cols)

        II.
        gen = sc.get_data_frame()
        for df, df_cols in gen:
            print("----")
            print(df)
            print(df_cols)

        :param units: количество подгружаемых строк, будет использоваться в num_row
        :return:
        """

        result = self.execute_olap_command(command_name="view", state="get_2", from_row=0, from_col=0,
                                           num_row=1,
                                           num_col=1)
        total_cols = self.h.parse_result(result, "total_col")

        start = 0
        while self.total_row > 0:
            self.total_row = self.total_row - units
            result = self.execute_olap_command(
                command_name="view",
                state="get_2",
                from_row=start,
                from_col=0,
                num_row=units + 1,
                num_col=total_cols
            )
            data = self.h.parse_result(result=result, key="data")
            df = pd.DataFrame(data[1:], columns=data[0])  # данные мультисферы
            df_cols = pd.DataFrame(data[0])  # названия колонок
            yield df, df_cols
            start += units
        return

    @timing
    def set_measure_level(self, measure_name: str, level: int) -> [Dict, str]:
        """
        Установить Уровень расчета факта
        :param measure_name: (str) имя факта
        :param level: (int) выставляет Уровень расчета
        :return: (Dict) результат выполнения команды: fact, state: set_level
        """
        # получить словать с размаерностями, фактами и данными
        self.get_multisphere_data()

        # получить id факта
        measure_id = self.h.get_measure_or_dim_id(self.multisphere_data, "facts", measure_name)
        if self.jupiter:
            if "ERROR" in measure_id:
                return measure_id

        # выполнить команду: fact, state: set_level
        command1 = self.olap_command.collect_command(module="olap",
                                                     command_name="fact",
                                                     state="set_level",
                                                     fact=measure_id,
                                                     level=level)

        command2 = self.olap_command.collect_command("olap", "fact", "list_rq")
        if self.jupiter:
            if "EXCEPTION" in str(command1):
                return command1
            if "EXCEPTION" in str(command2):
                return command2
        query = self.olap_command.collect_request(command1, command2)

        try:
            result = self.exec_request.execute_request(query)
        except BaseException as e:
            logger.exception(e)
            logger.exception("APPLICATION STOPPED")
            self.current_exception = str(e)
            if self.jupiter:
                return self.current_exception
            raise

        return result

    @timing
    def set_measure_precision(self, measure_names: List, precision: List) -> [Dict, str]:
        """
        Установить Уровень расчета факта
        :param measure_names: (List) список с именами фактов
        :param precision: (List) список с точностями фактов
                                (значения должны соответствовать значениям списка measure_names)
        :return: (Dict) результат выполнения команды: user_iface, state: save_settings
        """
        # проверки
        try:
            error_handler.checks(self, self.func_name, measure_names, precision)
        except BaseException as e:
            logger.exception(e)
            logger.exception("APPLICATION STOPPED")
            self.current_exception = str(e)
            if self.jupiter:
                return self.current_exception
            raise

        # получить словать с размаерностями, фактами и данными
        self.get_multisphere_data()

        # получить id фактов
        measure_ids = []
        for measure_name in measure_names:
            measure_id = self.h.get_measure_or_dim_id(self.multisphere_data, "facts", measure_name)
            if self.jupiter:
                if "ERROR" in measure_id:
                    return measure_id
            measure_ids.append(measure_id)

        # settings with precision for fact id
        settings = {"factsPrecision": {}}
        for idx, f_id in enumerate(measure_ids):
            settings["factsPrecision"].update({f_id: str(precision[idx])})

        # выполнить команду: user_iface, state: save_settings
        return self.execute_manager_command(command_name="user_iface",
                                            state="save_settings",
                                            module_id=self.multisphere_module_id,
                                            settings=settings)

    @timing
    def clone_current_olap_module(self, sid: str = None) -> Union[str, str]:
        """
        [ID-2994] Создать копию текущего OLAP-модуля.
        :param sid: 16-ричный идентификатор сессии; в случае, если он отсутствует, берётся текущее значение.
        :return: (str) uuid нового модуля
        :return: (str) название нового модуля
        :call_example:
            1. Инициализируем класс: bl_test = sc.BusinessLogic(login=<login>, password=<password>, url=<url>)
            2. Открываем произвольный куб: bl_test.get_cube(<cube_name>).
                Этот шаг обязательно нужен, т.к. без открытого OLAP-модуля копировать будет нечего.
            3. Вызов метода без передачи sid:
                new_module_uuid, new_module_name = bl_test.clone_current_olap_module()
            4. Вызов метода с передачей валидного sid:
                sid = <valid_sid>
                new_module_uuid, new_module_name = bl_test.clone_current_olap_module(sid=sid)
            5. Вызов метода с передачей невалидного sid:
                sid = <invalid_sid>
                new_module_uuid, new_module_name = bl_test.clone_current_olap_module(sid=sid)
                output: exception "Session does not exist".
        """
        if sid:
            session_bl = self._get_session_bl(sid)
            return session_bl.clone_current_olap_module()

        # если нет открытой мультисферы - бросаем ошибку
        if not self.multisphere_module_id:
            self.current_exception = 'No active OLAP-modules! To perform a clone operation, you must open the cube!'
            logger.exception(self.current_exception)
            logger.exception("APPLICATION STOPPED")
            if self.jupiter:
                return self.current_exception
            raise ValueError(self.current_exception)

        # клонирование модуля
        result = self.execute_manager_command(command_name="user_iface",
                                              state="clone_module",
                                              module_id=self.multisphere_module_id,
                                              layer_id=self.active_layer_id)

        # переключиться на module id созданной копии OLAP-модуля
        self.multisphere_module_id = self.h.parse_result(result=result, key="module_desc", nested_key="uuid")

        if self.jupiter:
            if "ERROR" in self.multisphere_module_id:
                return self.multisphere_module_id

        self.update_total_row()

        # возвращаем идентификатор нового модуля и его название (название совпадает с исходным OLAP-модулем)
        return self.multisphere_module_id, self.cube_name

    @timing
    def set_measure_visibility(self, measure_names: Union[str, List], is_visible: bool = False) -> [List, str]:
        """
        Изменение видимости факта (скрыть / показать факт).
        Можно изменять видимость одного факта или списка фактов.
        :param measure_names: (str, List) название факта/фактов
        :param is_visible: (bool) скрыть (False) / показать (True) факт. По умолчанию факт скрывается.
        :return: (List) список id фактов с изменной видимостью
        """
        # проверки
        try:
            error_handler.checks(self, self.func_name, is_visible)
        except BaseException as e:
            logger.exception(e)
            logger.exception("APPLICATION STOPPED")
            self.current_exception = str(e)
            if self.jupiter:
                return self.current_exception
            raise

        # список фактов с измененной видимостью
        m_ids = []

        # если передан один факт (строка)
        if isinstance(measure_names, str):
            m_id = self.get_measure_id(measure_name=measure_names)

            self.execute_olap_command(command_name="fact", state="set_visible", fact=m_id, is_visible=is_visible)
            m_ids.append(m_id)
            return m_ids

        # если передан список фактов
        for measure in measure_names:
            m_id = self.get_measure_id(measure_name=measure)
            if not m_id:
                logger.error("No such measure name: %s", measure)
                continue
            self.execute_olap_command(command_name="fact", state="set_visible", fact=m_id, is_visible=is_visible)
            m_ids.append(m_id)
        return m_ids

    @timing
    def sort_measure(self, measure_name: str, sort_type: str) -> [Dict, str]:
        """
        Сортировать значения факта по возрастанию или по убыванию
        :param measure_name: (str) Имя факта
        :param sort_type: (int) "ascending"/"descending" (по возрастанию / по убыванию)
        :return: (Dict) command_name="view", state="set_sort"
        """
        # проверки
        try:
            error_handler.checks(self, self.func_name, sort_type)
        except BaseException as e:
            logger.exception(e)
            logger.exception("APPLICATION STOPPED")
            self.current_exception = str(e)
            if self.jupiter:
                return self.current_exception
            raise

        sort_values = {"ascending": 1, "descending": 2}
        sort_type = sort_values[sort_type]

        # получить данные нескрытых фактов (те, которые вынесены в колонки)
        result = self.execute_olap_command(command_name="view", state="get", from_row=0, from_col=0,
                                           num_row=20, num_col=20)
        measures_data = self.h.parse_result(result=result, key="top")
        if self.jupiter:
            if "ERROR" in str(measures_data):
                return measures_data
        measures_list = []
        for i in measures_data:
            for elem in i:
                if "fact_id" in elem:
                    measure_id = elem["fact_id"].rstrip()
                    measures_list.append(self.get_measure_name(measure_id))

        # индекс нужного факта
        measure_index = measures_list.index(measure_name)

        return self.execute_olap_command(command_name="view", state="set_sort", line=measure_index, sort_type=sort_type)

    @timing
    def unfold_all_dims(self, position: str, level: int, num_row: int = 100, num_col: int = 100) -> [Dict, str]:
        """
        Развернуть все элементы размерности
        :param position: (str) "left" / "up"  (левые / верхние размерности )
        :param level: (int) 0, 1, 2, ... (считается слева-направо для левой позиции,
                            сверху - вниз для верхней размерности)
        :param num_row: (int) Количество строк, которые будут отображаться в мультисфере
        :param num_col: (int) Количество колонок, которые будут отображаться в мультисфере
        :return: (Dict) after request view get_hints
        """
        # проверки
        try:
            position = error_handler.checks(self, self.func_name, position)
        except BaseException as e:
            logger.exception(e)
            logger.exception("APPLICATION STOPPED")
            self.current_exception = str(e)
            if self.jupiter:
                return self.current_exception
            raise

        # view   fold_all_at_level
        arraysDict = []
        for i in range(0, level + 1):
            arraysDict.append(self.olap_command.collect_command(module="olap", command_name="view",
                                                                state="fold_all_at_level", position=position, level=i))
        query = self.olap_command.collect_request(*arraysDict)
        try:
            self.exec_request.execute_request(query)
        except BaseException as e:
            logger.exception(e)
            logger.exception("APPLICATION STOPPED")
            self.current_exception = str(e)
            if self.jupiter:
                return self.current_exception
            raise

        # view  get
        self.execute_olap_command(command_name="view", state="get", from_row=0, from_col=0,
                                  num_row=num_row, num_col=num_col)

        # view  get_hints
        command1 = self.olap_command.collect_command(module="olap",
                                                     command_name="view",
                                                     state="get_hints",
                                                     position=1,
                                                     hints_num=100)
        command2 = self.olap_command.collect_command(module="olap",
                                                     command_name="view",
                                                     state="get_hints",
                                                     position=2,
                                                     hints_num=100)
        if self.jupiter:
            if "EXCEPTION" in str(command1):
                return command1
            if "EXCEPTION" in str(command2):
                return command2
        query = self.olap_command.collect_request(command1, command2)
        try:
            result = self.exec_request.execute_request(query)
        except BaseException as e:
            logger.exception(e)
            logger.exception("APPLICATION STOPPED")
            self.current_exception = str(e)
            if self.jupiter:
                return self.current_exception
            raise

        self.update_total_row()

        return result

    @timing
    def move_measures(self, new_order: List) -> [str, Any]:
        """
        Функция, упорядочивающая факты в заданной последовательности

        Пример: self.move_measures(new_order=["факт1", "факт2", "факт3", "факт4"])

        :param new_order: (List) список упорядоченных фактов
        :return: (str) сообщение об ошибке или об успехе
        """
        c = 0
        for idx, new_elem in enumerate(new_order):
            # get ordered measures list
            result = self.execute_olap_command(command_name="fact", state="list_rq")
            measures_data = self.h.parse_result(result=result, key="facts")
            if self.jupiter:
                if "ERROR" in str(measures_data):
                    return measures_data
            measures_list = [i["name"].rstrip() for i in measures_data]  # measures list in polymatica

            # check if measures are already ordered
            if (measures_list == new_order) and (c == 0):
                logger.warning("WARNING!!! Facts are already ordered!")
                return

            measure_index = measures_list.index(new_elem)
            # если индекс элемента совпал, то перейти к следующей итерации
            if measures_list.index(new_elem) == idx:
                continue

            # id факта
            measure_id = self.get_measure_id(new_elem)

            # offset
            measure_index -= c

            self.execute_olap_command(command_name="fact", state="move", fact=measure_id, offset=-measure_index)
            c += 1
        self.update_total_row()
        return "Fact successfully ordered!"

    @timing
    def set_width_columns(self, measures: List, left_dims: List, width: int = 890, height: int = 540) -> [Dict, str]:
        """
        Установить ширину колонок
        :param measures: [List] спиок с новыми значениями ширины фактов.
            ВАЖНО! Длина списка должна совпадать с количеством нескрытых фактов в мультисфере
            пример списка: [300, 300, 300, 233, 154]
        :param left_dims: [List] спиок с новыми значениями ширины рзамерностей, вынесенных в левую размерность.
        :param width: (int) ширина таблицы
        :param height: (int) высота таблицы
        :return: user_iface save_settings
        """
        # получить список нескрытых фактов
        result = self.execute_olap_command(command_name="view", state="get", from_row=0, from_col=0,
                                           num_row=20, num_col=20)
        measures_data = self.h.parse_result(result=result, key="top")
        if self.jupiter:
            if "ERROR" in str(measures_data):
                return measures_data
        measures_list = []
        for i in measures_data:
            for elem in i:
                if "fact_id" in elem:
                    measure_id = elem["fact_id"].rstrip()
                    measures_list.append(self.get_measure_name(measure_id))

        # проверки
        try:
            error_handler.checks(self, self.func_name, measures, measures_list)
        except BaseException as e:
            logger.exception(e)
            logger.exception("APPLICATION STOPPED")
            self.current_exception = str(e)
            if self.jupiter:
                return self.current_exception
            raise

        settings = {"dimAndFactShow": True,
                    "itemWidth": measures,
                    "geometry": {"width": width, "height": height},
                    "workWidths": left_dims}

        return self.execute_manager_command(command_name="user_iface", state="save_settings",
                                            module_id=self.multisphere_module_id, settings=settings)

    @timing
    def load_profile(self, name: str) -> [Dict, str]:
        """
        Загрузить профиль по его названию
        :param name: (str) название нужного профиля
        :return: (Dict) user_iface, save_settings
        """
        # Получить множество слоев сессии
        result = self.execute_manager_command(command_name="user_layer", state="get_session_layers")
        layers = set()

        session_layers = self.h.parse_result(result=result, key="layers")
        if self.jupiter:
            if "ERROR" in str(session_layers):
                return session_layers

        for i in session_layers:
            layers.add(i["uuid"])

        # Получить сохраненные профили
        result = self.execute_manager_command(command_name="user_layer", state="get_saved_layers")

        # Получить uuid профиля по его интерфейсному названию
        layers_descriptions = self.h.parse_result(result=result, key="layers_descriptions")
        if self.jupiter:
            if "ERROR" in str(layers_descriptions):
                return layers_descriptions

        self.active_layer_id = ""
        for i in layers_descriptions:
            if i["name"] == name:
                self.active_layer_id = i["uuid"]
        if self.active_layer_id == "":
            logger.error("No such profile: %s", name)
            logger.error("APPLICATION STOPPED!!!")
            self.current_exception = "No such profile: %s" % name
            if self.jupiter:
                return self.current_exception
            raise

        # Загрузить сохраненный профиль
        self.execute_manager_command(command_name="user_layer", state="load_saved_layer", layer_id=self.active_layer_id)

        # Получить новое множество слоев сессии
        result = self.execute_manager_command(command_name="user_layer", state="get_session_layers")

        new_layers = set()
        session_layers = self.h.parse_result(result=result, key="layers")
        if self.jupiter:
            if "ERROR" in str(session_layers):
                return session_layers
        for i in session_layers:
            new_layers.add(i["uuid"])

        # получить id слоя, на котором запущен загруженный сценарий
        target_layer = new_layers - layers
        sc_layer = next(iter(target_layer))

        # параметр settings, для запроса, который делает слой активным
        settings = {"Profile": {
            "geometry": {"height": None, "width": 300, "x": 540.3125,
                         "y": "center", "z": 780}}, "cubes": {
            "geometry": {"height": 450, "width": 700, "x": "center",
                         "y": "center", "z": 813}}, "users": {
            "geometry": {"height": 450, "width": 700, "x": "center",
                         "y": "center", "z": 788}},
            "wm_layers2": {"lids": list(new_layers),
                           "active": sc_layer}}
        for i in session_layers:
            if i["uuid"] == sc_layer:
                try:
                    self.multisphere_module_id = i["module_descs"][0]["uuid"]
                except IndexError:
                    logger.error("ERROR!!! No module_descs for layer id %s\nlayer data: %s", sc_layer, i)
                    logger.error("APPLICATION STOPPED!!!")
                    self.current_exception = "No module_descs for layer id %s\nlayer data: %s" % (sc_layer, i)
                    if self.jupiter:
                        return self.current_exception
                    raise

                self.active_layer_id = i["uuid"]
                # инициализация модуля Olap (на случай, если нужно будет выполнять команды для работы с мультисферой)
                self.olap_command = olap_commands.OlapCommands(self.session_id, self.multisphere_module_id,
                                                               self.url, self.server_codes, self.jupiter)

                # Выбрать слой с запущенным скриптом
                self.execute_manager_command(command_name="user_layer", state="set_active_layer", layer_id=i["uuid"])

                self.execute_manager_command(command_name="user_layer", state="init_layer", layer_id=i["uuid"])

                # ожидание загрузки слоя
                result = self.execute_manager_command(command_name="user_layer", state="get_load_progress",
                                                      layer_id=i["uuid"])
                progress = self.h.parse_result(result, "progress")
                while progress < 100:
                    time.sleep(0.5)
                    result = self.execute_manager_command(command_name="user_layer", state="get_load_progress",
                                                          layer_id=i["uuid"])
                    progress = self.h.parse_result(result, "progress")

                result = self.execute_manager_command(command_name="user_iface", state="save_settings",
                                                      module_id=self.authorization_uuid, settings=settings)
                self.update_total_row()

                return result

    @timing
    def create_sphere(self, cube_name: str, source_name: str, file_type: str, update_params: Dict,
                      sql_params: Dict = None, user_interval: str = "с текущего дня", filepath: str = "", separator="",
                      increment_dim=None, encoding: str = False, delayed: bool = False) -> [str, Dict]:
        """
        Создать мультисферу через импорт из источника
        :param cube_name: (str) название мультисферы, которую будем создавать
        :param filepath: (str) путь к файлу, либо (если файл лежит в той же директории) название файла.
            Не обязательно для бд
        :param separator: (str) разделитель для csv-источника. По умолчанию разделитель не выставлен
        :param increment_dim: (str) название размерности, необходимое для инкрементального обновления.
                            На уровне API параметр называется increment_field
        :param sql_params: (Dict) параметры для источника данных SQL.
            Параметры, которые нужно передать в словарь: server, login, passwd, sql_query
            Пример: {"server": "10.8.0.115",
                     "login": "your_user",
                     "passwd": "your_password",
                     "sql_query": "SELECT * FROM DIFF_data.dbo.TableForTest"}
        :param update_params: (Dict) параметры обновления мультисферы.
            Типы обновления:
              - "ручное"
              - "по расписанию"
              - "интервальное"
              - "инкрементальное" (доступно ТОЛЬКО для источника SQL!)
            Для всех типов обновления, кроме ручного, нужно обязательно добавить параметр schedule.
            Его значение - словарь.
               В параметре schedule параметр type:
               {"Ежедневно": 1,
                "Еженедельно": 2,
                "Ежемесячно": 3}
            В параметре schedule параметр time записывается в формате "18:30" (в запрос передается UNIX-time).
            В параметре schedule параметр time_zone записывается как в server-codes: "UTC+3:00"
            В параметре schedule параметр week_day записывается как в списке:
               - "понедельник"
               - "вторник"
               - "среда"
               - "четверг"
               - "пятница"
               - "суббота"
               - "воскресенье"
            Пример: {"type": "по расписанию",
                     "schedule": {"type": "Ежедневно", "time": "18:30", "time_zone": "UTC+3:00"}}
        :param user_interval: (str) интервал обновлений. Указать значение:
               {"с текущего дня": 0,
                "с предыдущего дня": 1,
                "с текущей недели": 2,
                "с предыдущей недели
                "с и по указанную дату": 11}": 3,
                "с текущего месяца": 4,
                "с предыдущего месяца": 5,
                "с текущего квартала": 6,
                "с предыдущего квартала": 7,
                "с текущего года": 8,
                "с предыдущего года": 9,
                "с указанной даты": 10,
                "с и по указанную дату": 11}
        :param source_name: (str) поле Имя источника. Не должно быть пробелов, и длина должна быть больше 5 символов!
        :param file_type: (str) формат файла. См. значения в server-codes.json
        :param encoding: (str) кодировка, например, UTF-8 (обязательно для csv!)
        :param delayed: (bool) отметить чекбокс "Создать мультисферу при первом обновлении."
        :return: (Dict) command_name="user_cube", state="save_ext_info_several_sources_request"
        """

        encoded_file_name = ""  # response.headers["File-Name"] will be stored here after PUT upload of csv/excel

        interval = {"с текущего дня": 0,
                    "с предыдущего дня": 1,
                    "с текущей недели": 2,
                    "с предыдущей недели": 3,
                    "с текущего месяца": 4,
                    "с предыдущего месяца": 5,
                    "с текущего квартала": 6,
                    "с предыдущего квартала": 7,
                    "с текущего года": 8,
                    "с предыдущего года": 9,
                    "с указанной даты": 10,
                    "с и по указанную дату": 11}

        # часовые зоны
        time_zones = self.server_codes["manager"]["timezone"]
        # проверки
        try:
            error_handler.checks(self, self.func_name, update_params, UPDATES, file_type, sql_params,
                                 user_interval, interval, PERIOD, WEEK, time_zones, source_name, cube_name)
        except BaseException as e:
            logger.exception(e)
            logger.exception("APPLICATION STOPPED")
            self.current_exception = str(e)
            if self.jupiter:
                return self.current_exception
            raise

        interval = interval[user_interval]

        if update_params["type"] != "ручное":
            # установить значение периода для запроса
            user_period = update_params["schedule"]["type"]
            update_params["schedule"]["type"] = PERIOD[user_period]

            # установить значение часовой зоны для запроса
            h_timezone = update_params["schedule"]["time_zone"]
            update_params["schedule"]["time_zone"] = time_zones[h_timezone]

            # преобразование времение в UNIX time
            user_time = update_params["schedule"]["time"]
            h_m = user_time.split(":")
            d = datetime.datetime(1970, 1, 1, int(h_m[0]) + 3, int(h_m[1]), 0)
            unixtime = time.mktime(d.timetuple())
            unixtime = int(unixtime)
            update_params["schedule"]["time"] = unixtime

        # пармаметр server_types для различных форматов данных
        server_types = self.server_codes["manager"]["data_source_type"]
        server_type = server_types[file_type]

        # создать мультисферу, получить id куба
        result = self.execute_manager_command(command_name="user_cube", state="create_cube_request",
                                              cube_name=cube_name)
        self.cube_id = self.h.parse_result(result=result, key="cube_id")
        if self.jupiter:
            if "ERROR" in self.cube_id:
                return self.cube_id

        # upload csv file
        if (file_type == "excel") or (file_type == "csv"):
            try:
                response = self.exec_request.execute_request(params=filepath, method="PUT")
            except BaseException as e:
                logger.exception(e)
                logger.exception("APPLICATION STOPPED")
                self.current_exception = str(e)
                if self.jupiter:
                    return self.current_exception
                raise

            encoded_file_name = response.headers["File-Name"]

        # data preview request, выставить кодировку UTF-8
        preview_data = {"name": source_name,
                        "server": "",
                        "server_type": server_type,
                        "login": "",
                        "passwd": "",
                        "database": "",
                        "sql_query": separator,
                        "skip": -1}
        # для бд выставить параметры server, login, passwd:
        if (file_type != "csv") and (file_type != "excel"):
            preview_data.update({"server": sql_params["server"]})
            preview_data.update({"login": sql_params["login"]})
            preview_data.update({"passwd": sql_params["passwd"]})
            preview_data.update({"sql_query": ""})
            # для бд psql прописать параметр database=postgres
            if file_type == "psql":
                preview_data.update({"database": "postgres"})
            # соединиться с бд
            result = self.execute_manager_command(command_name="user_cube",
                                                  state="test_source_connection_request",
                                                  datasource=preview_data)

        # для формата данных csv выставить кодировку
        if file_type == "csv":
            preview_data.update({"encoding": encoding})
        # для файлов заполнить параметр server:
        if (file_type == "csv") or (file_type == "excel"):
            preview_data.update({"server": encoded_file_name})

        # для бд заполнить параметр sql_query
        if (file_type != "csv") and (file_type != "excel"):
            preview_data.update({"sql_query": sql_params["sql_query"]})
        # для бд psql прописать параметр database=postgres
        if file_type == "psql":
            preview_data.update({"database": "postgres"})

        self.execute_manager_command(command_name="user_cube",
                                     state="data_preview_request",
                                     datasource=preview_data)

        # для формата данных csv сделать связь данных
        if file_type == "csv":
            self.execute_manager_command(command_name="user_cube",
                                         state="structure_preview_request",
                                         cube_id=self.cube_id,
                                         links=[])

        # добавить источник данных
        preview_data = [{"name": source_name,
                         "server": "",
                         "server_type": server_type,
                         "login": "",
                         "passwd": "",
                         "database": "",
                         "sql_query": separator,
                         "skip": -1}]
        # для формата данных csv выставить кодировку
        if file_type == "csv":
            preview_data[0].update({"encoding": encoding})
        # для файлов заполнить параметр server:
        if (file_type == "csv") or (file_type == "excel"):
            preview_data[0].update({"server": encoded_file_name})
        # для бд
        if (file_type != "csv") and (file_type != "excel"):
            preview_data[0].update({"server": sql_params["server"]})
            preview_data[0].update({"login": sql_params["login"]})
            preview_data[0].update({"passwd": sql_params["passwd"]})
            preview_data[0].update({"sql_query": sql_params["sql_query"]})
        # для бд psql прописать параметр database=postgres
        if file_type == "psql":
            preview_data[0].update({"database": "postgres"})
        self.execute_manager_command(command_name="user_cube",
                                     state="get_fields_request",
                                     cube_id=self.cube_id,
                                     datasources=preview_data)

        # структура данных
        result = self.execute_manager_command(command_name="user_cube", state="structure_preview_request",
                                              cube_id=self.cube_id, links=[])

        # словари с данными о размерностях
        dims = self.h.parse_result(result=result, key="dims")
        if self.jupiter:
            if "ERROR" in str(dims):
                return dims
        # словари с данными о фактах
        measures = self.h.parse_result(result=result, key="facts")
        if self.jupiter:
            if "ERROR" in str(measures):
                return measures

        try:
            # циклично добавить для каждой размерности {"field_type": "field"}
            for i in dims:
                i.update({"field_type": "field"})
                if file_type == "csv":
                    error_handler.checks(self, self.func_name, i)
            # циклично добавить для каждого факта {"field_type": "field"}
            for i in measures:
                i.update({"field_type": "field"})
                if file_type == "csv":
                    error_handler.checks(self, self.func_name, i)
        except BaseException as e:
            logger.exception(e)
            logger.exception("APPLICATION STOPPED")
            self.current_exception = str(e)
            if self.jupiter:
                return self.current_exception
            raise

        # параметры для ручного обновления
        if update_params["type"] == "ручное":
            schedule = {"delayed": delayed, "items": []}
        elif update_params["type"] == "инкрементальное":
            # параметры для инкрементального обновления
            schedule = {"delayed": delayed, "items": [update_params["schedule"]]}
            interval = {"type": interval, "left_border": "", "right_border": "",
                        "dimension_id": "00000000"}
            # для сохранения id размерности инкремента
            increment_field = ""
            for dim in dims:
                if dim["name"] == increment_dim:
                    increment_field = dim["field_id"]
            if increment_dim is None:
                message = "Please fill in param increment_dim!"
                logger.error(message)
                logger.error("APPLICATION STOPPED!!!")
                self.current_exception = message
                if self.jupiter:
                    return self.current_exception
                raise
            if increment_field == "":
                logger.error("No such increment field in importing sphere: %s", increment_dim)
                logger.error("APPLICATION STOPPED!!!")
                self.current_exception = "No such increment field in importing sphere: %s" % increment_dim
                if self.jupiter:
                    return self.current_exception
                raise
            return self.execute_manager_command(command_name="user_cube", state="save_ext_info_several_sources_request",
                                                cube_id=self.cube_id, cube_name=cube_name, dims=dims, facts=measures,
                                                schedule=schedule, interval=interval, increment_field=increment_field)
        elif update_params["type"] == "по расписанию":
            # параметры для оставшихся видов обновлений
            schedule = {"delayed": delayed, "items": [update_params["schedule"]]}
        elif update_params["type"] == "интервальное":
            # параметры для оставшихся видов обновлений
            schedule = {"delayed": delayed, "items": [update_params["schedule"]]}
            interval = {"type": interval, "left_border": "", "right_border": "",
                        "dimension_id": None}
            return self.execute_manager_command(command_name="user_cube", state="save_ext_info_several_sources_request",
                                                cube_id=self.cube_id, cube_name=cube_name, dims=dims, facts=measures,
                                                schedule=schedule, interval=interval)
        else:
            logger.error("Unknown update type: %s", update_params["type"])
            logger.error("APPLICATION STOPPED!!!")
            self.current_exception = "Unknown update type: %s" % update_params["type"]
            if self.jupiter:
                return self.current_exception
            raise
        interval = {"type": interval, "left_border": "", "right_border": "",
                    "dimension_id": "00000000"}
        # финальный запрос для создания мультисферы, обновление мультисферы
        return self.execute_manager_command(command_name="user_cube", state="save_ext_info_several_sources_request",
                                            cube_id=self.cube_id, cube_name=cube_name, dims=dims, facts=measures,
                                            schedule=schedule, interval=interval)

    @timing
    def update_cube(self, cube_name: str, update_params: Dict, user_interval: str = "с текущего дня",
                    delayed: bool = False, increment_dim=None) -> [Dict, str]:
        """
        Обновить существующий куб
        :param cube_name: (str) название мультисферы
        :param update_params: (Dict) параметры обновления мультисферы.
           Типы обновления:
              - "ручное"
              - "по расписанию"
              - "интервальное"
              - "инкрементальное" (доступно ТОЛЬКО для источника SQL!)
           Для всех типов обновления, кроме ручного, нужно обязательно добавить параметр schedule.
           Его значение - словарь.
               В параметре schedule параметр type:
               {"Ежедневно": 1,
                "Еженедельно": 2,
                "Ежемесячно": 3}
           В параметре schedule параметр time записывается в формате "18:30" (в запрос передается UNIX-time).
           В параметре schedule параметр time_zone записывается как в server-codes: "UTC+3:00"
           В параметре schedule параметр week_day записывается как в списке:
               - "понедельник"
               - "вторник"
               - "среда"
               - "четверг"
               - "пятница"
               - "суббота"
               - "воскресенье"
        :param user_interval: (str) интервал обновлений. Указать значение:
               {"с текущего дня": 0,
                "с предыдущего дня": 1,
                "с текущей недели": 2,
                "с предыдущей недели
                "с и по указанную дату": 11}": 3,
                "с текущего месяца": 4,
                "с предыдущего месяца": 5,
                "с текущего квартала": 6,
                "с предыдущего квартала": 7,
                "с текущего года": 8,
                "с предыдущего года": 9,
                "с указанной даты": 10,
                "с и по указанную дату": 11}
        :param increment_dim: (str) increment_dim_id, параметр необходимый для инкрементального обновления
        :param delayed: (bool) отметить чекбокс "Создать мультисферу при первом обновлении."
        :return: (Dict)user_cube save_ext_info_several_sources_request
        """
        interval = {"с текущего дня": 0,
                    "с предыдущего дня": 1,
                    "с текущей недели": 2,
                    "с предыдущей недели": 3,
                    "с текущего месяца": 4,
                    "с предыдущего месяца": 5,
                    "с текущего квартала": 6,
                    "с предыдущего квартала": 7,
                    "с текущего года": 8,
                    "с предыдущего года": 9,
                    "с указанной даты": 10,
                    "с и по указанную дату": 11}

        # часовые зоны
        time_zones = self.server_codes["manager"]["timezone"]

        # get cube id
        self.cube_name = cube_name
        result = self.execute_manager_command(command_name="user_cube", state="list_request")

        # получение списка описаний мультисфер
        cubes_list = self.h.parse_result(result=result, key="cubes")
        if self.jupiter:
            if "ERROR" in str(cubes_list):
                return cubes_list

        try:
            self.cube_id = self.h.get_cube_id(cubes_list, cube_name)
        except ValueError as e:
            logger.exception("EXCEPTION!!! %s", e)
            logger.exception("APPLICATION STOPPED!!!")
            self.current_exception = str(e)
            if self.jupiter:
                return self.current_exception
            raise

        # получить информацию о фактах и размерностях куба
        result = self.execute_manager_command(command_name="user_cube", state="ext_info_several_sources_request",
                                              cube_id=self.cube_id)

        # словари с данными о размерностях
        dims = self.h.parse_result(result=result, key="dims")
        if self.jupiter:
            if "ERROR" in str(dims):
                return dims
        # словари с данными о фактах
        measures = self.h.parse_result(result=result, key="facts")
        if self.jupiter:
            if "ERROR" in str(measures):
                return measures

        # циклично добавить для каждой размерности {"field_type": "field"}
        for i in dims:
            i.update({"field_type": "field"})
            # циклично добавить для каждого факта {"field_type": "field"}
        for i in measures:
            i.update({"field_type": "field"})

        if user_interval not in interval:
            logger.error("ERROR!!! No such interval: %s", user_interval)
            logger.error("APPLICATION STOPPED!!!")
            self.current_exception = "ERROR!!! No such interval: %s" % user_interval
            if self.jupiter:
                return self.current_exception
            raise
        interval = interval[user_interval]

        if update_params["type"] != "ручное":
            # установить значение периода для запроса
            user_period = update_params["schedule"]["type"]
            update_params["schedule"]["type"] = PERIOD[user_period]

            # установить значение часовой зоны для запроса
            h_timezone = update_params["schedule"]["time_zone"]
            update_params["schedule"]["time_zone"] = time_zones[h_timezone]

            # преобразование времение в UNIX time
            user_time = update_params["schedule"]["time"]
            h_m = user_time.split(":")
            d = datetime.datetime(1970, 1, 1, int(h_m[0]) + 3, int(h_m[1]), 0)
            unixtime = time.mktime(d.timetuple())
            unixtime = int(unixtime)
            update_params["schedule"]["time"] = unixtime

        # параметры для ручного обновления
        if update_params["type"] == "ручное":
            schedule = {"delayed": delayed, "items": []}
        elif update_params["type"] == "инкрементальное":
            # параметры для инкрементального обновления
            schedule = {"delayed": delayed, "items": [update_params["schedule"]]}
            interval = {"type": interval, "left_border": "", "right_border": "",
                        "dimension_id": "00000000"}
            # для сохранения id размерности инкремента
            increment_field = ""
            for dim in dims:
                if dim["name"] == increment_dim:
                    increment_field = dim["field_id"]
            if increment_dim is None:
                message = "ERROR!!! Please fill in param increment_dim!"
                logger.error(message)
                logger.error("APPLICATION STOPPED!!!")
                self.current_exception = message
                if self.jupiter:
                    return self.current_exception
                raise
            if increment_field == "":
                logger.error("ERROR!!! No such increment field in importing sphere: %s", increment_dim)
                logger.error("APPLICATION STOPPED!!!")
                self.current_exception = "ERROR!!! No such increment field in importing sphere: %s" % increment_dim
                if self.jupiter:
                    return self.current_exception
                raise
            return self.execute_manager_command(command_name="user_cube", state="save_ext_info_several_sources_request",
                                                cube_id=self.cube_id, cube_name=cube_name, dims=dims, facts=measures,
                                                schedule=schedule, interval=interval, increment_field=increment_field)
        elif update_params["type"] == "по расписанию":
            # параметры для оставшихся видов обновлений
            schedule = {"delayed": delayed, "items": [update_params["schedule"]]}
        elif update_params["type"] == "интервальное":
            # параметры для оставшихся видов обновлений
            schedule = {"delayed": delayed, "items": [update_params["schedule"]]}
            interval = {"type": interval, "left_border": "", "right_border": "",
                        "dimension_id": None}
            return self.execute_manager_command(command_name="user_cube", state="save_ext_info_several_sources_request",
                                                cube_id=self.cube_id, cube_name=cube_name, dims=dims, facts=measures,
                                                schedule=schedule, interval=interval)
        else:
            logger.error("ERROR!!! Unknown update type: %s", update_params["type"])
            logger.error("APPLICATION STOPPED!!!")
            self.current_exception = "ERROR!!! Unknown update type: %s" % update_params["type"]
            if self.jupiter:
                return self.current_exception
            raise
        interval = {"type": interval, "left_border": "", "right_border": "",
                    "dimension_id": "00000000"}
        # финальный запрос для создания мультисферы, обновление мультисферы
        return self.execute_manager_command(command_name="user_cube", state="save_ext_info_several_sources_request",
                                            cube_id=self.cube_id, cube_name=cube_name, dims=dims, facts=measures,
                                            schedule=schedule, interval=interval)

    def wait_cube_loading(self, cube_name: str) -> str:
        """
        Ожидание загрузки мультисферы
        :param cube_name: (str) название мультисферы
        :return: информация из лога о создании мультисферы
        """
        # id куба
        self.cube_id = self.get_cube_without_creating_module(cube_name)

        # время старта загрузки мультисферы
        start = time.time()

        # Скачать лог мультисферы
        file_url = self.url + "/" + "resources/log?cube_id=" + self.cube_id
        # имя cookies: session (для скачивания файла)
        cookies = {'session': self.session_id}
        # выкачать файл GET-запросом
        r = requests.get(file_url, cookies=cookies)
        # override encoding by real educated guess as provided by chardet
        r.encoding = r.apparent_encoding
        # вывести лог мультисферы
        log_content = r.text

        logger.info("Содержание лога:\n")
        logger.info(log_content)
        logger.info("************************************************")
        while "Cube creation completed" not in log_content:
            logger.info("Cube '%s' is not created. Re-checking log file in 5 seconds...", cube_name)
            logger.info("Sphere loading time, sec: %s\n", int(time.time() - start))
            time.sleep(5)
            # выкачать файл GET-запросом
            r = requests.get(file_url, cookies=cookies)
            # override encoding by real educated guess as provided by chardet
            r.encoding = r.apparent_encoding
            # вывести лог мультисферы
            log_content = r.text
        # Сообщение об окончании загрузки файла
        output = log_content.split("\n")
        logger.info(output[-2])
        logger.info(output[-1])

        # Информация о времени создания сферы
        end = time.time()
        exec_time = end - start
        min = int(exec_time // 60)
        sec = int(exec_time % 60)
        logger.info("Время ожидания загрузки мультисферы: {} мин., {} сек".format(min, sec))

        return output

    @timing
    def group_dimensions(self, selected_dims: List) -> Dict:
        """
        Сгруппировать выбранные элементы самой левой размерности (работает, когда все размерности свернуты)
        :param selected_dims: (List) список выбранных значений
        :return: (Dict) view group
        """
        # подготовка данных
        result = self.execute_olap_command(command_name="view", state="get", from_row=0, from_col=0,
                                           num_row=500, num_col=100)
        top_dims = self.h.parse_result(result, "top_dims")
        top_dims_qty = len(top_dims)
        result = self.execute_olap_command(command_name="view", state="get_2", from_row=0, from_col=0,
                                           num_row=1000, num_col=100)
        data = self.h.parse_result(result, "data")

        data = data[1 + top_dims_qty:]  # исключает ячейки с названиями столбцов
        left_dim_values = [lst[0] for lst in data]  # получение самых левых размерностей элементов
        selected_indexes = set()
        for elem in left_dim_values:
            if elem in selected_dims:
                left_dim_values.index(elem)
                selected_indexes.add(left_dim_values.index(elem))  # только первые вхождения левых размерностей

        # отметить размерности из списка selected_dims
        sorted_indexes = sorted(selected_indexes)  # отстортировать первые вхождения левых размерностей
        for i in sorted_indexes:
            self.execute_olap_command(command_name="view", state="select", position=1, line=i, level=0)

        # сгруппировать выбранные размерности
        view_line = sorted_indexes[0]
        result = self.execute_olap_command(command_name="view", state="group", position=1, line=view_line, level=0)
        # обновить total_row
        self.update_total_row()
        return result

    @timing
    def group_measures(self, measures_list: List, group_name: str) -> Dict:
        """
        Группировка фактов в (левой) панели фактов
        :param measures_list: (List) список выбранных значений
        :param group_name: (str) новое название созданной группы
        :return: (Dict) command_name="fact", state="unselect_all"
        """
        for measure in measures_list:
            # выделить факты
            measure_id = self.get_measure_id(measure)
            self.execute_olap_command(command_name="fact", state="set_selection", fact=measure_id, is_seleceted=True)

        # сгруппировать выбранные факты
        self.execute_olap_command(command_name="fact", state="create_group", name=group_name)

        # снять выделение
        return self.execute_olap_command(command_name="fact", state="unselect_all")

    @timing
    def close_layer(self, layer_id: str) -> Dict:
        """
        Закрыть слой
        :param layer_id: ID активного слоя (self.active_layer_id)
        :return: (Dict) command="user_layer", state="close_layer
        """
        # cформировать список из всех неактивных слоев
        active_layer_set = set()
        active_layer_set.add(layer_id)
        unactive_layers_list = set(self.layers_list) - active_layer_set

        # если активный слой - единственный в списке слоев
        # создать и активировать новый слой
        if len(unactive_layers_list) == 0:
            result = self.execute_manager_command(command_name="user_layer", state="create_layer")
            other_layer = self.h.parse_result(result=result, key="layer", nested_key="uuid")
            if self.jupiter:
                if "ERROR" in str(other_layer):
                    return other_layer
            self.execute_manager_command(command_name="user_layer", state="set_active_layer", layer_id=other_layer)
            unactive_layers_list.add(other_layer)

        # активировать первый неактивный слой
        other_layer = next(iter(unactive_layers_list))
        self.execute_manager_command(command_name="user_layer", state="set_active_layer", layer_id=other_layer)

        # закрыть слой
        result = self.execute_manager_command(command_name="user_layer", state="close_layer", layer_id=layer_id)

        # удалить из переменных класса закрытый слой
        self.active_layer_id = ""
        self.layers_list.remove(layer_id)

        return result

    @timing
    def move_up_dims_to_left(self) -> [List, str]:
        """
        Переместить верхние размерности влево. После чего развернуть их
        :return: (List) преобразованный список id левых размерностей
        """
        self.get_multisphere_data()

        # выгрузить данные только из первой строчки мультисферы
        result = self.execute_olap_command(command_name="view",
                                           state="get",
                                           from_row=0,
                                           from_col=0,
                                           num_row=1,
                                           num_col=1)

        left_dims = self.h.parse_result(result=result, key="left_dims")
        if self.jupiter:
            if "ERROR" in str(left_dims):
                return left_dims
        top_dims = self.h.parse_result(result=result, key="top_dims")
        if self.jupiter:
            if "ERROR" in str(top_dims):
                return top_dims

        logger.info("left_dims:")
        logger.info(left_dims)
        logger.info("top_dims:")
        logger.info(top_dims)

        # если в мультисфере есть хотя бы одна верхняя размерность
        if len(top_dims) > 0:
            # вынести размерности влево, начиная с последней размерности списка
            for i in top_dims[::-1]:
                dim_name = self.get_dim_name(dim_id=i)
                self.move_dimension(dim_name=dim_name, position="left", level=0)

            commands = []
            for i in range(0, len(top_dims)):
                command = self.olap_command.collect_command(module="olap",
                                                            command_name="view",
                                                            state="fold_all_at_level",
                                                            level=i)
                if self.jupiter:
                    if "EXCEPTION" in str(command):
                        return command
                commands.append(command)
            # если в мультисфере нет ни одной левой размерности
            # удалить последнюю команду fold_all_at_level, т.к. ее нельзя развернуть
            if len(left_dims) == 0:
                del commands[-1]
            # если список команд fold_all_at_level не пуст
            # выполнить запрос command_name="view" state="fold_all_at_level",
            if len(commands) > 0:
                query = self.olap_command.collect_request(*commands)
                try:
                    self.exec_request.execute_request(query)
                except BaseException as e:
                    logger.exception(e)
                    logger.exception("APPLICATION STOPPED")
                    self.current_exception = str(e)
                    if self.jupiter:
                        return self.current_exception
                    raise
            output = top_dims[::-1] + left_dims
            self.update_total_row()
            return output
        return "No dimensions to move left"

    @timing
    def grant_permissions(self, user_name: str, clone_user: Union[str, bool] = False) -> [Dict, str]:
        """
        Предоставить пользователю Роли и Права доступа.
        Если не указывать параметр clone_user, то пользователю будут выставлены ВСЕ роли и права

        :param user_name: (str) имя пользователя
        :param clone_user: (str) имя пользователя, у которого будут скопированы Роли и Права доступа
        :return: (Dict) command_name="user", state="info" + command_name="user_cube", state="change_user_permissions"
        """
        # get user data
        result = self.execute_manager_command(command_name="user", state="list_request")

        users_data = self.h.parse_result(result=result, key="users")
        if self.jupiter:
            if "ERROR" in str(users_data):
                return users_data
        # user_permissions = {k: v for data in users_data for k, v in data.items() if data["login"] == user_name}
        # склонировать права пользователя
        if clone_user:
            clone_user_permissions = {k: v for data in users_data for k, v in data.items() if
                                      data["login"] == clone_user}
            user_permissions = {k: v for data in users_data for k, v in data.items() if data["login"] == user_name}
            requested_uuid = clone_user_permissions["uuid"]
            clone_user_permissions["login"], clone_user_permissions["uuid"] = user_permissions["login"], \
                                                                              user_permissions["uuid"]
            user_permissions = clone_user_permissions
        # или предоставить все права
        else:
            user_permissions = {k: v for data in users_data for k, v in data.items() if data["login"] == user_name}
            user_permissions["roles"] = ALL_PERMISSIONS
            requested_uuid = user_permissions["uuid"]
        # cubes permissions for user
        result = self.execute_manager_command(command_name="user_cube", state="user_permissions_request",
                                              user_id=requested_uuid)

        cube_permissions = self.h.parse_result(result=result, key="permissions")
        if self.jupiter:
            if "ERROR" in str(cube_permissions):
                return cube_permissions

        # для всех кубов проставить "accessible": True (если проставляете все права),
        # 'dimensions_denied': [], 'facts_denied': []
        if clone_user:
            cube_permissions = [dict(item, **{'dimensions_denied': [], 'facts_denied': []}) for item
                                in cube_permissions]
        else:
            cube_permissions = [dict(item, **{'dimensions_denied': [], 'facts_denied': [], "accessible": True}) for item
                                in cube_permissions]
        # для всех кубов удалить cube_name
        for cube in cube_permissions:
            del cube["cube_name"]

        # предоставить пользователю Роли и Права доступа
        command1 = self.manager_command.collect_command("manager", command_name="user", state="info",
                                                        user=user_permissions)
        command2 = self.manager_command.collect_command("manager", command_name="user_cube",
                                                        state="change_user_permissions",
                                                        user_id=user_permissions["uuid"],
                                                        permissions_set=cube_permissions)
        if self.jupiter:
            if "EXCEPTION" in str(command1):
                return command1
            if "EXCEPTION" in str(command2):
                return command2
        query = self.manager_command.collect_request(command1, command2)
        try:
            result = self.exec_request.execute_request(query)
        except BaseException as e:
            logger.exception(e)
            logger.exception("APPLICATION STOPPED")
            self.current_exception = str(e)
            if self.jupiter:
                return self.current_exception
            raise
        return result

    @timing
    def select_all_dims(self) -> [Dict, str]:
        """
        Выделение всех элементов крайней левой размерности
        :return: (Dict) command_name="view", state="sel_all"
        """
        # получение спмска элементов левой размерности (чтобы проверить, что список не пуст)
        result = self.execute_olap_command(command_name="view", state="get_2", from_row=0, from_col=0, num_row=1,
                                           num_col=1)
        left_dims = self.h.parse_result(result, "left_dims")
        if self.jupiter:
            if "ERROR" in str(left_dims):
                return left_dims

        # проверки
        try:
            error_handler.checks(self, self.func_name, left_dims)
        except BaseException as e:
            logger.exception(e)
            logger.exception("APPLICATION STOPPED")
            self.current_exception = str(e)
            if self.jupiter:
                return self.current_exception
            raise

        # выделить все элементы левой размерности
        return self.execute_olap_command(command_name="view", state="sel_all", position=1, line=1, level=0)

    @timing
    def load_sphere_chunk(self, units: int = 100):
        """
        Подгрузка мультисферы постранично, порциями строк.
        :param units: количество подгружаемых строк, будет использоваться в num_row и num_col
        :return: (Dict) command_name="view", state="get_2"
        """
        start = 0
        while self.total_row > 0:
            self.total_row = self.total_row - units
            result = self.sc.execute_olap_command(
                command_name="view",
                state="get_2",
                from_row=start,
                from_col=0,
                num_row=units + 1,
                num_col=self.total_cols
            )
            rows_data = self.h.parse_result(result=result, key="data")

            # if self.measure_duplicated or self.dim_duplicated is True
            # add dim1 or fact1 to dim/fact name
            if self.measure_duplicated or self.dim_duplicated:
                rows_data[0] = self.columns

            for item in rows_data[1:]:
                yield dict(zip(rows_data[0], item))
            start += units
        return

    @timing
    def logout(self) -> Dict:
        """
        Выйти из системы
        :return: command_name="user", state="logout"
        """
        return self.execute_manager_command(command_name="user", state="logout")

    def execute_olap_command(self, command_name: str, state: str, **kwargs) -> [Dict, str]:
        """
        Выполнить любую команду модуля OLAP

        Пример: self.execute_olap_command(command_name="fact", state="list_rq")

        Руководство по API http://docs.polymatica.ru/pages/viewpage.action?pageId=411208003

        :param command_name: (str) название команды
        :param state: (str) название состояния
        :param kwargs: доп. параметры в формате param=value
        :return: (Dict) ответ выполненного запроса
        """
        try:
            # проверки
            error_handler.checks(self, self.execute_olap_command.__name__)

            logger.info("*" * 60)
            logger.info("STARTING OLAP COMMAND! command_name='%s' state='%s'", command_name, state)
            logger.info("*" * 60)

            command1 = self.olap_command.collect_command("olap", command_name, state, **kwargs)
            if self.jupiter:
                if "EXCEPTION" in str(command1):
                    return command1
            query = self.olap_command.collect_request(command1)

            # executing query and profiling
            start = time.time()

            result = self.exec_request.execute_request(query)

            end = time.time()
            func_time = end - start
            logger.info("RESPONSE TIME, sec: %s", round(func_time, 2))
            return result

        except BaseException as e:
            logger.exception("EXCEPTION!!! %s" % e)
            logger.exception("APPLICATION STOPPED")
            self.current_exception = str(e)
            if self.jupiter:
                return self.current_exception
            raise

    def execute_manager_command(self, command_name: str, state: str, **kwargs) -> [Dict, str]:
        """
        Выполнить любую команду модуля Manager.

        Пример: self.execute_manager_command(command_name="user_layer", state="get_session_layers")

        Руководство по API http://docs.polymatica.ru/pages/viewpage.action?pageId=411208003

        :param command_name: (str) название команды
        :param state: (str) название состояния
        :param kwargs: доп. параметры в формате param=value
        :return: (Dict) ответ выполненного запроса
        """
        try:
            logger.info("*" * 60)
            if state == "logout":
                logger.info("LOGGING OUT...")
            logger.info("STARTING MANAGER COMMAND! command_name='%s' state='%s'", command_name, state)
            logger.info("*" * 60)
            command1 = self.manager_command.collect_command("manager", command_name, state, **kwargs)
            if self.jupiter:
                if "EXCEPTION" in str(command1):
                    return command1
            query = self.manager_command.collect_request(command1)

            # executing query and profiling
            start = time.time()

            result = self.exec_request.execute_request(query)

            end = time.time()
            func_time = end - start
            logger.info("RESPONSE TIME, sec: %s", round(func_time, 2))
            if state == "logout":
                logger.info(" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ")

            # fix issue with UnicodeEncodeError for admin get_user_list
            if command_name == "admin" and state == "get_user_list":
                return str(result).encode("utf-8")

            return result
        except BaseException as e:
            logger.exception("EXCEPTION!!! %s" % e)
            logger.exception("APPLICATION STOPPED")
            self.current_exception = str(e)
            if self.jupiter:
                return self.current_exception
            raise

    @timing
    def close_current_cube(self) -> Dict:
        """
        Закрыть текущую мультисферу
        :return: (Dict) command_name="user_iface", state="close_module"
        """
        current_module_id = self.multisphere_module_id
        self.multisphere_module_id = ""
        return self.execute_manager_command(command_name="user_iface", state="close_module",
                                            module_id=current_module_id)

    @timing
    def rename_group(self, group_name: str, new_name: str) -> [Dict, str]:
        """
        Переименовать группу пользователей
        :param group_name: (str) Название группы
        :param new_name: (str) Новое название группы
        :return: (Dict) command_name="group", state="edit_group"
        """
        # all groups data
        result = self.execute_manager_command(command_name="group",
                                              state="list_request")
        groups = self.h.parse_result(result, "groups")
        if self.jupiter:
            if "ERROR" in str(groups):
                return groups

        # empty group_data
        roles = ""
        group_uuid = ""
        group_members = ""
        description = ""

        # search for group_name
        for i in groups:
            # if group exists: saving group_data
            if i["name"] == group_name:
                roles = i["roles"]
                group_uuid = i["uuid"]
                group_members = i["members"]
                description = i["description"]
                break

        # check is group exist
        try:
            error_handler.checks(self, self.func_name, group_uuid, group_name)
        except BaseException as e:
            logger.exception(e)
            logger.exception("APPLICATION STOPPED")
            self.current_exception = str(e)
            if self.jupiter:
                return self.current_exception
            raise

        # group_data for request
        group_data = {}
        group_data.update({"uuid": group_uuid})
        group_data.update({"name": new_name})
        group_data.update({"description": description})
        group_data.update({"members": group_members})
        group_data.update({"roles": roles})

        return self.execute_manager_command(command_name="group",
                                            state="edit_group",
                                            group=group_data)

    def create_multisphere_module(self, num_row: int = 10000, num_col: int = 100) -> [Dict, str]:
        """
        Создать модуль мультисферы
        :param self: экземпляр класса BusinessLogic
        :param num_row: количество отображаемых строк
        :param num_col: количество отображаемых колонок
        :return: self.multisphere_data
        """
        # Получить список слоев сессии
        logger.info("Getting layers list:")
        result = self.execute_manager_command(command_name="user_layer", state="get_session_layers")

        # список слоев
        session_layers_lst = self.h.parse_result(result=result, key="layers")
        if self.jupiter:
            if "ERROR" in str(session_layers_lst):
                return session_layers_lst

        self.layers_list = []

        for i in session_layers_lst:
            self.layers_list.append(i["uuid"])

        try:
            # получить layer id
            self.active_layer_id = session_layers_lst[0]["uuid"]
        except KeyError as e:
            logger.exception("EXCEPTION!!! ERROR while parsing response: %s\n%s", e, traceback.format_exc())
            logger.exception("APPLICATION STOPPED")
            self.current_exception = str(e)
            if self.jupiter:
                return self.current_exception
            raise
        except IndexError as e:
            logger.exception("EXCEPTION!!! ERROR while parsing response: %s\n%s", e, traceback.format_exc())
            logger.exception("APPLICATION STOPPED")
            self.current_exception = str(e)
            if self.jupiter:
                return self.current_exception
            raise

        logger.info("layer_id: %s", self.active_layer_id)

        # Инициализировать слой
        logger.info("init layer %s...", self.active_layer_id)
        self.execute_manager_command(command_name="user_layer", state="init_layer", layer_id=self.active_layer_id)

        # Дождаться загрузки слоя
        result = self.execute_manager_command(command_name="user_layer", state="get_load_progress",
                                              layer_id=self.active_layer_id)

        progress = self.h.parse_result(result=result, key="progress")
        if self.jupiter:
            if "ERROR" in str(progress):
                return progress

        logger.info("Layer load progress...")
        logger.info("%s percent", progress)
        while progress < 100:
            result = self.execute_manager_command(command_name="user_layer", state="get_load_progress",
                                                  layer_id=self.active_layer_id)

            progress = self.h.parse_result(result=result, key="progress")
            if self.jupiter:
                if "ERROR" in str(progress):
                    return progress

            logger.info("%s percent", progress)

        # cоздать модуль мультисферы из <cube_id> на слое <layer_id>:
        result = self.execute_manager_command(command_name="user_cube",
                                              state="open_request",
                                              layer_id=self.active_layer_id,
                                              cube_id=self.cube_id,
                                              module_id="00000000-00000000-00000000-00000000")

        # получение id модуля мультисферы
        self.multisphere_module_id = self.h.parse_result(result=result, key="module_desc", nested_key="uuid")
        if self.jupiter:
            if "ERROR" in str(self.multisphere_module_id):
                return self.multisphere_module_id

        # инициализация модуля Olap
        self.olap_command = olap_commands.OlapCommands(self.session_id, self.multisphere_module_id,
                                                       self.url, self.server_codes)
        # рабочая область прямоугольника
        view_params = {
            "from_row": 0,
            "from_col": 0,
            "num_row": num_row,
            "num_col": num_col
        }
        # получить список размерностей и фактов, а также текущее состояние таблицы со значениями
        # (рабочая область модуля мультисферы)
        query = self.olap_command.multisphere_data(self.multisphere_module_id, view_params)
        if self.jupiter:
            if "EXCEPTION" in str(query):
                return query
        try:
            result = self.exec_request.execute_request(query)
        except BaseException as e:
            logger.exception(e)
            logger.exception("APPLICATION STOPPED")
            self.current_exception = str(e)
            if self.jupiter:
                return self.current_exception
            raise

        # multisphere data
        self.multisphere_data = {"dimensions": "", "facts": "", "data": ""}
        for item, index in [("dimensions", 0), ("facts", 1), ("data", 2)]:
            self.multisphere_data[item] = result["queries"][index]["command"][item]
        logger.info("Multisphere data successfully received: %s", self.multisphere_data)

        return self.multisphere_data

    @timing
    def rename_grouped_elems(self, name: str, new_name: str) -> [Dict, str]:
        """
        Переименовать сгруппированные элементы левой размерности
        :param name: название группы элементов
        :param new_name: новое название группы элементов
        :return: (Dict) command_name="group", state="set_name"
        """
        group_id = ""

        res = self.execute_olap_command(command_name="view", state="get", from_row=0, from_col=0,
                                        num_row=1000, num_col=1000)

        # взять id самой левой размерности
        left_dims = self.h.parse_result(res, "left_dims")
        if not len(left_dims):
            logger.error("No left dims!")
            logger.error("APPLICATION STOPPED!!!")
            self.current_exception = "No left dims!"
            if self.jupiter:
                return self.current_exception
            raise
        left_dim_id = left_dims[0]

        # элементы левой размерности
        left_dim_elems = self.h.parse_result(res, "left")
        # вытащить group_id элемента размерности (если он есть у этого элемента)
        try:
            for elem in left_dim_elems:
                if "value" in elem[0]:
                    if elem[0]["value"] == name:
                        group_id = elem[0]["group_id"]
        except KeyError:
            logger.error("grouped elems has NO group_id: %s", name)
            logger.error("APPLICATION STOPPED!!!")
            self.current_exception = "grouped elems has NO group_id: %s", name
            if self.jupiter:
                return self.current_exception
            raise

        if not group_id:
            logger.error("For the left dim: NO such elem: %s", name)
            logger.error("APPLICATION STOPPED!!!")
            self.current_exception = "For the left dim NO such elem: %s" % name
            if self.jupiter:
                return self.current_exception
            raise

        return self.execute_olap_command(command_name="group", state="set_name", dim_id=left_dim_id, group_id=group_id,
                                         name=new_name)

    @timing
    def get_cubes_for_scenarios_by_userid(self, user_name) -> List:
        """
        Для заданного пользователя получить список с данными о сценариях и используемых в этих сценариях мультисферах:

        [{"uuid": "b8ffd729",
          "name": "savinov_test",
          "description": "",
          "cube_ids": ["79ca1aa5", "9ce3ba59"],
          "cube_names": ["nvdia", "Роструд_БФТ_F_Measures_"]},
         ...
         ]
        :param user_name: имя пользователя, под которым запускается command_name="script", state="list_cubes_request"
        :return: (List) scripts_data
        """
        # авторизоваться поль пользователем user_name
        sc = BusinessLogic(login=user_name, url=self.url)

        scripts_data = []

        # script_descs
        script_lst = sc.execute_manager_command(command_name="script", state="list")
        script_descs = sc.h.parse_result(script_lst, "script_descs")

        # cubes data
        cubes = sc.execute_manager_command(command_name="user_cube", state="list_request")
        cubes_data = sc.h.parse_result(cubes, "cubes")

        for script in script_descs:
            # getting list of cube_ids for this scenario id
            res = sc.execute_manager_command(command_name="script", state="list_cubes_request",
                                             script_id=script["uuid"])
            cube_ids = sc.h.parse_result(res, "cube_ids")

            # saving cubes names in list
            cube_names = []
            for cube in cubes_data:
                for cube_id in cube_ids:
                    if cube_id == cube["uuid"]:
                        cube_name = cube["name"].rstrip()
                        cube_names.append(cube_name)

            # saving data for this scenario
            script_data = {
                "uuid": script["uuid"],
                "name": script["name"],
                "description": script["description"],
                "cube_ids": cube_ids,
                "cube_names": cube_names
            }
            scripts_data.append(script_data)

        # убить сессию пользователя user_name
        sc.logout()

        return scripts_data

    @timing
    def get_cubes_for_scenarios(self) -> List:
        """
        Получить список с данными о сценариях и используемых в этих сценариях мультисферах:

        [{"uuid": "b8ffd729",
          "name": "savinov_test",
          "description": "",
          "cube_ids": ["79ca1aa5", "9ce3ba59"],
          "cube_names": ["nvdia", "Роструд_БФТ_F_Measures_"]},
         ...
         ]
        :return: (List) scripts_data
        """
        scripts_data = []

        # script_descs
        script_lst = self.execute_manager_command(command_name="script", state="list")
        script_descs = self.h.parse_result(script_lst, "script_descs")

        # cubes data
        cubes = self.execute_manager_command(command_name="user_cube", state="list_request")
        cubes_data = self.h.parse_result(cubes, "cubes")

        for script in script_descs:
            # getting list of cube_ids for this scenario id
            res = self.execute_manager_command(command_name="script", state="list_cubes_request",
                                               script_id=script["uuid"])
            cube_ids = self.h.parse_result(res, "cube_ids")

            # saving cubes names in list
            cube_names = []
            for cube in cubes_data:
                for cube_id in cube_ids:
                    if cube_id == cube["uuid"]:
                        cube_name = cube["name"].rstrip()
                        cube_names.append(cube_name)

            # saving data for this scenario
            script_data = {
                "uuid": script["uuid"],
                "name": script["name"],
                "description": script["description"],
                "cube_ids": cube_ids,
                "cube_names": cube_names
            }
            scripts_data.append(script_data)

        return scripts_data

    @timing
    def polymatica_health_check_user_sessions(self) -> int:
        """
        Подсчет активных пользовательских сессий [ID-3040]
        :return: (int) user_sessions
        """
        res = self.execute_manager_command(command_name="admin", state="get_user_list")

        # преобразовать полученную строку к utf-8
        res = res.decode("utf-8")

        # преобразовать строку к словарю
        res = ast.literal_eval(res)

        users_info = self.h.parse_result(res, "users")

        user_sessions = 0
        for user in users_info:
            if user["is_online"]:
                user_sessions += 1

        return user_sessions

    @timing
    def polymatica_health_check_all_multisphere_updates(self) -> Dict:
        """
        [ID-3010] Проверка ошибок обновления мультисфер (для целей мониторинга):
        0, если ошибок обновления данных указанной мультисферы не обнаружено
        1, если последнее обновление указанной мультисферы завершилось с ошибкой, но мультисфера доступна пользователям для работы
        2, если последнее обновление указанной мультисферы завершилось с ошибкой и она не доступна пользователям для работы
        OTHER - другие значения update_error и available
        :return: (Dict) multisphere_upds
        """

        res = self.execute_manager_command(command_name="user_cube", state="list_request")

        cubes_list = self.h.parse_result(res, "cubes")

        # словарь со статусами обновлений мультисфер
        multisphere_upds = {}

        for cube in cubes_list:
            if cube["update_error"] and not cube["available"]:
                multisphere_upds.update({cube["name"]: 2})
                continue
            elif cube["update_error"] and cube["available"]:
                multisphere_upds.update({cube["name"]: 1})
                continue
            elif not cube["update_error"] and cube["available"]:
                multisphere_upds.update({cube["name"]: 0})
                continue
            else:
                multisphere_upds.update({cube["name"]: "OTHER"})

        return multisphere_upds

    @timing
    def polymatica_health_check_multisphere_updates(self, ms_name: str) -> [int, str]:
        """
        [ID-3010] Проверка ошибок обновления мультисферы (для целей мониторинга):
        0, не обнаружено ошибок обновления данных указанной мультисферы и мультисфера доступна.
            (Проверка, что "update_error"=False и "available"=True)
        1, ошибок обновления данных указанной мультисферы
            (Проверка, что "update_error"=True или "available"=False)
        :param ms_name: (str) Название мультисферы
        :return: (int) 0 или 1
        """
        res = self.execute_manager_command(command_name="user_cube", state="list_request")

        cubes_list = self.h.parse_result(res, "cubes")

        # Проверка названия мультисферы
        try:
            error_handler.checks(self, self.func_name, cubes_list, ms_name)
        except BaseException as e:
            logger.exception(e)
            logger.exception("APPLICATION STOPPED")
            self.current_exception = str(e)
            if self.jupiter:
                return self.current_exception
            raise

        for cube in cubes_list:
            if cube["name"] == ms_name:
                if cube["update_error"] or not cube["available"]:
                    return 1
                break

        return 0

    @timing
    def polymatica_health_check_data_updates(self) -> [List, int]:
        """
        [ID-3010] Один из методов проверки обновления мультисфер (для целей мониторинга)
        :return: (int, List) 0, если ошибок обновления данных не обнаружено (последнее обновление для всех мультисфер выполнено успешно, без ошибок)
            Перечень мультисфер, последнее обновление которых завершилось с ошибкой
        """
        res = self.execute_manager_command(command_name="user_cube", state="list_request")

        cubes_list = self.h.parse_result(res, "cubes")

        # словарь со статусами обновлений мультисфер
        multisphere_upds = []

        for cube in cubes_list:
            if cube["update_error"]:
                multisphere_upds.append(cube["name"])

        if not multisphere_upds:
            return 0

        return multisphere_upds

    def _get_session_bl(self, sid: str) -> 'BusinessLogic':
        """
        Подключение к БЛ по заданному идентификатору сессии.
        :param sid: 16-ричный идентификатор сессии.
        :return: (BusinessLogic) экземпляр класса BusinessLogic с заданным или текущим идентификатором сессии.
        """
        if sid:
            return BusinessLogic(
                login=self.login,
                url=self.url,
                authorization_id=self.authorization_uuid,
                session_id=sid
            )
        return self

    @timing
    def get_layer_list(self, sid: str = None) -> List:
        """
        [ID-3120] Загрузка данных о слоях.
        :param sid: 16-ричный идентификатор сессии; в случае, если он отсутствует, берётся текущее значение.
        :return: (list) список вида [[layer_id, layer_name], [...], ...], содержащий слои в том порядке,
            в котором они отображаются на интерфейсе.
        :call_example:
            1. Инициализируем класс: bl_test = sc.BusinessLogic(login=<login>, password=<password>, url=<url>)
            2. Вызов метода без передачи sid:
                layer_list = bl_test.get_layer_list()
                output: [[<id>, <name>], [<id>, <name>], ...] - список слоёв для текущей сессии.
            3. Вызов метода с передачей валидного sid:
                sid = <valid_sid>
                layer_list = bl_test.get_layer_list(sid)
                output: [[<id>, <name>], [<id>, <name>], ...] - список слоёв для заданной сессии.
            4. Вызов метода с передачей невалидного sid:
                sid = <invalid_sid>
                layer_list = bl_test.get_layer_list(sid)
                output: exception "Session does not exist".
        """
        # если указан идентификатор сессии, то обращаемся к нему
        if sid:
            session_bl = self._get_session_bl(sid)
            return session_bl.get_layer_list()

        # получаем список слоёв
        layers_result = self.execute_manager_command(command_name="user_layer", state="get_session_layers")
        layers_list = self.h.parse_result(result=layers_result, key="layers")

        # сортируем список слоёв по времени создания,
        # т.к. необходимо вернуть слои в том порядке, в котором они отображаются на интерфейсе
        layers_list.sort(key=lambda item: item.get('create_timestamp', 0))

        # проходим по списку слоёв и сохраняем их идентификаторы и названия
        layers = [[layer.get('uuid', str()), layer.get('name', str())] for layer in layers_list]
        return layers

    @timing
    def set_layer_focus(self, layer: str, sid: str = None) -> str:
        """
        [ID-3121] Установка активности заданного слоя.
        :param layer: идентификатор/название слоя
        :param sid: 16-ричный идентификатор сессии; в случае, если он отсутствует, берётся текущее значение.
        :return: (str) идентификатор установленного активного слоя.
        :call_example:
            1. Инициализируем класс: bl_test = sc.BusinessLogic(login=<login>, password=<password>, url=<url>)
            2. Вызов метода без передачи sid:
                layer = <layer_id or layer_name>
                layer_list = bl_test.set_layer_focus(layer=layer)
                output: <layer_id> - идентификатор установленного активного слоя.
            3. Вызов метода с передачей валидного sid:
                layer, sid = <layer_id or layer_name>, <valid_sid>
                layer_list = bl_test.set_layer_focus(layer=layer, sid=sid)
                output: <layer_id> - идентификатор установленного активного слоя (для заданной сессии).
            4. Вызов метода с передачей невалидного sid:
                layer, sid = <layer_id or layer_name>, <invalid_sid>
                layer_list = bl_test.set_layer_focus(layer=layer, sid=sid)
                output: exception "Session does not exist".
            5. Вызов метода с передачей неверного идентификатора/названия слоя:
                layer = <invalid_layer_id or invalid_layer_name>
                layer_list = bl_test.set_layer_focus(layer=layer)
                output: exception "Layer cannot be found by name or ID".
        """
        # если указан идентификатор сессии, то обращаемся к нему
        if sid:
            session_bl = self._get_session_bl(sid)
            layer_id = session_bl.set_layer_focus(layer)
            self.active_layer_id = layer_id
            return layer_id

        # получаем все слои мультисферы
        # layers имеет вид [[layer_id, layer_name], [...], ...]
        layers = self.get_layer_list(sid)

        # проходя по каждому слою, ищем соответствие среди имени/идентификатора
        for current_layer_params in layers:
            if layer in current_layer_params:
                layer_id = current_layer_params[0]
                s = {"wm_layers2": {"lids": [item[0] for item in layers], "active": layer_id}}
                self.execute_manager_command(
                    command_name="user_layer", state="set_active_layer", layer_id=layer_id)
                self.execute_manager_command(
                    command_name="user_iface", state="save_settings", module_id=self.authorization_uuid, settings=s)
                self.active_layer_id = layer_id
                return layer_id

        # если дошло сюда - слой с таким именем/идентификатором не найден, бросаем ошибку
        self.current_exception = "Layer cannot be found by name or ID"
        if self.jupiter:
            return self.current_exception
        raise Exception(self.current_exception)

    @timing
    def _get_active_layer_id(self) -> str:
        """
        Возвращает идентификатор активного слоя в текущей сессии.
        :return: (str) идентификатор активного слоя.
        """
        settings = self.execute_manager_command(
            command_name="user_iface", state="load_settings", module_id=self.authorization_uuid)
        return self.h.parse_result(result=settings, key="settings").get('wm_layers2', dict()).get('active')

    @timing
    def _get_modules_in_layer(self, layer_id: str) -> List:
        """
        Возвращает список модулей на заданном слое.
        :param layer_id: идентификатор слоя, модули которого необходимо получить.
        :return: (list) список вида [[module_id, module_name, module_type], [...], ...],
            содержащий информацию о модулях в текущем слое.
        """
        # получаем список всех модулей, находящихся в текущем слое
        settings = self.execute_manager_command(command_name="user_layer", state="get_layer", layer_id=layer_id)
        layer_info = self.h.parse_result(result=settings, key="layer") or dict()

        # проходя по каждому модулю, извлекаем из него информацию
        result = []
        for module in layer_info.get('module_descs'):
            module_id, module_type = module.get('uuid'), module.get('type_id')

            # имя модуля в этих настройках не указано - подгружаем отдельно и формируем общий результат
            module_setting = self.execute_manager_command(
                command_name="user_iface", state="load_settings", module_id=module_id)
            module_info = self.h.parse_result(result=module_setting, key="settings") or dict()
            result.append([module_id, module_info.get('title', str()), module_type])
        return result

    @timing
    def get_module_list(self, sid: str = None) -> List:
        """
        [ID-3123] Возвращает список модулей в активном слое в заданной (или текущей) сессии.
        :param sid: 16-ричный идентификатор сессии; в случае, если он отсутствует, берётся текущее значение.
        :return: (list) список вида [[module_id, module_name, module_type], [...], ...],
            содержащий информацию о модулях на активном слое.
        :call_example:
            1. Инициализируем класс: bl_test = sc.BusinessLogic(login=<login>, password=<password>, url=<url>)
            2. Вызов метода без передачи sid:
                module_list = bl_test.get_module_list()
                output: [[<module_id>, <module_name>, <module_type>], [...], ...] - список модулей в активном слое
                    в текущей сессии.
            3. Вызов метода с передачей валидного sid:
                sid = <valid_sid>
                module_list = bl_test.get_module_list(sid)
                output: [[<module_id>, <module_name>, <module_type>], [...], ...] - список модулей
                    в активном слое в заданной сессии.
            4. Вызов метода с передачей невалидного sid:
                sid = <invalid_sid>
                module_list = bl_test.get_module_list(sid)
                output: exception "Session does not exist".
        """
        # если указан идентификатор сессии, то обращаемся к нему
        if sid:
            session_bl = self._get_session_bl(sid)
            return session_bl.get_module_list()

        # получаем идентификатор активного слоя
        active_layer_id = self._get_active_layer_id()
        if not active_layer_id:
            self.current_exception = "Active layer not set!"
            if self.jupiter:
                return self.current_exception
            raise Exception(self.current_exception)

        return self._get_modules_in_layer(active_layer_id)

    @timing
    def set_module_focus(self, module: str, sid: str = None):
        """
        [ID-3122] Установка фокуса на заданный модуль. Слой, на котором находится модуль, также становится активным.
        Ничего не возвращает.
        :param module: идентификатор/название модуля.
        :param sid: 16-ричный идентификатор сессии; в случае, если он отсутствует, берётся текущее значение.
        :call_example:
            1. Инициализируем класс: bl_test = sc.BusinessLogic(login=<login>, password=<password>, url=<url>)
            2. Вызов метода без передачи sid:
                module = <module_id or module_name>
                bl_test.set_module_focus(module=module)
            3. Вызов метода с передачей валидного sid:
                module, sid = <module_id or module_name>, <valid_sid>
                bl_test.set_module_focus(module=module, sid=sid)
            4. Вызов метода с передачей невалидного sid:
                module, sid = <module_id or module_name>, <invalid_sid>
                bl_test.set_module_focus(module=module, sid=sid)
                output: exception "Session does not exist".
            5. Вызов метода с передачей неверного идентификатора/названия модуля:
                module = <invalid_module_id or invalid_module_name>
                bl_test.set_module_focus(module=module)
                output: exception "Module cannot be found by ID or name".
        """
        # если указан идентификатор сессии, то обращаемся к нему
        if sid:
            session_bl = self._get_session_bl(sid)
            session_bl.set_module_focus(module)
            return

        # получаем все слои; layers имеет вид [[layer_id, layer_name], [...], ...]
        layers = self.get_layer_list()

        # проходя по каждому слою, получаем список его модулей
        for layer in layers:
            layer_id = layer[0]
            modules_info = self._get_modules_in_layer(layer_id)

            # module_info имеет формат [module_id, module_name, module_type]
            # перебираем все модули в текущем слое
            for module_info in modules_info:
                if module in module_info:
                    # делаем активным текущий слой
                    self.set_layer_focus(layer_id)

                    # делаем активным искомый модуль
                    self.multisphere_module_id = module_info[0]
                    return

        # если дошло сюда - модуль с таким именем/идентификатором не найден, бросаем ошибку
        self.current_exception = "Module cannot be found by ID or name"
        if self.jupiter:
            return self.current_exception
        raise Exception(self.current_exception)

    @timing
    def manual_update_cube(self, cube_name: str) -> [Dict, str]:
        """
        Запуск обновления мультисферы вручную.
        :param cube_name: (str) название мультисферы
        """
        self.cube_name = cube_name
        # получение списка описаний мультисфер
        result = self.execute_manager_command(command_name="user_cube", state="list_request")
        if "ERROR" in str(result):
            if self.jupiter:
                return result
            else:
                raise Exception(str(result))
        cubes_list = self.h.parse_result(result=result, key="cubes")
        if "ERROR" in str(cubes_list):
            if self.jupiter:
                return cubes_list
            else:
                raise Exception(str(cubes_list))
        # получить cube_id из списка мультисфер
        try:
            self.cube_id = self.h.get_cube_id(cubes_list, cube_name)
        except ValueError as e:
            logger.exception("EXCEPTION!!! %s", e)
            logger.exception("APPLICATION STOPPED")
            self.current_exception = str(e)
            if self.jupiter:
                return self.current_exception
            raise
        # запуск обновления мультисферы вручную
        result = self.execute_manager_command(command_name="user_cube", state="manual_update", cube_id=self.cube_id)
        if "ERROR" in str(result):
            if self.jupiter:
                return result
            else:
                raise Exception(str(result))
        return result

    def _get_measures_list(self) -> List:
        """
            Получить список фактов мультисферы.
        """
        result = self.execute_olap_command(command_name="fact", state="list_rq")
        return self.h.parse_result(res, "facts")

    def _raise_measure_group_error(self, group_name: str):
        """
            Генерация исключения в случае, когда в мультисфере отсутствует заданная группа.
        """
        msg_error = "Group <{}> not exist".format(group_name)
        logger.error("ERROR!!! {}".format(msg_error))
        logger.error("APPLICATION STOPPED!!!")
        self.current_exception = msg_error
        if self.jupiter:
            return msg_error
        raise ValueError(msg_error)

    @timing
    def measure_rename_group(self, curr_name: str, new_name: str) -> [Dict, str]:
        """
        [ID-2992] Переименовать группу фактов.
        :param curr_name: (str) текущее название группы фактов
        :param new_name: (str) новое название группы фактов
        :return: (Dict) command_name="fact" state="rename"
        """
        query = ""
        measures_list = self._get_measures_list()

        # если в мультисфере есть такая такая группа фактов - переименовываем группу
        for item in measures_list:
            if item["name"] == curr_name:
                query = self.execute_olap_command(command_name="fact", state="rename", fact=item["id"], name=new_name)
                break

        # если же нет - выбрасываем исключение
        if not query:
            self._raise_measure_group_error(curr_name)

        return query

    @timing
    def measure_remove_group(self, group_name: str) -> [Dict, str]:
        """
        [ID-2992] Удаление группы фактов.
        :param group_name: название группы фактов (которую нужно удалить)
        :return: (Dict) command_name="fact", state="del"
        """
        query = ""
        measures_list = self._get_measures_list()

        # удалить группу, если в мультисфере есть такая такая группа фактов
        for item in measures_list:
            if item["name"] == group_name:
                query = self.execute_olap_command(command_name="fact", state="del", fact=item["id"])
                break

        # если же нет - выбрасываем исключение
        if not query:
            self._raise_measure_group_error(group_name)

        return query

    @timing
    def module_fold(self, module_id: None, minimize: bool) -> Dict:
        """
        Свернуть модуль мультисферы [ID-2993]
        :param module_id: id модуля, который нужно свернуть
        :param minimize: (bool) True - свернуть / True - развернуть
        :return: user_iface save_settings
        """
        if module_id:
            self.multisphere_module_id = module_id

        # проверка параметра minimize
        if minimize not in [True, False]:
            raise ValueError("Arg 'minimize' can only be True OR False!")

        settings = {"minimize": minimize}

        return self.execute_manager_command(
            command_name="user_iface",
            state="save_settings",
            module_id=self.multisphere_module_id,
            settings=settings
        )

    @timing
    def graph_create(self, settings: str, grid: int, labels: List, graph_type="Линии") -> Dict:
        """
        Создать график
        :param graph_type: (str) название типа графика
        :param settings: (str) Настройки. Значения могут равнятся только 0 или 1. Порядок:
            Заголовок, Легенда, Названия осей, Подписи на осях, Вертикальная ось справа
        :param labels: (List) нподписи на графике (3 элемента в списке!)
            [OX - диапазон 5-30, OY - диапазон 5-30, сокращение подписей False / True]
        :param grid: (int) Сетка. 0 - все линии, 1 - горизонтальные линии, 2 - вертикальные линии, 3 - без сетки
        :return: command_name="user_iface", state="save_settings"
        """

        settings_dict = {
            "titleShow": settings[0],
            "legend": settings[1],
            "axis": settings[2],
            "axisNotes": settings[3],
            "axisPosition": settings[4],
        }

        if not isinstance(grid, int):
            raise ValueError("Grids values can only be Integers")
        if (0 > grid) or (grid > 3):
            raise ValueError("Grids can be only in interval [0, 3]")

        grids = {
            0: "all",  # Все линии
            1: "h",  # Горизонтальные линии
            2: "v",  # Вертикальные линии
            3: "none"  # Без сетки
        }
        grid = grids[grid]

        all_types = {
            "Цилиндры": "plot-cylinder",
            "Линии": "plot-2d-lines",
            "Радар": "plot-radar",
            "Цилиндры с накоплением": "plot-stacked-bars",
            "Области": "plot-area",
            "Пироги": "plot-pies"
        }

        if len(settings) != 5:
            raise ValueError("Settings length can only equals 5!")

        settings2 = settings.replace("1", "")
        settings2 = settings2.replace("0", "")
        if len(settings2) > 0:
            raise ValueError("Settings string can only contain 0 or 1!")

        for i in settings_dict:
            if settings_dict[i] == "0":
                settings_dict[i] = False
            else:
                settings_dict[i] = True

        if len(labels) != 3:
            raise ValueError("Labels list length must be == 3!")

        frequencyOX, frequencyOY, axisXShortFormat = labels
        if (frequencyOX % 5 != 0) or (frequencyOX < 0) or (frequencyOX > 30):
            raise ValueError("frequencyOX must be set in interval [0, 30] with step 5!")
        if (frequencyOY % 5 != 0) or (frequencyOY < 0) or (frequencyOY > 30):
            raise ValueError("frequencyOY must be set in interval [0, 30] with step 5!")
        if graph_type not in all_types:
            raise ValueError("No such graph type: %s" % graph_type)

        # 15. Создать график с типом Линии (тип по умолчанию)
        self.execute_manager_command(command_name="user_iface",
                                     state="create_module",
                                     module_id=self.multisphere_module_id,
                                     module_type=600,
                                     layer_id=self.active_layer_id,
                                     after_module_id=self.multisphere_module_id)

        graph_settings = {"geometry": {"width": 840, "height": 540},
                          "plotName": all_types[graph_type], "plotData": {
                "plot-2d-lines": {"config": {
                    "base": {"titleShow": settings_dict["titleShow"], "legend": settings_dict["legend"],
                             "axis": settings_dict["axis"], "axisNotes": settings_dict["axisNotes"],
                             "axisPosition": settings_dict["axisPosition"], "wireShow": grid,
                             "axisNotesPeriodX": frequencyOX,
                             "axisNotesPeriodY": frequencyOY,
                             "axisXShortFormat": axisXShortFormat},
                    "lines": {"showPoints": True, "hints": False}},
                    "state": {"colors": {"facts": {
                        "6c788397": "rgb(0, 175, 215)"}},
                        "title": False,
                        "zoom": {"k": 1, "x": 0,
                                 "y": 0}},
                    "query": {}}}}

        return self.execute_manager_command(command_name="user_iface", state="save_settings",
                                            module_id=self.multisphere_module_id, settings=graph_settings)

    def column_resize(self):
        """
        Кнопка Показать контент (расширяет столбцы, чтобы текст становился видимым).
        :return: command_name="user_iface", state="save_settings"
        """
        settings = {"dimAndFactShow": True}
        return self.execute_manager_command(command_name="user_iface", state="save_settings",
                                            module_id=self.multisphere_module_id, settings=settings)


class GetDataChunk:
    """ Класс для получения данных чанками """

    def __init__(self, sc: BusinessLogic):
        """
        Инициализация класса GetDataChunk
        :param sc: экземпляр класса BusinessLogic
        """
        logger.info("INIT CLASS GetDataChunk")
        self.jupiter = sc.jupiter

        # helper class
        self.h = helper.Helper(self)

        # экзмепляр класса BusinessLogic
        self.sc = sc
        # флаги наличия дубликатов размерностей и фактов
        self.measure_duplicated, self.dim_duplicated = False, False

        result = sc.execute_olap_command(command_name="view", state="get", from_row=0, from_col=0,
                                         num_row=1, num_col=1)
        json_left_dims = self.h.parse_result(result, "left_dims")

        # количество левых размерностей
        self.dims_qty = len(json_left_dims)
        # список имён размерностей
        self.dim_lst = []
        # количество фактов в строке
        self.facts_qty = 0
        # getting multisphere total rows
        result = self.sc.execute_olap_command(command_name="view", state="get_2", from_row=0, from_col=0,
                                              num_row=1, num_col=1)
        self.total_row = self.h.parse_result(result, "total_row")
        # словарь типов размерностей Полиматики
        self.olap_types = self.sc.server_codes["olap"]["olap_data_type"]
        # колонки в формате {"название размерности": str, "название факта": float}
        self.columns = self.get_col_types()

        # total number of cols
        # self.total_cols = len(self.columns)
        self.total_cols = self.dims_qty + self.facts_qty

    def _get_data(self) -> List:
        """
        Получение строки данных. Необходимо для дальнейшего определения типов столбцов.
        """
        columns_data = self.sc.execute_olap_command(
            command_name="view", state="get_2", from_row=0, from_col=0, num_row=10, num_col=1000)
        data = self.h.parse_result(columns_data, "data")
        return data[1]

    def _get_all_dims(self) -> List:
        """
        Получение всех размерностей мультисферы.
        """
        all_dims_data = self.sc.execute_olap_command(command_name="dimension", state="list_rq")
        return self.h.parse_result(all_dims_data, "dimensions")

    def _get_measures(self) -> List:
        """
        Получение всех фактов мультисферы.
        """
        all_measures_data = self.sc.execute_olap_command(command_name="fact", state="list_rq")
        return self.h.parse_result(all_measures_data, "facts")

    def _get_dim_type(self, olap_type: int) -> str:
        """
        Возвращает тип размерности.
        """
        return list(self.olap_types.keys())[list(self.olap_types.values()).index(olap_type)]

    def _update_or_append_key(self, dict_container: dict, key: str):
        """
        Добавляет ключ в словарь, если его ещё там нет, иначе значение ключа увеличивает на 1.
        """
        if key not in dict_container:
            dict_container.update({key: 1})
        else:
            dict_container[key] += 1

    def _get_active_measure_ids(self, data: dict) -> List:
        """
        Получение активных фактов (т.е. фактов, отображаемых в таблице мультисферы)
        """
        top, measure_data = self.h.parse_result(data, "top"), dict()
        for i in top:
            if "fact_id" in str(i):
                measure_data = i
                break
        return [measure.get("fact_id") for measure in measure_data]

    def get_col_types(self) -> List:
        """
        [ID-3169] Получить текущие колонки мультисферы в заданном формате.
        :return: (list) колонки мультисферы в формате
            [{"name": <column_name>, "type": <column_type>, "data_type": <column_data_type>}, ...]
        """
        # список колонок,
        # содержащий словари вида {"name": <column_name>, "type": <column_type>, "data_type": <column_data_type>}
        columns = list()
        exists_columns = set()

        # получение строки, содержащей данные мультисферы
        data = self._get_data()

        # command="get" (necessary fields: left_dims, top)
        get_command = self.sc.execute_olap_command(
            command_name="view", state="get", from_row=0, from_col=0, num_row=10, num_col=1000)

        # получение идентификаторов размерностей, вынесенных влево, и список всех размерностей
        left_dims = self.h.parse_result(get_command, "left_dims")
        all_dims = self._get_all_dims()
        dim_name_list = [item["name"] for item in all_dims]

        # получение всех фактов и формирование из них вспомогательных данных
        measures_data = self._get_measures()
        measure_id_map = {measure.get("id"): measure.get("name") for measure in measures_data}
        measures_name_list = [item["name"] for item in measures_data]

        # для накопления списка всех размерностей-дубликатов и фактов-дубликатов
        dims_dups, measure_dups = dict(), dict()

        # добавление размерностей в список колонок
        for my_dim in left_dims:
            for dim in all_dims:
                if my_dim == dim.get("id"):
                    dim_name = dim.get("name")
                    if dim_name in exists_columns:
                        self._update_or_append_key(dims_dups, dim_name)
                        dim_name = "{} (dim{})".format(dim_name, dims_dups.get(dim_name))
                        self.dim_duplicated = True

                    # составляем итоговый словарь и добавляем его в список колонок
                    dim_data = {
                        "name": dim_name,
                        "type": self._get_dim_type(dim.get("olap_type")),
                        "data_type": "fact_dimension" if dim_name in measures_name_list else "dimension"
                    }
                    columns.append(dim_data)
                    exists_columns.add(dim_name)
                    self.dim_lst.append(dim_name)
                    break

        # получение идентификаторов активных фактов
        measure_ids = self._get_active_measure_ids(get_command)

        # добавление фактов в список колонок
        for measure_id in measure_ids:
            measure_name = self.sc.get_measure_name(measure_id)
            check_measure_name = measure_name
            if measure_name in exists_columns:
                self._update_or_append_key(measure_dups, measure_name)
                measure_name = "{} (fact{})".format(measure_name, measure_dups.get(measure_name))
                self.measure_duplicated = True

            # получаем элемент для определения типа факта
            current_elem = data[self.dims_qty + len(columns) - self.dims_qty]

            # составляем итоговый словарь и добавляем его в список колонок
            measure_data = {
                "name": measure_name,
                "type": "double" if isinstance(current_elem, float) else "uint32",
                "data_type": "fact_dimension" if check_measure_name in dim_name_list else "fact"
            }
            columns.append(measure_data)
            exists_columns.add(dim_name)
            self.facts_qty += 1

        return columns

    def load_sphere_chunk(self, units: int = 100):
        """
        Подгрузка мультисферы постранично, порциями строк.
        :param units: количество подгружаемых строк, будет использоваться в num_row и num_col
        :return: (Dict) command_name="view", state="get_2"
        """
        start = 0
        while self.total_row > 0:
            self.total_row = self.total_row - units
            result = self.sc.execute_olap_command(
                command_name="view",
                state="get_2",
                from_row=start,
                from_col=0,
                num_row=units + 1,
                num_col=self.total_cols
            )
            rows_data = self.h.parse_result(result=result, key="data")

            # if self.measure_duplicated or self.dim_duplicated is True
            # add dim1 or fact1 to dim/fact name
            if self.measure_duplicated or self.dim_duplicated:
                rows_data[0] = self.columns

            for item in rows_data[1:]:
                yield dict(zip(rows_data[0], item))
            start += units
        return
