polyapi 5.9.16__tar.gz → 5.9.17__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. {polyapi-5.9.16 → polyapi-5.9.17}/PKG-INFO +1 -1
  2. {polyapi-5.9.16 → polyapi-5.9.17}/polyapi.egg-info/PKG-INFO +1 -1
  3. {polyapi-5.9.16 → polyapi-5.9.17}/polyapi.egg-info/SOURCES.txt +6 -1
  4. {polyapi-5.9.16 → polyapi-5.9.17}/polyapi.egg-info/top_level.txt +1 -0
  5. {polyapi-5.9.16 → polyapi-5.9.17}/polymatica/business_scenarios.py +192 -62
  6. {polyapi-5.9.16 → polyapi-5.9.17}/polymatica/error_handler.py +13 -8
  7. {polyapi-5.9.16 → polyapi-5.9.17}/polymatica/graph/graph_interface.py +15 -0
  8. {polyapi-5.9.16 → polyapi-5.9.17}/pyproject.toml +6 -1
  9. {polyapi-5.9.16 → polyapi-5.9.17}/setup.py +1 -1
  10. polyapi-5.9.17/tests/__init__.py +1 -0
  11. polyapi-5.9.17/tests/conftest.py +65 -0
  12. polyapi-5.9.17/tests/const.py +312 -0
  13. polyapi-5.9.17/tests/test_create_sphere.py +735 -0
  14. polyapi-5.9.17/tests/test_update_cube.py +594 -0
  15. {polyapi-5.9.16 → polyapi-5.9.17}/LICENSE.txt +0 -0
  16. {polyapi-5.9.16 → polyapi-5.9.17}/README.md +0 -0
  17. {polyapi-5.9.16 → polyapi-5.9.17}/polyapi.egg-info/dependency_links.txt +0 -0
  18. {polyapi-5.9.16 → polyapi-5.9.17}/polyapi.egg-info/requires.txt +0 -0
  19. {polyapi-5.9.16 → polyapi-5.9.17}/polymatica/__init__.py +0 -0
  20. {polyapi-5.9.16 → polyapi-5.9.17}/polymatica/authorization.py +0 -0
  21. {polyapi-5.9.16 → polyapi-5.9.17}/polymatica/business_logic_doc.py +0 -0
  22. {polyapi-5.9.16 → polyapi-5.9.17}/polymatica/commands/__init__.py +0 -0
  23. {polyapi-5.9.16 → polyapi-5.9.17}/polymatica/commands/base_command.py +0 -0
  24. {polyapi-5.9.16 → polyapi-5.9.17}/polymatica/commands/olap_module.py +0 -0
  25. {polyapi-5.9.16 → polyapi-5.9.17}/polymatica/commands/other_modules.py +0 -0
  26. {polyapi-5.9.16 → polyapi-5.9.17}/polymatica/common/__init__.py +0 -0
  27. {polyapi-5.9.16 → polyapi-5.9.17}/polymatica/common/consts.py +0 -0
  28. {polyapi-5.9.16 → polyapi-5.9.17}/polymatica/common/helper_funcs.py +0 -0
  29. {polyapi-5.9.16 → polyapi-5.9.17}/polymatica/common/params_models.py +0 -0
  30. {polyapi-5.9.16 → polyapi-5.9.17}/polymatica/exceptions.py +0 -0
  31. {polyapi-5.9.16 → polyapi-5.9.17}/polymatica/executor.py +0 -0
  32. {polyapi-5.9.16 → polyapi-5.9.17}/polymatica/graph/__init__.py +0 -0
  33. {polyapi-5.9.16 → polyapi-5.9.17}/polymatica/graph/base_graph.py +0 -0
  34. {polyapi-5.9.16 → polyapi-5.9.17}/polymatica/graph/types/__init__.py +0 -0
  35. {polyapi-5.9.16 → polyapi-5.9.17}/polymatica/graph/types/areas.py +0 -0
  36. {polyapi-5.9.16 → polyapi-5.9.17}/polymatica/graph/types/balls.py +0 -0
  37. {polyapi-5.9.16 → polyapi-5.9.17}/polymatica/graph/types/chord.py +0 -0
  38. {polyapi-5.9.16 → polyapi-5.9.17}/polymatica/graph/types/circles.py +0 -0
  39. {polyapi-5.9.16 → polyapi-5.9.17}/polymatica/graph/types/circles_series.py +0 -0
  40. {polyapi-5.9.16 → polyapi-5.9.17}/polymatica/graph/types/corridors.py +0 -0
  41. {polyapi-5.9.16 → polyapi-5.9.17}/polymatica/graph/types/cumulative_areas.py +0 -0
  42. {polyapi-5.9.16 → polyapi-5.9.17}/polymatica/graph/types/cumulative_cylinders.py +0 -0
  43. {polyapi-5.9.16 → polyapi-5.9.17}/polymatica/graph/types/cylinders.py +0 -0
  44. {polyapi-5.9.16 → polyapi-5.9.17}/polymatica/graph/types/graph.py +0 -0
  45. {polyapi-5.9.16 → polyapi-5.9.17}/polymatica/graph/types/lines.py +0 -0
  46. {polyapi-5.9.16 → polyapi-5.9.17}/polymatica/graph/types/pies.py +0 -0
  47. {polyapi-5.9.16 → polyapi-5.9.17}/polymatica/graph/types/point.py +0 -0
  48. {polyapi-5.9.16 → polyapi-5.9.17}/polymatica/graph/types/point_series.py +0 -0
  49. {polyapi-5.9.16 → polyapi-5.9.17}/polymatica/graph/types/pools.py +0 -0
  50. {polyapi-5.9.16 → polyapi-5.9.17}/polymatica/graph/types/pools_3d.py +0 -0
  51. {polyapi-5.9.16 → polyapi-5.9.17}/polymatica/graph/types/radar.py +0 -0
  52. {polyapi-5.9.16 → polyapi-5.9.17}/polymatica/graph/types/sankey.py +0 -0
  53. {polyapi-5.9.16 → polyapi-5.9.17}/polymatica/graph/types/surface.py +0 -0
  54. {polyapi-5.9.16 → polyapi-5.9.17}/polymatica/helper.py +0 -0
  55. {polyapi-5.9.16 → polyapi-5.9.17}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polyapi
