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/api/base.py ADDED
@@ -0,0 +1,983 @@
1
+ import enum
2
+ import json
3
+ import re
4
+ import weakref
5
+ from importlib import import_module
6
+ from shlex import quote
7
+ from typing import (
8
+ Union, Tuple, TYPE_CHECKING, Callable, List, Dict,
9
+ get_args, get_origin, Optional, get_type_hints
10
+ )
11
+ from collections.abc import Awaitable
12
+ from deepfos.lib.decorator import cached_property
13
+ from urllib.parse import urlencode
14
+ from reprlib import repr
15
+
16
+ from requests.utils import CaseInsensitiveDict, to_key_val_list
17
+ from pydantic import parse_obj_as, BaseModel as PydanticBaseModel
18
+ from cachetools import TTLCache
19
+ from loguru import logger
20
+
21
+ from deepfos.cache import Manager, AppSeperatedLRUCache
22
+ from deepfos.lib.httpcli import AioHttpCli
23
+ from deepfos.lib.utils import concat_url, retry, to_version_tuple, repr_version, trim_text
24
+ from deepfos.lib.asynchronous import evloop
25
+ from deepfos.lib.discovery import ServiceDiscovery
26
+ from deepfos.lib.constant import UNSET, RE_SYS_SERVER_PARSER
27
+ from deepfos.options import OPTION
28
+ from deepfos.exceptions import APIResponseError, APIRequestError
29
+
30
+ __all__ = [
31
+ 'DynamicRootAPI',
32
+ 'RootAPI',
33
+ 'ChildAPI',
34
+ 'get',
35
+ 'post'
36
+ ]
37
+
38
+ T_DictPydanticModel = Union[dict, PydanticBaseModel]
39
+ VERSIONED_MODULE = "deepfos.api.V{version}.{name}"
40
+
41
+
42
+ class ContentType(str, enum.Enum):
43
+ json = "json"
44
+ bytes = "bytes"
45
+ text = "text"
46
+
47
+
48
+ class RequestInfo:
49
+ """请求参数wrapper,提供参数的展示,合并,构造功能"""
50
+ _repr_attr = ['url', 'method', 'header', 'body']
51
+ _setable_attr = _repr_attr + ['param', 'path']
52
+
53
+ def __init__(
54
+ self,
55
+ method: str = None,
56
+ url: str = None,
57
+ body: T_DictPydanticModel = None,
58
+ header: dict = None,
59
+ param: T_DictPydanticModel = None,
60
+ path: str = None,
61
+ ):
62
+ self._method = method
63
+ self._url = url
64
+ self._body = body
65
+ self._header = header
66
+ self._param = param
67
+ self._path = path
68
+
69
+ @staticmethod
70
+ def parse_nested(nested_model):
71
+ if not isinstance(nested_model, list):
72
+ return nested_model
73
+ parsed = []
74
+ for model in nested_model:
75
+ if isinstance(model, PydanticBaseModel):
76
+ parsed.append(model.dict(by_alias=True))
77
+ else:
78
+ parsed.append(model)
79
+ return parsed
80
+
81
+ @property
82
+ def url(self) -> str:
83
+ """把 attr:`param` 拼接至 attr:`url` ,形成完整的请求地址"""
84
+ if self._path:
85
+ url = concat_url(self._url, self._path)
86
+ else:
87
+ url = self._url
88
+
89
+ if param := self._param:
90
+ if isinstance(param, PydanticBaseModel):
91
+ param = param.dict(exclude_none=True)
92
+ return f"{url.rstrip('/')}?{urlencode(param)}"
93
+ else:
94
+ param_list = []
95
+ for k, v in param.items():
96
+ if v is not None:
97
+ if not isinstance(v, list):
98
+ param_list.append((k, v))
99
+ else:
100
+ param_list.extend((k, item) for item in v)
101
+ if param_list:
102
+ url = f"{url.rstrip('/')}?{urlencode(param_list)}"
103
+ return url
104
+ return url
105
+
106
+ @cached_property
107
+ def method(self):
108
+ return self._method.upper()
109
+
110
+ @cached_property
111
+ def body(self) -> Union[Dict, List]:
112
+ if isinstance(self._body, PydanticBaseModel):
113
+ return self._body.dict(by_alias=True, exclude_none=(self.method == 'GET'))
114
+ return self.parse_nested(self._body)
115
+
116
+ @cached_property
117
+ def header(self) -> dict:
118
+ return self._header
119
+
120
+ def setdefault(
121
+ self,
122
+ key: str,
123
+ value: Union[str, T_DictPydanticModel]
124
+ ):
125
+ """
126
+ 更新可写属性的值。仅在该值为None时被更新
127
+
128
+ Args:
129
+ key: 更新属性,必须在 :attr: _setable_attr 中
130
+ value: 需要设置的值
131
+ """
132
+ if key not in self._setable_attr:
133
+ return
134
+
135
+ key = '_' + key
136
+ attr = getattr(self, key)
137
+ if attr is None:
138
+ setattr(self, key, value)
139
+
140
+ def __str__(self): # pragma: no cover
141
+ attr_str = []
142
+ for attr in self._repr_attr:
143
+ val = getattr(self, attr)
144
+ if val is not None:
145
+ attr_str.append(f"[{attr}: {repr(val)}]")
146
+
147
+ return '\t\t'.join(attr_str)
148
+
149
+ def update_default(self, request: Union[dict, 'RequestInfo']):
150
+ """
151
+ 从其他 :class:`RequestInfo` 或者 :class:`dict` 中更新属性
152
+
153
+ Tips:
154
+ 只会更新自身未设置的属性(即值为None的属性)
155
+
156
+ Args:
157
+ request: 更新数据源
158
+ """
159
+ if not request:
160
+ return
161
+ if isinstance(request, dict):
162
+ kv_pairs = request.items()
163
+ else:
164
+ kv_pairs = request.__dict__.items()
165
+
166
+ for k, v in kv_pairs:
167
+ self.setdefault(k, v)
168
+
169
+ def to_curl(self, header: Dict): # pragma: no cover
170
+ # Curlify not available for form-data
171
+ if 'multipart/form-data' in header['Content-Type']:
172
+ return
173
+
174
+ parts = [
175
+ ('curl', None),
176
+ ('-X', self.method),
177
+ ]
178
+
179
+ for k, v in sorted(header.items()):
180
+ parts += [('-H', '{0}: {1}'.format(k, v))]
181
+
182
+ if self.body:
183
+ if 'Content-Type' in header and 'application/json' in header['Content-Type']:
184
+ body = json.dumps(self.body)
185
+ else:
186
+ body = self._encode_params(self.body)
187
+
188
+ if isinstance(body, bytes):
189
+ body = body.decode('utf-8')
190
+
191
+ parts += [('-d', body)]
192
+
193
+ parts += [(None, self.url)]
194
+
195
+ flat_parts = []
196
+ for k, v in parts:
197
+ if k:
198
+ flat_parts.append(quote(k))
199
+ if v:
200
+ flat_parts.append(quote(v))
201
+
202
+ curl_str = ' '.join(flat_parts)
203
+
204
+ if len(curl_str) > 200 and OPTION.general.dev_mode:
205
+ with open('curl_string.txt', 'a') as fp:
206
+ fp.write(f"{curl_str}\n")
207
+ else:
208
+ logger.debug(f"Curl command: {curl_str}")
209
+
210
+ @staticmethod
211
+ def _encode_params(data):
212
+ """Encode parameters in a piece of data.
213
+
214
+ Will successfully encode parameters when passed as a dict or a list of
215
+ 2-tuples. Order is retained if data is a list of 2-tuples but arbitrary
216
+ if parameters are supplied as a dict.
217
+ """
218
+
219
+ if isinstance(data, (str, bytes)):
220
+ return data
221
+ elif hasattr(data, 'read'):
222
+ return data
223
+ elif hasattr(data, '__iter__'):
224
+ result = []
225
+ for k, vs in to_key_val_list(data):
226
+ if isinstance(vs, str) or not hasattr(vs, '__iter__'):
227
+ vs = [vs]
228
+ for v in vs:
229
+ if v is not None:
230
+ result.append(
231
+ (k.encode('utf-8') if isinstance(k, str) else k,
232
+ v.encode('utf-8') if isinstance(v, str) else v))
233
+ return urlencode(result, doseq=True)
234
+ else:
235
+ return data
236
+
237
+ def __hash__(self):
238
+ return hash((self.url, self.method, json.dumps(self.header), json.dumps(self.body)))
239
+
240
+
241
+ class DummyDeco:
242
+ """装饰器,用于给所有api方法添加标记,方法的替换在metaclass中进行"""
243
+
244
+ def __init__(self, method):
245
+ self.method = method
246
+
247
+ def __call__(
248
+ self,
249
+ endpoint: str,
250
+ resp_model: PydanticBaseModel = None,
251
+ retries: int = 0,
252
+ allow_none: bool = True,
253
+ raise_false: bool = True,
254
+ data_wrapped: bool = True,
255
+ ):
256
+ def execute(func):
257
+ args = {
258
+ '__method__': self.method,
259
+ 'endpoint': endpoint,
260
+ 'resp_model': resp_model,
261
+ 'retries': retries,
262
+ 'allow_none': allow_none,
263
+ 'raise_false': raise_false,
264
+ 'data_wrapped': data_wrapped,
265
+ }
266
+ setattr(func, '__api_meta__', args)
267
+ return func
268
+
269
+ return execute
270
+
271
+
272
+ api_cache = Manager.create_cache(AppSeperatedLRUCache, maxsize=128)
273
+
274
+
275
+ class Route:
276
+ """装饰器,用于简化系统api的封装流程"""
277
+ _RE_CONTENT_TYPE = re.compile(r'^\s*application/(?P<ctype>.*);.*')
278
+ _RE_MULTI_TYPE = re.compile(r'^multipart/form-data; boundary=.*')
279
+
280
+ def __init__(self, method: str, sync: bool):
281
+ self.method = method
282
+ self.sync = sync
283
+
284
+ @staticmethod
285
+ def resolve_actual_type(_type):
286
+ origin = get_origin(_type)
287
+ if origin is not Union:
288
+ return _type
289
+
290
+ args = get_args(_type)
291
+ if len(args) != 2:
292
+ return _type
293
+
294
+ maybe_actual_type, await_wrapped_type = args
295
+ wrapped_origin = get_origin(await_wrapped_type)
296
+ if wrapped_origin is not Awaitable:
297
+ return _type
298
+
299
+ wrapped_type = get_args(await_wrapped_type)
300
+ if len(wrapped_type) != 1:
301
+ return _type
302
+ if maybe_actual_type is not wrapped_type[0]:
303
+ return _type
304
+ return maybe_actual_type
305
+
306
+ def __call__(
307
+ self,
308
+ endpoint: str,
309
+ resp_model: PydanticBaseModel = None,
310
+ retries: int = 0,
311
+ allow_none: bool = True,
312
+ raise_false: bool = True,
313
+ data_wrapped: bool = True,
314
+ content_type: Union[ContentType, str] = ContentType.json
315
+ ):
316
+ """
317
+ 装饰器主入口,同时是所有API接口的实际入口。
318
+
319
+ Notes:
320
+ 这个函数很长,但这是出于性能考虑。由于所有请求都会经过这个函数,
321
+ 所以把所有调用的函数都inline处理了,尽管这不如提取出函数容易维护,
322
+ 但这是必须的。
323
+
324
+ Args:
325
+ endpoint: 请求地址末端路径
326
+ resp_model: 接口的返回模型
327
+ retries: 接口调用失败时的重试次数
328
+ allow_none: 接口返回的data是否允许为None,如果允许,在返回None时,
329
+ 将不会试图把response.data解析成resp_model。一般来说,当接口
330
+ 返回status=True,data=None时使用。(这种情况一般由于接口编写不规范导致)
331
+ raise_false: 当接口返回status为false时,是否抛出异常
332
+ data_wrapped: 响应的数据是否被data字段包装
333
+
334
+ 函数调用可选参数:
335
+ resp_model: 接口的返回模型,可覆盖装饰器的同名参数
336
+ retries: 接口调用失败时的重试次数
337
+ use_cache: 是否读取缓存,默认为false
338
+
339
+ """
340
+
341
+ # noinspection PyPep8Naming
342
+ def execute(func):
343
+ anno = func.__annotations__.get('return', resp_model)
344
+ default_model = self.resolve_actual_type(anno)
345
+ method = self.method
346
+ is_get_request = method == 'get'
347
+ REQUEST = AioHttpCli.get if is_get_request else AioHttpCli.post
348
+ RE_CONTENT_TYPE = self._RE_CONTENT_TYPE
349
+
350
+ async def do_request(ins, *args, **kwargs):
351
+ # ---------------------------------------------------------------------
352
+ # handle extra args (多余kwargs在此前pop)
353
+ if 'resp_model' in kwargs:
354
+ model = kwargs.pop('resp_model')
355
+ else:
356
+ model = default_model
357
+
358
+ if 'content_type' in kwargs:
359
+ _content_type = ContentType(kwargs.pop('content_type'))
360
+ else:
361
+ _content_type = content_type
362
+
363
+ _retries = kwargs.pop('retries', retries)
364
+ use_cache = kwargs.pop('use_cache', False)
365
+
366
+ # ---------------------------------------------------------------------
367
+ # get request info
368
+ req = RequestInfo(url=concat_url(ins.base_url, endpoint), method=method)
369
+ req.update_default(func(ins, *args, **kwargs))
370
+ url, body, ext_header = req.url, req.body, req.header
371
+ raw_result = model is None
372
+
373
+ if ext_header is None:
374
+ header = ins.header
375
+ else:
376
+ header = CaseInsensitiveDict(ins.header, **ext_header)
377
+
378
+ if use_cache:
379
+ if ((req_key := hash(req)), raw_result) in api_cache:
380
+ return api_cache[(req_key, raw_result)]
381
+
382
+ if OPTION.api.dump_always:
383
+ req.to_curl(header)
384
+
385
+ # ---------------------------------------------------------------------
386
+ # send request
387
+ if is_get_request:
388
+ logger.opt(lazy=True).debug(
389
+ "Sending request by [aiohttp]: GET {url} {params}",
390
+ url=lambda: url,
391
+ params=lambda: repr(body)
392
+ )
393
+ req_args = {
394
+ 'url': url,
395
+ 'params': body,
396
+ 'headers': header
397
+ }
398
+ else:
399
+ logger.opt(lazy=True).debug(
400
+ "Sending request by [aiohttp]: POST {url} {body}",
401
+ url=lambda: req.url,
402
+ body=lambda: repr(req.body)
403
+ )
404
+ if ext_header is None:
405
+ body_key = 'json'
406
+ else:
407
+ # ------------------------------------------------------------
408
+ # parse content type
409
+ if (ctype := header.get('content-type')) is None:
410
+ logger.warning('Missing content-type in request header.')
411
+ elif matched := RE_CONTENT_TYPE.match(ctype):
412
+ ctype = matched.group('ctype').lower()
413
+ elif self._RE_MULTI_TYPE.match(ctype):
414
+ ctype = 'data'
415
+ else:
416
+ logger.warning(f'Unknow content-type: {ctype}')
417
+
418
+ body_key = ctype if ctype in ('json', 'data') else 'body'
419
+
420
+ req_args = {
421
+ 'url': url,
422
+ body_key: body,
423
+ 'headers': header
424
+ }
425
+
426
+ try:
427
+ if _retries > 0:
428
+ resp = await retry(
429
+ func=REQUEST, retries=_retries, name=func.__qualname__
430
+ )(**req_args)
431
+ else:
432
+ resp = await REQUEST(**req_args)
433
+ except OSError as e: # pragma: no cover
434
+ if not OPTION.api.dump_always and OPTION.api.dump_on_failure:
435
+ req.to_curl(header)
436
+
437
+ raise APIRequestError(e) from None
438
+
439
+ # -----------------------------------------------------------------------------
440
+ # parse response
441
+ if _content_type is ContentType.bytes:
442
+ return await resp.read()
443
+
444
+ text = await resp.text()
445
+ if _content_type is ContentType.text:
446
+ return text
447
+
448
+ err_code = None
449
+
450
+ if not 200 <= (status_code := resp.status) < 300:
451
+ logger.opt(lazy=True).error(
452
+ "Call API: {url} failed because status code is not 2XX. "
453
+ "Detail: {text}.",
454
+ url=lambda: req.url,
455
+ code=lambda: status_code,
456
+ text=lambda: trim_text(text, OPTION.general.response_display_length_on_error),
457
+ )
458
+ flag, obj, err = False, None, f"[code: {status_code}] ErrMsg from server: {text}"
459
+
460
+ else:
461
+ try:
462
+ resp = json.loads(text)
463
+ if data_wrapped:
464
+ if 'status' not in resp:
465
+ logger.opt(lazy=True).error(
466
+ "Call API: {url} failed. "
467
+ "Bad response because 'status' field is missing.",
468
+ url=lambda: req.url,
469
+ )
470
+ flag, obj, err = False, None, "status field is missing."
471
+
472
+ elif resp['status'] is False:
473
+ logger.opt(lazy=True).warning(
474
+ "Call API: {url} failed. "
475
+ "Bad response because status is False. Detail: {text}.",
476
+ url=lambda: req.url,
477
+ text=lambda: trim_text(text, OPTION.general.response_display_length_on_error),
478
+ )
479
+ flag, obj, err = False, resp.get('data'), resp.get('message', text)
480
+ err_code = resp.get('code')
481
+
482
+ else:
483
+ flag, obj, err = True, resp.get('data'), None
484
+ err_code = resp.get('code')
485
+ else:
486
+ flag, obj, err = True, resp, None
487
+
488
+ except (TypeError, ValueError):
489
+ logger.exception(
490
+ f'Call API: {req.url} failed.'
491
+ f'Response << {text} >> cannot be decoded as json.')
492
+ flag, obj, err = False, text, 'Response cannot be decoded as json'
493
+
494
+ if flag is False and raise_false:
495
+ if not OPTION.api.dump_always and OPTION.api.dump_on_failure:
496
+ req.to_curl(header)
497
+ raise APIResponseError(err, code=err_code)
498
+
499
+ if raw_result or (allow_none and obj is None):
500
+ if use_cache:
501
+ api_cache[(hash(req), raw_result)] = obj
502
+ return obj
503
+
504
+ try:
505
+ result = parse_obj_as(model, obj)
506
+ if use_cache:
507
+ api_cache[(hash(req), raw_result)] = result
508
+ return result
509
+ except Exception: # pragma: no cover
510
+ if not OPTION.api.dump_always and OPTION.api.dump_on_failure:
511
+ req.to_curl(header)
512
+ logger.exception(f"Parse model failed.")
513
+ raise APIResponseError(
514
+ f"Failed to parse response data. "
515
+ f"Expect model: '{model}', Got '{repr(obj)}'"
516
+ ) from None
517
+
518
+ if self.sync:
519
+ def sync_request(ins, *args, **kwargs):
520
+ return evloop.run(do_request(ins, *args, **kwargs))
521
+
522
+ return sync_request
523
+ else:
524
+ return do_request
525
+
526
+ return execute
527
+
528
+
529
+ get = DummyDeco(method='get')
530
+ post = DummyDeco(method='post')
531
+
532
+
533
+ class APIBase:
534
+ endpoint = '/'
535
+
536
+ def __init__(
537
+ self,
538
+ header: Union[T_DictPydanticModel, CaseInsensitiveDict] = None,
539
+ prefix: str = '',
540
+ ):
541
+ if isinstance(header, PydanticBaseModel):
542
+ self.header = CaseInsensitiveDict(header.dict(by_alias=True))
543
+ elif isinstance(header, dict):
544
+ self.header = CaseInsensitiveDict(header)
545
+ elif header is None:
546
+ if OPTION.general.for_server_use:
547
+ # noinspection PyUnresolvedReferences
548
+ from starlette_context import context
549
+ self.header = CaseInsensitiveDict(context.data['header'])
550
+ else:
551
+ self.header = CaseInsensitiveDict(OPTION.api.header)
552
+ else:
553
+ self.header = header
554
+
555
+ if mat := RE_SYS_SERVER_PARSER.match(prefix):
556
+ self.base_url = concat_url(OPTION.server.base, mat.group(1), self.endpoint)
557
+ else:
558
+ self.base_url = concat_url(prefix, self.endpoint)
559
+ self.header.update({
560
+ "Content-Type": "application/json;charset=UTF8",
561
+ "Connection": "close",
562
+ })
563
+
564
+
565
+ class APIMeta(type):
566
+ sync = True
567
+
568
+ def __new__(mcs, name, bases, namespace, **kwargs):
569
+ all_attrs = {}
570
+ for base in bases:
571
+ all_attrs.update(base.__dict__)
572
+ all_attrs.update(namespace)
573
+
574
+ for _name, attr in all_attrs.items():
575
+ if (schema := getattr(attr, '__api_meta__', None)) is None:
576
+ continue
577
+ schema = schema.copy()
578
+ method = schema.pop('__method__')
579
+ namespace[_name] = Route(method=method, sync=mcs.sync)(**schema)(attr)
580
+
581
+ return super().__new__(mcs, name, bases, namespace, **kwargs)
582
+
583
+
584
+ class AysncAPIMeta(APIMeta):
585
+ sync = False
586
+
587
+
588
+ class AsyncAPIBase(APIBase, metaclass=AysncAPIMeta):
589
+ pass
590
+
591
+
592
+ class SyncAPIBase(APIBase, metaclass=APIMeta):
593
+ pass
594
+
595
+
596
+ # noinspection PyUnresolvedReferences
597
+ class _DynamicAPIMixin:
598
+ server_cache = TTLCache(maxsize=128, ttl=3600)
599
+ module_type: str = UNSET
600
+ server_known = False
601
+ version = None
602
+
603
+ def get_module_id(self, version: Union[float, str], module_id: str):
604
+ if (module_type := self.module_type) is UNSET:
605
+ raise NotImplementedError(f"class variable {module_type} is not implemented.")
606
+ if module_id is not None:
607
+ if not (mid := module_id.upper()).startswith(module_type):
608
+ raise NameError(f"Module id {mid} is not valid for module: {module_type}.")
609
+ return mid
610
+ if version is not None:
611
+ self.version = to_version_tuple(version)
612
+ return f"{module_type}{repr_version(self.version, '_')}"
613
+
614
+ def _add_to_memo(self, server_meta):
615
+ if not server_meta:
616
+ raise RuntimeError(f"Module: {self.module_id} is not avaliable")
617
+ server_name = server_meta.serverName
618
+ self.server_cache[self.module_id] = server_name
619
+ return server_name
620
+
621
+ def get_server_name(self):
622
+ return NotImplemented
623
+
624
+ # noinspection PyAttributeOutsideInit
625
+ def set_url(self, server_name):
626
+ self.base_url = concat_url(OPTION.server.base, server_name, self.base_url)
627
+ self.server_known = True
628
+
629
+
630
+ class DynamicAPIBase(SyncAPIBase, _DynamicAPIMixin):
631
+ def __init__(
632
+ self,
633
+ version: Union[float, str] = None,
634
+ header: T_DictPydanticModel = None,
635
+ module_id: str = None,
636
+ lazy: bool = False
637
+ ):
638
+ super().__init__(header)
639
+ self.module_id = module_id = self.get_module_id(version, module_id)
640
+ # lazy = True means called from element/base, will be set url in ElementBase
641
+ if module_id is not None and not lazy:
642
+ if module_id in self.server_cache:
643
+ logger.debug(f'Find server name for module: {module_id} from cache.')
644
+ server_name = self.server_cache[module_id]
645
+ else:
646
+ server_name = self.get_server_name()
647
+ self.set_url(server_name)
648
+
649
+ def get_server_name(self):
650
+ from .space import SpaceAPI
651
+ api = SpaceAPI(self.header, sync=True)
652
+ server_meta = api.module.detail(self.module_id)
653
+ return self._add_to_memo(server_meta)
654
+
655
+ def set_url(self, server_name):
656
+ if OPTION.discovery.enabled: # pragma: no cover
657
+ discovery = ServiceDiscovery.instantiate()
658
+ base_url = discovery.sync_get_url(server_name)
659
+ self.base_url = concat_url(base_url, self.endpoint)
660
+ self.server_known = True
661
+ else:
662
+ super().set_url(server_name)
663
+
664
+
665
+ class ADynamicAPIBase(AsyncAPIBase, _DynamicAPIMixin):
666
+ def __init__(
667
+ self,
668
+ version: Union[float, str] = None,
669
+ header: T_DictPydanticModel = None,
670
+ module_id: str = None,
671
+ lazy: bool = False, # noqa
672
+ ):
673
+ super().__init__(header)
674
+ self.module_id = self.get_module_id(version, module_id)
675
+ self.lazy = lazy
676
+
677
+ def __await__(self):
678
+ return self.init().__await__()
679
+
680
+ async def get_server_name(self):
681
+ from .space import SpaceAPI
682
+ api = SpaceAPI(self.header, sync=False)
683
+ server_meta = await api.module.detail(self.module_id) # noqa
684
+ return self._add_to_memo(server_meta)
685
+
686
+ async def init(self):
687
+ if self.module_id is None:
688
+ return self
689
+ # lazy = True means called from element/base, will be set url in ElementBase
690
+ if not self.lazy:
691
+ if self.module_id in self.server_cache:
692
+ server_name = self.server_cache[self.module_id]
693
+ else:
694
+ server_name = await self.get_server_name()
695
+ await self.set_url(server_name)
696
+ return self
697
+
698
+ async def set_url(self, server_name):
699
+ if OPTION.discovery.enabled: # pragma: no cover
700
+ discovery = ServiceDiscovery.instantiate()
701
+ base_url = await discovery.get_url(server_name)
702
+ self.base_url = concat_url(base_url, self.endpoint)
703
+ self.server_known = True
704
+ else:
705
+ super().set_url(server_name)
706
+
707
+
708
+ class RootAPI:
709
+ """
710
+ API基类。 所有 **固定url** 的API应该继承这个类。
711
+
712
+ 同时提供同步和异步的http调用方法,根据初始化参数sync,
713
+ 对于封装的接口,会自动采取同步或者异步的调用方式。
714
+ """
715
+ prefix: Callable[[], str] = lambda: None
716
+ url_need_format = False
717
+ endpoint = ''
718
+ __cls_cache__ = {}
719
+ multi_version = False
720
+ default_version = None
721
+ api_version = None
722
+ cls_name = None
723
+ module_name = None
724
+ builtin = True
725
+
726
+ if TYPE_CHECKING: # pragma: no cover
727
+ # 由APIBase带入,此处定义仅用于ide提示
728
+ header: Dict[str, str] = {}
729
+ base_url: str = ''
730
+
731
+ @classmethod
732
+ def collect_endpoints(cls) -> List[str]:
733
+
734
+ def _resolve_direct_endpoints(cls) -> List[str]:
735
+ eps = []
736
+ for attr in cls.__dict__.values():
737
+ if meta := getattr(attr, '__api_meta__', None):
738
+ eps.append(meta['endpoint'])
739
+ return eps
740
+
741
+ endpoints = _resolve_direct_endpoints(cls)
742
+
743
+ for name, attr in cls.__dict__.items():
744
+ if not (
745
+ isinstance(attr, cached_property)
746
+ and (anno := get_type_hints(attr.func))
747
+ and (api := anno.get('return'))
748
+ and issubclass(api, ChildAPI)
749
+ ):
750
+ continue
751
+
752
+ tag = api.endpoint
753
+ endpoints.extend(
754
+ concat_url(tag, ep)
755
+ for ep in _resolve_direct_endpoints(api)
756
+ )
757
+
758
+ return endpoints
759
+
760
+ @classmethod
761
+ def resolve_cls(cls, sync, sync_base, async_base, extra=None):
762
+ if sync:
763
+ base = sync_base
764
+ prefix = "Sync"
765
+ else:
766
+ base = async_base
767
+ prefix = "Async"
768
+
769
+ if not isinstance(base, tuple):
770
+ base = (base, )
771
+
772
+ class_name = f"_{'_'.join(cls.__module__.split('.'))}_{prefix}{cls.__name__}_"
773
+ if class_name in cls.__cls_cache__:
774
+ clz = RootAPI.__cls_cache__[class_name]
775
+ else:
776
+ extra = extra or {}
777
+ initial = {}
778
+
779
+ for parent_cls in cls.__mro__:
780
+ if parent_cls in [DynamicRootAPI, ChildAPI, RootAPI]:
781
+ break
782
+ initial = {**parent_cls.__dict__, **initial}
783
+
784
+ clz = type(class_name, base, {
785
+ **initial, **extra,
786
+ "__new__": base[0].__new__,
787
+ })
788
+ RootAPI.__cls_cache__[class_name] = clz
789
+ return clz
790
+
791
+ @classmethod
792
+ def resolve_version_cls(
793
+ cls, sync, sync_base, async_base,
794
+ version: Union[float, str, Tuple[int]] = None,
795
+ extra=None
796
+ ):
797
+ if version is not None and not isinstance(version, tuple):
798
+ version = to_version_tuple(version)
799
+
800
+ if sync:
801
+ base = sync_base
802
+ prefix = "Sync"
803
+ else:
804
+ base = async_base
805
+ prefix = "Async"
806
+
807
+ if not isinstance(base, tuple):
808
+ base = (base,)
809
+
810
+ if version is not None:
811
+ class_name = f"_{'_'.join(cls.__module__.split('.'))}" \
812
+ f"_{prefix}{cls.__name__}{repr_version(version, '_')}_"
813
+ else:
814
+ class_name = f"_{'_'.join(cls.__module__.split('.'))}_{prefix}{cls.__name__}_"
815
+
816
+ if class_name in cls.__cls_cache__:
817
+ return RootAPI.__cls_cache__[class_name]
818
+
819
+ extra = extra or {}
820
+
821
+ # Called from a multiversion API class
822
+ # And the required version is not default version
823
+ if version is not None and (version != cls.default_version or version != cls.api_version):
824
+ if version < cls.api_version:
825
+ raise ValueError(f'Version of current API class should not be '
826
+ f'earlier than {repr_version(cls.api_version)}.')
827
+ try:
828
+ module = import_module(VERSIONED_MODULE.format(version=repr_version(version, "_"),
829
+ name=cls.module_name.rpartition('.')[-1]))
830
+ versioned_cls = getattr(module, cls.cls_name)
831
+ initial = {**versioned_cls.__dict__}
832
+
833
+ if not cls.builtin:
834
+ initial = {**initial, **cls.__dict__}
835
+
836
+ clz = type(class_name, base, {
837
+ **initial, **extra,
838
+ "__new__": base[0].__new__
839
+ })
840
+ except (ImportError, AttributeError):
841
+ raise NotImplementedError(
842
+ f"{cls.__name__} with version: V{repr_version(version)} is not implemented.")
843
+ else:
844
+ clz = type(class_name, base, {
845
+ **cls.__dict__, **extra,
846
+ "__new__": base[0].__new__,
847
+ })
848
+
849
+ RootAPI.__cls_cache__[class_name] = clz
850
+ return clz
851
+
852
+ def __new__(cls, header=None, sync=OPTION.api.io_sync):
853
+ """
854
+
855
+ Args:
856
+ header: 请求头
857
+ sync: 是否使用同步方式请求
858
+
859
+ """
860
+ clz = cls.resolve_cls(sync, SyncAPIBase, AsyncAPIBase)
861
+ ins = clz(header=header, prefix=cls.prefix())
862
+ ins.sync = sync
863
+ ins.multi_version = cls.multi_version
864
+ ins.default_version = cls.default_version
865
+ ins.cls_name = cls.cls_name
866
+ ins.module_name = cls.module_name
867
+ ins.api_version = cls.api_version
868
+ ins.builtin = cls.builtin
869
+ ins.url_need_format = cls.url_need_format
870
+ return ins
871
+
872
+
873
+ class _ChildAPI:
874
+ """用来做ChildAPi的标识"""
875
+ if TYPE_CHECKING: # pragma: no cover
876
+ root = None
877
+
878
+
879
+ class ChildAPI(RootAPI):
880
+ def __new__(cls, root: RootAPI):
881
+ clz = cls.resolve_cls(
882
+ root.sync, (SyncAPIBase, _ChildAPI), (AsyncAPIBase, _ChildAPI),
883
+ extra={'root': cls.root}
884
+ )
885
+ ins = clz(header=root.header, prefix=root.base_url)
886
+ if root.url_need_format:
887
+ ins.base_url = ins.base_url.format(**root.header)
888
+ ins.__root = weakref.ref(root)
889
+ return ins
890
+
891
+ @property
892
+ def root(self):
893
+ return self.__root()
894
+
895
+
896
+ class DynamicRootAPI(RootAPI):
897
+ """
898
+ 动态API基类。 所有 **非固定url** 的API应该继承这个类
899
+
900
+ Examples:
901
+ .. code-block:: python
902
+
903
+ class ExampleAPI(DynamicRootAPI):
904
+ module_type = 'EXAMPLE'
905
+
906
+ @get('/test')
907
+ def test(self):
908
+ return {}
909
+
910
+ 对于上述api,存在2种初始化方法。
911
+
912
+ .. code-block:: python
913
+
914
+ api = ExampleAPI(version='1.0', sync=True)
915
+ api = await ExampleAPI(version='1.0', sync=False)
916
+
917
+ 当sync=False时,必须使用await初始化。
918
+ 以这种方式初始化的api,必须使用await调用,即:
919
+
920
+ .. code-block:: python
921
+
922
+ await api.test()
923
+
924
+ 以sync=True初始化的api,可以同步调用,即:
925
+
926
+ .. code-block:: python
927
+
928
+ api.test()
929
+
930
+ """
931
+
932
+ module_type = ''
933
+ if TYPE_CHECKING: # pragma: no cover
934
+ server_cache: TTLCache
935
+ module_id: str
936
+ version: Optional[Tuple[int, int]]
937
+
938
+ def __new__(
939
+ cls,
940
+ version: Union[float, str] = None,
941
+ header: dict = None,
942
+ sync: bool = UNSET,
943
+ module_id: str = None,
944
+ lazy: bool = False
945
+ ):
946
+ """
947
+
948
+ Args:
949
+ version: 组件版本
950
+ header: 请求头
951
+ sync: 是否使用同步方式请求
952
+ module_id: 组件ID
953
+
954
+ """
955
+ if cls.__base__ is not DynamicRootAPI and issubclass(cls.__base__, DynamicRootAPI):
956
+ extra = {k: v for k, v in cls.__base__.__dict__.items() if k not in cls.__dict__}
957
+ else:
958
+ extra = {}
959
+
960
+ if getattr(cls, 'multi_version', False):
961
+ clz = cls.resolve_version_cls(sync, DynamicAPIBase, ADynamicAPIBase, version, extra=extra)
962
+ else:
963
+ clz = cls.resolve_cls(sync, DynamicAPIBase, ADynamicAPIBase, extra=extra)
964
+ ins = clz(version=version, header=header, module_id=module_id, lazy=lazy)
965
+ if sync is UNSET:
966
+ ins.sync = OPTION.api.io_sync
967
+ else:
968
+ ins.sync = sync
969
+ ins.multi_version = cls.multi_version
970
+ ins.default_version = cls.default_version
971
+ ins.cls_name = cls.cls_name
972
+ ins.module_name = cls.module_name
973
+ ins.api_version = cls.api_version
974
+ ins.builtin = cls.builtin
975
+ ins.url_need_format = cls.url_need_format
976
+ return ins
977
+
978
+ def __init_subclass__(cls, *, builtin=False):
979
+ setattr(cls, 'builtin', builtin)
980
+
981
+ def __await__(self): # pragma: no cover
982
+ # defined here to help ide
983
+ pass