deepfos 1.1.60__py3-none-any.whl

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 (175) hide show
  1. deepfos/__init__.py +6 -0
  2. deepfos/_version.py +21 -0
  3. deepfos/algo/__init__.py +0 -0
  4. deepfos/algo/graph.py +171 -0
  5. deepfos/algo/segtree.py +31 -0
  6. deepfos/api/V1_1/__init__.py +0 -0
  7. deepfos/api/V1_1/business_model.py +119 -0
  8. deepfos/api/V1_1/dimension.py +599 -0
  9. deepfos/api/V1_1/models/__init__.py +0 -0
  10. deepfos/api/V1_1/models/business_model.py +1033 -0
  11. deepfos/api/V1_1/models/dimension.py +2768 -0
  12. deepfos/api/V1_2/__init__.py +0 -0
  13. deepfos/api/V1_2/dimension.py +285 -0
  14. deepfos/api/V1_2/models/__init__.py +0 -0
  15. deepfos/api/V1_2/models/dimension.py +2923 -0
  16. deepfos/api/__init__.py +0 -0
  17. deepfos/api/account.py +167 -0
  18. deepfos/api/accounting_engines.py +147 -0
  19. deepfos/api/app.py +626 -0
  20. deepfos/api/approval_process.py +198 -0
  21. deepfos/api/base.py +983 -0
  22. deepfos/api/business_model.py +160 -0
  23. deepfos/api/consolidation.py +129 -0
  24. deepfos/api/consolidation_process.py +106 -0
  25. deepfos/api/datatable.py +341 -0
  26. deepfos/api/deep_pipeline.py +61 -0
  27. deepfos/api/deepconnector.py +36 -0
  28. deepfos/api/deepfos_task.py +92 -0
  29. deepfos/api/deepmodel.py +188 -0
  30. deepfos/api/dimension.py +486 -0
  31. deepfos/api/financial_model.py +319 -0
  32. deepfos/api/journal_model.py +119 -0
  33. deepfos/api/journal_template.py +132 -0
  34. deepfos/api/memory_financial_model.py +98 -0
  35. deepfos/api/models/__init__.py +3 -0
  36. deepfos/api/models/account.py +483 -0
  37. deepfos/api/models/accounting_engines.py +756 -0
  38. deepfos/api/models/app.py +1338 -0
  39. deepfos/api/models/approval_process.py +1043 -0
  40. deepfos/api/models/base.py +234 -0
  41. deepfos/api/models/business_model.py +805 -0
  42. deepfos/api/models/consolidation.py +711 -0
  43. deepfos/api/models/consolidation_process.py +248 -0
  44. deepfos/api/models/datatable_mysql.py +427 -0
  45. deepfos/api/models/deep_pipeline.py +55 -0
  46. deepfos/api/models/deepconnector.py +28 -0
  47. deepfos/api/models/deepfos_task.py +386 -0
  48. deepfos/api/models/deepmodel.py +308 -0
  49. deepfos/api/models/dimension.py +1576 -0
  50. deepfos/api/models/financial_model.py +1796 -0
  51. deepfos/api/models/journal_model.py +341 -0
  52. deepfos/api/models/journal_template.py +854 -0
  53. deepfos/api/models/memory_financial_model.py +478 -0
  54. deepfos/api/models/platform.py +178 -0
  55. deepfos/api/models/python.py +221 -0
  56. deepfos/api/models/reconciliation_engine.py +411 -0
  57. deepfos/api/models/reconciliation_report.py +161 -0
  58. deepfos/api/models/role_strategy.py +884 -0
  59. deepfos/api/models/smartlist.py +237 -0
  60. deepfos/api/models/space.py +1137 -0
  61. deepfos/api/models/system.py +1065 -0
  62. deepfos/api/models/variable.py +463 -0
  63. deepfos/api/models/workflow.py +946 -0
  64. deepfos/api/platform.py +199 -0
  65. deepfos/api/python.py +90 -0
  66. deepfos/api/reconciliation_engine.py +181 -0
  67. deepfos/api/reconciliation_report.py +64 -0
  68. deepfos/api/role_strategy.py +234 -0
  69. deepfos/api/smartlist.py +69 -0
  70. deepfos/api/space.py +582 -0
  71. deepfos/api/system.py +372 -0
  72. deepfos/api/variable.py +154 -0
  73. deepfos/api/workflow.py +264 -0
  74. deepfos/boost/__init__.py +6 -0
  75. deepfos/boost/py_jstream.py +89 -0
  76. deepfos/boost/py_pandas.py +20 -0
  77. deepfos/cache.py +121 -0
  78. deepfos/config.py +6 -0
  79. deepfos/core/__init__.py +27 -0
  80. deepfos/core/cube/__init__.py +10 -0
  81. deepfos/core/cube/_base.py +462 -0
  82. deepfos/core/cube/constants.py +21 -0
  83. deepfos/core/cube/cube.py +408 -0
  84. deepfos/core/cube/formula.py +707 -0
  85. deepfos/core/cube/syscube.py +532 -0
  86. deepfos/core/cube/typing.py +7 -0
  87. deepfos/core/cube/utils.py +238 -0
  88. deepfos/core/dimension/__init__.py +11 -0
  89. deepfos/core/dimension/_base.py +506 -0
  90. deepfos/core/dimension/dimcreator.py +184 -0
  91. deepfos/core/dimension/dimension.py +472 -0
  92. deepfos/core/dimension/dimexpr.py +271 -0
  93. deepfos/core/dimension/dimmember.py +155 -0
  94. deepfos/core/dimension/eledimension.py +22 -0
  95. deepfos/core/dimension/filters.py +99 -0
  96. deepfos/core/dimension/sysdimension.py +168 -0
  97. deepfos/core/logictable/__init__.py +5 -0
  98. deepfos/core/logictable/_cache.py +141 -0
  99. deepfos/core/logictable/_operator.py +663 -0
  100. deepfos/core/logictable/nodemixin.py +673 -0
  101. deepfos/core/logictable/sqlcondition.py +609 -0
  102. deepfos/core/logictable/tablemodel.py +497 -0
  103. deepfos/db/__init__.py +36 -0
  104. deepfos/db/cipher.py +660 -0
  105. deepfos/db/clickhouse.py +191 -0
  106. deepfos/db/connector.py +195 -0
  107. deepfos/db/daclickhouse.py +171 -0
  108. deepfos/db/dameng.py +101 -0
  109. deepfos/db/damysql.py +189 -0
  110. deepfos/db/dbkits.py +358 -0
  111. deepfos/db/deepengine.py +99 -0
  112. deepfos/db/deepmodel.py +82 -0
  113. deepfos/db/deepmodel_kingbase.py +83 -0
  114. deepfos/db/edb.py +214 -0
  115. deepfos/db/gauss.py +83 -0
  116. deepfos/db/kingbase.py +83 -0
  117. deepfos/db/mysql.py +184 -0
  118. deepfos/db/oracle.py +131 -0
  119. deepfos/db/postgresql.py +192 -0
  120. deepfos/db/sqlserver.py +99 -0
  121. deepfos/db/utils.py +135 -0
  122. deepfos/element/__init__.py +89 -0
  123. deepfos/element/accounting.py +348 -0
  124. deepfos/element/apvlprocess.py +215 -0
  125. deepfos/element/base.py +398 -0
  126. deepfos/element/bizmodel.py +1269 -0
  127. deepfos/element/datatable.py +2467 -0
  128. deepfos/element/deep_pipeline.py +186 -0
  129. deepfos/element/deepconnector.py +59 -0
  130. deepfos/element/deepmodel.py +1806 -0
  131. deepfos/element/dimension.py +1254 -0
  132. deepfos/element/fact_table.py +427 -0
  133. deepfos/element/finmodel.py +1485 -0
  134. deepfos/element/journal.py +840 -0
  135. deepfos/element/journal_template.py +943 -0
  136. deepfos/element/pyscript.py +412 -0
  137. deepfos/element/reconciliation.py +553 -0
  138. deepfos/element/rolestrategy.py +243 -0
  139. deepfos/element/smartlist.py +457 -0
  140. deepfos/element/variable.py +756 -0
  141. deepfos/element/workflow.py +560 -0
  142. deepfos/exceptions/__init__.py +239 -0
  143. deepfos/exceptions/hook.py +86 -0
  144. deepfos/lazy.py +104 -0
  145. deepfos/lazy_import.py +84 -0
  146. deepfos/lib/__init__.py +0 -0
  147. deepfos/lib/_javaobj.py +366 -0
  148. deepfos/lib/asynchronous.py +879 -0
  149. deepfos/lib/concurrency.py +107 -0
  150. deepfos/lib/constant.py +39 -0
  151. deepfos/lib/decorator.py +310 -0
  152. deepfos/lib/deepchart.py +778 -0
  153. deepfos/lib/deepux.py +477 -0
  154. deepfos/lib/discovery.py +273 -0
  155. deepfos/lib/edb_lexer.py +789 -0
  156. deepfos/lib/eureka.py +156 -0
  157. deepfos/lib/filterparser.py +751 -0
  158. deepfos/lib/httpcli.py +106 -0
  159. deepfos/lib/jsonstreamer.py +80 -0
  160. deepfos/lib/msg.py +394 -0
  161. deepfos/lib/nacos.py +225 -0
  162. deepfos/lib/patch.py +92 -0
  163. deepfos/lib/redis.py +241 -0
  164. deepfos/lib/serutils.py +181 -0
  165. deepfos/lib/stopwatch.py +99 -0
  166. deepfos/lib/subtask.py +572 -0
  167. deepfos/lib/sysutils.py +703 -0
  168. deepfos/lib/utils.py +1003 -0
  169. deepfos/local.py +160 -0
  170. deepfos/options.py +670 -0
  171. deepfos/translation.py +237 -0
  172. deepfos-1.1.60.dist-info/METADATA +33 -0
  173. deepfos-1.1.60.dist-info/RECORD +175 -0
  174. deepfos-1.1.60.dist-info/WHEEL +5 -0
  175. deepfos-1.1.60.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1485 @@