3
- Version: 5.9.16
3
+ Version: 5.9.17
4
4
  Summary: Wrapper for Polymatica API
5
5
  Home-page: https://slsoft.ru/products/polymatica/
6
6
  Author: Polymatica Rus LLC
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polyapi
3
- Version: 5.9.16
3
+ Version: 5.9.17
4
4
  Summary: Wrapper for Polymatica API
5
5
  Home-page: https://slsoft.ru/products/polymatica/
6
6
  Author: Polymatica Rus LLC
@@ -46,4 +46,9 @@ polymatica/graph/types/pools.py
46
46
  polymatica/graph/types/pools_3d.py
47
47
  polymatica/graph/types/radar.py
48
48
  polymatica/graph/types/sankey.py
49
- polymatica/graph/types/surface.py
49
+ polymatica/graph/types/surface.py
50
+ tests/__init__.py
51
+ tests/conftest.py
52
+ tests/const.py
53
+ tests/test_create_sphere.py
54
+ tests/test_update_cube.py
@@ -1 +1,2 @@
1
1
  polymatica
2
+ tests
@@ -1214,6 +1214,14 @@ class BusinessLogic:
1214
1214
  def put_dim_filter_by_value(
1215
1215
  self, value: str, dim_id: str, clear_filter: bool = False
1216
1216
  ):
1217
+ """
1218
+ Установка фильтра по значению элемента.
1219
+ В первом вызове метода необходимо выставить clear_filter=True, иначе фильтр не применится.
1220
+ :param value: (str) Значение, по которому происходит фильтрация.
1221
+ :param dim_id: (str) ID размерности, по которой производится фильтрация.
1222
+ :param clear_filter: (bool) снять ли все отметки перед наложением фильтра, по умолчанию False.
1223
+ :return: (dict) результат выполнения команд (("filter", "apply_data"), ("filter", "set")).
1224
+ """
1217
1225
  if clear_filter:
