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/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}'
@@ -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