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.
- deepfos/__init__.py +6 -0
- deepfos/_version.py +21 -0
- deepfos/algo/__init__.py +0 -0
- deepfos/algo/graph.py +171 -0
- deepfos/algo/segtree.py +31 -0
- deepfos/api/V1_1/__init__.py +0 -0
- deepfos/api/V1_1/business_model.py +119 -0
- deepfos/api/V1_1/dimension.py +599 -0
- deepfos/api/V1_1/models/__init__.py +0 -0
- deepfos/api/V1_1/models/business_model.py +1033 -0
- deepfos/api/V1_1/models/dimension.py +2768 -0
- deepfos/api/V1_2/__init__.py +0 -0
- deepfos/api/V1_2/dimension.py +285 -0
- deepfos/api/V1_2/models/__init__.py +0 -0
- deepfos/api/V1_2/models/dimension.py +2923 -0
- deepfos/api/__init__.py +0 -0
- deepfos/api/account.py +167 -0
- deepfos/api/accounting_engines.py +147 -0
- deepfos/api/app.py +626 -0
- deepfos/api/approval_process.py +198 -0
- deepfos/api/base.py +983 -0
- deepfos/api/business_model.py +160 -0
- deepfos/api/consolidation.py +129 -0
- deepfos/api/consolidation_process.py +106 -0
- deepfos/api/datatable.py +341 -0
- deepfos/api/deep_pipeline.py +61 -0
- deepfos/api/deepconnector.py +36 -0
- deepfos/api/deepfos_task.py +92 -0
- deepfos/api/deepmodel.py +188 -0
- deepfos/api/dimension.py +486 -0
- deepfos/api/financial_model.py +319 -0
- deepfos/api/journal_model.py +119 -0
- deepfos/api/journal_template.py +132 -0
- deepfos/api/memory_financial_model.py +98 -0
- deepfos/api/models/__init__.py +3 -0
- deepfos/api/models/account.py +483 -0
- deepfos/api/models/accounting_engines.py +756 -0
- deepfos/api/models/app.py +1338 -0
- deepfos/api/models/approval_process.py +1043 -0
- deepfos/api/models/base.py +234 -0
- deepfos/api/models/business_model.py +805 -0
- deepfos/api/models/consolidation.py +711 -0
- deepfos/api/models/consolidation_process.py +248 -0
- deepfos/api/models/datatable_mysql.py +427 -0
- deepfos/api/models/deep_pipeline.py +55 -0
- deepfos/api/models/deepconnector.py +28 -0
- deepfos/api/models/deepfos_task.py +386 -0
- deepfos/api/models/deepmodel.py +308 -0
- deepfos/api/models/dimension.py +1576 -0
- deepfos/api/models/financial_model.py +1796 -0
- deepfos/api/models/journal_model.py +341 -0
- deepfos/api/models/journal_template.py +854 -0
- deepfos/api/models/memory_financial_model.py +478 -0
- deepfos/api/models/platform.py +178 -0
- deepfos/api/models/python.py +221 -0
- deepfos/api/models/reconciliation_engine.py +411 -0
- deepfos/api/models/reconciliation_report.py +161 -0
- deepfos/api/models/role_strategy.py +884 -0
- deepfos/api/models/smartlist.py +237 -0
- deepfos/api/models/space.py +1137 -0
- deepfos/api/models/system.py +1065 -0
- deepfos/api/models/variable.py +463 -0
- deepfos/api/models/workflow.py +946 -0
- deepfos/api/platform.py +199 -0
- deepfos/api/python.py +90 -0
- deepfos/api/reconciliation_engine.py +181 -0
- deepfos/api/reconciliation_report.py +64 -0
- deepfos/api/role_strategy.py +234 -0
- deepfos/api/smartlist.py +69 -0
- deepfos/api/space.py +582 -0
- deepfos/api/system.py +372 -0
- deepfos/api/variable.py +154 -0
- deepfos/api/workflow.py +264 -0
- deepfos/boost/__init__.py +6 -0
- deepfos/boost/py_jstream.py +89 -0
- deepfos/boost/py_pandas.py +20 -0
- deepfos/cache.py +121 -0
- deepfos/config.py +6 -0
- deepfos/core/__init__.py +27 -0
- deepfos/core/cube/__init__.py +10 -0
- deepfos/core/cube/_base.py +462 -0
- deepfos/core/cube/constants.py +21 -0
- deepfos/core/cube/cube.py +408 -0
- deepfos/core/cube/formula.py +707 -0
- deepfos/core/cube/syscube.py +532 -0
- deepfos/core/cube/typing.py +7 -0
- deepfos/core/cube/utils.py +238 -0
- deepfos/core/dimension/__init__.py +11 -0
- deepfos/core/dimension/_base.py +506 -0
- deepfos/core/dimension/dimcreator.py +184 -0
- deepfos/core/dimension/dimension.py +472 -0
- deepfos/core/dimension/dimexpr.py +271 -0
- deepfos/core/dimension/dimmember.py +155 -0
- deepfos/core/dimension/eledimension.py +22 -0
- deepfos/core/dimension/filters.py +99 -0
- deepfos/core/dimension/sysdimension.py +168 -0
- deepfos/core/logictable/__init__.py +5 -0
- deepfos/core/logictable/_cache.py +141 -0
- deepfos/core/logictable/_operator.py +663 -0
- deepfos/core/logictable/nodemixin.py +673 -0
- deepfos/core/logictable/sqlcondition.py +609 -0
- deepfos/core/logictable/tablemodel.py +497 -0
- deepfos/db/__init__.py +36 -0
- deepfos/db/cipher.py +660 -0
- deepfos/db/clickhouse.py +191 -0
- deepfos/db/connector.py +195 -0
- deepfos/db/daclickhouse.py +171 -0
- deepfos/db/dameng.py +101 -0
- deepfos/db/damysql.py +189 -0
- deepfos/db/dbkits.py +358 -0
- deepfos/db/deepengine.py +99 -0
- deepfos/db/deepmodel.py +82 -0
- deepfos/db/deepmodel_kingbase.py +83 -0
- deepfos/db/edb.py +214 -0
- deepfos/db/gauss.py +83 -0
- deepfos/db/kingbase.py +83 -0
- deepfos/db/mysql.py +184 -0
- deepfos/db/oracle.py +131 -0
- deepfos/db/postgresql.py +192 -0
- deepfos/db/sqlserver.py +99 -0
- deepfos/db/utils.py +135 -0
- deepfos/element/__init__.py +89 -0
- deepfos/element/accounting.py +348 -0
- deepfos/element/apvlprocess.py +215 -0
- deepfos/element/base.py +398 -0
- deepfos/element/bizmodel.py +1269 -0
- deepfos/element/datatable.py +2467 -0
- deepfos/element/deep_pipeline.py +186 -0
- deepfos/element/deepconnector.py +59 -0
- deepfos/element/deepmodel.py +1806 -0
- deepfos/element/dimension.py +1254 -0
- deepfos/element/fact_table.py +427 -0
- deepfos/element/finmodel.py +1485 -0
- deepfos/element/journal.py +840 -0
- deepfos/element/journal_template.py +943 -0
- deepfos/element/pyscript.py +412 -0
- deepfos/element/reconciliation.py +553 -0
- deepfos/element/rolestrategy.py +243 -0
- deepfos/element/smartlist.py +457 -0
- deepfos/element/variable.py +756 -0
- deepfos/element/workflow.py +560 -0
- deepfos/exceptions/__init__.py +239 -0
- deepfos/exceptions/hook.py +86 -0
- deepfos/lazy.py +104 -0
- deepfos/lazy_import.py +84 -0
- deepfos/lib/__init__.py +0 -0
- deepfos/lib/_javaobj.py +366 -0
- deepfos/lib/asynchronous.py +879 -0
- deepfos/lib/concurrency.py +107 -0
- deepfos/lib/constant.py +39 -0
- deepfos/lib/decorator.py +310 -0
- deepfos/lib/deepchart.py +778 -0
- deepfos/lib/deepux.py +477 -0
- deepfos/lib/discovery.py +273 -0
- deepfos/lib/edb_lexer.py +789 -0
- deepfos/lib/eureka.py +156 -0
- deepfos/lib/filterparser.py +751 -0
- deepfos/lib/httpcli.py +106 -0
- deepfos/lib/jsonstreamer.py +80 -0
- deepfos/lib/msg.py +394 -0
- deepfos/lib/nacos.py +225 -0
- deepfos/lib/patch.py +92 -0
- deepfos/lib/redis.py +241 -0
- deepfos/lib/serutils.py +181 -0
- deepfos/lib/stopwatch.py +99 -0
- deepfos/lib/subtask.py +572 -0
- deepfos/lib/sysutils.py +703 -0
- deepfos/lib/utils.py +1003 -0
- deepfos/local.py +160 -0
- deepfos/options.py +670 -0
- deepfos/translation.py +237 -0
- deepfos-1.1.60.dist-info/METADATA +33 -0
- deepfos-1.1.60.dist-info/RECORD +175 -0
- deepfos-1.1.60.dist-info/WHEEL +5 -0
- 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)
|