1218
1226
  self.execute_olap_command(
1219
1227
  command_name="filter", state="filter_all_flag", dimension=dim_id
@@ -1246,14 +1254,14 @@ class BusinessLogic:
1246
1254
  query = self.olap_command.collect_request(command1, command2)
1247
1255
 
1248
1256
  try:
1249
- self.exec_request.execute_request(query)
1257
+ result = self.exec_request.execute_request(query)
1250
1258
  except Exception as e:
1251
1259
  return self._raise_exception(PolymaticaException, str(e))
1252
1260
 
1253
1261
  self.update_total_row()
1254
1262
  self.func_name = "put_dim_filter_by_value"
1255
1263
 
1256
- return
1264
+ return result
1257
1265
 
1258
1266
  @timing
1259
1267
  def put_dim_filter(
@@ -1577,7 +1585,7 @@ class BusinessLogic:
1577
1585
  :param module: (str) название/идентификатор OLAP-модуля, в котором нужно переименовать группу фактов;
1578
1586
  если модуль указан, но такого нет - сгенерируется исключение;
1579
1587
  если модуль не указан, то берётся текущий (активный) модуль (если его нет - сгенерируется исключение).
1580
- :return: (dict) результат команды ("fact", "rename").
1588
+ :return: (dict) результат команды ("fact", "tree_rename_group_request").
1581
1589
  :call_example:
1582
1590
  1. Инициализируем класс БЛ: bl_test = BusinessLogic(login="login", password="password", url="url")
1583
1591
  2. Вызов метода:
@@ -1811,28 +1819,49 @@ class BusinessLogic:
1811
1819
  log("End download file")
1812
1820
 
1813
1821
  @timing
1814
- def export(self, path: str, file_format: str) -> Tuple[str, str]:
1822
+ def export(self, path: str, file_format: str, mode: str = "standard") -> Tuple[str, str]:
1815
1823
  """
1816
- Экспортировать мультисферу в файл в заданную директорию. Если указанной директории не сущестует - она будет
1824
+ Экспортировать мультисферу в файл в заданную директорию. Если указанной директории не существует - она будет
1817
1825
  создана. Непосредственно имя файла будет сгенерировано автоматически.
1818
1826
  :param path: (str) директория, в которой нужно сохранить файл; также, директория не может быть пустой
1819
1827
  (т.е. не может содержать пустую строку или None).
1820
- :param file_format: (str) формат сохраненного файла: "csv", "xls", "json".
1821
- :return (str): file_name - название файла.
1822
- :return (str): path - директория файла.
1828
+ :param file_format: (str) формат сохраненного файла: "csv", "xlsx", "ods", "json".
1829
+ :param mode: (str) режим экспорта. Возможные значения: standard — для выбора стандартного режима,
1830
+ сохраняющего текущий порядок строк и столбцов в выгружаемом OLAP-модуле; fast — для выбора ускоренного
1831
+ режима, не сохраняющего текущий порядок строк и столбцов. Необязательный аргумент.
1832
+ Значение по умолчанию — standard.
1833
+ :return (Tuple[str, str]): (file_name, path) - название файла, путь к файлу
1823
1834
  """
1824
1835
  # проверки
1825
1836
  try:
1826
- self.checks(self.func_name, file_format, path)
1837
+ self.checks(self.func_name, file_format, path, mode)
1827
1838
  except Exception as e:
1828
1839
  return self._raise_exception(ValueError, str(e), with_traceback=False)
1829
1840
 
1841
+ # загружаем форматы фактов и преобразуем их для запроса
1842
+ measure_formats = self.get_measure_format()
1843
+ measure_precisions = []
1844
+ measure_units = []
1845
+ for measure_name, measure_format in measure_formats.items():
1846
+ measure_id = self.get_measure_id(measure_name)
1847
+ measure_precision = measure_format.get("precision", 2)
1848
+ measure_unit = measure_format.get("measureUnit", "")
1849
+ measure_precisions.append({"key": measure_id, "value": int(measure_precision)})
1850
+ measure_units.append({"key": measure_id, "value": measure_unit})
1851
+
1852
+ # преобразуем другие параметры для корректности запроса
1853
+ file_format = "xls" if file_format == "xlsx" else file_format
1854
+ disable_sorting = True if mode == "fast" else False
1855
+
1830
1856
  # начать экспорт данных и дождаться загрузки
1831
1857
  self.execute_olap_command(
1832
1858
  command_name="xls_export",
1833
1859
  state="start",
1834
1860
  export_format=file_format,
1835
1861
  export_destination_type="local",
1862
+ disable_sorting=disable_sorting,
1863
+ facts_precision=measure_precisions,
1864
+ measure_units=measure_units,
1836
1865
  )
1837
1866
  need_check_progress = True
1838
1867
  while need_check_progress:
@@ -1850,11 +1879,11 @@ class BusinessLogic:
1850
1879
  "code", -1
1851
1880
  ), status_info.get("message", "Unknown error!")
1852
1881
  log(f"Export data: status: {status_code}, progress: {progress_value}")
1853
- except Exception:
1882
+ except Exception as e:
1854
1883
  # если упала ошибка - не удалось получить ответ от сервера: возможно, он недоступен
1855
1884
  return self._raise_exception(
1856
1885
  ExportError,
1857
- "Failed to export data! Possible server is unavailable.",
1886
+ f"Failed to export data! Possible server is unavailable. Error: {e}",
1858
1887
  )
1859
1888
 
1860
1889
  # анализируем статус загрузки
@@ -1882,24 +1911,22 @@ class BusinessLogic:
1882
1911
 
1883
1912
  # имя файла в результате команды ('xls_export', 'check') приходит только после полной загрузки файла
1884
1913
  # формируем название нужного файла
1885
- format_map = {"csv": ["csv"], "xls": ["xls", "xlsx"], "json": ["json"]}
1886
- server_f_name = self.h.parse_result(result=progress_data, key="file_name")
1887
- download_file_name = server_f_name.replace(":", "-")
1888
- download_file_name = (
1889
- download_file_name[:-8]
1890
- if download_file_name.split(".")[-1] not in format_map.get(file_format)
1891
- else download_file_name
1892
- )
1914
+ server_file_name = self.h.parse_result(result=progress_data, key="file_name")
1915
+ download_file_name = self.h.parse_result(result=progress_data, key="file_name_hint")
1893
1916
 
1894
1917
  # скачивание файла
1895
1918
  self._download_file(
1896
- f"{self.resources_url}/{server_f_name}", path, download_file_name
1919
+ f"{self.resources_url}/{server_file_name}", path, download_file_name
1897
1920
  )
1898
1921
 
1899
1922
  # проверка что файл скачался после экспорта
1900
- assert download_file_name in os.listdir(
1901
- path
1902
- ), f'File "{download_file_name}" not in path "{path}"!'
1923
+ if download_file_name not in os.listdir(path):
1924
+ return self._raise_exception(
1925
+ ExportError,
1926
+ f'File "{download_file_name}" was not found in directory "{path}" after download. '
1927
+ f'Please check file permissions, available disk space, and directory access rights.',
1928
+ with_traceback=False
1929
+ )
1903
1930
  return download_file_name, path
1904
1931
 
1905
1932
  @staticmethod
@@ -2607,7 +2634,7 @@ class BusinessLogic:
2607
2634
  :param user_password: (str) пароль пользователя, под которым запускается сценарий;
2608
2635
  не нужно указывать, если требуется запустить сценарий под пользователем, по-умолчанию не имеющим пароля,
2609
2636
  например, временный пользователь.
2610
- :param units: (int) число выгружаемых строк мультисферы (по-умолчанию 500).
2637
+ :param units: (int) число выгружаемых строк мультисферы (по-умолчанию 1000).
2611
2638
  :return: (tuple) данные мультисферы и данные о колонках мультсферы (аналогично методу "get_data_frame").
2612
2639
  :call_example:
2613
2640
  1. Инициализируем класс БЛ: bl_test = BusinessLogic(login="login", password="password", url="url")
@@ -2730,7 +2757,7 @@ class BusinessLogic:
2730
2757
  иначе будут возвращаться неполные данные.
2731
2758
  ВАЖНО: генерация строк не учитывает промежуточные и общие итоги (тоталы) по строкам и колонкам.
2732
2759
  :param units: (int) количество подгружаемых строк; ожидается целое положительное число больше 0;
2733
- по-умолчанию 100.
2760
+ по-умолчанию 1000.
2734
2761
  :param show_all_columns: (bool) установка показа всех колонок датафрейма.
2735
2762
  :param show_all_rows: (bool) установка показа всех строк датафрейма.
2736
2763
  :return: (DataFrame, DataFrame) данные мультисферы и колонки мультисферы в формате DataFrame.
@@ -4086,9 +4113,13 @@ class BusinessLogic:
4086
4113
  опционально также может быть задан порт подключения, в таком случае он должен идти после указания
4087
4114
  хоста через двоеточие (например, "10.18.0.132:5433", "polymatica.database1.ru:5433");
4088
4115
  в случае, если порт явно не указан, подразумевается порт по-умолчанию 5432.
4116
+ Для источника данных JDBC в этом поле необходимо указать DSN в формате,
4117
+ например, jdbc:mysql://192.111.11.11:3306/database. Если database стандартный, то возможно
4118
+ подключение и без указания его в DSN.
4089
4119
  "login" - логин пользователя.
4090
4120
  "passwd" - пароль пользователя.
4091
- "database" - имя базы данных.
4121
+ "database" - имя базы данных. Для источника данных JDBC указывать "database" не надо, оно указывается
4122
+ после хоста в строке DSN (см. описание поля "server")
4092
4123
  "sql_query" - запрос, который необходимо выполнить на сервере.
4093
4124
  Пример задания параметра:
4094
4125
  {
@@ -4115,7 +4146,13 @@ class BusinessLogic:
4115
4146
  при этом второе значение должно быть больше первого. Формат значений времени: "DD.MM.YYYY". Все остальные
4116
4147
  значения, если они будут переданы, будут игнорироваться. Актуально только для интервального обновления.
4117
4148
  :param encoding: (str) кодировка, например UTF-8; обязательна для csv-источника.
4118
- :param delayed: (bool) создать мультисферу при первом обновлении (как аналогичный чек-бокс на интерфейсе).
4149
+ :param delayed: (bool) параметр, определяющий, будет ли отложено создание мультисферы.
4150
+ Если False, то не будет, и мультисфера будет автоматически создана.
4151
+ Если True: если настроено расписание обновлений (schedule в update_params),
4152
+ то импорт данных мультисферы начнется при первом срабатывании заданного расписания,
4153
+ а если расписание не настроено, то импорт данных мультисферы начнется при следующем
4154
+ принудительном обновлении (через метод manual_update_cube или через веб-интерфейс).
4155
+ По умолчанию False.
4119
4156
  :param modified_records_params: (dict) параметры обновления для типа "обновление измененных записей".
4120
4157
  Поля, передаваемые в словарь:
4121
4158
  "modified_records_key" - поле, которому осуществляется сопоставление данных (имя размерности).
@@ -4197,7 +4234,12 @@ class BusinessLogic:
4197
4234
  modified_records_params = dict()
4198
4235
  else:
4199
4236
  copy.deepcopy(modified_records_params)
4200
- if modified_records_params.get("version") != 0:
4237
+ algo_version = modified_records_params.get("version")
4238
+ if algo_version is None:
4239
+ modified_records_params["version"] = 1
4240
+ elif algo_version not in (0, 1):
4241
+ self.logger.warning(f"Param 'modified_records_algo_version' must be 0 or 1, "
4242
+ f"not {algo_version}. Changed to 1")
4201
4243
  modified_records_params["version"] = 1
4202
4244
 
4203
4245
  # проверки
@@ -4392,7 +4434,7 @@ class BusinessLogic:
4392
4434
  "datetime",
4393
4435
  ):
4394
4436
  error_msg = (
4395
- f'Dimension "{interval_dim}" has type "{current_type}", '
4437
+ f'Dimension "{increment_dim}" has type "{current_type}", '
4396
4438
  f'one of types is expected: ["uint8", "uint16", "uint32", '
4397
4439
  f'"uint64", "double", "date", "time", "datetime"]!'
4398
4440
  )
@@ -4584,7 +4626,13 @@ class BusinessLogic:
4584
4626
  используется для замены файла в источнике. Можно заменять на файл того же типа, что и был,
4585
4627
  например "csv" на "csv". В пути файла обязательно должно быть расширение, например, ".csv".
4586
4628
  :param separator: (str) разделитель столбцов. Обязателен для csv-источника (при замене источника).
4587
- :param delayed: (bool) создать мультисферу при первом обновлении (как аналогичный чек-бокс на интерфейсе).
4629
+ :param delayed: (bool) параметр, определяющий, будет ли отложено обновление мультисферы.
4630
+ Если False, то не будет, и мультисфера будет автоматически обновлена.
4631
+ Если True: если настроено расписание обновлений (schedule в update_params),
4632
+ то мультисфера обновится при первом срабатывании заданного расписания, а если расписание не настроено,
4633
+ то мультисфера обновится при следующем принудительном обновлении
4634
+ (через метод manual_update_cube или через веб-интерфейс).
4635
+ По умолчанию False.
4588
4636
  :param increment_dim: (str) название размерности, необходимой для инкрементального обновления.
4589
4637
  :param interval_dim: (str) название размерности для интервального обновления; размерность должна иметь
4590
4638
  один из следующих типов: date, datetime.
@@ -4673,7 +4721,12 @@ class BusinessLogic:
4673
4721
  modified_records_params = dict()
4674
4722
  else:
4675
4723
  copy.deepcopy(modified_records_params)
4676
- if modified_records_params.get("version") != 0:
4724
+ algo_version = modified_records_params.get("version")
4725
+ if algo_version is None:
4726
+ modified_records_params["version"] = 1
4727
+ elif algo_version not in (0, 1):
4728
+ self.logger.warning(f"Param 'modified_records_algo_version' must be 0 or 1, "
4729
+ f"not {algo_version}. Changed to 1")
4677
4730
  modified_records_params["version"] = 1
4678
4731
  cubes_list = self.get_cubes_list()
4679
4732
  self.func_name = "update_cube"
@@ -5138,12 +5191,14 @@ class BusinessLogic:
5138
5191
  dim_elems = self.h.parse_result(res, dim_items_key)
5139
5192
 
5140
5193
  # вытащить идентификатор группировки размерности (если он есть у этого элемента)
5194
+ # TODO: при доработке для использования метода с любым уровнем размерности вместо all_dims_ids[0] и
5195
+ # dim_elems[0] использовать индекс соответствующего уровня
5141
5196
  try:
5142
5197
  for elem in dim_elems:
5143
- current_elem = elem[0]
5144
- if "value" in current_elem and current_elem["value"] == name:
5145
- group_id = current_elem["group_id"]
5146
- break
5198
+ for current_elem in elem:
5199
+ if current_elem.get("type") == 3 and current_elem.get("value") == name:
5200
+ group_id = current_elem["group_id"]
5201
+ break
5147
5202
  except KeyError:
5148
5203
  msg = f'No grouped dimensions with name "{name}"!'
5149
5204
  return self._raise_exception(ValueError, msg)
@@ -5302,7 +5357,7 @@ class BusinessLogic:
5302
5357
  if position == "left":
5303
5358
  total_dim_items = [lst[0].strip() for lst in data[1 + top_dims_count :]]
5304
5359
  else:
5305
- total_dim_items = [item.strip() for item in data[0][left_dims_count:-1]]
5360
+ total_dim_items = [item.strip() for item in data[0][left_dims_count:]]
5306
5361
 
5307
5362
  # проверяем, все ли элементы размерности, указанные пользователем, есть в общем списке элементов
5308
5363
  # если текущий элемент есть - сохраняем его индекс, в противном случае - генерируем ошибку
@@ -6106,7 +6161,7 @@ class BusinessLogic:
6106
6161
 
6107
6162
  result = self.execute_olap_command(
6108
6163
  command_name="view",
6109
- state="select",
6164
+ state="select_change",
6110
6165
  position=1,
6111
6166
  level=len(dim_values) - 1,
6112
6167
  line=line,
@@ -6133,7 +6188,7 @@ class BusinessLogic:
6133
6188
  чтобы перед вызовом метода были развёрнуты все узлы мультисферы.
6134
6189
  3. Все тоталы (как промежуточные, так и итоговые) генератором выведены не будут вне зависимости от того,
6135
6190
  включены они или нет.
6136
- :param units: (int) количество подгружаемых строк; по-умолчанию 100.
6191
+ :param units: (int) количество подгружаемых строк; по-умолчанию 1000.
6137
6192
  :param convert_type: (bool) нужно ли преобразовывать данные из типов, определённых Полиматикой, к Python-типам;
6138
6193
  по-умолчанию False (т.е. не нужно).
6139
6194
  :param default_value: (Any) актуален только при convert_type = True;
@@ -7895,7 +7950,6 @@ class BusinessLogic:
7895
7950
  },
7896
7951
  ...
7897
7952
  ]
