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