1
+ import asyncio
2
+ import re
3
+ from enum import Enum
4
+ from itertools import chain
5
+ from typing import (
6
+ List, Dict, Optional, Union,
7
+ Tuple, Iterable, TYPE_CHECKING, Any, Set, Literal
8
+ )
9
+
10
+ import numpy as np
11
+ import pandas as pd
12
+ from pydantic import Field, parse_obj_as
13
+ from loguru import logger
14
+ import datetime
15
+ from multidict import MultiDict
16
+
17
+ from .base import ElementBase, SyncMeta
18
+ from .dimension import AsyncDimension, Dimension
19
+ from .datatable import (
20
+ DataTableClickHouse, AsyncDataTableClickHouse, get_table_class,
21
+ AsyncDataTableMySQL, DataTableMySQL, T_DatatableInstance, T_AsyncDatatableInstance
22
+ )
23
+ from deepfos.api.app import AppAPI
24
+ from deepfos.lib.asynchronous import future_property
25
+ from deepfos.lib.utils import (
26
+ unpack_expr, dict_to_expr, LazyDict, expr_to_dict,
27
+ dict_to_sql, split_dataframe, find_str, concat_url, CIEnumMeta
28
+ )
29
+ from deepfos.lib.constant import (
30
+ DFLT_DATA_COLUMN, VIEW, VIEW_DICT,
31
+ HIERARCHY, DECIMAL_COL, STRING_COL,
32
+ DFLT_COMMENT_COLUMN, COLUMN_USAGE_FIELD,
33
+ USED_FOR_COMMENT, USED_FOR_DATA
34
+ )
35
+ from deepfos.boost import pandas as bp
36
+ from deepfos.api.financial_model import FinancialModelAPI
37
+ from deepfos.api.models.financial_model import (
38
+ FinancialModelDto as CubeModel,
39
+ CubeQueryForOutVo, ReactSpreadsheetSaveForm,
40
+ SpreadsheetSingleData, ResultObj,
41
+ PcParams, CopyCalculateDTO,
42
+ TaskExecutionParam,
43
+ ParameterDefineDto, # noqa
44
+ FinancialDataDto
45
+ )
46
+ from deepfos.api.models.base import BaseModel
47
+ from deepfos.options import OPTION
48
+ from deepfos.lib.decorator import cached_property
49
+ from deepfos.exceptions import MDXExecuteTimeout, MDXExecuteFail
50
+
51
+ __all__ = [
52
+ 'AsyncFinancialCube',
53
+ 'FinancialCube',
54
+ 'RoundType',
55
+ ]
56
+
57
+
58
+ # -----------------------------------------------------------------------------
59
+ # utils
60
+ def is_valid_pov(body: str):
61
+ """维度表达式花括号内是否可以转化为pov"""
62
+ return not (';' in body or '(' in body)
63
+
64
+
65
+ def need_query(body: str):
66
+ return "(" in body
67
+
68
+
69
+ # -----------------------------------------------------------------------------
70
+ # models
71
+ class Description(BaseModel):
72
+ zh_cn: str = Field(None, alias='zh-cn')
73
+ en: Optional[str]
74
+
75
+
76
+ class DimensionInfo(BaseModel):
77
+ name: str
78
+ dimensionType: int
79
+ id: str
80
+ moduleId: str
81
+ table_closure: str
82
+ table_dimension: str
83
+ description: Description = Field(None, alias='multilingual')
84
+ folderId: str
85
+
86
+
87
+ class CubeInfo(BaseModel):
88
+ cubeFolderId: str
89
+ cubeName: str
90
+
91
+
92
+ class DataTableInfo(BaseModel):
93
+ name: str
94
+ actual_name: str
95
+
96
+
97
+ class MDXVariableParameter(ParameterDefineDto):
98
+ type = 0
99
+
100
+
101
+ class MDXCubeParameter(ParameterDefineDto):
102
+ type = 1
103
+
104
+
105
+ class RoundType(int, Enum, metaclass=CIEnumMeta):
106
+ """小数位数保留类型"""
107
+ #: 去尾法
108
+ floor = 0
109
+ #: 进一法
110
+ ceil = 1
111
+ #: 四舍五入
112
+ round = 2
113
+
114
+
115
+ _RE_USE_SECTION = re.compile(r'.*USE\s+\w+;', re.I | re.S)
116
+
117
+ # 可直接转为mdx member或简单集合函数方法的维度表达式
118
+ # 维度名由字母数字下划线和中划线组成,或者等于#root
119
+ _RE_SIMPLE_EXPR = re.compile(
120
+ r'(?P<hierarchy>i?(base|descendant|children))'
121
+ r'\s*\((?P<mbr>(\x23root)|([\w\.\-\[\]]+))\s*,'
122
+ r'\s*[01]\s*(,\s*(?P<with_parent>[01])\s*)?\)',
123
+ re.I
124
+ )
125
+ RE_NAME_WITH_PARENT = re.compile(r'\[(.+)]\.\[(.+)]')
126
+
127
+
128
+ # -----------------------------------------------------------------------------
129
+ # core classes
130
+ class AsyncFinancialCube(ElementBase[FinancialModelAPI]):
131
+ """财务模型
132
+
133
+ Args:
134
+ entry_object: 数据来源名模板,支持替换的字段为脚本元素名称或脚本全名,默认为python
135
+
136
+
137
+ Note:
138
+
139
+ 例如当前脚本元素名为demo,则如下初始化方式可在保存时,显示数据来源将为 ``Script for: demo``
140
+
141
+ .. code-block:: python
142
+
143
+ cube = FinancialCube(
144
+ element_name='test_cube',
145
+ entry_object='Script for: {script_name}'
146
+ )
147
+
148
+ entry_object的自定义命名逻辑实际实现:
149
+
150
+ 通过 .format 将 full_name 替换为 ``OPTION.general.task_info['script_name']``,
151
+ 将 ``script_name`` 替换为 full_name 被'.' split 后的最后一个名称
152
+
153
+ 本地测试时,可通过增加如下语句为OPTION.general.task_info赋值
154
+
155
+ .. code-block:: python
156
+
157
+ from deepfos.options import OPTION
158
+
159
+ OPTION.general.task_info = {'script_name': 'python.tt', 'task_id': ''}
160
+
161
+ 其中的值可以通过在平台上运行如下得到:
162
+
163
+ .. code-block:: python
164
+
165
+ print(OPTION.general.task_info)
166
+
167
+
168
+ """
169
+ def __init__(
170
+ self,
171
+ element_name: str,
172
+ folder_id: Optional[str] = None,
173
+ path: Optional[str] = None,
174
+ entry_object='python',
175
+ server_name: Optional[str] = None,
176
+ ):
177
+ full_name = OPTION.general.task_info.get('script_name', 'python')
178
+ self.entry_object = entry_object.format(script_name=full_name.split('.')[-1],
179
+ full_name=full_name)
180
+ super().__init__(element_name, folder_id, path, server_name)
181
+
182
+ @future_property(on_demand=True)
183
+ async def meta(self) -> CubeModel:
184
+ """财务Cube的元数据信息"""
185
+ api = await self.wait_for('async_api')
186
+ ele_info = await self.wait_for('element_info')
187
+ return await api.cube.data(
188
+ cubeName=self.element_name,
189
+ folderId=ele_info.folderId,
190
+ )
191
+
192
+ @future_property
193
+ async def _meta(self) -> FinancialDataDto:
194
+ """财务Cube的元数据信息"""
195
+ api = await self.wait_for('async_api')
196
+ ele_info = await self.wait_for('element_info')
197
+ return await api.cube.find_cube_data(
198
+ cubeName=self.element_name,
199
+ folderId=ele_info.folderId,
200
+ )
201
+
202
+ @cached_property
203
+ def dimensions(self) -> Dict[str, DimensionInfo]:
204
+ """财务Cube的维度信息"""
205
+ dim_memo = {}
206
+ for dim in self.meta.dimensions:
207
+ dim_info = parse_obj_as(DimensionInfo, dim["dimensionInfo"])
208
+ dim_memo[dim_info.name] = dim_info
209
+
210
+ return dim_memo
211
+
212
+ @cached_property
213
+ def account_col(self) -> str:
214
+ for dim in self._meta.cubeDimensionList:
215
+ if dim.dimensionUsage == 4:
216
+ return dim.datatableColumn
217
+
218
+ @cached_property
219
+ def dim_elements(self) -> LazyDict[str, AsyncDimension]:
220
+ """财务Cube的维度元素
221
+
222
+ 维度名 -> 维度元素的字典,延迟初始化,
223
+ 只会在使用时创建维度元素
224
+ """
225
+ dims = LazyDict[str, AsyncDimension]()
226
+ for dim in self._meta.cubeDimensionList or []:
227
+ if dim.dimensionName is None:
228
+ continue
229
+ dims[dim.dimensionName] = (
230
+ AsyncDimension,
231
+ dim.dimensionName,
232
+ dim.dimensionFolderId,
233
+ dim.dimensionPath,
234
+ False,
235
+ dim.dimensionServerName,
236
+ )
237
+ return dims
238
+
239
+ @cached_property
240
+ def dim_col_map(self) -> MultiDict[str]:
241
+ """维度名 -> 数据列名的字典"""
242
+ dc_map = MultiDict[str]()
243
+
244
+ for dim in self._meta.cubeDimensionList:
245
+ if dim.dimensionName is not None:
246
+ dc_map.add(dim.dimensionName, dim.datatableColumn)
247
+ return dc_map
248
+
249
+ @cached_property
250
+ def col_dim_map(self) -> Dict[str, str]:
251
+ """数据列名 -> 维度名的字典"""
252
+ return {
253
+ dim.datatableColumn: dim.dimensionName
254
+ for dim in self._meta.cubeDimensionList
255
+ if dim.dimensionName is not None
256
+ }
257
+
258
+ @cached_property
259
+ def fact_table(self) -> T_AsyncDatatableInstance:
260
+ """事实表对应的数据表"""
261
+ table_info = self._meta.datatable
262
+ init_args = dict(
263
+ element_name=table_info.name,
264
+ folder_id=table_info.folderId,
265
+ path=table_info.path,
266
+ table_name=table_info.actualTableName,
267
+ server_name=self._meta.datatableServerName
268
+ )
269
+ if (server_name := self._meta.datatableServerName) is None:
270
+ if self._meta.dataSync == 1:
271
+ return AsyncDataTableClickHouse(**init_args)
272
+ else:
273
+ return AsyncDataTableMySQL(**init_args)
274
+
275
+ return get_table_class(server_name, sync=False)(**init_args)
276
+
277
+ def _split_expr(
278
+ self,
279
+ cube_expr: str,
280
+ pov: Dict[str, str],
281
+ default_hierarchy: str = 'Base',
282
+ validate_expr: bool = True,
283
+ ) -> Tuple[str, Dict[str, str]]:
284
+ """解析维度表达式和pov
285
+
286
+ 取出维度表达式中的pov部分和当前pov合并,
287
+ 返回完整的表达式及pov
288
+ """
289
+ full_pov = {**pov}
290
+ exprs = []
291
+
292
+ if validate_expr:
293
+ get_colname = self._get_column_from_dim
294
+ all_cols = set(self.dim_col_map.values())
295
+ else:
296
+ get_colname = lambda x: x
297
+ all_cols = set()
298
+
299
+ cols_appeared = set(pov.keys())
300
+ for expr in cube_expr.split('->'):
301
+ dim, body = unpack_expr(expr)
302
+ dim = get_colname(dim)
303
+ cols_appeared.add(dim)
304
+ if is_valid_pov(body):
305
+ full_pov[dim] = body
306
+ else:
307
+ exprs.append(expr)
308
+
309
+ if validate_expr and self._meta.autoCalculation and VIEW not in cols_appeared:
310
+ raise ValueError(f"Missing dimension: '{VIEW}' in expression and pov.")
311
+
312
+ if default_hierarchy not in HIERARCHY:
313
+ raise ValueError(
314
+ f"Unknown hirerachy: {default_hierarchy}. "
315
+ f"Supported hierarchies are {list(HIERARCHY.values())}")
316
+
317
+ hierarchy = HIERARCHY[default_hierarchy]
318
+ exprs += [
319
+ "%s{%s(#root,0)}" % (dim, hierarchy)
320
+ for dim in all_cols - cols_appeared
321
+ ]
322
+ return '->'.join(exprs), full_pov
323
+
324
+ def _get_column_from_dim(self, dim: str) -> str:
325
+ """把维度名转化为数据表列名"""
326
+ dc_map = self.dim_col_map
327
+ if dim in dc_map.values():
328
+ return dim
329
+ if dim in dc_map:
330
+ return dc_map[dim]
331
+ if self._meta.autoCalculation and dim in VIEW_DICT:
332
+ return VIEW_DICT[dim]
333
+
334
+ raise ValueError(f"Dimension: '{dim}' does not belong to cube: '{self.element_name}'.")
335
+
336
+ def _maybe_get_column_from_dim(self, dim: str) -> str:
337
+ try:
338
+ return self._get_column_from_dim(dim)
339
+ except ValueError:
340
+ return dim
341
+
342
+ def _resolve_pov_as_dict(
343
+ self,
344
+ pov: Union[str, Dict[str, str]],
345
+ reslove_dim: bool = True,
346
+ ) -> Dict[str, str]:
347
+ """把pov转换为字典格式"""
348
+ if not pov:
349
+ return {}
350
+
351
+ new_pov = {}
352
+ if reslove_dim:
353
+ get_colname = self._get_column_from_dim
354
+ else:
355
+ get_colname = lambda x: x
356
+
357
+ def set_pov(dim, body):
358
+ if not is_valid_pov(body):
359
+ raise ValueError(f"Cannot convert expression: '{body}' to pov.")
360
+ new_pov[get_colname(dim)] = body
361
+
362
+ if isinstance(pov, str):
363
+ for expr in pov.split('->'):
364
+ set_pov(*unpack_expr(expr))
365
+ else:
366
+ for k, v in pov.items():
367
+ set_pov(k, v)
368
+ return new_pov
369
+
370
+ async def query(
371
+ self,
372
+ expression: str,
373
+ pov: Optional[Union[str, Dict[str, str]]] = None,
374
+ compact: bool = True,
375
+ pivot_dim: Optional[str] = None,
376
+ validate_expr: bool = True,
377
+ verify_access: bool = False,
378
+ include_ignored: bool = False,
379
+ normalize_view: bool = False,
380
+ ) -> Union[pd.DataFrame, Tuple[pd.DataFrame, Dict[str, str]]]:
381
+ """
382
+ 根据维度表达式以及pov获取cube数据
383
+
384
+ Args:
385
+ expression: 维度表达式
386
+ pov: Point Of View,维度表达式或者KV键值对格式,仅取一个维度成员。
387
+ compact: 是否将pov与查询数据分开输出以减少数据量
388
+ pivot_dim: 需要pivot的维度,将该维度的成员pivot到列上
389
+ validate_expr: 是否需要python校验/修改表达式,开启可能会导致额外的接口请求
390
+ verify_access: 是否带权限查询
391
+ include_ignored: 包含多版本实体维时,是否在结果中包含无效数据(即i列为1的数据)
392
+ normalize_view: 是否把大小写View统一成"View"
393
+
394
+ .. admonition:: 示例
395
+
396
+ .. code-block:: python
397
+ :emphasize-lines: 3,4
398
+
399
+ expr = 'Year{2021;2022}->Entiy{Base(TotalEntity,0)}'
400
+ cube = FinancialCube('example')
401
+ data, pov = cube.query(expr)
402
+ data = cube.query(expr, compact=False)
403
+
404
+ **注意最后2行的区别!**
405
+
406
+
407
+ Important:
408
+ 如果开启 ``validate_expr`` ,入参中的维度表达式(expression)
409
+ 将能够同时支持维度名和维度在事实表的数据列列名。
410
+ 但由于方法内部依赖的财务模型HTTP接口只支持数据列名,所以目前返回的
411
+ ``DataFrame`` 的列名将与数据列列名保持一致。
412
+
413
+ Returns:
414
+ 如果 ``compact=True`` (默认),返回 ``(DataFrame, dict)``
415
+ 格式的二元组,其中 ``DataFrame`` 为查询的主数据, ``dict`` 部分是pov
416
+
417
+ 如果指定 ``compact=False`` ,则会将pov部分的数据复制到主数据中,
418
+ 只返回一个 ``DataFrame``
419
+
420
+ """
421
+ pov = self._resolve_pov_as_dict(pov, validate_expr)
422
+ expression, full_pov = self._split_expr(expression, pov, validate_expr=validate_expr)
423
+ pov_expr = dict_to_expr(full_pov)
424
+
425
+ if not expression: # only pov
426
+ expression, pov_expr = pov_expr, expression
427
+ full_pov = {}
428
+
429
+ query_info = CubeQueryForOutVo(
430
+ cubeName=self.element_name,
431
+ folderId=self.element_info.folderId,
432
+ needAccess=verify_access,
433
+ commonScript=pov_expr,
434
+ script=expression
435
+ )
436
+ logger.debug(f"Query cube with expression: {expression}, pov: {pov_expr}")
437
+ rslt = await self.async_api.data.query(query_info)
438
+ data = pd.DataFrame(rslt['data'])
439
+
440
+ # i列为无效列
441
+ if 'i' in data.columns and not include_ignored:
442
+ data = data[data['i'] != 1]
443
+ data = data.drop(columns=['i'])
444
+
445
+ if data.empty:
446
+ columns = [*expr_to_dict(expression).keys(), DFLT_DATA_COLUMN]
447
+ data = pd.DataFrame(columns=columns)
448
+ data[DFLT_DATA_COLUMN] = data[DFLT_DATA_COLUMN].astype(float)
449
+
450
+ if normalize_view:
451
+ if (view := VIEW.lower()) in data.columns:
452
+ if VIEW in data.columns:
453
+ data[view] = np.where(data[VIEW].isnull(), data[view], data[VIEW])
454
+ data = data.drop(columns=[VIEW])
455
+ data = data.rename(columns=VIEW_DICT)
456
+
457
+ if pivot_dim is not None:
458
+ pivot_col = self._get_column_from_dim(pivot_dim)
459
+
460
+ if pivot_col in full_pov:
461
+ val = full_pov.pop(pivot_col)
462
+ data = data.rename(columns={DFLT_DATA_COLUMN: val})
463
+ elif pivot_col not in data.columns:
464
+ raise ValueError(
465
+ f"Pivot dimension: {pivot_dim} does not "
466
+ f"belong to cube: {self.element_name}.")
467
+ elif data.empty:
468
+ data = data.drop(columns=[pivot_col, DFLT_DATA_COLUMN])
469
+ else:
470
+ index = data.columns.difference({DFLT_DATA_COLUMN, pivot_col}).tolist()
471
+ drop_index = not index
472
+
473
+ data = data.pivot_table(
474
+ index=index, values=DFLT_DATA_COLUMN,
475
+ columns=pivot_col, aggfunc='first', fill_value=None
476
+ ).reset_index(drop=drop_index)
477
+ data.columns.name = None
478
+
479
+ if not compact:
480
+ return data.assign(**full_pov)
481
+ return data, full_pov
482
+
483
+ async def save(
484
+ self,
485
+ data: pd.DataFrame,
486
+ pov: Optional[Union[str, Dict[str, str]]] = None,
487
+ data_column: str = DFLT_DATA_COLUMN,
488
+ need_check: bool = True,
489
+ data_audit: bool = True,
490
+ chunksize: Optional[int] = None,
491
+ callback: bool = True,
492
+ comment_column: str = DFLT_COMMENT_COLUMN,
493
+ auth_mode: Literal[0, 1, 2, 3] = 0,
494
+ ):
495
+ """
496
+ 将DataFrame的数据保存至cube。
497
+
498
+ Args:
499
+ data: 需要保存的数据
500
+ pov: Point Of View,维度表达式或者KV键值对格式。
501
+ data_column: 数据列的列名
502
+ need_check: 是否需要java接口校验脏数据
503
+ data_audit: 是否需要记录到数据审计
504
+ chunksize: 单次调用保存接口时最大的dataframe行数。
505
+ 当data的行数超过此值时,将会分多次进行保存。
506
+ callback: 是否回调
507
+ comment_column: 备注列的列名,默认为VirtualMeasure_220922
508
+ auth_mode: 数据保存权鉴模式,默认为0,模式对应如下:
509
+ - 0: 继承财务模型权鉴模式
510
+ - 1: 宽松模式
511
+ - 2: 严格模式
512
+ - 3: 普通模式
513
+
514
+ Note:
515
+ 此方法会对落库数据做以下处理:
516
+
517
+ - 列名重命名:维度名->数据表列名
518
+ - 忽略多余数据列
519
+
520
+ See Also:
521
+ :meth:`save_unpivot`
522
+
523
+ """
524
+ if data.empty:
525
+ logger.info("Will not save to cube because dataframe is empty.")
526
+ return
527
+
528
+ if data_column not in data.columns:
529
+ raise ValueError(f"Missing data column: {data_column}.")
530
+
531
+ # set data column
532
+ if data_column != DFLT_DATA_COLUMN:
533
+ data = data.rename(columns={data_column: DFLT_DATA_COLUMN})
534
+
535
+ # set comment column
536
+ if comment_column != DFLT_COMMENT_COLUMN:
537
+ data = data.rename(columns={comment_column: DFLT_COMMENT_COLUMN})
538
+
539
+ # convert pov to dict
540
+ pov = self._resolve_pov_as_dict(pov)
541
+ # rename dimension columns to datatable columns
542
+ data = data.rename(columns=self._maybe_get_column_from_dim)
543
+ # check if all dimensions are presented
544
+ required_cols = set(self.dim_col_map.values()).\
545
+ union({DFLT_DATA_COLUMN}).difference(pov.keys())
546
+
547
+ if self._meta.autoCalculation:
548
+ # add column "view/View" to required columns
549
+ if VIEW in data.columns:
550
+ required_cols.add(VIEW)
551
+ elif not find_str(VIEW, pov, ignore_case=True):
552
+ raise ValueError(f"Missing column: '{VIEW}' in dataframe.")
553
+
554
+ if missing_dims := required_cols - set(data.columns):
555
+ raise ValueError(
556
+ f"Cannot save data because following columns are missing: {missing_dims}"
557
+ )
558
+
559
+ # include comment col if provided
560
+ if DFLT_COMMENT_COLUMN in data:
561
+ # use cmt col as data col for cmt data
562
+ cmt_data = data.drop(columns=[DFLT_DATA_COLUMN]).rename(
563
+ columns={DFLT_COMMENT_COLUMN: DFLT_DATA_COLUMN}
564
+ )
565
+ cmt_data = cmt_data.assign(**{COLUMN_USAGE_FIELD: USED_FOR_COMMENT})
566
+
567
+ data = data.drop(columns=[DFLT_COMMENT_COLUMN]).assign(
568
+ **{COLUMN_USAGE_FIELD: USED_FOR_DATA}
569
+ )
570
+ data = pd.concat([data, cmt_data])
571
+ required_cols.add(COLUMN_USAGE_FIELD)
572
+
573
+ return await self._save_impl(
574
+ data[list(required_cols)],
575
+ pov, need_check, data_audit, chunksize, callback, auth_mode
576
+ )
577
+
578
+ async def save_unpivot(
579
+ self,
580
+ data: pd.DataFrame,
581
+ unpivot_dim: str,
582
+ pov: Optional[Union[str, Dict[str, str]]] = None,
583
+ need_check: bool = True,
584
+ data_audit: bool = True,
585
+ chunksize: Optional[int] = None,
586
+ save_nan: bool = False,
587
+ callback: bool = True
588
+ ):
589
+ """保存有某个维度所有成员在列上的 ``DataFrame``
590
+
591
+ 为了方便后续计算,在调用 :meth:`query` 时,经常会指定
592
+ ``povit_dim='Account'`` 把科目维度成员转到列上,
593
+ 此方法可以方便地保存这类 ``DataFrame`` 。如果使用
594
+ :meth:`save`,则需要使用者重新把科目列转到行上。
595
+
596
+ Args:
597
+ data: 需要保存的数据
598
+ unpivot_dim: 成员在列上的维度
599
+ pov: Point Of View,维度表达式或者KV键值对格式。
600
+ need_check: 是否需要java接口校验脏数据
601
+ data_audit: 是否需要记录到数据审计
602
+ chunksize: 单次调用保存接口时最大的dataframe行数。
603
+ 当data的行数超过此值时,将会分多次进行保存。
604
+ save_nan: 当把数据列成员转换到行上时,data为空的数据是否保存
605
+ callback: 是否回调
606
+
607
+ Warnings:
608
+ 由于数据完整性等原因,此方法接收的dataframe数据列经常会有一些额外的空值。
609
+ 这些空值一般由计算带入,你并不希望保存它们,因此 ``save_nan`` 默认值为
610
+ ``False`` 。假如你确实需要保存nan值,请显式声明 ``save_nan=True``,
611
+ 注意,这实际会删除对应单元格的数据!
612
+
613
+ Note:
614
+ 出于性能考虑,此方法并不会查询 ``unpivot_dim`` 维度的所有成员,
615
+ 在保存的data中除去cube的所有维度后,剩下的数据列都会被认为是属于
616
+ ``unpivot_dim`` 维度的,因此为了确保正常保存且不引入垃圾数据,
617
+ 使用者需要保证传入的dataframe不含有多余数据列。
618
+
619
+ See Also:
620
+ | :meth:`query`
621
+ | :meth:`save`
622
+
623
+ """
624
+ if data.empty:
625
+ logger.info("Will not save to cube because dataframe is empty.")
626
+ return
627
+
628
+ data = data.rename(columns=self._maybe_get_column_from_dim)
629
+ dim = self._get_column_from_dim(unpivot_dim)
630
+ pov = self._resolve_pov_as_dict(pov)
631
+ data_cols = set(data.columns)
632
+ unpivot_cols = data_cols.difference(pov.keys(), self.dim_col_map.values())
633
+
634
+ if self._meta.autoCalculation:
635
+ unpivot_cols.discard(VIEW)
636
+
637
+ id_cols = data_cols - unpivot_cols
638
+ data = data.melt(
639
+ id_vars=id_cols, value_vars=unpivot_cols,
640
+ var_name=dim, value_name=DFLT_DATA_COLUMN
641
+ )
642
+
643
+ if not save_nan:
644
+ data = data.dropna()
645
+ if data.empty:
646
+ logger.info("Will not save to cube because dataframe is empty.")
647
+ return
648
+
649
+ return await self._save_impl(
650
+ data, pov, need_check, data_audit, chunksize, callback
651
+ )
652
+
653
+ async def _save_impl(
654
+ self,
655
+ data: pd.DataFrame,
656
+ pov: Optional[Dict[str, str]] = None,
657
+ need_check: bool = True,
658
+ data_audit: bool = True,
659
+ chunksize: Optional[int] = None,
660
+ callback: bool = True,
661
+ auth_mode: Literal[0, 1, 2, 3] = 0,
662
+ ):
663
+ # replace NaN to standard None
664
+ data = data.mask(data.isna(), None)
665
+ # ensure view is capitalized
666
+ if self._meta.autoCalculation:
667
+ data = data.rename(columns=VIEW_DICT)
668
+ # save data
669
+ resp = []
670
+ for batch_data in split_dataframe(data, chunksize):
671
+ row_data = [
672
+ {"columnDimensionMemberMap": row}
673
+ for row in bp.dataframe_to_dict(batch_data)
674
+ ]
675
+ payload = ReactSpreadsheetSaveForm(
676
+ entryObject=self.entry_object,
677
+ sheetDatas=[SpreadsheetSingleData(
678
+ cubeName=self.element_info.elementName,
679
+ cubeFolderId=self.element_info.folderId,
680
+ rowDatas=row_data,
681
+ commonMember=pov,
682
+ )],
683
+ needCheck=need_check,
684
+ dataAuditSwitch=data_audit,
685
+ entryMode=1,
686
+ validateDimensionMember=need_check,
687
+ callback=callback,
688
+ saveDataAuthMode=auth_mode
689
+ )
690
+ r = await self.async_api.reactspreadsheet.save(
691
+ payload.dict(exclude_unset=True)
692
+ )
693
+ resp.append(r)
694
+ return resp
695
+
696
+ async def delete_with_mdx(
697
+ self,
698
+ expression: Union[str, Dict[str, Union[List[str], str]]]
699
+ ):
700
+ """通过MDX脚本删除数据
701
+
702
+ 根据维度表达式删除Cube数据
703
+
704
+ Warnings:
705
+ 此方法将根据维度表达式生成对应的MDX脚本并执行MDX的Cleardata
706
+ 对于只有成员和单集合方法的表达式,可以直接转换为MDX的成员集合或集合函数表达式
707
+ 如为复杂表达式(例如包含聚合方法),则会查询实际对应的成员后,再组成MDX的成员集合
708
+
709
+ Args:
710
+ expression: 维度表达式
711
+
712
+ .. admonition:: 示例
713
+
714
+ 两种调用方式等价:
715
+
716
+ .. code-block:: python
717
+ :emphasize-lines: 3,8
718
+
719
+ cube = FinancialCube('example')
720
+ expr = 'Year{2021;2022}->Entiy{Base(TotalEntity,0)}'
721
+ r = cube.delete_with_mdx(expr)
722
+ expr_dict = {
723
+ "Year": ['2021', '2022'],
724
+ "Entity": "Base(TotalEntity,0)"
725
+ }
726
+ r = cube.delete_with_mdx(expr_dict)
727
+
728
+ Returns:
729
+ MDX执行结果
730
+
731
+ See Also:
732
+ :meth:`insert_null` :meth:`delete`
733
+
734
+ """
735
+ if isinstance(expression, dict):
736
+ expression = dict_to_expr(expression)
737
+
738
+ query_dims = []
739
+ all_cols = set(self.dim_col_map.values())
740
+ dimexprs = {}
741
+ cols_appeared = set()
742
+
743
+ def normalize_name(name_: str) -> str:
744
+ if RE_NAME_WITH_PARENT.match(name_):
745
+ return name_
746
+ return f'[{name_}]'
747
+
748
+ async def query_dim(
749
+ col_: str,
750
+ part_: str,
751
+ dim_name_: str
752
+ ) -> Tuple[str, Set[str]]:
753
+ result: List[Dict[str, Any]] = await self.dim_elements[dim_name_].query(
754
+ part_, fields=['name'], as_model=False
755
+ )
756
+ mbrs_ = set()
757
+ for item in result:
758
+ if (name := item.get('expectedName')) is not None:
759
+ mbrs_.add(f"[{col_}].{normalize_name(name)}")
760
+ else:
761
+ mbrs_.add(f"[{col_}].{normalize_name(item['name'])}")
762
+ return col_, mbrs_
763
+
764
+ for expr in expression.split('->'):
765
+ dim, body = unpack_expr(expr)
766
+ col = self._get_column_from_dim(dim)
767
+
768
+ for part in body.split(';'):
769
+ part = part.replace(' ', '')
770
+ if not part:
771
+ continue
772
+
773
+ cols_appeared.add(col)
774
+
775
+ if is_valid_pov(part):
776
+ dimexprs.setdefault(col, set()).add(
777
+ f"[{col}].{normalize_name(part)}"
778
+ )
779
+ elif match := _RE_SIMPLE_EXPR.match(part):
780
+ mbr = match.group('mbr')
781
+ hier = match.group('hierarchy').capitalize()
782
+ with_parent = match.group('with_parent')
783
+
784
+ if with_parent == '1':
785
+ dimexprs.setdefault(col, set()).add(
786
+ f"{hier}([{col}].{normalize_name(mbr)},WITH_PARENT)"
787
+ )
788
+ else:
789
+ dimexprs.setdefault(col, set()).add(
790
+ f"{hier}([{col}].{normalize_name(mbr)})"
791
+ )
792
+ else:
793
+ dim_name = self.col_dim_map.get(col, col)
794
+ query_dims.append(query_dim(col, part, dim_name))
795
+
796
+ for col in all_cols - cols_appeared:
797
+ dimexprs[col] = {f"Base([{col}].[#root])"}
798
+
799
+ dim_mbrs = await asyncio.gather(*query_dims)
800
+
801
+ for col, mbrs in dim_mbrs:
802
+ dimexprs.setdefault(col, set())
803
+ dimexprs[col] = dimexprs[col].union(mbrs)
804
+
805
+ # ClearData方法的维度范围优先使用科目维度
806
+ if account_col := self.account_col:
807
+ clear_data_expr = dimexprs.pop(account_col)
808
+ scope_expr = chain(*dimexprs.values())
809
+ else:
810
+ expr_list = list(dimexprs.values())
811
+ clear_data_expr = expr_list[0]
812
+ scope_expr = chain(*expr_list[1::])
813
+
814
+ script = """Scope(%s);\nCleardata(%s);\nEnd Scope;
815
+ """ % (','.join(scope_expr), ','.join(clear_data_expr))
816
+
817
+ return await self.mdx_execution(script)
818
+
819
+ async def delete(
820
+ self,
821
+ expression: Union[str, Dict[str, Union[List[str], str]]],
822
+ chunksize: Optional[int] = None,
823
+ use_mdx: bool = False,
824
+ callback: bool = True
825
+ ):
826
+ """删除数据
827
+
828
+ 根据维度表达式删除Cube数据。
829
+
830
+ Warnings:
831
+ 此方法首先查询数据,并且替换为null再调用保存接口。
832
+ 因此如果要删除的数据量大,可能导致内存不足等问题。
833
+ 如果不需要数据审计功能,请使用 :meth:`insert_null`
834
+
835
+ Args:
836
+ expression: 维度表达式
837
+ chunksize: 单次调用保存接口时最大的dataframe行数。
838
+ 当data的行数超过此值时,将会分多次进行保存。
839
+ use_mdx: 是否使用MDX脚本实现,默认为否,等效于调用 :meth:`delete_with_mdx`
840
+ callback: 是否回调
841
+
842
+ .. admonition:: 示例
843
+
844
+ 两种调用方式等价:
845
+
846
+ .. code-block:: python
847
+ :emphasize-lines: 3,8
848
+
849
+ cube = FinancialCube('example')
850
+ expr = 'Year{2021;2022}->Entiy{Base(TotalEntity,0)}'
851
+ r = cube.delete(expr)
852
+ expr_dict = {
853
+ "Year": ['2021', '2022'],
854
+ "Entity": "Base(TotalEntity,0)"
855
+ }
856
+ r = cube.delete(expr_dict)
857
+
858
+ Returns:
859
+ 删除结果
860
+
861
+ See Also:
862
+ :meth:`insert_null` :meth:`delete_with_mdx`
863
+
864
+ """
865
+ if use_mdx:
866
+ return await self.delete_with_mdx(expression)
867
+
868
+ if self._meta.autoCalculation:
869
+ if isinstance(expression, str):
870
+ expression = expr_to_dict(expression)
871
+ expression = {**expression}
872
+
873
+ if isinstance(expression, dict):
874
+ expression = dict_to_expr(expression)
875
+
876
+ data, pov = await self.query(expression)
877
+ data[DFLT_DATA_COLUMN] = None
878
+ return await self.save(
879
+ data, pov, data_audit=True, chunksize=chunksize, callback=callback
880
+ )
881
+
882
+ async def queries(
883
+ self,
884
+ expressions: Iterable[str],
885
+ drop_duplicates: bool = True,
886
+ normalize_view: bool = False,
887
+ ) -> pd.DataFrame:
888
+ """查询多个表达式
889
+
890
+ 协程并发查询多个维度表达式,并且将查询结果合并为一个
891
+ :obj:`DataFrame` 。
892
+
893
+ Args:
894
+ expressions: 待查询的维度表达式列表
895
+ drop_duplicates: 是否需要去重
896
+ normalize_view: 是否把大小写View统一成"View"
897
+
898
+ Returns:
899
+ 查询结果
900
+
901
+ """
902
+ if isinstance(expressions, str):
903
+ return await self.query(
904
+ expressions, compact=False, normalize_view=normalize_view)
905
+
906
+ expressions = list(expressions)
907
+ if len(expressions) == 1:
908
+ return await self.query(
909
+ expressions[0], compact=False, normalize_view=normalize_view)
910
+
911
+ df_list = await asyncio.gather(*(
912
+ self.query(expr, compact=False, normalize_view=True)
913
+ for expr in expressions
914
+ ))
915
+
916
+ if not df_list:
917
+ dflt_cols = list(self.dim_col_map.values()) + [DFLT_DATA_COLUMN]
918
+ data = pd.DataFrame(columns=dflt_cols)
919
+ data[DFLT_DATA_COLUMN] = data[DFLT_DATA_COLUMN].astype(float)
920
+ else:
921
+ data = pd.concat(df_list, sort=False)
922
+ if drop_duplicates:
923
+ dim_cols = data.columns.difference([DFLT_DATA_COLUMN])
924
+ data = data.drop_duplicates(dim_cols)
925
+ if not normalize_view:
926
+ data = data.rename(columns={VIEW: "view"})
927
+ return data
928
+
929
+ async def pc_init(
930
+ self,
931
+ process_map: Optional[Dict[str, str]] = None,
932
+ data_block_map: Optional[Dict[str, str]] = None,
933
+ block_name: Optional[str] = None,
934
+ block_list: Optional[list] = None,
935
+ status: Optional[str] = None,
936
+ ) -> ResultObj:
937
+ """
938
+ cube权限初始化
939
+
940
+ Args:
941
+ process_map: 流程控制字段(key:字段名,value:维度表达式)
942
+ data_block_map: 审批单元(key:字段名,value:维度表达式) -- 自动创建
943
+ block_name: 审批单元名称 -- 非自动创建
944
+ block_list: 审批单元集合 -- 非自动创建
945
+ status: 初始化后的审批状态
946
+
947
+ Returns:
948
+ 初始化结果
949
+
950
+ .. admonition:: 示例
951
+
952
+ .. code-block:: python
953
+
954
+ cube = FinancialCube('example')
955
+ process_map = {'Year': 'Year{2021;2022}'}
956
+ data_block_map = {'Entity': 'Entity{Base(T01,0)}'}
957
+ r = cube.pc_init(process_map=process_map, data_block_map=data_block_map, status='1')
958
+
959
+ """
960
+ return await self.async_api.block.pc_init(PcParams(
961
+ blockList=block_list,
962
+ blockName=block_name,
963
+ cubeFolderId=self.element_info.folderId,
964
+ cubeName=self.element_name,
965
+ datablockMap=data_block_map,
966
+ processMap=process_map,
967
+ status=status))
968
+
969
+ async def pc_update(
970
+ self,
971
+ status: str,
972
+ process_map: Optional[Dict[str, str]] = None,
973
+ data_block_map: Optional[Dict[str, str]] = None,
974
+ block_name: Optional[str] = None,
975
+ block_list: Optional[list] = None,
976
+ ) -> Any:
977
+ """
978
+ cube权限状态更新
979
+
980
+ 与 :meth:`pc_upsert` 区别在于,此方法调用的接口为 /block/pc-status,
981
+ 该接口对指定范围的数据块权限进行update操作,无则不做更改和新增,有则更新状态
982
+
983
+ Args:
984
+ process_map: 流程控制字段(key:字段名,value:维度表达式)
985
+ data_block_map: 审批单元(key:字段名,value:维度表达式) -- 自动创建
986
+ block_name: 审批单元名称 -- 非自动创建
987
+ block_list: 审批单元集合 -- 非自动创建
988
+ status: 更新后的审批状态
989
+
990
+ Returns:
991
+ 更新结果
992
+
993
+ .. admonition:: 示例
994
+
995
+ .. code-block:: python
996
+
997
+ cube = FinancialCube('example')
998
+ process_map = {'Year': 'Year{2021;2022}'}
999
+ data_block_map = {'Entity': 'Entity{T0101}'}
1000
+ r = cube.pc_update(process_map=process_map, data_block_map=data_block_map, status='2')
1001
+
1002
+ See Also:
1003
+ :meth:`pc_upsert`
1004
+
1005
+ """
1006
+ return await self.async_api.block.pc_status(PcParams(
1007
+ blockList=block_list,
1008
+ blockName=block_name,
1009
+ cubeFolderId=self.element_info.folderId,
1010
+ cubeName=self.element_name,
1011
+ datablockMap=data_block_map,
1012
+ processMap=process_map,
1013
+ status=status))
1014
+
1015
+ async def pc_upsert(
1016
+ self,
1017
+ status: str,
1018
+ process_map: Optional[Dict[str, str]] = None,
1019
+ data_block_map: Optional[Dict[str, str]] = None,
1020
+ block_name: Optional[str] = None,
1021
+ block_list: Optional[list] = None,
1022
+ ) -> Any:
1023
+ """
1024
+ cube权限状态upsert更新
1025
+
1026
+ 与 :meth:`pc_update` 区别在于,此方法调用的接口为 /block/pc-status-upsert,
1027
+ 该接口对指定范围的数据块权限进行upsert操作,无则新增,有则更新状态
1028
+
1029
+ Args:
1030
+ process_map: 流程控制字段(key:字段名,value:维度表达式)
1031
+ data_block_map: 审批单元(key:字段名,value:维度表达式) -- 自动创建
1032
+ block_name: 审批单元名称 -- 非自动创建
1033
+ block_list: 审批单元集合 -- 非自动创建
1034
+ status: 更新后的审批状态
1035
+
1036
+ Returns:
1037
+ 更新结果
1038
+
1039
+ .. admonition:: 示例
1040
+
1041
+ .. code-block:: python
1042
+
1043
+ cube = FinancialCube('example')
1044
+ process_map = {'Year': 'Year{2021;2022}'}
1045
+ data_block_map = {'Entity': 'Entity{T0101}'}
1046
+ r = cube.pc_upsert(process_map=process_map, data_block_map=data_block_map, status='2')
1047
+
1048
+ See Also:
1049
+ :meth:`pc_update`
1050
+
1051
+ """
1052
+ return await self.async_api.block.pc_status_upsert(PcParams(
1053
+ blockList=block_list,
1054
+ blockName=block_name,
1055
+ cubeFolderId=self.element_info.folderId,
1056
+ cubeName=self.element_name,
1057
+ datablockMap=data_block_map,
1058
+ processMap=process_map,
1059
+ status=status))
1060
+
1061
+ async def copy_calculate(self, formula: str, fix_members: str):
1062
+ """
1063
+ cube copy计算接口
1064
+
1065
+ Args:
1066
+ formula: 维度成员来源和目的的表达式
1067
+ fix_members: 维度成员的筛选表达式
1068
+
1069
+ Returns:
1070
+ 更新结果
1071
+
1072
+ .. admonition:: 示例
1073
+
1074
+ .. code-block:: python
1075
+
1076
+ cube = FinancialCube('test')
1077
+ r = cube.copy_calculate(formula="Account{a1}=Account{a2}",
1078
+ fix_members="Entity{e1}->Year{y1}")
1079
+
1080
+ 将把Entity{e1}->Year{y1}->Account{a2}的数据复制一份到Entity{e1}->Year{y1}->Account{a1}
1081
+
1082
+ """
1083
+ return await self.async_api.extra.copyCalculate(
1084
+ CopyCalculateDTO(
1085
+ cubeFolderId=self.element_info.folderId,
1086
+ cubeName=self.element_name,
1087
+ cubePath=self._path,
1088
+ formula=formula,
1089
+ fixMembers=fix_members,
1090
+ entryObject="python"
1091
+ )
1092
+ )
1093
+
1094
+ async def insert_null(
1095
+ self,
1096
+ expression: Union[str, Dict[str, Union[List[str], str]]],
1097
+ query_all: bool = False,
1098
+ ):
1099
+ """使用insert null方式删除数据
1100
+
1101
+ 根据维度表达式删除Cube数据。
1102
+ 入参与 :meth:`delete` 相同,
1103
+ 在使用clickhouse作事实表时推荐使用本方法,
1104
+ 在使用MySQL作事实表时,等同于调用 :meth:`delete` 。
1105
+
1106
+ Args:
1107
+ expression: 维度表达式
1108
+ query_all: 是否查询所有维度,在事实表为clickhouse时起作用
1109
+
1110
+ .. admonition:: 示例
1111
+
1112
+ 两种调用方式等价:
1113
+
1114
+ .. code-block:: python
1115
+ :emphasize-lines: 3,8
1116
+
1117
+ cube = FinancialCube('example')
1118
+ expr = 'Year{2021;2022}->Entiy{Base(TotalEntity,0)}'
1119
+ r = cube.insert_null(expr)
1120
+ expr_dict = {
1121
+ "Year": ['2021', '2022'],
1122
+ "Entity": "Base(TotalEntity,0)"
1123
+ }
1124
+ r = cube.insert_null(expr_dict)
1125
+
1126
+ Returns:
1127
+ insert sql的执行结果
1128
+
1129
+ See Also:
1130
+ :meth:`delete`
1131
+
1132
+ """
1133
+ if not isinstance(
1134
+ self.fact_table,
1135
+ (AsyncDataTableClickHouse, DataTableClickHouse)
1136
+ ):
1137
+ return await self.delete(expression)
1138
+
1139
+ if isinstance(expression, str):
1140
+ expression = expr_to_dict(expression)
1141
+
1142
+ if self._meta.autoCalculation:
1143
+ expression = {**expression}
1144
+ expression.pop(VIEW, None)
1145
+
1146
+ member_dict = {}
1147
+ coros = []
1148
+ columns = []
1149
+
1150
+ for dim, expr in expression.items():
1151
+ col = self.dim_col_map.get(dim, dim)
1152
+ dim = self.col_dim_map.get(dim, dim)
1153
+
1154
+ if isinstance(expr, list):
1155
+ member_dict[col] = expr
1156
+ elif query_all or need_query(expr):
1157
+ coros.append(self.dim_elements[dim].query(
1158
+ expr, fields=['name'], as_model=False
1159
+ ))
1160
+ columns.append(col)
1161
+ else:
1162
+ member_dict[col] = expr.split(';')
1163
+
1164
+ for col, rslt in zip(columns, await asyncio.gather(*coros)):
1165
+ member_dict[col] = [item['name'] for item in rslt]
1166
+
1167
+ where = dict_to_sql(member_dict, '=', bracket=False)
1168
+ fact_table = self.fact_table
1169
+
1170
+ all_dims = ','.join(f"`{col}`" for col in self.dim_col_map.values())
1171
+ decimal_val = f"if(" \
1172
+ f"argMax(ifNull(toString({DECIMAL_COL}),'isnull'),createtime)='isnull'," \
1173
+ f"null," \
1174
+ f"argMax({DECIMAL_COL},createtime)" \
1175
+ f") as `{DECIMAL_COL}`"
1176
+ string_val = f"if(" \
1177
+ f"argMax(ifNull({STRING_COL},'isnull'),createtime)='isnull'," \
1178
+ f"null," \
1179
+ f"argMax({STRING_COL},createtime)" \
1180
+ f") AS `{STRING_COL}`"
1181
+
1182
+ sub_query = f"SELECT " \
1183
+ f"{all_dims},{decimal_val},{string_val} " \
1184
+ f"from {fact_table.table_name} " \
1185
+ f"where {where} " \
1186
+ f"GROUP BY {all_dims}"
1187
+
1188
+ sub_dims = ','.join(f"main_table.{col}" for col in self.dim_col_map.values())
1189
+ # now = int(datetime.datetime.now().timestamp() * 1000) + OPTION.api.clock_offset
1190
+ now = "toUnixTimestamp64Milli(now64(3))"
1191
+ main_query = f"SELECT * from ({sub_query}) sub_table " \
1192
+ f"where `{DECIMAL_COL}` is not null or `{STRING_COL}` is not null"
1193
+ replace_null = f"SELECT {sub_dims},null as {DECIMAL_COL},null as {STRING_COL},{now} " \
1194
+ f"from ({main_query}) main_table " \
1195
+
1196
+ sql = f"INSERT INTO {fact_table.table_name} " \
1197
+ f"({all_dims},{DECIMAL_COL},{STRING_COL}, createtime) {replace_null};"
1198
+ return await fact_table.run_sql(sql)
1199
+
1200
+ async def mdx_execution(
1201
+ self,
1202
+ script: str,
1203
+ parameters: Optional[Dict[str, str]] = None,
1204
+ precision: Optional[int] = None,
1205
+ timeout: Optional[int] = None,
1206
+ round_type: Union[RoundType, str] = RoundType.floor,
1207
+ ):
1208
+ """执行MDX计算语句
1209
+
1210
+ Args:
1211
+ script: MDX计算语句
1212
+ parameters: MDX执行所需的标量参数信息键值对
1213
+ precision: 计算精度,默认为财务模型小数精度
1214
+ timeout: 超时时间(ms),默认为180秒(与OPTION.api.timeout保持一致),
1215
+ 如为None,则为接口的默认值60秒,
1216
+ 目前该接口不支持设置为无限等待执行结果
1217
+ round_type: 小数保留类型,默认为去尾法
1218
+
1219
+ .. admonition:: 示例
1220
+
1221
+ .. code-block:: python
1222
+
1223
+ cube = FinancialCube('example')
1224
+
1225
+ # 用2022年每个月份所有产品的销售量
1226
+ # 乘以各产品设定在Begbalance期间成员上的单价
1227
+ # 得到各个产品的销售额
1228
+ script = '''
1229
+ Scope(strToMember($scenario),
1230
+ [Version].[V1],
1231
+ [Year].[2022],
1232
+ MemberSet(strToMember('Period',$period)),
1233
+ Base([Product].[TotalProduct]),
1234
+ Base([Entity].[TotalEntity])
1235
+ );
1236
+ [Account].[Total_Sales] = [Account].[Volume]*[Account].[Price]->[Period].[Begbalance];
1237
+ End Scope;
1238
+ '''
1239
+
1240
+ # 执行MDX语句,并指定参数scenario为'[Scenario].[actual]',period为'Q1'
1241
+ # 小数保留类型为四舍五入
1242
+ cube.mdx_execution(
1243
+ script=script,
1244
+ parameters={'scenario': '[Scenario].[actual]','period': 'Q1'},
1245
+ round_type='round'
1246
+ )
1247
+
1248
+
1249
+ Returns:
1250
+ 执行结果
1251
+
1252
+ Important:
1253
+ script不可包含use section部分,use的Cube固定为当前Financial Cube
1254
+
1255
+ """
1256
+ if _RE_USE_SECTION.match(script.upper()):
1257
+ raise ValueError(
1258
+ 'MDX语句中发现use section,在FinancialCube中使用时,'
1259
+ '固定为当前Cube,不支持指定其他Cube'
1260
+ )
1261
+
1262
+ if timeout is None:
1263
+ timeout = OPTION.api.timeout * 1000
1264
+
1265
+ business_id = (
1266
+ f"PythonScript_{OPTION.general.task_info.get('script_name', '')}_MDX"
1267
+ f"-{datetime.datetime.now().strftime('%Y%m%d_%H%M%S_%f')}"
1268
+ )
1269
+ params = []
1270
+
1271
+ if parameters is not None:
1272
+ if not isinstance(parameters, dict):
1273
+ raise TypeError('parameters参数应为字典')
1274
+
1275
+ for key, value in parameters.items():
1276
+ params.append(MDXVariableParameter(key=key, value=value))
1277
+
1278
+ path = self._path
1279
+
1280
+ if path is None:
1281
+ path = await AppAPI(sync=False).folder.get_folder_full(
1282
+ self.element_info.folderId
1283
+ )
1284
+
1285
+ path = path.replace('\\', '/')
1286
+
1287
+ params.append(
1288
+ MDXCubeParameter(
1289
+ key=self.element_name,
1290
+ value=concat_url(path, f"{self.element_name}.cub")
1291
+ )
1292
+ )
1293
+
1294
+ res = await self.async_api.mdxtask.execution(
1295
+ TaskExecutionParam(
1296
+ businessId=business_id,
1297
+ decimalDigitsType=RoundType[round_type],
1298
+ parameters=params,
1299
+ precision=precision,
1300
+ script=f'Use {self.element_name};\n{script}',
1301
+ timeout=timeout
1302
+ )
1303
+ )
1304
+
1305
+ if res.status == 1:
1306
+ raise MDXExecuteTimeout(f'MDX执行超时,具体响应:\n{res}')
1307
+
1308
+ if res.result is False:
1309
+ raise MDXExecuteFail(
1310
+ f'MDX执行失败,失败原因:\n{res.failReason}'
1311
+ )
1312
+
1313
+ return res
1314
+
1315
+
1316
+ class FinancialCube(AsyncFinancialCube, metaclass=SyncMeta):
1317
+ synchronize = (
1318
+ 'query',
1319
+ 'queries',
1320
+ 'save',
1321
+ 'save_unpivot',
1322
+ 'delete',
1323
+ 'delete_with_mdx',
1324
+ 'pc_init',
1325
+ 'pc_update',
1326
+ 'pc_upsert',
1327
+ 'insert_null',
1328
+ 'copy_calculate',
1329
+ 'mdx_execution'
1330
+ )
1331
+
1332
+ if TYPE_CHECKING: # pragma: no cover
1333
+ def queries(
1334
+ self,
1335
+ expressions: Iterable[str],
1336
+ drop_duplicates: bool = True,
1337
+ normalize_view: bool = False,
1338
+ ) -> pd.DataFrame:
1339
+ ...
1340
+
1341
+ def query(
1342
+ self,
1343
+ expression: str,
1344
+ pov: Optional[Union[str, Dict[str, str]]] = None,
1345
+ compact: bool = True,
1346
+ pivot_dim: Optional[str] = None,
1347
+ validate_expr: bool = True,
1348
+ verify_access: bool = False,
1349
+ include_ignored: bool = False,
1350
+ normalize_view: bool = False,
1351
+ ) -> Union[pd.DataFrame, Tuple[pd.DataFrame, Dict[str, str]]]:
1352
+ ...
1353
+
1354
+ def save(
1355
+ self,
1356
+ data: pd.DataFrame,
1357
+ pov: Optional[Union[str, Dict[str, str]]] = None,
1358
+ data_column: str = DFLT_DATA_COLUMN,
1359
+ need_check: bool = True,
1360
+ data_audit: bool = True,
1361
+ chunksize: Optional[int] = None,
1362
+ callback: bool = True,
1363
+ comment_column: str = DFLT_COMMENT_COLUMN,
1364
+ auth_mode: Literal[0, 1, 2, 3] = 0,
1365
+ ):
1366
+ ...
1367
+
1368
+ def save_unpivot(
1369
+ self,
1370
+ data: pd.DataFrame,
1371
+ unpivot_dim: str,
1372
+ pov: Optional[Union[str, Dict[str, str]]] = None,
1373
+ need_check: bool = True,
1374
+ data_audit: bool = True,
1375
+ chunksize: Optional[int] = None,
1376
+ save_nan: bool = False,
1377
+ callback: bool = True
1378
+ ):
1379
+ ...
1380
+
1381
+ def delete(
1382
+ self,
1383
+ expression: Union[str, Dict[str, Union[List[str], str]]],
1384
+ chunksize: Optional[int] = None,
1385
+ use_mdx: bool = False,
1386
+ callback: bool = True
1387
+ ):
1388
+ ...
1389
+
1390
+ def delete_with_mdx(
1391
+ self,
1392
+ expression: Union[str, Dict[str, Union[List[str], str]]]
1393
+ ):
1394
+ ...
1395
+
1396
+ def pc_init(
1397
+ self,
1398
+ process_map: Optional[Dict[str, str]] = None,
1399
+ data_block_map: Optional[Dict[str, str]] = None,
1400
+ block_name: Optional[str] = None,
1401
+ block_list: Optional[list] = None,
1402
+ status: Optional[str] = None,
1403
+ ):
1404
+ ...
1405
+
1406
+ def pc_update(
1407
+ self,
1408
+ status: str,
1409
+ process_map: Optional[Dict[str, str]] = None,
1410
+ data_block_map: Optional[Dict[str, str]] = None,
1411
+ block_name: Optional[str] = None,
1412
+ block_list: Optional[list] = None,
1413
+ ):
1414
+ ...
1415
+
1416
+ def pc_upsert(
1417
+ self,
1418
+ status: str,
1419
+ process_map: Optional[Dict[str, str]] = None,
1420
+ data_block_map: Optional[Dict[str, str]] = None,
1421
+ block_name: Optional[str] = None,
1422
+ block_list: Optional[list] = None,
1423
+ ) -> Any:
1424
+ ...
1425
+
1426
+ def insert_null(
1427
+ self,
1428
+ expression: Union[str, Dict[str, Union[List[str], str]]],
1429
+ query_all: bool = False,
1430
+ ):
1431
+ ...
1432
+
1433
+ def copy_calculate(self, formula: str, fix_members: str):
1434
+ ...
1435
+
1436
+ def mdx_execution(
1437
+ self,
1438
+ script: str,
1439
+ parameters: Optional[Dict[str, str]] = None,
1440
+ precision: Optional[int] = None,
1441
+ timeout: Optional[int] = OPTION.api.timeout * 1000,
1442
+ round_type: Union[RoundType, str] = RoundType.floor,
1443
+ ):
1444
+ ...
1445
+
1446
+ @cached_property
1447
+ def dim_elements(self) -> LazyDict[str, Dimension]:
1448
+ """财务Cube的维度元素
1449
+
1450
+ 维度名 -> 维度元素的字典,延迟初始化,
1451
+ 只会在使用时创建维度元素
1452
+ """
1453
+ dims = LazyDict()
1454
+ for dim in self._meta.cubeDimensionList:
1455
+ if dim.dimensionName is None:
1456
+ continue
1457
+ dims[dim.dimensionName] = (
1458
+ Dimension,
1459
+ dim.dimensionName,
1460
+ dim.dimensionFolderId,
1461
+ dim.dimensionPath,
1462
+ False,
1463
+ dim.dimensionServerName,
1464
+ )
1465
+ return dims
1466
+
1467
+ @cached_property
1468
+ def fact_table(self) -> T_DatatableInstance:
1469
+ """事实表对应的数据表"""
1470
+ table_info = self._meta.datatable
1471
+ init_args = dict(
1472
+ element_name=table_info.name,
1473
+ folder_id=table_info.folderId,
1474
+ path=table_info.path,
1475
+ table_name=table_info.actualTableName,
1476
+ server_name=self._meta.datatableServerName
1477
+ )
1478
+
1479
+ if (server_name := self._meta.datatableServerName) is None:
1480
+ if self._meta.dataSync == 1:
1481
+ return DataTableClickHouse(**init_args)
1482
+ else:
1483
+ return DataTableMySQL(**init_args)
1484
+
1485
+ return get_table_class(server_name)(**init_args)