7898
- В случае, если мультисфер в сценарии нет, вернётся пустой список.
7899
7953
  В случае, если в мультисфере нет вынесенных размерностей, то в "used_dimensions"
7900
7954
  будет пустой список.
7901
7955
  """
@@ -8161,14 +8215,29 @@ class BusinessLogic:
8161
8215
  'is_composite': <value>, # является ли размерность составной; для факта - None
8162
8216
  'position': <value>, # позиция размерности ("left"/"up"/"out"); для факта - None
8163
8217
  'have_filter': <value>, # наложен ли фильтр на размерность; для факта - None,
8164
- 'visible': <value>, # видимость факта; для размерности - None
8218
+ 'visible': <value>, # видимость размерности (используется при расчете факта
8219
+ по фиксированной размерности) или факта
8165
8220
  'horizontal': <value>, # включён ли горизонтальный расчёт для факта; для размерности - None
8166
8221
  'is_calculated': <value>, # является ли факт вычислимым; для размерности - None
8167
- 'is_group': <value>, # является ли факт группировкой других фактов; для размерности - None
8222
+ 'is_group': False, # является ли факт группировкой других фактов; для размерности - None
8168
8223
  'group_id': <value>, # идентификатор группы, в которую ходит данный факт;
8169
8224
  если факт не входит ни в какую группу, то None;
8170
8225
  для размерности - None
8171
- 'type': <value> # тип факта; для размерности - None
8226
+ 'type': <value>, # тип факта; для размерности - None
8227
+ 'level': <value>, # уровень, на который вынесена размерность или уровень для установки
8228
+ расчёта сложного вида факта по уровню
8229
+ 'is_shown': <value>, # флаг скрытых размерностей или фактов, флаг, сообщающий о том, что
8230
+ у пользователя не должно быть прямой возможности взаимодействовать
8231
+ с этой размерностью или фактом
8232
+ 'level_fixed_dim': <value>,# идентификатор опорной размерности, по которой производится расчет
8233
+ сложного факта. Для простого, относительного факта или сложного факта
8234
+ с расчетом по уровню отображается значение '00000000';
8235
+ для размерности - None
8236
+ 'is_level_fixed': <value>, # признак расчета по опорной размерности. Для сложного факта с расчетом
8237
+ по опорной размерности — True. Для простого, относительного факта или
8238
+ сложного факта с расчетом по уровню — False;
8239
+ для размерности - None
8240
+ 'selected': <value>, # выбран ли факт; для размерности - None
8172
8241
  }
8173
8242
  Группы фактов представляют собой словарь следующего формата:
8174
8243
  {
@@ -8176,13 +8245,22 @@ class BusinessLogic:
8176
8245
  'type': 'group', # тип: группа
8177
8246
  'name': <value>, # имя группы
8178
8247
  'visibility': 'visible', # видимость группы
8248
+ 'is_group': True, # признак группы
8179
8249
  'nodes': # узлы (факты и группы), входящие в группу, в формате списка словарей
8180
8250
  [
8181
8251
  {
8182
- 'id': идентификатор узла,
8252
+ 'id': идентификатор узла - подгруппы,
8253
+ 'type': тип, для подгруппы - 'group',
8254
+ 'name': имя подгруппы,
8255
+ 'visibility': видимость подгруппы,
8256
+ 'nodes': узлы (факты и группы), входящие в подгруппу,
8257
+ в формате списка словарей
8258
+ },
8259
+ {
8260
+ 'id': идентификатор узла - факта,
8183
8261
  'type': тип, для факта - 'measure',
8184
8262
  'measure': идентификатор самого факта,
8185
- group_id': идентификатор группы, в которую входит этот узел
8263
+ 'group_id': идентификатор группы, в которую входит этот узел
8186
8264
  },
8187
8265
  {...}
8188
8266
  ]
@@ -8197,25 +8275,30 @@ class BusinessLogic:
8197
8275
  # получаем список размерностей и фактов
8198
8276
  dimensions, measures = self._get_dimensions_list(), self._get_measures_list()
8199
8277
  dim_base_dict = dict.fromkeys(
8200
- ["visible", "horizontal", "is_calculated", "is_group", "group_id", "type"],
8201
- None,
8202
- )
8203
- measure_base_dict = dict.fromkeys(
8204
- ["data_type", "is_composite", "position", "have_filter"], None
8278
+ ["horizontal", "is_calculated", "is_group", "group_id", "type", "level_fixed_dim", "is_level_fixed",
8279
+ "selected"], None,
8205
8280
  )
8281
+ measure_base_dict = {
8282
+ **dict.fromkeys(["data_type", "is_composite", "position", "have_filter"], None),
8283
+ "is_group": False,
8284
+ }
8206
8285
 
8207
8286
  # конфигурация размерностей
8208
8287
  for dimension in dimensions:
8209
8288
  current_dim_dict = {
8210
8289
  "name": dimension.get("name"),
8211
8290
  "id": dimension.get("id"),
8212
- "is_copy": dimension.get("base_id") != "00000000",
8291
+ "is_copy": dimension.get("base_id") != EMPTY_ID,
8213
8292
  "data_type": TYPES_MAP.get(
8214
8293
  POLYMATICA_INT_TYPES_MAP.get(dimension.get("olap_type"))
8215
8294
  ),
8216
8295
  "is_composite": dimension.get("olap3_type") == 3,
8217
8296
  "position": POSITION_MAP.get(dimension.get("position")),
8218
8297
  "have_filter": dimension.get("haveFilter"),
8298
+ "level": dimension.get("level"),
8299
+ "visible": dimension.get("visible"),
8300
+ "is_shown": dimension.get("is_shown"),
8301
+
8219
8302
  }
8220
8303
  current_dim_dict.update(dim_base_dict)
8221
8304
  olap_config["dimensions"].append(current_dim_dict)
@@ -8226,24 +8309,30 @@ class BusinessLogic:
8226
8309
  nodes_list
8227
8310
  )
8228
8311
  for measure in measures:
8312
+ node = nodes_dict_with_group[measure["id"]]
8229
8313
  current_measure_dict = {
8230
8314
  "name": measure.get("name"),
8231
8315
  "id": measure.get("id"),
8232
- "is_copy": measure.get("base_id") != "00000000",
8316
+ "is_copy": measure.get("base_id") != EMPTY_ID,
8233
8317
  "visible": measure.get("visible"),
8234
8318
  "horizontal": measure.get("horizontal"),
8235
8319
  "is_calculated": measure.get("olap3_type") == 3,
8236
- "is_group": measure.get("olap3_type") == 4,
8237
- "group_id": measure.get("fgroup_id"), # deprecated
8320
+ "group_id": node["group_id"],
8238
8321
  "type": MEASURE_INT_STR_TYPES_MAP.get(measure.get("plm_type")),
8322
+ "level": measure.get("level"),
8323
+ "level_fixed_dim": measure.get("level_fixed_dim"),
8324
+ "is_level_fixed": measure.get("is_level_fixed"),
8325
+ "selected": measure.get("selected"),
8326
+ "is_shown": measure.get("is_shown"),
8239
8327
  }
8240
- node = nodes_dict_with_group[measure["id"]]
8241
- current_measure_dict["group_id"] = node["group_id"]
8328
+
8242
8329
  current_measure_dict.update(measure_base_dict)
8243
8330
  olap_config["measures"].append(current_measure_dict)
8244
8331
 
8245
8332
  # добавляем группы фактов в список фактов
8246
8333
  measures_groups = [node for node in nodes_list if node.get("type") == "group"]
8334
+ for group_node in measures_groups:
8335
+ group_node["is_group"] = True
8247
8336
  olap_config["measures"].extend(measures_groups)
8248
8337
  # меняем фокус на исходную мультисферу и возвращаем данные
8249
8338
  self.set_multisphere_module_id(current_ms_id)
@@ -8537,10 +8626,21 @@ class BusinessLogic:
8537
8626
  url: str,
8538
8627
  method: str,
8539
8628
  headers: dict,
8540
- cookies=None,
8541
- data=None,
8542
- json: dict = None,
8629
+ cookies: Optional[dict] = None,
8630
+ data: Optional[Union[dict, List[Tuple], bytes]] = None,
8631
+ json: Optional[dict] = None,
8543
8632
  ):
8633
+ """
8634
+ Выполнить HTTP-запрос к API_v2 Аналитикс.
8635
+
8636
+ :param url: (str) URL-адрес стенда Аналитикс.
8637
+ :param method: (str) HTTP-метод (например, "GET", "PATCH", "POST", "PUT", "DELETE").
8638
+ :param headers: (dict) Заголовки HTTP-запроса.
8639
+ :param cookies: (dict) Куки запроса; по умолчанию `{ "session": self.session_id }`.
8640
+ :param data: (dict | list of tuples | bytes) Тело запроса.
8641
+ :param json: (dict) Тело запроса в формате JSON.
8642
+ :return: `requests.models.Response` — необработанный ответ библиотеки `requests`.
8643
+ """
8544
8644
  if cookies is None:
8545
8645
  cookies = {"session": self.session_id}
8546
8646
 
@@ -8557,7 +8657,8 @@ class BusinessLogic:
8557
8657
  def switch_session(self, new_owner_uuid: str) -> int:
8558
8658
  """
