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,778 @@
1
+ """python作为图表组件数据源"""
2
+ import collections
3
+ import functools
4
+ import asyncio
5
+ from inspect import isfunction
6
+ from pandas import DataFrame
7
+ from typing import *
8
+ import pandas as pd
9
+ from pydantic import Field, parse_obj_as
10
+
11
+ from deepfos.lib.asynchronous import evloop
12
+ from deepfos.api.models import BaseModel
13
+ from deepfos.api.dimension import DimensionAPI
14
+ from deepfos.api.smartlist import SmartListAPI
15
+ from deepfos.element.base import ElementBase
16
+ from deepfos.lib.decorator import cached_property
17
+ from deepfos.lib.filterparser import set_date_type_fields, set_dt_precision, Parameter, TimeType, FilterParser, \
18
+ FieldType, Sql
19
+ from deepfos.lib.utils import unpack_expr, dict_to_expr
20
+
21
+ __all__ = [
22
+ 'BaseField',
23
+ 'BoundField',
24
+ 'Text',
25
+ 'Number',
26
+ 'Date',
27
+ 'Dimension',
28
+ 'SmartList',
29
+ 'Person',
30
+ 'Struct',
31
+ 'as_datasource',
32
+ 'StructMeta',
33
+ 'ChartEngine',
34
+ 'HandleExpr',
35
+ 'Filter'
36
+ ]
37
+
38
+ FLAG_FOR_META = 'describe'
39
+
40
+
41
+ class DimElement(ElementBase):
42
+ api_class = DimensionAPI
43
+ api: DimensionAPI
44
+
45
+
46
+ class SmlElement(ElementBase):
47
+ api_class = SmartListAPI
48
+ api: SmartListAPI
49
+
50
+
51
+ def as_dict(self=None, exclude_none: bool = True) -> Dict:
52
+ # will be bound to NamedTuple, self is actually positional
53
+ result = {}
54
+ for k, v in self._asdict().items():
55
+ if v is not None or (v is None and not exclude_none):
56
+ try:
57
+ result[k] = v.as_dict(exclude_none) # noqa
58
+ except AttributeError:
59
+ result[k] = v
60
+ return result
61
+
62
+
63
+ # 字段名
64
+ COLUMN_NAME = 'columnName'
65
+ # 字段描述
66
+ DESC = 'description'
67
+ # 字段逻辑信息
68
+ LOGIC_INFO = 'logicInfo'
69
+ # 表示元素字段的类型
70
+ VALUE_KEY = 'valueKey'
71
+ # 表示值列表的选中值
72
+ VALUE_FIELD = 'valueField'
73
+
74
+
75
+ # -----------------------------------------------------------------------------
76
+ # Data Source Struct
77
+ class ElementDetail(NamedTuple):
78
+ elementName: str
79
+ elementType: str
80
+ folderId: str = None
81
+ path: str = None
82
+ serverName: str = None
83
+ absoluteTag: bool = True
84
+
85
+ as_dict = as_dict
86
+
87
+
88
+ class LogicInfo(NamedTuple):
89
+ valueType: int
90
+ valueKey: str = None
91
+ elementDetail: ElementDetail = None
92
+
93
+ as_dict = as_dict
94
+
95
+
96
+ class BaseField(BaseModel):
97
+ code: int = None
98
+ columnName: str = None
99
+ description: str = None
100
+ value_key: str = None
101
+
102
+ def __init__(self, name: str = None, description: str = None, **data):
103
+ super().__init__(columnName=name, description=description, **data)
104
+
105
+ @property
106
+ def logic_info(self) -> LogicInfo:
107
+ return LogicInfo(valueType=self.code)
108
+
109
+ def to_dict(self) -> Dict:
110
+ if self.description is None:
111
+ self.description = self.columnName
112
+ result = self.dict(include={COLUMN_NAME, DESC})
113
+ result.update({LOGIC_INFO: self.logic_info.as_dict()})
114
+ return result
115
+
116
+ @property
117
+ def name(self):
118
+ return self.columnName
119
+
120
+ def __setattr__(self, name, value):
121
+ if name == 'name':
122
+ self.columnName = value
123
+ else:
124
+ super().__setattr__(name, value)
125
+
126
+
127
+ class Text(BaseField):
128
+ code = 1
129
+
130
+
131
+ class Date(BaseField):
132
+ code = 11
133
+ # 前端时间筛选器颗粒度字段;4:最宽泛的颗粒度
134
+ value_key = '4'
135
+ precision = TimeType.day
136
+
137
+ @property
138
+ def logic_info(self) -> LogicInfo:
139
+ return LogicInfo(valueType=self.code, valueKey=self.value_key)
140
+
141
+
142
+ class Number(BaseField):
143
+ code = 15
144
+
145
+
146
+ class Person(BaseField):
147
+ code = 12
148
+
149
+
150
+ class BoundField(BaseField):
151
+ code: int = None
152
+ element_name: str = None
153
+ folder_id: str = None
154
+ path: str = None
155
+ bound_class: Type[ElementBase] = None
156
+ value_field: List[Any] = Field(default_factory=list)
157
+
158
+ def __init__(
159
+ self,
160
+ element_name: str,
161
+ folder_id: str = None,
162
+ path: str = None,
163
+ value_key: str = None,
164
+ value_field: list = None,
165
+ name: str = None,
166
+ description: str = None,
167
+ **data
168
+ ):
169
+ super().__init__(name=name, description=description, **data)
170
+ self.element_name = element_name
171
+ self.folder_id = folder_id
172
+ self.path = path
173
+ self.value_field = value_field or []
174
+
175
+ async def get_element_detail(self) -> ElementDetail:
176
+ ele = await self.bound_class( # noqa
177
+ element_name=self.element_name,
178
+ folder_id=self.folder_id,
179
+ path=self.path
180
+ )._get_element_info()
181
+ return ElementDetail(
182
+ elementName=self.element_name,
183
+ elementType=ele.elementType,
184
+ folderId=ele.folderId,
185
+ path=self.path,
186
+ serverName=ele.serverName
187
+ )
188
+
189
+ async def async_to_dict(self) -> Dict:
190
+ result = super().to_dict()
191
+ element_detail = await self.get_element_detail()
192
+ logic_info = self.logic_info._replace(elementDetail=element_detail)
193
+ result.update({LOGIC_INFO: logic_info.as_dict()})
194
+ return result
195
+
196
+
197
+ class SmartList(BoundField):
198
+ code = 3
199
+ bound_class: Type[ElementBase] = SmlElement
200
+
201
+ async def async_to_dict(self) -> Dict:
202
+ result = super().to_dict()
203
+ element_detail = await self.get_element_detail()
204
+ logic_info = self.logic_info._replace(elementDetail=element_detail)
205
+ result.update({LOGIC_INFO: logic_info.as_dict()})
206
+ result[VALUE_KEY] = logic_info.elementDetail.elementName
207
+ result[VALUE_FIELD] = self.value_field
208
+ return result
209
+
210
+
211
+ class Dimension(BoundField):
212
+ code = 8
213
+ bound_class: Type[ElementBase] = DimElement
214
+
215
+
216
+ class StructMeta(type):
217
+ def __new__(mcs, cls_name, bases, namespace: dict):
218
+ fields = []
219
+
220
+ for field_name, anno in namespace.get('__annotations__', {}).items():
221
+ if issubclass(anno, BoundField):
222
+ if field_name not in namespace:
223
+ raise ValueError(f"Bound field: <{field_name}> must have a default value.")
224
+
225
+ field = namespace.pop(field_name)
226
+ if field.columnName is None:
227
+ field.columnName = field_name
228
+ if not isinstance(field, anno):
229
+ raise TypeError(
230
+ f"Expect type {anno} for field: <{field_name}>, but got {type(field)}")
231
+ fields.append(field)
232
+
233
+ elif issubclass(anno, BaseField):
234
+ if field_name not in namespace:
235
+ fields.append(anno(name=field_name))
236
+
237
+ collected = []
238
+
239
+ for k, v in namespace.items():
240
+ if isinstance(v, BaseField):
241
+ collected.append(k)
242
+ if v.columnName is None: # 如果name字段赋值了,就取name字段的值
243
+ v.columnName = k
244
+ fields.append(v)
245
+
246
+ for k in collected:
247
+ namespace.pop(k)
248
+
249
+ namespace['fields'] = fields
250
+
251
+ return super().__new__(mcs, cls_name, bases, namespace)
252
+
253
+ def to_dict(cls):
254
+ columns_meta = []
255
+ sml_or_dim = []
256
+ futures = []
257
+ for field in cls.fields:
258
+ if isinstance(field, BoundField):
259
+ sml_or_dim.append(field)
260
+ elif isinstance(field, BaseField):
261
+ columns_meta.append(field.to_dict())
262
+
263
+ for field in sml_or_dim:
264
+ future = evloop.apply(field.async_to_dict())
265
+ futures.append(future)
266
+
267
+ for future in futures:
268
+ columns_meta.append(future.result())
269
+
270
+ return {
271
+ "__AsSource__": True,
272
+ "columns": columns_meta
273
+ }
274
+
275
+ def get_dim_fields(cls):
276
+ result = {}
277
+ for field in cls.fields:
278
+ if isinstance(field, Dimension):
279
+ result[field.columnName] = (field.path, field.folder_id)
280
+ return result
281
+
282
+
283
+ class Struct(metaclass=StructMeta):
284
+ """help class"""
285
+
286
+
287
+ def _agg_to_dict(agg_func_col, df, column_name_to_alias):
288
+ """针对只有聚合操作但是没有分组的情况做处理:
289
+ 1.先求聚合
290
+ 2.将结果按照'分组且聚合的方式'转为dict
291
+ 3.支持返回字段为alias的形式
292
+ """
293
+ df = df.agg(agg_func_col)
294
+ res = dict()
295
+ for col in df.columns:
296
+ for ind in df.index:
297
+ if str(df.loc[ind, col]) == 'nan':
298
+ continue
299
+ res[f'{col}#{ind}'] = df.loc[ind, col]
300
+
301
+ for k, v in column_name_to_alias.items():
302
+ if v in res:
303
+ value = res.pop(v)
304
+ res[k] = value
305
+ return [res]
306
+
307
+
308
+ def _batch_get_mbrs_by_expr(
309
+ fix: Dict[str, Union[str, list]],
310
+ paths: Union[str, Dict[str, Tuple[str, str]]] = None,
311
+ ) -> dict:
312
+ """根据批量维度表达式异步调用获取解析结果"""
313
+ from deepfos.element.dimension import AsyncDimension
314
+
315
+ if isinstance(paths, dict):
316
+ path_getter = paths.__getitem__
317
+ else:
318
+ path_getter = lambda _: (paths, None)
319
+
320
+ # 遍历fix,如果fix的值为str,则认为是维度表达式,将表达式转换为成员list
321
+ mbrs = {}
322
+ futures = []
323
+
324
+ for dim, exp in fix.items():
325
+ if isinstance(exp, str):
326
+ if "(" not in exp:
327
+ exp = exp.strip(dim)
328
+ exp = exp.strip('{').strip('}')
329
+ mbrs[dim] = exp.split(';')
330
+ else:
331
+ path, folder_id = path_getter(dim)
332
+ future = evloop.apply(AsyncDimension(element_name=dim, path=path, folder_id=folder_id).query(
333
+ expression=exp, fields=['name'], as_model=False
334
+ ))
335
+
336
+ futures.append((dim, future))
337
+ else:
338
+ mbrs[dim] = exp
339
+
340
+ for dim, future in futures:
341
+ mbrs[dim] = [item['name'] for item in future.result()]
342
+ return mbrs
343
+
344
+
345
+ class Filter:
346
+ def __init__(
347
+ self,
348
+ params: Union[Dict, Parameter],
349
+ struct: Type[Struct] = None,
350
+ all_enable_dim_columns=None,
351
+ date_round=False
352
+ ):
353
+ if isinstance(params, dict):
354
+ params = parse_obj_as(Parameter, params)
355
+ self.params = params
356
+ self.struct = struct
357
+ self._all_enable_dim_columns = all_enable_dim_columns
358
+ self.date_round = date_round
359
+
360
+ def apply(
361
+ self,
362
+ df: pd.DataFrame,
363
+ exclude_dim_expr: bool = True
364
+ ) -> pd.DataFrame:
365
+ """根据筛选条件对数据源进行过滤处理
366
+
367
+ Args:
368
+ df: 需要过滤的数据源
369
+ exclude_dim_expr: 是否排除维度表达式,默认为True,只对维度类型之外的字段进行条件过滤
370
+ """
371
+ where = self.as_pandas_sql(exclude_dim_expr=exclude_dim_expr)
372
+ if not where:
373
+ return df
374
+
375
+ return df.query(where)
376
+
377
+ def _axis_where(self, sql_type, exclude_dim_expr: bool = True):
378
+ if len(self.params.queryInfo.axisRanges) == 0:
379
+ return ''
380
+
381
+ parser = FilterParser(
382
+ self.params.queryInfo.axisRanges[0],
383
+ self._all_enable_dim_columns,
384
+ exclude_dim_expr=exclude_dim_expr,
385
+ date_round=self.date_round
386
+ )
387
+ parser.parse()
388
+ dim_mbrs = {}
389
+
390
+ if not exclude_dim_expr and parser.axis_dim_fields_to_expr:
391
+ # 如果不排除维度,则需要在获取where表达式之前解析所有维度表达式
392
+ # 异步请求获取批量维度表达式的解析结果
393
+ # 多组合中可能出现多个相同的维度,将list中 多个维度处理为或(;)的关系
394
+ _axis_dim_fields_to_expr = collections.defaultdict(list)
395
+ dim_expr = {} # 存储维度元素的元素名和表达式的映射
396
+ column_name_to_dim_name = {} # 存储字段列名和对应的维度元素名称的映射
397
+
398
+ for column_name, expr_list in parser.axis_dim_fields_to_expr.items():
399
+ # 同column_name对应的表达式为同一个维度的,dim_name不变,以第一个为准即可
400
+ dim_name = None
401
+ for expr in expr_list:
402
+ _dim_name, v = unpack_expr(expr)
403
+
404
+ if dim_name is None:
405
+ dim_name = _dim_name
406
+ if dim_name != column_name:
407
+ column_name_to_dim_name[column_name] = dim_name
408
+ self._all_enable_dim_columns[dim_name] = self._all_enable_dim_columns[column_name]
409
+
410
+ _axis_dim_fields_to_expr[dim_name].append(v)
411
+
412
+ single_dim_expr = dict_to_expr(_axis_dim_fields_to_expr)
413
+ dim_expr[dim_name] = single_dim_expr
414
+ _axis_dim_fields_to_expr.clear()
415
+
416
+ dim_mbrs = _batch_get_mbrs_by_expr(fix=dim_expr, paths=self._all_enable_dim_columns)
417
+ empty_dim_mbrs = [dim for dim, member in dim_mbrs.items() if not member]
418
+
419
+ if empty_dim_mbrs:
420
+ raise ValueError(f"({','.join(empty_dim_mbrs)})解析维度成员结果为空!")
421
+
422
+ # 将键为维度名,值为维度成员的字典替换为键为列名,值为维度成员的字典
423
+ for c, d in column_name_to_dim_name.items():
424
+ if d in dim_mbrs:
425
+ dim_mbrs[c] = dim_mbrs.pop(d)
426
+
427
+ return parser.make_query(dim_mbrs=dim_mbrs, sql=sql_type)
428
+
429
+ def _pov_where(self, sql_type=Sql.PANDAS_SQL):
430
+ parser = FilterParser(self.params.queryInfo.selected.selectedFilter, date_round=self.date_round)
431
+ parser.parse()
432
+ # 兼容2023-02-20迭代前,前置format date
433
+ # 导致SelectGroup和filterGroup内时间信息取自timeColumns的逻辑
434
+ date_fields = {group.columnName: group.formatType for group in self.params.queryInfo.timeColumns}
435
+ return parser.make_query(date_fields=date_fields, sql=sql_type)
436
+
437
+ def _generate_where(self, sql_type=Sql.PANDAS_SQL, exclude_dim_expr: bool = True):
438
+ # pov:where条件
439
+ pov_where = self._pov_where(sql_type)
440
+ # axis:where条件
441
+ axis_where = self._axis_where(sql_type, exclude_dim_expr=exclude_dim_expr)
442
+
443
+ if pov_where and axis_where:
444
+ return f'{axis_where} and ({pov_where})'
445
+ if axis_where:
446
+ return axis_where
447
+ if pov_where:
448
+ return f'({pov_where})'
449
+
450
+ def as_sql(self, exclude_dim_expr: bool = True) -> str:
451
+ """获取MySql格式的过滤条件"""
452
+ return self._generate_where(Sql.SQL, exclude_dim_expr)
453
+
454
+ def as_pandas_sql(self, exclude_dim_expr: bool = True) -> str:
455
+ """组装符合pandas格式的sql"""
456
+ return self._generate_where(exclude_dim_expr=exclude_dim_expr)
457
+
458
+ def agg(self, df: pd.DataFrame) -> pd.DataFrame:
459
+ """对df 进行分组,聚合,重命名处理"""
460
+ # 分组,聚合,排序,重命名alias
461
+ group_by_col = []
462
+ agg_func_col = {}
463
+ column_name_to_alias = {}
464
+
465
+ # 分组普通字段
466
+ for group in self.params.queryInfo.columns:
467
+ group_by_col.append(group.columnName)
468
+ column_name_to_alias[group.alias] = group.columnName
469
+
470
+ # 分组时间类型字段
471
+ for group in self.params.queryInfo.timeColumns:
472
+ column_name = group.columnName
473
+ group_by_col.append(column_name)
474
+ column_name_to_alias[group.alias] = group.columnName
475
+
476
+ # 聚合字段
477
+ for column in self.params.queryInfo.measures:
478
+ column_name = column.columnName
479
+ alias = column.alias
480
+ collect_method = column.collectMethod
481
+ # 聚合操作中:会有一个字段多个不同类型的聚合,会有一个字段多次同种聚合类型的运算
482
+ if collect_method:
483
+ column_alias = f'{column_name}#{collect_method.lower()}'
484
+ # alias设为key,因为column_alias可能存在重复
485
+ column_name_to_alias[alias] = column_alias
486
+ if column_name in agg_func_col:
487
+ agg_func_col[column_name].append(collect_method.lower())
488
+ else:
489
+ agg_func_col[column_name] = [collect_method.lower()]
490
+
491
+ # 分组字段:处理存在为空的列重新赋值为 '',防止记录丢失
492
+ for item in group_by_col:
493
+ if item in df:
494
+ df[item] = df[item].fillna('')
495
+
496
+ if group_by_col and agg_func_col:
497
+ df = df.groupby(group_by_col, as_index=False, sort=False).agg(agg_func_col)
498
+ elif agg_func_col:
499
+ data = _agg_to_dict(agg_func_col, df, column_name_to_alias)
500
+ return pd.DataFrame(data)
501
+ elif group_by_col:
502
+ raise ValueError('数据分组后,缺少聚合函数')
503
+
504
+ # 将df的MultiIndex转为Index,并且调整列名格式
505
+ columns = ['#'.join(col).strip().rstrip('#') for col in df.columns.values]
506
+ for k, v in column_name_to_alias.items():
507
+ if v in columns:
508
+ columns[columns.index(v)] = k
509
+
510
+ df.columns = columns
511
+
512
+ return df
513
+
514
+
515
+ class HandleExpr:
516
+ def __init__(self, params: Union[Dict, Parameter], struct: Type[Struct] = None, all_enable_dim_columns=None):
517
+ if isinstance(params, dict):
518
+ params = parse_obj_as(Parameter, params)
519
+ self.params = params
520
+ self.struct = struct
521
+ self._all_enable_dim_columns = all_enable_dim_columns
522
+ self.parser = None
523
+ self._load_axis_expr()
524
+
525
+ def _load_axis_expr(self):
526
+ """将组合(axisRanges)中的字段进行解析"""
527
+ if self.params.queryInfo.axisRanges:
528
+ parser = FilterParser(self.params.queryInfo.axisRanges[0], self._all_enable_dim_columns)
529
+ parser.parse() # 此解析过程会将维度类型字段和对应的维度表达式进行映射
530
+ self.parser = parser
531
+
532
+ def as_string(self) -> str:
533
+ """将维度表达式作为字符串返回"""
534
+ result = collections.defaultdict(list)
535
+ expr_pov = self.get_pov()
536
+ expr_axis = self.get_axis()
537
+
538
+ for k, v in expr_pov.items():
539
+ result[k].extend(v)
540
+
541
+ for column_name, values in expr_axis.items():
542
+ # 同一个维度可能会同时出现在多个行列组合中,故在axis中也会存在一个字段多个维度表达式的情况
543
+ for value in values:
544
+ k, v = unpack_expr(value)
545
+ # 维度表达式拼接是的字段列名+表达式(筛选器和行列都是都是如此)
546
+ result[column_name].append(v)
547
+
548
+ result = dict_to_expr(result)
549
+ return result
550
+
551
+ def get_dim_expr_dict(self) -> Dict[str, Dict[str, Union[str, List[str]]]]:
552
+ """将维度表达式以字典形式返回"""
553
+ result = {
554
+ 'selectedFilter': self.get_pov(),
555
+ 'axisRanges': self.get_axis()
556
+ }
557
+ return result
558
+
559
+ def get_pov(self, include_variable: bool = False) -> Dict[str, Union[List[str]]]:
560
+ """只获取筛选器上的维度表达式
561
+
562
+ 在组合行列中出现的字段如果是被@cur替换过了,
563
+ 那么筛选器上的该相同字段的值就失效了,
564
+ 此处根据参数决定是否仍然获取失效的字段值
565
+
566
+ Args:
567
+ include_variable: 是否获取@cur替换后的失效的字段值,默认False不获取
568
+ """
569
+ expr_dict = collections.defaultdict(list)
570
+ selected = self.params.queryInfo.selected
571
+
572
+ if param := selected.selectedFilter:
573
+ for item in param.selectGroups:
574
+ if item.columnName in self._all_enable_dim_columns:
575
+ expr_dict[item.columnName].extend(item.real_conditions[0].value)
576
+
577
+ if not include_variable:
578
+ return expr_dict
579
+
580
+ if param := selected.selectedParam:
581
+ for item in param.selectGroups:
582
+ if item.columnName in self._all_enable_dim_columns:
583
+ expr_dict[item.columnName].extend(item.real_conditions[0].value)
584
+
585
+ return expr_dict
586
+
587
+ def get_axis(self) -> Dict[str, Union[List[str]]]:
588
+ """只获取组合中的维度表达式"""
589
+ if self.parser: # 有组合类型(axisRanges)数据
590
+ expr_dict = self.parser.axis_dim_fields_to_expr
591
+ else:
592
+ expr_dict = collections.defaultdict(list)
593
+ return expr_dict
594
+
595
+
596
+ class ChartEngine:
597
+ def __init__(self, params: Dict, struct: Type[Struct] = None, before_return=None, date_round=False):
598
+ self.raw_params = params
599
+ self.params = parse_obj_as(Parameter, params)
600
+ self.struct = struct
601
+ self.all_enable_dim_columns = struct.get_dim_fields()
602
+ self.before_return = before_return
603
+ self.date_round = date_round
604
+ self.init_parser_env()
605
+
606
+ def init_parser_env(self):
607
+ set_date_type_fields(
608
+ {col.columnName: col.formatType
609
+ for col in self.params.queryInfo.timeColumns}
610
+ )
611
+ set_dt_precision(
612
+ {col.columnName: col.precision
613
+ for col in self.struct.fields if isinstance(col, Date)}
614
+ )
615
+
616
+ @cached_property
617
+ def filter(self) -> Filter:
618
+ return Filter(self.params, self.struct, self.all_enable_dim_columns, self.date_round)
619
+
620
+ @cached_property
621
+ def expr(self) -> HandleExpr:
622
+ return HandleExpr(self.params, self.struct, self.all_enable_dim_columns)
623
+
624
+ def get_dim_expr_dict(self) -> Dict[str, Dict[str, Union[str, List[str]]]]:
625
+ """以字典格式获取维度表达式结果"""
626
+ return self.expr.get_dim_expr_dict()
627
+
628
+ def get_dim_expr_str(self) -> str:
629
+ """以字符串格式获取维度表达式结果"""
630
+ return self.expr.as_string()
631
+
632
+ def get_pov(self, include_variable: bool = False) -> Dict[str, Union[List[str]]]:
633
+ """只获取筛选器上的维度表达式
634
+
635
+ 在组合行列中出现的字段如果是被@cur替换过了,
636
+ 那么筛选器上的该相同字段的值就失效了,
637
+ 此处根据参数决定是否仍然获取失效的字段值
638
+
639
+ Args:
640
+ include_variable: 是否获取@cur替换后的失效的字段值,默认False不获取
641
+ """
642
+ return self.expr.get_pov(include_variable)
643
+
644
+ def get_axis(self) -> Dict[str, Union[List[str]]]:
645
+ """获取行列组合上的维度表达式"""
646
+ return self.expr.get_axis()
647
+
648
+ def get_sql(self, exclude_dim_expr: bool = True) -> str:
649
+ """获取MySql格式的where条件表达式
650
+
651
+ Args:
652
+ exclude_dim_expr: 过滤时是否去除维度表达式,默认True:去除
653
+ """
654
+ return self.filter.as_sql(exclude_dim_expr=exclude_dim_expr)
655
+
656
+ def apply_filter(
657
+ self,
658
+ df: pd.DataFrame,
659
+ exclude_dim_expr: bool = True
660
+ ) -> pd.DataFrame:
661
+ """根据筛选条件对数据源进行过滤处理
662
+
663
+ Args:
664
+ df: 要进行过滤的数据源
665
+ exclude_dim_expr: 过滤时是否去除维度表达式,默认True:去除
666
+ """
667
+ df = self.filter.apply(df, exclude_dim_expr=exclude_dim_expr)
668
+ return self._format_date(df)
669
+
670
+ def apply_agg(self, df: pd.DataFrame, date_formated: bool = False) -> pd.DataFrame:
671
+ """根据分组和聚合函数进行聚合处理"""
672
+ if not date_formated:
673
+ df = self._format_date(df)
674
+ return self.filter.agg(df)
675
+
676
+ def _format_date(self, df: pd.DataFrame):
677
+ """对日期类型进行格式化处理"""
678
+ for column in self.params.queryInfo.timeColumns:
679
+ column_name = column.columnName
680
+ if column_name in df.columns:
681
+ df[column_name] = pd.to_datetime(df[column_name])
682
+
683
+ if column.formatType == FieldType.year:
684
+ df[column_name] = df[column_name].dt.strftime('%Y')
685
+ elif column.formatType == FieldType.month:
686
+ df[column_name] = df[column_name].dt.strftime('%Y-%m')
687
+ elif column.formatType == FieldType.day:
688
+ df[column_name] = df[column_name].dt.strftime('%Y-%m-%d')
689
+ elif column.formatType == FieldType.quarter:
690
+ year_col = df[column_name].dt.strftime('%Y-')
691
+ quarter_col = df[column_name].dt.quarter.astype('str')
692
+ df[column_name] = year_col + 'Q' + quarter_col
693
+
694
+ df[column_name] = df[column_name].fillna('')
695
+ return df
696
+
697
+ def _main(self, df: DataFrame):
698
+ """入口函数"""
699
+ after_where_df = self.apply_filter(df, exclude_dim_expr=False)
700
+ df = self.apply_agg(after_where_df, True)
701
+ if self.before_return:
702
+ df = self.before_return(df, self.raw_params)
703
+ return df
704
+
705
+
706
+ # -----------------------------------------------------------------------------
707
+ # main decorator
708
+ def _resolve_param(args: tuple):
709
+ if len(args) == 2:
710
+ return args[1]
711
+ if len(args) == 1:
712
+ return args[0]
713
+ raise ValueError("Bad signature for main function.")
714
+
715
+
716
+ def as_datasource(
717
+ func=None,
718
+ struct: Type[Struct] = None,
719
+ engine: Optional[Type[ChartEngine]] = ChartEngine,
720
+ before_return: Optional[Callable[[pd.DataFrame, Dict], pd.DataFrame]] = None,
721
+ date_round: bool = False
722
+ ):
723
+ """用作图表数据源的main函数装饰器
724
+
725
+ Args:
726
+ func: main方法
727
+ struct: 定义字段及其字段类型的类名称,必填
728
+ engine: 用于处理结果DataFrame的engine, 默认为ChartEngine;
729
+ 如需自定义, 需继承ChartEngine;
730
+ 为None时, 不对结果DataFrame作处理
731
+ before_return: 自定义同步function,作为ChartEngine处理的后置逻辑
732
+ 接受处理后的DataFrame和来自图表原始参数为入参
733
+ date_round: 是否允许低精度日期值与筛选条件内高精度日期值进行比较,默认不允许;
734
+ 允许后则精度缺失(例如2012与2012-10,缺失了月份部分,而2011与2012-10本身在年份可比,不属于精度缺失)时,
735
+ 除了相等以外,统一判定为不符合条件
736
+
737
+ """
738
+ if func is None:
739
+ return functools.partial(as_datasource,
740
+ before_return=before_return,
741
+ engine=engine,
742
+ struct=struct)
743
+
744
+ if struct is None:
745
+ raise ValueError("需定义图表数据源的字段信息")
746
+
747
+ if engine and not issubclass(engine, ChartEngine):
748
+ raise TypeError(f"engine参数应为ChartEngine子类")
749
+
750
+ if before_return is not None and not isfunction(before_return):
751
+ raise TypeError(f"before_return参数应为函数")
752
+
753
+ if asyncio.iscoroutinefunction(func):
754
+ async def wrapper(*args):
755
+ param = _resolve_param(args)
756
+ if param == FLAG_FOR_META:
757
+ return struct.to_dict()
758
+
759
+ df = await func(*args)
760
+ if engine is not None:
761
+ handler = engine(param, struct=struct, before_return=before_return, date_round=date_round)
762
+ df = handler._main(df) # noqa
763
+
764
+ return df.to_dict('records')
765
+ else:
766
+ def wrapper(*args):
767
+ param = _resolve_param(args)
768
+ if param == FLAG_FOR_META:
769
+ return struct.to_dict()
770
+
771
+ df = func(*args)
772
+ if engine is not None:
773
+ handler = engine(param, struct=struct, before_return=before_return, date_round=date_round)
774
+ df = handler._main(df) # noqa
775
+
776
+ return df.to_dict('records')
777
+
778
+ return functools.wraps(func)(wrapper)