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/db/damysql.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
from reprlib import aRepr
|
|
2
|
+
import asyncio
|
|
3
|
+
|
|
4
|
+
from loguru import logger
|
|
5
|
+
from cachetools import TTLCache
|
|
6
|
+
import pandas as pd
|
|
7
|
+
import aiomysql
|
|
8
|
+
from pymysql.constants import FIELD_TYPE
|
|
9
|
+
|
|
10
|
+
from deepfos import OPTION
|
|
11
|
+
from deepfos.api.datatable import MySQLAPI
|
|
12
|
+
from deepfos.lib.constant import UNSET
|
|
13
|
+
from deepfos.lib.asynchronous import register_on_loop_shutdown
|
|
14
|
+
from .utils import (
|
|
15
|
+
decrypt, AbsLeaseManager, ACCOUNT_EXPIRE,
|
|
16
|
+
PENDING, INITIALIZED, INITIALIZING, DBConnecetionError
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
aRepr.maxstring = 200
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# cdef class Client:
|
|
23
|
+
# cdef:
|
|
24
|
+
# object pool
|
|
25
|
+
# int status
|
|
26
|
+
# object inited
|
|
27
|
+
# object lease
|
|
28
|
+
# bytes secret
|
|
29
|
+
class Client:
|
|
30
|
+
def __init__(self):
|
|
31
|
+
self.pool = UNSET
|
|
32
|
+
self.status = PENDING
|
|
33
|
+
self.inited = asyncio.Event()
|
|
34
|
+
self.lease = LeaseManager(ACCOUNT_EXPIRE / 3)
|
|
35
|
+
self.secret = "!ABCD-EFGH-IJKL@".encode()
|
|
36
|
+
register_on_loop_shutdown(self.close, True)
|
|
37
|
+
|
|
38
|
+
async def init_pool(self):
|
|
39
|
+
if self.status == INITIALIZED:
|
|
40
|
+
return self.pool
|
|
41
|
+
|
|
42
|
+
if self.status == PENDING:
|
|
43
|
+
self.status = INITIALIZING
|
|
44
|
+
else:
|
|
45
|
+
await self.inited.wait()
|
|
46
|
+
if self.status != INITIALIZED:
|
|
47
|
+
raise RuntimeError("Failed to initialze connection pool.")
|
|
48
|
+
return self.pool
|
|
49
|
+
|
|
50
|
+
conf = await self.lease.renew()
|
|
51
|
+
self.lease.schedule(slow_start=True)
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
self.pool = await aiomysql.create_pool(
|
|
55
|
+
minsize=5,
|
|
56
|
+
maxsize=10,
|
|
57
|
+
host=conf.host,
|
|
58
|
+
port=conf.port,
|
|
59
|
+
user=conf.name,
|
|
60
|
+
password=decrypt(self.secret, conf.password),
|
|
61
|
+
db=conf.dbName,
|
|
62
|
+
)
|
|
63
|
+
except Exception as e:
|
|
64
|
+
self.status = PENDING
|
|
65
|
+
raise DBConnecetionError(e) from None
|
|
66
|
+
else:
|
|
67
|
+
self.status = INITIALIZED
|
|
68
|
+
self.inited.set()
|
|
69
|
+
return self.pool
|
|
70
|
+
|
|
71
|
+
async def ensure_connected(self):
|
|
72
|
+
max_retry = 3
|
|
73
|
+
retries = 0
|
|
74
|
+
interval = 0.5
|
|
75
|
+
|
|
76
|
+
while self.pool is UNSET or self.pool._closed: # noqa
|
|
77
|
+
retries += 1
|
|
78
|
+
try:
|
|
79
|
+
await self.init_pool()
|
|
80
|
+
except DBConnecetionError:
|
|
81
|
+
if retries > max_retry:
|
|
82
|
+
self.inited.set()
|
|
83
|
+
raise
|
|
84
|
+
logger.exception(f'Failed to get connection pool, '
|
|
85
|
+
f'starting {retries} times retry.')
|
|
86
|
+
await asyncio.sleep(interval)
|
|
87
|
+
interval *= 2
|
|
88
|
+
except Exception:
|
|
89
|
+
self.inited.set()
|
|
90
|
+
self.status = PENDING
|
|
91
|
+
raise
|
|
92
|
+
|
|
93
|
+
return self.pool
|
|
94
|
+
|
|
95
|
+
async def execute(self, sql):
|
|
96
|
+
pool = await self.ensure_connected()
|
|
97
|
+
async with pool.acquire() as conn:
|
|
98
|
+
async with conn.cursor() as cur:
|
|
99
|
+
logger.opt(lazy=True).debug("Run sql: {sql}",
|
|
100
|
+
sql=lambda: aRepr.repr(sql))
|
|
101
|
+
await cur.execute(sql)
|
|
102
|
+
await conn.commit()
|
|
103
|
+
return conn.affected_rows()
|
|
104
|
+
|
|
105
|
+
async def select(self, sql):
|
|
106
|
+
pool = await self.ensure_connected()
|
|
107
|
+
async with pool.acquire() as conn:
|
|
108
|
+
async with conn.cursor() as cur:
|
|
109
|
+
logger.opt(lazy=True).debug("Run sql: {sql}",
|
|
110
|
+
sql=lambda: aRepr.repr(sql))
|
|
111
|
+
await cur.execute(sql)
|
|
112
|
+
return await cur.fetchall()
|
|
113
|
+
|
|
114
|
+
async def query_dataframe(self, sql):
|
|
115
|
+
pool = await self.ensure_connected()
|
|
116
|
+
async with pool.acquire() as conn:
|
|
117
|
+
async with conn.cursor() as cur:
|
|
118
|
+
logger.opt(lazy=True).debug("Run sql: {sql}",
|
|
119
|
+
sql=lambda: aRepr.repr(sql))
|
|
120
|
+
await cur.execute(sql)
|
|
121
|
+
columns = [d[0] for d in cur.description]
|
|
122
|
+
decimal_cols = [
|
|
123
|
+
d[0] for d in cur.description
|
|
124
|
+
if d[1] == FIELD_TYPE.NEWDECIMAL
|
|
125
|
+
]
|
|
126
|
+
data = await cur.fetchall()
|
|
127
|
+
df = pd.DataFrame(data=data, columns=columns)
|
|
128
|
+
for col in decimal_cols:
|
|
129
|
+
df[col] = pd.to_numeric(df[col], downcast='float')
|
|
130
|
+
return df
|
|
131
|
+
|
|
132
|
+
async def trxn_execute(self, sqls):
|
|
133
|
+
pool = await self.ensure_connected()
|
|
134
|
+
async with pool.acquire() as conn:
|
|
135
|
+
async with conn.cursor() as cur:
|
|
136
|
+
logger.opt(lazy=True).debug("Run sql: {sql}",
|
|
137
|
+
sql=lambda: aRepr.repr(sqls))
|
|
138
|
+
for sql in sqls:
|
|
139
|
+
await cur.execute(sql)
|
|
140
|
+
await conn.commit()
|
|
141
|
+
return conn.affected_rows()
|
|
142
|
+
|
|
143
|
+
async def close(self):
|
|
144
|
+
self.lease.cancel()
|
|
145
|
+
self.status = PENDING
|
|
146
|
+
|
|
147
|
+
if self.pool is not UNSET:
|
|
148
|
+
self.pool.close()
|
|
149
|
+
await self.pool.wait_closed()
|
|
150
|
+
self.pool = UNSET
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# cdef class ClientManager:
|
|
154
|
+
class ClientManager:
|
|
155
|
+
cache = TTLCache(maxsize=10, ttl=1800)
|
|
156
|
+
|
|
157
|
+
def new_client(self):
|
|
158
|
+
key = (
|
|
159
|
+
OPTION.api.header.get('space'),
|
|
160
|
+
OPTION.api.header.get('app'),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
if key not in self.cache:
|
|
164
|
+
self.cache[key] = Client()
|
|
165
|
+
|
|
166
|
+
return self.cache[key]
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class LeaseManager(AbsLeaseManager):
|
|
170
|
+
DB = 'mysql'
|
|
171
|
+
|
|
172
|
+
async def new_api(self):
|
|
173
|
+
return await MySQLAPI(version=1.0, sync=False)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
async def select(sql):
|
|
177
|
+
return await ClientManager().new_client().select(sql)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
async def execute(sql):
|
|
181
|
+
return await ClientManager().new_client().execute(sql)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
async def query_dataframe(sql):
|
|
185
|
+
return await ClientManager().new_client().query_dataframe(sql)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
async def trxn_execute(sqls):
|
|
189
|
+
return await ClientManager().new_client().trxn_execute(sqls)
|
deepfos/db/dbkits.py
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Dict, Union, List, Iterable, Tuple
|
|
3
|
+
from typing_extensions import Protocol
|
|
4
|
+
|
|
5
|
+
from deepfos.cache import Manager
|
|
6
|
+
from deepfos.lib.decorator import cached_property
|
|
7
|
+
|
|
8
|
+
from cachetools import TTLCache
|
|
9
|
+
from pandas.core.dtypes.common import is_datetime64_dtype, is_numeric_dtype
|
|
10
|
+
import numpy as np
|
|
11
|
+
import pandas as pd
|
|
12
|
+
|
|
13
|
+
from deepfos.options import OPTION
|
|
14
|
+
from deepfos.api.models.app import ModuleServerNameVO as ElementModel
|
|
15
|
+
from deepfos.api.models.datatable_mysql import BaseElementInfo
|
|
16
|
+
from deepfos.lib.asynchronous import cache_async
|
|
17
|
+
from deepfos.lib.decorator import singleton
|
|
18
|
+
from deepfos.lib.utils import FrozenClass
|
|
19
|
+
from deepfos.api.space import SpaceAPI
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DataTable(Protocol):
|
|
23
|
+
table_name: str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
T_DataInfo = Dict[str, Union[Dict, DataTable, BaseElementInfo]]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@singleton
|
|
30
|
+
class APIFinder:
|
|
31
|
+
async def modules(self):
|
|
32
|
+
mdl = await SpaceAPI(sync=False).module.get_usable_module()
|
|
33
|
+
return mdl or []
|
|
34
|
+
|
|
35
|
+
async def find_api(self, api_cls, version=None):
|
|
36
|
+
if version is not None:
|
|
37
|
+
return await api_cls(version=version, sync=False)
|
|
38
|
+
|
|
39
|
+
modules = await self.modules()
|
|
40
|
+
target_module = api_cls.module_type
|
|
41
|
+
|
|
42
|
+
for mdl in modules:
|
|
43
|
+
if mdl.moduleType == target_module and mdl.status == 1:
|
|
44
|
+
api = api_cls(sync=False)
|
|
45
|
+
# noinspection PyUnresolvedReferences
|
|
46
|
+
await api.set_url(mdl.serverName)
|
|
47
|
+
api.__class__.server_cache[mdl.moduleId] = mdl.serverName
|
|
48
|
+
return api
|
|
49
|
+
|
|
50
|
+
raise RuntimeError( # pragma: no cover
|
|
51
|
+
f"Cannot find a valid {api_cls.__name__} "
|
|
52
|
+
f"in space: {OPTION.api.header['space']}.")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class BaseSqlParser:
|
|
56
|
+
_RE_TABLE_PLACEHOLDER = re.compile(r"\${(.*?)}")
|
|
57
|
+
api_cls = None
|
|
58
|
+
|
|
59
|
+
def __init__(self):
|
|
60
|
+
self.table_cache = Manager.create_cache(TTLCache, maxsize=100, ttl=3600)
|
|
61
|
+
|
|
62
|
+
async def build_api(self):
|
|
63
|
+
return await APIFinder().find_api(self.api_cls, version=1.0)
|
|
64
|
+
|
|
65
|
+
@cached_property
|
|
66
|
+
def datatable_cls(self): # pragma: no cover
|
|
67
|
+
raise NotImplementedError
|
|
68
|
+
|
|
69
|
+
async def parse(
|
|
70
|
+
self,
|
|
71
|
+
sql_list: Iterable[str],
|
|
72
|
+
table_info: T_DataInfo
|
|
73
|
+
) -> List[str]:
|
|
74
|
+
tbl_placeholders = set()
|
|
75
|
+
api = await self.build_api()
|
|
76
|
+
|
|
77
|
+
for sql in sql_list:
|
|
78
|
+
tbl_placeholders.update(self._RE_TABLE_PLACEHOLDER.findall(sql))
|
|
79
|
+
|
|
80
|
+
table_info = table_info or {}
|
|
81
|
+
ph_to_actualname = {} # 占位符中的“表名” -> 实际表名(最终替换占位符的依据)
|
|
82
|
+
#: 待查询表名的数据表的元素信息
|
|
83
|
+
query_info: List[Tuple[str, Union[BaseElementInfo, ElementModel]]] = []
|
|
84
|
+
#: 占位符中的“表名” -> 元素名,用于处理同表不同占位符的特情况
|
|
85
|
+
ph_to_elename = {}
|
|
86
|
+
|
|
87
|
+
for table in tbl_placeholders:
|
|
88
|
+
if table not in table_info:
|
|
89
|
+
element = await self.datatable_cls(table)._get_element_info() # noqa
|
|
90
|
+
query_info.append((table, element))
|
|
91
|
+
ph_to_elename[table] = element.elementName
|
|
92
|
+
else:
|
|
93
|
+
info = table_info[table]
|
|
94
|
+
|
|
95
|
+
if isinstance(info, BaseElementInfo):
|
|
96
|
+
query_info.append((table, info))
|
|
97
|
+
ph_to_elename[table] = info.elementName
|
|
98
|
+
elif isinstance(info, self.datatable_cls):
|
|
99
|
+
ph_to_actualname[table] = info.table_name
|
|
100
|
+
else:
|
|
101
|
+
ele = BaseElementInfo(**info)
|
|
102
|
+
query_info.append((table, ele))
|
|
103
|
+
ph_to_elename[table] = ele.elementName
|
|
104
|
+
|
|
105
|
+
query_table, known = self._resolve_query(query_info)
|
|
106
|
+
ph_to_actualname.update(known)
|
|
107
|
+
|
|
108
|
+
if query_table:
|
|
109
|
+
tables = await self.query_table_names(api, query_table)
|
|
110
|
+
if not tables or tables.pop('errorMessageList', None):
|
|
111
|
+
raise ValueError("One or more table is not found.")
|
|
112
|
+
|
|
113
|
+
self._update_cache(query_table, tables)
|
|
114
|
+
|
|
115
|
+
ph_to_actualname.update({
|
|
116
|
+
placeholder: tables[elename]
|
|
117
|
+
for placeholder, elename in ph_to_elename.items()
|
|
118
|
+
if elename in tables
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
return [
|
|
122
|
+
self._RE_TABLE_PLACEHOLDER.sub(
|
|
123
|
+
lambda m: ph_to_actualname[m.group(1)],
|
|
124
|
+
sql
|
|
125
|
+
) for sql in sql_list
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
@staticmethod
|
|
129
|
+
async def query_table_names(api, query_table):
|
|
130
|
+
return await api.dml.batch_tablename(query_table)
|
|
131
|
+
|
|
132
|
+
def _resolve_query(
|
|
133
|
+
self,
|
|
134
|
+
query_info: List[Tuple[str, Union[BaseElementInfo, ElementModel]]]
|
|
135
|
+
) -> Tuple[List[Union[BaseElementInfo, ElementModel]], Dict[str, str]]:
|
|
136
|
+
need_query = []
|
|
137
|
+
known_tables = {}
|
|
138
|
+
table_cache = self.table_cache
|
|
139
|
+
|
|
140
|
+
for placeholder, tbl in query_info:
|
|
141
|
+
key = (tbl.elementName, tbl.folderId)
|
|
142
|
+
if key in table_cache:
|
|
143
|
+
known_tables[placeholder] = table_cache[key]
|
|
144
|
+
else:
|
|
145
|
+
need_query.append(tbl)
|
|
146
|
+
|
|
147
|
+
return need_query, known_tables
|
|
148
|
+
|
|
149
|
+
def _update_cache(
|
|
150
|
+
self,
|
|
151
|
+
query_table: List[Union[BaseElementInfo, ElementModel]],
|
|
152
|
+
table_names: Dict[str, str]
|
|
153
|
+
):
|
|
154
|
+
for tbl in query_table:
|
|
155
|
+
key = (tbl.elementName, tbl.folderId)
|
|
156
|
+
if tbl.elementName in table_names:
|
|
157
|
+
self.table_cache[key] = table_names[tbl.elementName]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class SyncMeta(type):
|
|
161
|
+
def __new__(mcs, name, bases, namespace, **kwargs):
|
|
162
|
+
base = bases[0]
|
|
163
|
+
methods = namespace.pop('synchronize', [])
|
|
164
|
+
from deepfos.element.base import synchronize
|
|
165
|
+
|
|
166
|
+
for attr in methods:
|
|
167
|
+
namespace[attr] = synchronize(mcs._get_from_bases(base, attr))
|
|
168
|
+
|
|
169
|
+
cls = super().__new__(mcs, name, bases, namespace, **kwargs)
|
|
170
|
+
return cls
|
|
171
|
+
|
|
172
|
+
@staticmethod
|
|
173
|
+
def _get_from_bases(base, attr):
|
|
174
|
+
while base is not object:
|
|
175
|
+
if attr in base.__dict__:
|
|
176
|
+
return base.__dict__[attr]
|
|
177
|
+
base = base.__base__
|
|
178
|
+
raise AttributeError(attr)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
_escape_table = [chr(x) for x in range(128)]
|
|
182
|
+
_escape_table[ord("'")] = u"''"
|
|
183
|
+
|
|
184
|
+
_escape_table_mysql = list(_escape_table)
|
|
185
|
+
_escape_table_mysql[ord('\\')] = u'\\\\'
|
|
186
|
+
|
|
187
|
+
_escape_table_pg = list(_escape_table)
|
|
188
|
+
_escape_table_pg[0] = ''
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def escape_string(value):
|
|
192
|
+
return value.translate(_escape_table)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def escape_mysql_string(value):
|
|
196
|
+
# for mysql & ck & deepengine & sqlserver
|
|
197
|
+
return value.translate(_escape_table_mysql)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def escape_pg_string(value):
|
|
201
|
+
# for pg & kingbase & gauss
|
|
202
|
+
return value.translate(_escape_table_pg)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# noinspection PyPep8Naming
|
|
206
|
+
class null(metaclass=FrozenClass):
|
|
207
|
+
"""mysql数据库中的null"""
|
|
208
|
+
|
|
209
|
+
@classmethod
|
|
210
|
+
def translate(cls, *args): # noqa
|
|
211
|
+
# 模仿string的translate
|
|
212
|
+
return cls
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class Skip(metaclass=FrozenClass):
|
|
216
|
+
"""在更新dataframe中跳过更新的字段"""
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class DataframeSQLConvertor:
|
|
220
|
+
escape_string = escape_string
|
|
221
|
+
|
|
222
|
+
def __init__(self, quote_char=None):
|
|
223
|
+
self.quote_char = quote_char
|
|
224
|
+
|
|
225
|
+
def iter_sql(
|
|
226
|
+
self,
|
|
227
|
+
dataframe: pd.DataFrame,
|
|
228
|
+
tablename: str,
|
|
229
|
+
updatecol: Iterable = None,
|
|
230
|
+
chunksize: int = None,
|
|
231
|
+
**opts
|
|
232
|
+
) -> Iterable[str]:
|
|
233
|
+
""" :class:`DataFrame` 对象转换为sql生成器
|
|
234
|
+
|
|
235
|
+
如果传了updatecol,将使用 ``INSERT INTO ON DUPLICATE`` 语法,
|
|
236
|
+
无主键重复时作为插入,主键相同时更新指定列。
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
dataframe: 待插入数据
|
|
240
|
+
tablename: 数据库表名
|
|
241
|
+
updatecol: 更新的列
|
|
242
|
+
chunksize: 单条sql对应的最大dataframe行数
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
sql语句生成器
|
|
246
|
+
|
|
247
|
+
Attention:
|
|
248
|
+
当单个DataFrame生成的sql太长导致无法入库,可以指定 ``chuncksize`` ,
|
|
249
|
+
使DataFrame生成多条sql。
|
|
250
|
+
|
|
251
|
+
"""
|
|
252
|
+
# 获取sql
|
|
253
|
+
if (nrows := len(dataframe)) == 0:
|
|
254
|
+
return
|
|
255
|
+
|
|
256
|
+
if chunksize is None or chunksize > nrows:
|
|
257
|
+
yield self.convert(dataframe, tablename, updatecol, **opts)
|
|
258
|
+
elif chunksize <= 0:
|
|
259
|
+
raise ValueError("chunksize must be greater than 0.")
|
|
260
|
+
else:
|
|
261
|
+
for i in range(0, nrows, chunksize):
|
|
262
|
+
yield self.convert(dataframe.iloc[i: i + chunksize], tablename, updatecol, **opts)
|
|
263
|
+
|
|
264
|
+
@classmethod
|
|
265
|
+
def _quote_escape(cls, value):
|
|
266
|
+
if pd.isna(value):
|
|
267
|
+
return value
|
|
268
|
+
if not isinstance(value, str):
|
|
269
|
+
return str(value)
|
|
270
|
+
return f"'{cls.escape_string(value)}'"
|
|
271
|
+
|
|
272
|
+
@staticmethod
|
|
273
|
+
def format_datetime(maybe_datetime: pd.Series) -> pd.Series:
|
|
274
|
+
return "'" + maybe_datetime.dt.strftime("%Y-%m-%d %H:%M:%S") + "'"
|
|
275
|
+
|
|
276
|
+
@classmethod
|
|
277
|
+
def format_series(cls, series: pd.Series) -> pd.Series:
|
|
278
|
+
"""格式化Series以适合sql语句
|
|
279
|
+
|
|
280
|
+
1. 日期型重置格式到秒级别
|
|
281
|
+
2. 字符串型列转义、加引号
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
series (pd.Series): 需要格式化的Series
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
pd.Series: 格式化后的新的Series,不影响原Series
|
|
288
|
+
"""
|
|
289
|
+
# 对日期型重置格式到秒级别
|
|
290
|
+
if is_datetime64_dtype(series.dtype):
|
|
291
|
+
return cls.format_datetime(series)
|
|
292
|
+
# 对字符串型列转义,加引号
|
|
293
|
+
elif not is_numeric_dtype(series.dtype):
|
|
294
|
+
return series.apply(cls._quote_escape)
|
|
295
|
+
return series.copy()
|
|
296
|
+
|
|
297
|
+
def convert(
|
|
298
|
+
self,
|
|
299
|
+
dataframe: pd.DataFrame,
|
|
300
|
+
tablename: str,
|
|
301
|
+
updatecol: Iterable[str] = None,
|
|
302
|
+
**opts
|
|
303
|
+
) -> str:
|
|
304
|
+
""" :class:`DataFrame` 对象转换为插库sql
|
|
305
|
+
|
|
306
|
+
如果传了updatecol,将使用 ``INSERT INTO ON DUPLICATE`` 语法,
|
|
307
|
+
无主键重复时作为插入,主键相同时更新指定列
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
dataframe: 待插入数据
|
|
311
|
+
tablename: 数据库表名
|
|
312
|
+
updatecol: 更新的列
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
sql语句
|
|
316
|
+
|
|
317
|
+
"""
|
|
318
|
+
if dataframe.empty:
|
|
319
|
+
return ''
|
|
320
|
+
# 格式化Series以适合sql语句
|
|
321
|
+
data_df = dataframe.apply(self.format_series)
|
|
322
|
+
# 空值填充
|
|
323
|
+
data_df = data_df.fillna(null)
|
|
324
|
+
# 全部转化为字符串类型
|
|
325
|
+
data_df = data_df.astype(str, errors='ignore')
|
|
326
|
+
values = "(" + pd.Series(data_df.values.tolist()).str.join(',') + ")"
|
|
327
|
+
columns = self.build_column_string(dataframe.columns)
|
|
328
|
+
return self.build_sql(columns, values, tablename, updatecol, **opts)
|
|
329
|
+
|
|
330
|
+
def build_column_string(self, columns: pd.Index) -> str:
|
|
331
|
+
if self.quote_char:
|
|
332
|
+
columns = ','.join(columns.map(
|
|
333
|
+
lambda x: f'{self.quote_char}{x}{self.quote_char}'
|
|
334
|
+
))
|
|
335
|
+
else:
|
|
336
|
+
columns = ','.join(columns)
|
|
337
|
+
return columns
|
|
338
|
+
|
|
339
|
+
def build_sql(
|
|
340
|
+
self,
|
|
341
|
+
columns: str,
|
|
342
|
+
values_in_line: Iterable[str],
|
|
343
|
+
tablename: str,
|
|
344
|
+
updatecol: Iterable[str] = None,
|
|
345
|
+
**opts
|
|
346
|
+
):
|
|
347
|
+
values = ','.join(values_in_line)
|
|
348
|
+
if updatecol is None:
|
|
349
|
+
return f'INSERT INTO {self.quote_char}{tablename}{self.quote_char} ({columns}) VALUES {values}'
|
|
350
|
+
|
|
351
|
+
update_str = ','.join([f"{self.quote_char}{x}{self.quote_char}="
|
|
352
|
+
f"VALUES({self.quote_char}{x}{self.quote_char})" for x in updatecol])
|
|
353
|
+
if not update_str:
|
|
354
|
+
return f'INSERT INTO {self.quote_char}{tablename}{self.quote_char} ({columns}) VALUES {values}'
|
|
355
|
+
|
|
356
|
+
return f'INSERT INTO {self.quote_char}{tablename}{self.quote_char} ({columns}) ' \
|
|
357
|
+
f'VALUES {values} ' \
|
|
358
|
+
f'ON DUPLICATE KEY UPDATE {update_str}'
|
deepfos/db/deepengine.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""DeepEngine客户端"""
|
|
2
|
+
from typing import Union, List, Iterable, TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
from deepfos.api.datatable import DeepEngineAPI
|
|
7
|
+
from deepfos.cache import Manager, SpaceSeperatedTTLCache
|
|
8
|
+
from deepfos.lib.utils import cachedclass
|
|
9
|
+
from deepfos.lib.decorator import singleton, cached_property
|
|
10
|
+
from .dbkits import BaseSqlParser, SyncMeta, T_DataInfo
|
|
11
|
+
from .connector import DeepEngineAPIConnector
|
|
12
|
+
from .mysql import _AbsAsyncMySQLClient, MySQLConvertor # noqa
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
'DeepEngineClient',
|
|
17
|
+
'AsyncDeepEngineClient',
|
|
18
|
+
'DeepEngineDFSQLConvertor',
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@singleton
|
|
23
|
+
class SqlParser(BaseSqlParser):
|
|
24
|
+
api_cls = DeepEngineAPI
|
|
25
|
+
|
|
26
|
+
@cached_property
|
|
27
|
+
def datatable_cls(self):
|
|
28
|
+
from deepfos.element.datatable import AsyncDataTableDeepEngine
|
|
29
|
+
return AsyncDataTableDeepEngine
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DeepEngineDFSQLConvertor(MySQLConvertor):
|
|
33
|
+
def build_sql(
|
|
34
|
+
self,
|
|
35
|
+
columns: str,
|
|
36
|
+
values_in_line: Iterable[str],
|
|
37
|
+
tablename: str,
|
|
38
|
+
updatecol: Iterable[str] = None,
|
|
39
|
+
**opts
|
|
40
|
+
):
|
|
41
|
+
if updatecol is not None:
|
|
42
|
+
raise NotImplementedError("`updatecol` is not yet implemented for DeepEngineDB.")
|
|
43
|
+
|
|
44
|
+
return super().build_sql(columns, values_in_line, tablename, updatecol, **opts)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# -----------------------------------------------------------------------------
|
|
48
|
+
# core
|
|
49
|
+
class _AsyncDeepEngineClient(_AbsAsyncMySQLClient):
|
|
50
|
+
convertor = DeepEngineDFSQLConvertor(quote_char='`')
|
|
51
|
+
|
|
52
|
+
def __init__(self, version: Union[float, str] = None): # noqa
|
|
53
|
+
self.parser = SqlParser()
|
|
54
|
+
self.connector = DeepEngineAPIConnector(version)
|
|
55
|
+
self.connector.trxn_execute = self.connector.execute_many
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class _DeepEngineClient(_AsyncDeepEngineClient, metaclass=SyncMeta):
|
|
59
|
+
synchronize = (
|
|
60
|
+
'exec_sqls',
|
|
61
|
+
'query_dfs',
|
|
62
|
+
'insert_df',
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
66
|
+
def exec_sqls(
|
|
67
|
+
self,
|
|
68
|
+
sqls: Union[str, Iterable[str]],
|
|
69
|
+
table_info: T_DataInfo = None
|
|
70
|
+
):
|
|
71
|
+
...
|
|
72
|
+
|
|
73
|
+
def query_dfs(
|
|
74
|
+
self,
|
|
75
|
+
sqls: Union[str, Iterable[str]],
|
|
76
|
+
table_info: T_DataInfo = None
|
|
77
|
+
) -> Union[pd.DataFrame, List[pd.DataFrame]]:
|
|
78
|
+
...
|
|
79
|
+
|
|
80
|
+
def insert_df(
|
|
81
|
+
self,
|
|
82
|
+
dataframe: pd.DataFrame,
|
|
83
|
+
element_name: str = None,
|
|
84
|
+
table_name: str = None,
|
|
85
|
+
updatecol: Iterable[str] = None,
|
|
86
|
+
table_info: T_DataInfo = None,
|
|
87
|
+
chunksize: int = None,
|
|
88
|
+
):
|
|
89
|
+
...
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@cachedclass(Manager.create_cache(SpaceSeperatedTTLCache, maxsize=5, ttl=3600))
|
|
93
|
+
class AsyncDeepEngineClient(_AsyncDeepEngineClient):
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@cachedclass(Manager.create_cache(SpaceSeperatedTTLCache, maxsize=5, ttl=3600))
|
|
98
|
+
class DeepEngineClient(_DeepEngineClient):
|
|
99
|
+
pass
|