8559
8659
  Переключить владельца сессии.
8560
- :param new_owner_uuid: (str) UUID_4 - идентификатор пользователя, который должен стать владельцем текущей сессии
8660
+ :param new_owner_uuid: (str) uuid - идентификатор пользователя, который должен стать владельцем текущей сессии.
8661
+ Можно получить uuid с помощью метода get_user_uuid(login).
8561
8662
  :return: (int) статус код:
8562
8663
  Статусы ответа:
8563
8664
  204 No Content - успешно.
@@ -8579,10 +8680,39 @@ class BusinessLogic:
8579
8680
  response = self.request_to_api_v2(
8580
8681
  url=url, method="PATCH", headers=headers, json=data
8581
8682
  )
8582
- assert response.status_code == 204, "Response status code != 204"
8583
-
8683
+ if response.status_code != 204:
8684
+ response_reasons_mapping = {
8685
+ 400: "Bad Request (wrong uuid format).",
8686
+ 401: "Request from unauthorized user.",
8687
+ 403: "The current user does not have admin role.",
8688
+ 404: f"User with uuid {new_owner_uuid} not found.",
8689
+ 500: "Internal server error."
8690
+ }
8691
+ self.logger.error(f"Response status code != 204, session was not switched. "
8692
+ f"Reason: {response_reasons_mapping.get(response.status_code)}")
8584
8693
  return response.status_code
