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
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
|