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
deepfos/lib/utils.py ADDED
@@ -0,0 +1,1003 @@
1
+ """开发用工具类/函数"""
2
+
3
+ import functools
4
+ import asyncio
5
+ import os
6
+ import sys
7
+ import time
8
+ import weakref
9
+ from collections import UserList, UserDict, defaultdict
10
+ from enum import EnumMeta, Enum
11
+ import random
12
+ from typing import (
13
+ Tuple, Optional, Dict,
14
+ List, Union, Callable, Any,
15
+ TypeVar, MutableMapping, Container,
16
+ Iterator, Iterable, DefaultDict
17
+ )
18
+ from itertools import groupby, count
19
+
20
+ from cachetools.keys import hashkey
21
+ from loguru import logger
22
+ import pandas as pd
23
+
24
+ from deepfos.lib.constant import RE_DIMNAME_PARSER, ACCEPT_LANS, UNSET
25
+
26
+
27
+ FORCE_POP = [
28
+ 'deepfos.element.datatable',
29
+ 'deepfos.db.mysql',
30
+ 'deepfos.db.clickhouse',
31
+ 'deepfos.lib.subtask',
32
+ ]
33
+
34
+ # -----------------------------------------------------------------------------
35
+ # typing
36
+ KT = TypeVar('KT')
37
+ VT = TypeVar('VT')
38
+
39
+
40
+ # -----------------------------------------------------------------------------
41
+ # core
42
+ def unpack_expr(dim_expr, silent=False) -> Tuple[Optional[str], str]:
43
+ """匹配出维度表达式的维度名和表达式
44
+
45
+ Args:
46
+ dim_expr: 维度表达式
47
+ silent: 不符合格式时是否报错
48
+
49
+ Returns:
50
+ 维度名和表达式
51
+
52
+ Raises:
53
+ 当维度表达式参数缺失时,抛出异常 `ValueError`
54
+
55
+ """
56
+ rslt = RE_DIMNAME_PARSER.match(dim_expr)
57
+ if rslt:
58
+ return rslt.group('name'), rslt.group('body')
59
+ elif silent:
60
+ return None, dim_expr
61
+ else:
62
+ raise ValueError("Failed to resolve dimension name from given expression.")
63
+
64
+
65
+ def dict_to_expr(
66
+ dict_: Dict[str, Union[List[str], str]],
67
+ hierarchy: str = None,
68
+ ) -> str:
69
+ """字典转化为维度表达式
70
+
71
+ Args:
72
+ dict_: 维度名 -> 维度成员(成员列表)
73
+ hierarchy: 层级关系
74
+
75
+ Returns:
76
+ 维度表达式
77
+
78
+ >>> dict_to_expr({
79
+ ... "Year": ['2021', '2022'],
80
+ ... "Entity": 'TotalEntity',
81
+ ... "Version": "Base(NoVersion,0)"
82
+ ... })
83
+ 'Year{2021;2022}->Entity{TotalEntity}->Version{Base(NoVersion,0)}'
84
+ >>> dict_to_expr({
85
+ ... "Year": ['2021', '2022'],
86
+ ... "Entity": 'TotalEntity',
87
+ ... }, hierarchy='Base')
88
+ 'Year{Base(2021,0);Base(2022,0)}->Entity{Base(TotalEntity,0)}'
89
+ """
90
+ exprs = []
91
+ if hierarchy is not None:
92
+ template = hierarchy + "({mbr},0)"
93
+ else:
94
+ template = "{mbr}"
95
+
96
+ for dim, mbr in dict_.items():
97
+ if isinstance(mbr, str):
98
+ body = template.format(mbr=mbr)
99
+ else:
100
+ body = ";".join(template.format(mbr=m) for m in mbr)
101
+ exprs.append(f"{dim}{{{body}}}")
102
+
103
+ return '->'.join(exprs)
104
+
105
+
106
+ def expr_to_dict(
107
+ expr: str
108
+ ) -> Dict[str, str]:
109
+ """维度表达式转化为字典
110
+
111
+ Args:
112
+ expr: 维度表达式
113
+
114
+ Returns:
115
+ 字典
116
+
117
+ >>> d = expr_to_dict('Year{Base(2021,0);Base(2022,0)}->Entity{Base(TotalEntity,0)}')
118
+ >>> d == {
119
+ ... 'Year': 'Base(2021,0);Base(2022,0)',
120
+ ... 'Entity': 'Base(TotalEntity,0)'
121
+ ... }
122
+ True
123
+ >>> expr = 'Year{Base(2021,0);Base(2022,0)}->Entity{Base(TotalEntity,0)}'
124
+ >>> expr == dict_to_expr(expr_to_dict(expr))
125
+ True
126
+ """
127
+ return dict(unpack_expr(ex, silent=False) for ex in expr.split('->'))
128
+
129
+
130
+ def get_ignore_case(
131
+ dict_: Dict[str, VT],
132
+ key: str,
133
+ default: Any = UNSET
134
+ ) -> VT:
135
+ """类似于 ``dict.get``,但忽略大小写
136
+
137
+ 仅在字典很小时推荐使用。
138
+ 会首先创建一个和待查字典相比key小写,value相同的字典。
139
+ 再在创建出的字典中执行查询。
140
+
141
+ Args:
142
+ dict_: 查询的字典
143
+ key: 查询的键
144
+ default: 查询不到时返回的默认值
145
+
146
+ Returns:
147
+ 在字典中 ``key`` 对应的值
148
+
149
+ """
150
+ if key in dict_:
151
+ return dict_.get(key)
152
+
153
+ lower_case_dict = {}
154
+ for k, v in dict_.items():
155
+ if not isinstance(k, str):
156
+ continue
157
+ lower_case_dict[k.lower()] = v
158
+
159
+ if default is UNSET:
160
+ return lower_case_dict[key.lower()]
161
+ else:
162
+ return lower_case_dict.get(key.lower(), default)
163
+
164
+
165
+ def _concat_url_single(head: str, tail: str):
166
+ slash_count = 0
167
+ if head.endswith('/'):
168
+ slash_count += 1
169
+ if tail.startswith('/'):
170
+ slash_count += 1
171
+
172
+ if slash_count == 0:
173
+ return head + '/' + tail
174
+ elif slash_count == 1:
175
+ return head + tail
176
+ else:
177
+ return head[:-1] + '/' + tail[1:]
178
+
179
+
180
+ def concat_url(*parts: str):
181
+ """拼接url"""
182
+ return functools.reduce(_concat_url_single, parts)
183
+
184
+
185
+ def auto_call(caller):
186
+ def deco(func):
187
+ if asyncio.iscoroutinefunction(func):
188
+ async def wrapper(self, *args, **kwargs):
189
+ method = getattr(self, caller)
190
+ if asyncio.iscoroutinefunction(method):
191
+ await method()
192
+ else:
193
+ method()
194
+ setattr(self, func.__name__, func.__get__(self, self.__class__))
195
+ return await func(self, *args, **kwargs)
196
+ else:
197
+ def wrapper(self, *args, **kwargs):
198
+ method = getattr(self, caller)
199
+ method()
200
+
201
+ setattr(self, func.__name__, func.__get__(self, self.__class__))
202
+ return func(self, *args, **kwargs)
203
+
204
+ return functools.wraps(func)(wrapper)
205
+
206
+ return deco
207
+
208
+
209
+ auto_setup = auto_call('setup')
210
+
211
+
212
+ @functools.total_ordering
213
+ class Inf:
214
+ def __lt__(self, other):
215
+ return False
216
+
217
+
218
+ class Wait:
219
+ INF = Inf()
220
+
221
+ def __init__(self, base: float, algo: str, maximum: float = None):
222
+ if base < 0:
223
+ raise ValueError("base < 0 !")
224
+ self.base = base
225
+ self.algo = algo
226
+ if maximum is None:
227
+ self.maximum = self.__class__.INF
228
+ elif maximum < base:
229
+ raise ValueError("maximum < base !")
230
+ else:
231
+ self.maximum = maximum
232
+
233
+ self._counter = count().__next__
234
+
235
+ def __iter__(self):
236
+ algo = getattr(self.copy(), self.algo)
237
+
238
+ while True:
239
+ yield algo()
240
+
241
+ def fixed(self):
242
+ return self.base
243
+
244
+ def exp_backoff(self):
245
+ w = min(self.base, self.maximum)
246
+ self.base *= 2
247
+ return w
248
+
249
+ def random(self):
250
+ if self.maximum is self.__class__.INF: # pragma: no cover
251
+ return random.uniform(self.base, self.maximum)
252
+ else:
253
+ return random.uniform(0, self.base)
254
+
255
+ def copy(self):
256
+ return self.__class__(self.base, self.algo, self.maximum)
257
+
258
+
259
+ def retry(
260
+ func=None,
261
+ retries=2,
262
+ wait: Union[int, Wait] = 5,
263
+ catches=(Exception,),
264
+ fix=None,
265
+ name=None,
266
+ reraise=True,
267
+ ):
268
+ """
269
+ 在被装饰函数执行失败时,重新执行
270
+
271
+ Args:
272
+ func: 待执行函数
273
+ retries: 重试次数
274
+ wait: 每次重试的时间间隔
275
+ catches: 仅当函数抛出这些错误时重试
276
+ fix: 可能的补救函数。如果指定,会在每次重试之前调用。
277
+ name: 显示在日志中的函数名
278
+ reraise: 超过重试次数后是否抛出错误
279
+
280
+ """
281
+ if func is None:
282
+ return functools.partial(
283
+ retry, retries=retries, wait=wait,
284
+ catches=catches, fix=fix, name=name, reraise=reraise)
285
+
286
+ if retries < 0:
287
+ raise ValueError("retries must be positive.")
288
+
289
+ if isinstance(wait, int):
290
+ wait = Wait(wait, 'fixed')
291
+
292
+ def record_retry(retried, at):
293
+ if retried > 0 and fix is not None and callable(fix):
294
+ fix()
295
+ retried += 1
296
+ logger.exception(
297
+ f"Func: '{name or func.__qualname__}' failed. "
298
+ f"Start {retried} times retry in {at} secs.")
299
+ return retried
300
+
301
+ if asyncio.iscoroutinefunction(func):
302
+ async def run_func(*args, **kwargs):
303
+ retried = 0
304
+ waits = iter(wait)
305
+ while True:
306
+ try:
307
+ return await func(*args, **kwargs)
308
+ except catches:
309
+ if retried >= retries: # pragma: no cover
310
+ if reraise:
311
+ raise
312
+ else:
313
+ break
314
+ after = next(waits)
315
+ retried = record_retry(retried, after)
316
+ await asyncio.sleep(after)
317
+ else:
318
+ def run_func(*args, **kwargs):
319
+ retried = 0
320
+ waits = iter(wait)
321
+ while True:
322
+ try:
323
+ return func(*args, **kwargs)
324
+ except catches:
325
+ if retried >= retries: # pragma: no cover
326
+ if reraise:
327
+ raise
328
+ else:
329
+ break
330
+ after = next(waits)
331
+ retried = record_retry(retried, after)
332
+ time.sleep(after)
333
+
334
+ return functools.wraps(func)(run_func)
335
+
336
+
337
+ def i_am(who): # pragma: no cover
338
+ """当前执行脚本主入口名"""
339
+ arg0 = sys.argv[0]
340
+ name, *_ = os.path.basename(arg0).rsplit('.', maxsplit=1)
341
+ return name == who
342
+
343
+
344
+ def dict_to_key(dictionary: dict) -> str:
345
+ """字典->键
346
+
347
+ 把不可哈希的字典转化成可哈希的键(字符串)。
348
+
349
+ Args:
350
+ dictionary: 需转化的字典
351
+
352
+ Returns:
353
+
354
+
355
+ """
356
+ kv_pairs = sorted(
357
+ (k, v) for k, v in dictionary.items()
358
+ if isinstance(v, str)
359
+ )
360
+ return "##".join("::".join(pair) for pair in kv_pairs)
361
+
362
+
363
+ def cachedclass(cache, key=hashkey, lock=None):
364
+ """类的缓存装饰器
365
+
366
+ 基于类的初始化参数将类的实例进行缓存。同初始化参数将返回同一个实例。
367
+
368
+ Note:
369
+ 相较于普通的缓存装饰器,这个装饰器保证装饰的结果仍然被识别为一个类,
370
+ 并且文档能够正常被sphinx获取。
371
+ """
372
+
373
+ # noinspection PyPep8Naming
374
+ def decorator(clz):
375
+ if cache is None:
376
+ return clz
377
+
378
+ elif lock is None:
379
+ class wrapper:
380
+ def __new__(cls, *args, **kwargs):
381
+ k = key(*args, **kwargs)
382
+ try:
383
+ return cache[k]
384
+ except KeyError:
385
+ pass # key not found
386
+ v = clz(*args, **kwargs)
387
+ try:
388
+ cache[k] = v
389
+ except ValueError: # pragma: no cover
390
+ pass # value too large
391
+ return v
392
+
393
+ else:
394
+ class wrapper:
395
+ def __new__(cls, *args, **kwargs):
396
+ k = key(*args, **kwargs)
397
+ try:
398
+ with lock:
399
+ return cache[k]
400
+ except KeyError:
401
+ pass # key not found
402
+ v = clz(*args, **kwargs)
403
+ # in case of a race, prefer the item already in the cache
404
+ try:
405
+ with lock:
406
+ return cache.setdefault(k, v)
407
+ except ValueError: # pragma: no cover
408
+ return v # value too large
409
+
410
+ extra_assign = [k for k in dir(clz) if not k.startswith('_')]
411
+
412
+ return functools.update_wrapper(
413
+ wrapper, clz,
414
+ assigned=list(functools.WRAPPER_ASSIGNMENTS) + extra_assign,
415
+ updated=[]
416
+ )
417
+
418
+ return decorator
419
+
420
+
421
+ class CIEnumMeta(EnumMeta):
422
+ def __getitem__(cls, name):
423
+ try:
424
+ if isinstance(name, cls):
425
+ return name
426
+ if not isinstance(name, str):
427
+ raise KeyError(name)
428
+
429
+ return cls._member_map_[name.casefold()]
430
+ except KeyError:
431
+ choices = f"[{', '.join(cls.__members__)}]"
432
+ raise KeyError(
433
+ f"{cls.__name__}: {name!r} is not valid. "
434
+ f"Possible choices are: {choices}") from None
435
+
436
+
437
+ class CIEnum(str, Enum, metaclass=CIEnumMeta):
438
+ """大小写非敏感的枚举类
439
+
440
+ 经常用于入参为一组可选字符串的情况。
441
+ 可以简化参数检查逻辑代码的书写。
442
+ 当入参不支持时,提供友好的报错提示。
443
+
444
+ >>> class Flag(CIEnum):
445
+ ... zero = 'zero'
446
+ ... negative = 'negative'
447
+ ... positive = 'positive'
448
+ >>> def example(flag: Union[Flag, str]):
449
+ ... flag = Flag[flag]
450
+ ... if flag is Flag.zero:
451
+ ... return '0'
452
+ ... elif flag is Flag.negative:
453
+ ... return '-'
454
+ ... else:
455
+ ... return '+'
456
+ >>> example('zero')
457
+ '0'
458
+ >>> example('Zero')
459
+ '0'
460
+ >>> example(Flag.negative)
461
+ '-'
462
+ >>> example('unknown')
463
+ Traceback (most recent call last):
464
+ ...
465
+ KeyError: "Flag: 'unknown' is not valid. Possible choices are: [zero, negative, positive]"
466
+ """
467
+
468
+
469
+ class FrozenClass(type):
470
+ """不可修改的类
471
+
472
+ 元类,指定此元类的类将不可以设置属性,
473
+ 不可初始化。
474
+
475
+ >>> class Frozen(metaclass=FrozenClass):
476
+ ... pass
477
+ >>> Frozen.x = 1
478
+ Traceback (most recent call last):
479
+ ...
480
+ NotImplementedError: Frozen is read-only.
481
+ >>> Frozen()
482
+ Traceback (most recent call last):
483
+ ...
484
+ NotImplementedError: Frozen cannot be instantiated.
485
+
486
+ """
487
+ def __str__(cls):
488
+ return cls.__name__
489
+
490
+ __repr__ = __str__
491
+
492
+ def __setattr__(cls, key, value):
493
+ raise NotImplementedError(f'{cls} is read-only.')
494
+
495
+ def _init_(self, *args, **kwargs):
496
+ raise NotImplementedError(f'{self.__class__} cannot be instantiated.')
497
+
498
+ def __new__(mcs, name, base, namespace):
499
+ namespace['__init__'] = mcs._init_
500
+ cls = super().__new__(mcs, name, base, namespace)
501
+ return cls
502
+
503
+
504
+ class SettableDescriptor:
505
+ """限定写次数的描述符
506
+
507
+ 可读/写描述符,但是限定写次数。
508
+ 用于严格防止属性被使用者错误设置。
509
+
510
+ Args:
511
+ limit: 最大允许的写次数
512
+ default: 属性默认值
513
+
514
+ See Also:
515
+ :obj:`SettableOnce`
516
+
517
+ """
518
+ def __init__(self, limit: int, default: Any = None):
519
+ self._limit = limit
520
+ self._value = default
521
+
522
+ def __get__(self, instance, owner):
523
+ if instance is None:
524
+ return self
525
+ return self._value
526
+
527
+ def __set__(self, instance, value):
528
+ cnt_dict = instance.__dict__.setdefault(
529
+ '__set_count__', {self._attr: 0}
530
+ )
531
+ cnt = cnt_dict[self._attr]
532
+
533
+ if cnt >= self._limit:
534
+ attr = f"{instance.__class__.__name__}.{self._attr}"
535
+ if self._limit == 1:
536
+ set_times = "once"
537
+ else: # pragma: no cover
538
+ set_times = f"{cnt} times"
539
+ raise RuntimeError(f"{attr} can only be set {set_times}.")
540
+ self._value = value
541
+ cnt_dict[self._attr] += 1
542
+
543
+ def __set_name__(self, owner, name):
544
+ self._attr = name
545
+
546
+
547
+ #: 只能赋值一次的属性
548
+ SettableOnce = functools.partial(SettableDescriptor, 1)
549
+
550
+
551
+ class LazyList(UserList, Container[KT]):
552
+ """元素延迟初始化的列表
553
+
554
+ 元素的值只会在使用时被计算
555
+
556
+ >>> def call(*args):
557
+ ... print('calc')
558
+ ... return sum(args)
559
+ >>> ll = LazyList()
560
+ >>> ll.append(call, 1, 2, 3)
561
+ >>> ll.append(call, 4, 5, 6)
562
+ >>> ll[0]
563
+ calc
564
+ 6
565
+ >>> ll[0]
566
+ 6
567
+
568
+ See Also:
569
+ :class:`LazyDict`
570
+
571
+ """
572
+ def __getitem__(self, item: int) -> KT:
573
+ value, initialized = super().__getitem__(item)
574
+ if not initialized:
575
+ func, args, kwargs = value
576
+ result = func(*args, **kwargs)
577
+ self.data[item] = (result, True)
578
+ return result
579
+ else:
580
+ return value
581
+
582
+ def append(self, func: Callable, *args: Any, **kwargs) -> None:
583
+ self.data.append(((func, args, kwargs), False))
584
+
585
+
586
+ class LazyDict(UserDict, MutableMapping[KT, VT]):
587
+ """元素延迟初始化的字典
588
+
589
+ 元素的值只会在使用时被计算
590
+
591
+ >>> def call(*args):
592
+ ... print('calc')
593
+ ... return sum(args)
594
+ >>> ld = LazyDict()
595
+ >>> ld['a'] = (call, 1, 2, 3)
596
+ >>> ld['b'] = (call, 4, 5, 6)
597
+ >>> ld['a']
598
+ calc
599
+ 6
600
+ >>> ld['a']
601
+ 6
602
+
603
+ See Also:
604
+ :class:`LazyList`
605
+
606
+ """
607
+ def __getitem__(self, item) -> VT:
608
+ value, initialized = super().__getitem__(item)
609
+ if not initialized:
610
+ func, args = value
611
+ result = func(*args)
612
+ self.data[item] = (result, True)
613
+ return result
614
+ else:
615
+ return value
616
+
617
+ def __setitem__(self, key, value: Tuple):
618
+ func, *args = value
619
+ self.data[key] = ((func, args), False)
620
+
621
+
622
+ class Group:
623
+ """
624
+ 维护唯一键名信息的集合
625
+ """
626
+
627
+ def __init__(self):
628
+ self._keys = set()
629
+
630
+ def add(self, k):
631
+ self._keys.add(k)
632
+
633
+ def delete(self, k):
634
+ self._keys.remove(k)
635
+
636
+ def keys(self):
637
+ return self._keys
638
+
639
+ def clear(self):
640
+ self._keys.clear()
641
+
642
+
643
+ class GroupDict(UserDict, MutableMapping[KT, VT]):
644
+ """
645
+ 传入唯一键名集合group,在新增字典的键名已存在在group中时,raise KeyError
646
+
647
+ >>> shared_key_group = Group()
648
+ >>> a = GroupDict(shared_key_group)
649
+ >>> b = GroupDict(shared_key_group)
650
+ >>> a['a'] = 1
651
+ >>> a,b
652
+ ({'a': 1}, {})
653
+ >>> b['a'] = 2
654
+ Traceback (most recent call last):
655
+ ...
656
+ KeyError: 'Key a has been existed in key_group. Cannot be added to current dict.'
657
+
658
+ >>> a['a'] = 'a'
659
+ >>> a
660
+ {'a': 'a'}
661
+
662
+ See Also:
663
+ :class:`get_groupdicts`
664
+
665
+ """
666
+
667
+ def __init__(self, group: Group, **kwargs):
668
+ super().__init__(**kwargs)
669
+ self.group = group
670
+
671
+ def __setitem__(self, key: KT, item: VT):
672
+ if key in self.group.keys():
673
+ if key in self.data:
674
+ self.data[key] = item
675
+ else:
676
+ raise KeyError(f"Key {key} has been existed in key_group. Cannot be added to current dict.")
677
+ else:
678
+ self.data[key] = item
679
+ self.group.add(key)
680
+
681
+ def __delitem__(self, key: KT):
682
+ self.group.delete(key)
683
+ del self.data[key]
684
+
685
+
686
+ def get_groupdicts(n: int = 1) -> Tuple[GroupDict, ...]:
687
+ """获取 n 个字典,其中的键值共享一个集合,在字典间保持唯一
688
+
689
+ >>> a, b, c = get_groupdicts(3)
690
+ >>> a,b,c
691
+ ({}, {}, {})
692
+ >>> a['a'] = 1
693
+ >>> a,b,c
694
+ ({'a': 1}, {}, {})
695
+ >>> a.pop('a')
696
+ 1
697
+ >>> b['a'] = 2
698
+ >>> c['a'] = 3
699
+ Traceback (most recent call last):
700
+ ...
701
+ KeyError: 'Key a has been existed in key_group. Cannot be added to current dict.'
702
+
703
+ >>> a,b,c
704
+ ({}, {'a': 2}, {})
705
+
706
+ See Also:
707
+ :class:`GroupDict`
708
+
709
+ """
710
+ group = Group()
711
+ return tuple(GroupDict(group=group) for _ in range(n))
712
+
713
+
714
+ class ConcealableAttr:
715
+ """可隐藏变量
716
+
717
+ 可读/写描述符,调用 :meth:`expose` 后会暴露变量,
718
+ 是该变量可访问。调用 :meth:`conceal` 则会隐藏变量,
719
+ 此时访问会引发 :class:`AttributeError`。
720
+
721
+ 典型应用场景:
722
+ 有一个属性值仅在特定上下文中有意义,为了防止在其他
723
+ 代码中意外访问到该变量而产生难以debug的错误,可以在
724
+ 进入上下文前将该变量暴露,退出上下文时将该变量隐藏。
725
+
726
+ >>> class T:
727
+ ... attr = ConcealableAttr("test")
728
+ >>> t = T()
729
+ >>> T.attr.expose(t)
730
+ >>> t.attr
731
+ 'test'
732
+ >>> T.attr.conceal(t)
733
+ >>> t.attr
734
+ Traceback (most recent call last):
735
+ ...
736
+ AttributeError: Attribute 'attr' is concealed.
737
+ """
738
+ def __init__(self, default=None):
739
+ self._value = default
740
+ self.__conceal = weakref.WeakKeyDictionary()
741
+
742
+ def __get__(self, instance, owner):
743
+ if instance is None:
744
+ return self
745
+ if self.__conceal.get(instance, True):
746
+ raise AttributeError(f"Attribute '{self._attr}' is concealed.")
747
+ return self._value
748
+
749
+ def __set__(self, instance, value):
750
+ self._value = value
751
+
752
+ def __set_name__(self, owner, name):
753
+ self._attr = name
754
+
755
+ def conceal(self, inst):
756
+ """隐藏变量"""
757
+ self.__conceal[inst] = True
758
+
759
+ def expose(self, inst):
760
+ """暴露变量"""
761
+ self.__conceal[inst] = False
762
+
763
+
764
+ class MultiKeyDict(MutableMapping[KT, VT]):
765
+ """分组字典
766
+
767
+ 多个key对应一个值,同值的key属于一组,遍历时
768
+ 只有“组长”会作为key出现。
769
+
770
+ >>> mkd = MultiKeyDict()
771
+ >>> mkd['group1'] = 1
772
+ >>> mkd['v1', 'group1'] = 1
773
+ >>> mkd['v2', 'group1'] = 1
774
+ >>> mkd['v3', 'group2'] = 2
775
+ >>> mkd['v4', 'group2'] = 2
776
+ >>> list(mkd.keys())
777
+ ['group1', 'group2']
778
+ >>> list(mkd.items())
779
+ [('group1', 1), ('group2', 2)]
780
+ >>> mkd['v1']
781
+ 1
782
+ >>> mkd['group1']
783
+ 1
784
+ >>> 'v3' in mkd
785
+ True
786
+
787
+ """
788
+ def __init__(self, *args, **kwargs):
789
+ self.data = dict(*args, **kwargs)
790
+ #: key -> 对应的group
791
+ self._key_group = {}
792
+
793
+ def __iter__(self) -> Iterator[KT]:
794
+ return self.data.__iter__()
795
+
796
+ def __len__(self) -> int:
797
+ return self.data.__len__()
798
+
799
+ def __getitem__(self, k: KT) -> VT:
800
+ return self.data.__getitem__(self._key_group[k])
801
+
802
+ def __delitem__(self, v: KT) -> None:
803
+ del self._key_group[v]
804
+
805
+ def __setitem__(self, k: Union[Tuple[KT, KT], KT], v: VT) -> None:
806
+ if isinstance(k, tuple):
807
+ key, group = k
808
+ else:
809
+ key = group = k
810
+ self._key_group[key] = group
811
+ if group not in self._key_group:
812
+ self._key_group[group] = group
813
+ return self.data.__setitem__(group, v)
814
+
815
+ def __str__(self):
816
+ def item_gen():
817
+ for key, group in groupby(self._key_group.items(), lambda x: x[1]):
818
+ keys = [item[0] for item in group]
819
+ yield f'{keys}: {self.data[key]!r}'
820
+
821
+ return "{" + ', '.join(item_gen()) + "}"
822
+
823
+
824
+ def get_language_key_map(language_keys: Dict[str, str]):
825
+ prefix = 'language_'
826
+ lan_map = {}
827
+
828
+ for lan in ACCEPT_LANS:
829
+ key = prefix + lan.replace('-', '_')
830
+ lan_map[lan] = language_keys.get(key, prefix + lan)
831
+
832
+ return lan_map
833
+
834
+
835
+ def ask_for_kwargs(*keys: str, kwargs: Dict[str, Any]):
836
+ for k in keys:
837
+ if k not in kwargs:
838
+ raise TypeError(f"Missing required keyword argument: {k!r}")
839
+ yield kwargs[k]
840
+
841
+
842
+ def dict_to_sql(
843
+ dict_: Dict[str, Iterable[str]],
844
+ eq: str,
845
+ concat: str = 'and',
846
+ bracket: bool = True,
847
+ ):
848
+ sql_list = []
849
+
850
+ for k, v in dict_.items():
851
+ if isinstance(v, str):
852
+ v = [v]
853
+ else:
854
+ v = tuple(set(v))
855
+
856
+ if len(v) == 1:
857
+ sql = f"{k}{eq}{v[0]!r}"
858
+ else:
859
+ mbrs = ','.join(map(repr, v))
860
+ sql = f"{k} in ({mbrs})"
861
+
862
+ sql_list.append(sql)
863
+
864
+ sql = f" {concat} ".join(sql_list)
865
+ if bracket:
866
+ return '(' + sql + ')'
867
+ return sql
868
+
869
+
870
+ def split_dataframe(data: pd.DataFrame, chunksize: int = None):
871
+ nrows = len(data)
872
+ if chunksize is None or chunksize > nrows:
873
+ yield data
874
+ elif chunksize <= 0:
875
+ raise ValueError("chunksize must be greater than 0.")
876
+ else:
877
+ for i in range(0, nrows, chunksize):
878
+ yield data.iloc[i: i + chunksize]
879
+
880
+
881
+ def find_str(
882
+ target: str,
883
+ candidates: Iterable[str],
884
+ ignore_case: bool = False
885
+ ) -> Union[None, str]:
886
+ """查找目标字符串
887
+
888
+ >>> find_str('foo', ['foo', 'bar'])
889
+ 'foo'
890
+ >>> find_str('foo', ['Foo', 'bar']) is None
891
+ True
892
+ >>> find_str('foo', ['Foo', 'bar'], ignore_case=True)
893
+ 'Foo'
894
+ """
895
+
896
+ if ignore_case:
897
+ match = lambda x, y: x.lower() == y.lower()
898
+ else:
899
+ match = lambda x, y: x == y
900
+
901
+ for candidate in candidates:
902
+ if match(target, candidate):
903
+ return candidate
904
+
905
+
906
+ def to_version_tuple(ver: Union[float, str], max_split: int = 1):
907
+ """返回版本元组
908
+
909
+ Args:
910
+ ver: 表示版本的字符串或数字,例如'1.0','1_0',1.0
911
+ max_split: 以'.'和'_'分割的最大次数,如为-1,则无限制
912
+
913
+ Returns:
914
+ 形如(1, 0)的版本元组
915
+ """
916
+ if isinstance(ver, float):
917
+ ver = str(ver)
918
+ version_parts = ver.replace('.', '_').split('_', max_split)
919
+ version_list = [int(part) for part in version_parts]
920
+ return tuple(version_list)
921
+
922
+
923
+ def repr_version(version: Tuple, splitter='.'):
924
+ """显示version信息"""
925
+ return splitter.join([str(e) for e in version])
926
+
927
+
928
+ async def fetch_all_pages(
929
+ fn,
930
+ count_getter,
931
+ page_no_key,
932
+ page_size_key,
933
+ page_size
934
+ ):
935
+ cur_page_no = count(1).__next__
936
+ cnt = page_size
937
+
938
+ rtn = []
939
+
940
+ while cnt == page_size:
941
+ kwargs = {
942
+ page_no_key: cur_page_no(),
943
+ page_size_key: page_size
944
+ }
945
+ r = await fn(**kwargs)
946
+ cnt = count_getter(r)
947
+ rtn.append(r)
948
+
949
+ return rtn
950
+
951
+
952
+ def prepare_module():
953
+ modules = list(sys.modules.keys())
954
+ for key in modules:
955
+ package, *_ = key.split('.', maxsplit=1)
956
+ if package in FORCE_POP:
957
+ sys.modules.pop(key)
958
+ # for key added by LazyModule _load
959
+ globals().pop(key, None)
960
+ if any(key.startswith(module) for module in FORCE_POP):
961
+ sys.modules.pop(key)
962
+ # for key added by LazyModule _load
963
+ globals().pop(key, None)
964
+
965
+
966
+ def cleanup_module():
967
+ from deepfos.lib.filterparser import set_dt_precision, set_date_type_fields, set_dim_members
968
+ set_dt_precision({})
969
+ set_date_type_fields({})
970
+ set_dim_members({})
971
+
972
+ from deepfos.element import pyscript
973
+ pyscript.WAITING_TASKS = 0
974
+ pyscript.CONCURRENCY_KEY = None
975
+ pyscript.GLOBAL_LOCK = None
976
+
977
+
978
+ class SimpleCounter:
979
+ counts: DefaultDict[str, int]
980
+
981
+ def __init__(self) -> None:
982
+ self.counts = defaultdict(int)
983
+
984
+ def nextval(self, name: str = 'default') -> int:
985
+ self.counts[name] += 1
986
+ return self.counts[name]
987
+
988
+ def reset(self, name: str = 'default'):
989
+ self.counts.pop(name, None)
990
+
991
+
992
+ class AliasGenerator(SimpleCounter):
993
+ def get(self, hint: str = '') -> str:
994
+ idx = self.nextval(hint)
995
+ return f'{hint}{idx}'
996
+
997
+
998
+ def trim_text(content: str, limit: Union[int, None], appendor: str = '...') -> str:
999
+ limit = limit or 0
1000
+ if 0 < limit < len(content):
1001
+ return f"{content[:limit]} {appendor}"
1002
+ else:
1003
+ return content