8585
8694
 
8695
+ def get_user_uuid(self, login: str = None) -> str:
8696
+ """
8697
+ Метод для получения uuid пользователя. UUID используется в методе switch_session.
8698
+ :param login: (str) логин пользователя, если не указан, то используется логин текущего пользователя.
8699
+ :return: (str) uuid пользователя
8700
+ """
8701
+ user_uuid = None
8702
+ if login is None:
8703
+ login = self.login
8704
+ users_result = self.execute_manager_command(command_name="user", state="list_request")
8705
+ users_data = self.h.parse_result(result=users_result, key="users")
8706
+ for user in users_data:
8707
+ if user.get('login') == login:
8708
+ user_uuid = user.get('uuid')
8709
+ if user_uuid:
8710
+ return user_uuid
8711
+ else:
8712
+ self._raise_exception(UserNotFoundError,
8713
+ f'User "{login}" not found on server {self.base_url}',
8714
+ with_traceback=False)
8715
+
8586
8716
  @timing
8587
8717
  def cleanup_multisphere_data(
8588
8718
  self,
@@ -9023,7 +9153,7 @@ class GetDataChunk:
9023
9153
  3. Все тоталы (как промежуточные, так и итоговые) генератором выведены не будут вне зависимости от того,
9024
9154
  включены они или нет.
9025
9155
  :param units: (int) количество подгружаемых строк; ожидается целое положительное число больше 0;
9026
- по-умолчанию 100.
9156
+ по-умолчанию 1000.
9027
9157
  :param convert_type: (bool) нужно ли преобразовывать данные из типов, определённых Полиматикой, к Python-типам;
9028
9158
  по-умолчанию False (т.е. не нужно).
9029
9159
  :param default_value: (Any) актуален только при convert_type = True;
@@ -483,19 +483,22 @@ class Validator:
483
483
  return base
484
484
 
485
485
  @staticmethod
486
- def check_export(file_format, file_path):
486
+ def check_export(file_format, file_path, mode):
487
487
  """
488
488
  Проверка параметров для функции export.
489
489
  :param file_format: формат файла
490
490
  :param file_path: путь к файлу
491
+ :param mode: (str) режим экспорта
491
492
  :return: True, если проверка прошла успешно
492
493
  """
493
- if file_format not in ["csv", "xls", "json"]:
494
+ if file_format not in ["csv", "xls", "xlsx", "ods", "json"]:
494
495
  raise ValueError(
495
- f'Wrong file format: "{file_format}". Only .csv, .xls, .json formats allowed!'
496
+ f'Wrong file format: "{file_format}". Only .csv, .xlsx, .ods, .json formats allowed!'
496
497
  )
497
498
  if not file_path:
498
499
  raise ValueError("Empty file path!")
500
+ if mode not in ("standard", "fast"):
501
+ raise ValueError(f'Param mode must be "standard" or "fast", not {mode}')
499
502
  return True
500
503
 
501
504
  @staticmethod
@@ -858,18 +861,20 @@ class Validator:
858
861
  :param file_type: тип файла
859
862
  :param sql_params: параметры SQL
860
863
  """
861
- if (file_type != "excel") and (file_type != "csv"):
864
+ if file_type not in ("excel", "csv"):
862
865
  if sql_params is None:
863
866
  raise ValueError(
864
867
  'If your sourse is sql: fill in param "sql_params"!\n\n'
865
868
  'In other cases: it is wrong param "file_type": %s\n\nIt can be only:\n'
866
869
  "excel OR csv" % file_type
867
870
  )
868
- if not (
869
- {"server", "login", "passwd", "sql_query"} <= set(sql_params.keys())
870
- ):
871
+ required_keys_jdbc = {"server", "login", "passwd", "sql_query"}
872
+ required_keys_sql = required_keys_jdbc | {"database"}
873
+ required_keys = required_keys_jdbc if file_type == "jdbc" else required_keys_sql
874
+ missing_keys = required_keys - set(sql_params.keys())
875
+ if missing_keys:
871
876
  raise ValueError(
872
- "Please check the following params names in sql_params:\n-server\n-login\n-passwd\n-sql_query"
877
+ f"Missing required sql_params: {', '.join(sorted(missing_keys))}"
873
878
  )
874
879
 
875
880
  @staticmethod
@@ -452,6 +452,21 @@ class IGraph:
452
452
  raise ValueError(error_msg)
453
453
 
454
454
  layer_id, graph_module_id = graph_module_ids[0]
455
+
456
+ # выгружаем текущие параметры графика
457
+ settings = self._base_bl.execute_manager_command(
458
+ command_name="user_iface",
459
+ state="load_settings",
460
+ module_id=graph_module_id,
461
+ )
462
+ current_module_settings = self._base_bl.h.parse_result(settings, "settings")
463
+ plot_name = current_module_settings.get('plotName')
464
+ current_state = current_module_settings['plotData'][plot_name]['state']
465
+
466
+ # подставляем текущее имя графика из настроек, если его не указали
467
+ if 'name' not in self._other:
468
+ self._other['name'] = current_state.get('title')
469
+
455
470
  # получаем идентификатор OLAP-модуля, на основе которого построен график
456
471
  layer_settings = self._base_bl.execute_manager_command(
457
472
  command_name="user_layer", state="get_layer", layer_id=layer_id