ab_engine 0.1.1__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.
- ab_engine/__init__.py +4 -0
- ab_engine/class_tools.py +59 -0
- ab_engine/db/__init__.py +3 -0
- ab_engine/db/driver.py +339 -0
- ab_engine/db/driver_mysql.py +110 -0
- ab_engine/db/driver_postgresql.py +122 -0
- ab_engine/db/driver_sqlite.py +145 -0
- ab_engine/db/option.py +391 -0
- ab_engine/db/processor.py +90 -0
- ab_engine/db/table.py +654 -0
- ab_engine/env/__init__.py +2 -0
- ab_engine/env/config.py +530 -0
- ab_engine/env/db_context.py +214 -0
- ab_engine/env/timer.py +104 -0
- ab_engine/error.py +81 -0
- ab_engine/rpc/__init__.py +2 -0
- ab_engine/rpc/fnc.py +123 -0
- ab_engine/rpc/json_rpc.py +158 -0
- ab_engine/rpc/rpc.py +143 -0
- ab_engine-0.1.1.dist-info/METADATA +1272 -0
- ab_engine-0.1.1.dist-info/RECORD +23 -0
- ab_engine-0.1.1.dist-info/WHEEL +5 -0
- ab_engine-0.1.1.dist-info/top_level.txt +1 -0
ab_engine/__init__.py
ADDED
ab_engine/class_tools.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
class ClassPropertyDescriptor(object):
|
|
2
|
+
|
|
3
|
+
def __init__(self, fget, fset=None):
|
|
4
|
+
self.fget = fget
|
|
5
|
+
self.fset = fset
|
|
6
|
+
|
|
7
|
+
def __get__(self, obj, klass=None):
|
|
8
|
+
if klass is None:
|
|
9
|
+
klass = type(obj)
|
|
10
|
+
return self.fget.__get__(obj, klass)()
|
|
11
|
+
|
|
12
|
+
def __set__(self, obj, value):
|
|
13
|
+
if not self.fset:
|
|
14
|
+
raise AttributeError("can't set attribute")
|
|
15
|
+
type_ = type(obj)
|
|
16
|
+
return self.fset.__get__(obj, type_)(value)
|
|
17
|
+
|
|
18
|
+
def setter(self, func):
|
|
19
|
+
if not isinstance(func, (classmethod, staticmethod)):
|
|
20
|
+
func = classmethod(func)
|
|
21
|
+
self.fset = func
|
|
22
|
+
return self
|
|
23
|
+
|
|
24
|
+
def classproperty(func):
|
|
25
|
+
if not isinstance(func, (classmethod, staticmethod)):
|
|
26
|
+
func = classmethod(func)
|
|
27
|
+
|
|
28
|
+
return ClassPropertyDescriptor(func)
|
|
29
|
+
|
|
30
|
+
def find_subclass(base, name):
|
|
31
|
+
if base.__name__ == name:
|
|
32
|
+
return base
|
|
33
|
+
for cls in base.__subclasses__():
|
|
34
|
+
cls = find_subclass(cls, name)
|
|
35
|
+
if cls:
|
|
36
|
+
return cls
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ReadOnlyPropDict:
|
|
41
|
+
|
|
42
|
+
def __init__(self, **kwargs):
|
|
43
|
+
self._data = kwargs
|
|
44
|
+
for x in kwargs:
|
|
45
|
+
self._set_f(x)
|
|
46
|
+
|
|
47
|
+
def _set_f(self, x):
|
|
48
|
+
setattr(self.__class__, x, property(lambda p: self._data[x]))
|
|
49
|
+
|
|
50
|
+
def __getitem__(self, item):
|
|
51
|
+
return self._data[item]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class PropDict(ReadOnlyPropDict):
|
|
55
|
+
|
|
56
|
+
def _set_f(self, x):
|
|
57
|
+
def set_f(p, v):
|
|
58
|
+
self._data[x] = v
|
|
59
|
+
setattr(self.__class__, x, property(lambda p: self._data[x], set_f))
|
ab_engine/db/__init__.py
ADDED
ab_engine/db/driver.py
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from json import dumps
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from collections import namedtuple
|
|
6
|
+
|
|
7
|
+
_is_option = None
|
|
8
|
+
|
|
9
|
+
def _set_is_option(f):
|
|
10
|
+
global _is_option
|
|
11
|
+
_is_option = f
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RowFactory(Enum):
|
|
15
|
+
"""
|
|
16
|
+
Варианты фабрик для возврата набора данных
|
|
17
|
+
"""
|
|
18
|
+
ANY = 0
|
|
19
|
+
TUPLE = 1
|
|
20
|
+
DICT = 2
|
|
21
|
+
NAMED_TUPLE = 3
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def reencode(s):
|
|
25
|
+
if s is None:
|
|
26
|
+
return " NULL"
|
|
27
|
+
elif isinstance(s, str):
|
|
28
|
+
return f"'{s}'"
|
|
29
|
+
return s
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Driver(ABC):
|
|
33
|
+
|
|
34
|
+
LIKE = "LIKE"
|
|
35
|
+
|
|
36
|
+
def __init__(self, connection_string, on_open_close=None):
|
|
37
|
+
self._conn = None
|
|
38
|
+
if isinstance(connection_string, Driver):
|
|
39
|
+
self._conn_str = connection_string.connection_string
|
|
40
|
+
self._on_open_close = on_open_close or connection_string._on_open_close
|
|
41
|
+
return
|
|
42
|
+
self._on_open_close = on_open_close
|
|
43
|
+
self._conn_str = connection_string
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def connection_string(self):
|
|
47
|
+
return self._conn_str
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def in_transaction(self)->bool:
|
|
51
|
+
"""возвращает открыта ли транзакция"""
|
|
52
|
+
return self._conn is not None
|
|
53
|
+
|
|
54
|
+
async def _before_open(self)->dict:
|
|
55
|
+
if self._conn:
|
|
56
|
+
raise RuntimeError("Transaction already open")
|
|
57
|
+
if x := self._on_open_close:
|
|
58
|
+
params = await x(False)
|
|
59
|
+
if not params:
|
|
60
|
+
params = {}
|
|
61
|
+
else:
|
|
62
|
+
params = {}
|
|
63
|
+
return params
|
|
64
|
+
|
|
65
|
+
@abstractmethod
|
|
66
|
+
async def begin(self):
|
|
67
|
+
"""открывает транзакцию"""
|
|
68
|
+
...
|
|
69
|
+
|
|
70
|
+
@abstractmethod
|
|
71
|
+
async def sql(self, query, one_row=False, row_factory=RowFactory.DICT):
|
|
72
|
+
"""выполняет запрос
|
|
73
|
+
query текст запроса
|
|
74
|
+
one_row если True, то выполняется fetchone иначе fetch
|
|
75
|
+
row_factory способ представления результата
|
|
76
|
+
если набор данных пустой и one_row==False - возвращает []
|
|
77
|
+
если набор данных пустой и one_row==True - возвращает None
|
|
78
|
+
если запрос не возвращает результат - возвращает число обработанных строк (целое)
|
|
79
|
+
"""
|
|
80
|
+
...
|
|
81
|
+
|
|
82
|
+
@abstractmethod
|
|
83
|
+
async def commit(self):
|
|
84
|
+
"""подтверждает транзакцию и закрывает соединение"""
|
|
85
|
+
...
|
|
86
|
+
|
|
87
|
+
async def rollback(self):
|
|
88
|
+
"""откатывает транзакцию и закрывает соединение"""
|
|
89
|
+
if x:=self._on_open_close:
|
|
90
|
+
await x(True)
|
|
91
|
+
self._conn = None
|
|
92
|
+
|
|
93
|
+
@staticmethod
|
|
94
|
+
def var_to_sql(var, str_after=" "):
|
|
95
|
+
if var is None:
|
|
96
|
+
return " NULL"
|
|
97
|
+
elif isinstance(var, str):
|
|
98
|
+
var = var.replace("'", "''")
|
|
99
|
+
return reencode(var)
|
|
100
|
+
elif isinstance(var, (list, tuple)):
|
|
101
|
+
if str_after[:6].lower() == '::json' or str_after[0] != ':':
|
|
102
|
+
var = dumps(var, ensure_ascii=False).replace("'", "''")
|
|
103
|
+
return reencode(var)
|
|
104
|
+
else:
|
|
105
|
+
var = ','.join(['NULL' if x is None else str(x) for x in var])
|
|
106
|
+
return reencode(var)
|
|
107
|
+
elif isinstance(var, set):
|
|
108
|
+
var = ','.join('NULL' if x is None else str(x) for x in var)
|
|
109
|
+
return reencode(var)
|
|
110
|
+
elif isinstance(var, dict):
|
|
111
|
+
var = dumps(var, ensure_ascii=False).replace("'", "''")
|
|
112
|
+
return reencode(var)
|
|
113
|
+
elif isinstance(var, datetime):
|
|
114
|
+
return f" '{var}'"
|
|
115
|
+
else:
|
|
116
|
+
return f" {var}"
|
|
117
|
+
|
|
118
|
+
async def parse_query(self, query, *args, **kwargs):
|
|
119
|
+
"""подставляет параметры в текст запроса"""
|
|
120
|
+
if callback:=kwargs.get("__PARAM_CALLBACK_GETTER"):
|
|
121
|
+
del kwargs["__PARAM_CALLBACK_GETTER"]
|
|
122
|
+
qs = None
|
|
123
|
+
for x in query.split("$"):
|
|
124
|
+
if qs is None:
|
|
125
|
+
qs = x
|
|
126
|
+
continue
|
|
127
|
+
n, p = '', 0
|
|
128
|
+
if str(x[p]).isdigit():
|
|
129
|
+
while p < len(x) and x[p].isdigit():
|
|
130
|
+
n += x[p]
|
|
131
|
+
p += 1
|
|
132
|
+
n = int(n) - 1
|
|
133
|
+
if n < 0 or n >= len(args):
|
|
134
|
+
raise AttributeError(f"Attribute with index {n} is not exists")
|
|
135
|
+
n = args[n]
|
|
136
|
+
else:
|
|
137
|
+
while p < len(x) and not(x[p].isspace() or x[p] in "(:,)"):
|
|
138
|
+
n += x[p]
|
|
139
|
+
p += 1
|
|
140
|
+
if n in kwargs:
|
|
141
|
+
n = kwargs[n]
|
|
142
|
+
elif callback:
|
|
143
|
+
n = callback[n]
|
|
144
|
+
else:
|
|
145
|
+
raise AttributeError(f"Attribute with name {n} is not exists")
|
|
146
|
+
qs += f"{self.var_to_sql(n, x)}{x[p:]}"
|
|
147
|
+
|
|
148
|
+
if qs:
|
|
149
|
+
query = qs
|
|
150
|
+
|
|
151
|
+
return query
|
|
152
|
+
|
|
153
|
+
@staticmethod
|
|
154
|
+
def is_option(what):
|
|
155
|
+
global _is_option
|
|
156
|
+
if _is_option:
|
|
157
|
+
return _is_option(what)
|
|
158
|
+
else:
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
async def parse_func(self, func_name, *args, **kwargs):
|
|
162
|
+
"""
|
|
163
|
+
Возвращает запрос для вызова функции func_name
|
|
164
|
+
:param args: неименованные параметры
|
|
165
|
+
:param kwargs: именованные параметры
|
|
166
|
+
:return: запрос
|
|
167
|
+
"""
|
|
168
|
+
ret = f"select {func_name}("
|
|
169
|
+
for n in range(len(args)):
|
|
170
|
+
if self.is_option(args[n]):
|
|
171
|
+
break
|
|
172
|
+
if n > 0:
|
|
173
|
+
ret += ", "
|
|
174
|
+
ret += f"${n+1}"
|
|
175
|
+
for n, x in enumerate(kwargs):
|
|
176
|
+
if n > 0:
|
|
177
|
+
ret += ", "
|
|
178
|
+
ret += f"${x}"
|
|
179
|
+
ret += ")"
|
|
180
|
+
return await self.parse_query(ret, *args, **kwargs)
|
|
181
|
+
|
|
182
|
+
async def cast(self, param_name, to_type):
|
|
183
|
+
"""
|
|
184
|
+
возвращает часть запроса для приведения типа
|
|
185
|
+
:param param_name:
|
|
186
|
+
:param to_type:
|
|
187
|
+
:return:
|
|
188
|
+
"""
|
|
189
|
+
return f"{param_name}::{to_type}"
|
|
190
|
+
|
|
191
|
+
@staticmethod
|
|
192
|
+
def ident_name(name:str)->str:
|
|
193
|
+
return name
|
|
194
|
+
|
|
195
|
+
TypeSpec = namedtuple("TypeSpec", ["type_name", "python_type", "autoincrement"])
|
|
196
|
+
|
|
197
|
+
@staticmethod
|
|
198
|
+
def _specify_type(defs):
|
|
199
|
+
if defs.data_type in ("smallint", "integer", "bigint", "smallserial", "serial", "bigserial", "int"):
|
|
200
|
+
pt = int
|
|
201
|
+
elif defs.data_type in ("double precision", "numeric", "real", "decimal", "money", "float"):
|
|
202
|
+
pt = float
|
|
203
|
+
else:
|
|
204
|
+
pt = str
|
|
205
|
+
return Driver.TypeSpec(type_name=defs.data_type, python_type=pt, autoincrement=False)
|
|
206
|
+
|
|
207
|
+
async def table_struct(self, table_name) -> dict:
|
|
208
|
+
"""
|
|
209
|
+
возвращает структуру заданной таблицы
|
|
210
|
+
"""
|
|
211
|
+
if not self.in_transaction:
|
|
212
|
+
await self.begin()
|
|
213
|
+
schema_name, table_name = f"{table_name}.".split(".", 1)
|
|
214
|
+
if table_name=="":
|
|
215
|
+
table_name = self.ident_name(schema_name)
|
|
216
|
+
schema_name = None
|
|
217
|
+
else:
|
|
218
|
+
table_name = self.ident_name(table_name[:-1])
|
|
219
|
+
schema_name = self.ident_name(schema_name)
|
|
220
|
+
fltr = f"\nwhere ku.table_name='{table_name}'"
|
|
221
|
+
if schema_name is not None:
|
|
222
|
+
fltr += f" and ku.table_schema='{schema_name}'"
|
|
223
|
+
qry =f"""select ku.column_name, ku.data_type, ku.is_nullable, ku.character_maximum_length,
|
|
224
|
+
ku.numeric_precision, ku.numeric_scale, ku.column_default
|
|
225
|
+
from information_schema.columns ku {fltr}
|
|
226
|
+
order by ku.ordinal_position"""
|
|
227
|
+
qry = await self.sql(qry, one_row=False, row_factory=RowFactory.NAMED_TUPLE)
|
|
228
|
+
if not qry:
|
|
229
|
+
return {}
|
|
230
|
+
fields = []
|
|
231
|
+
for x in qry:
|
|
232
|
+
t = self._specify_type(x)
|
|
233
|
+
field = {
|
|
234
|
+
"name": x.column_name,
|
|
235
|
+
"type": t.type_name,
|
|
236
|
+
"not_null": x.is_nullable is not None and f'{x.is_nullable} '[0].upper() in "FNН",
|
|
237
|
+
"python_type": t.python_type,
|
|
238
|
+
}
|
|
239
|
+
if t.autoincrement:
|
|
240
|
+
field["autoincrement"] =True
|
|
241
|
+
n = self.ident_name(field["name"])
|
|
242
|
+
if n!=field["name"]:
|
|
243
|
+
field["field"] = n
|
|
244
|
+
if x.character_maximum_length:
|
|
245
|
+
field["size"] = x.character_maximum_length
|
|
246
|
+
elif field["python_type"]!=int and x.numeric_precision:
|
|
247
|
+
n = f'{x.numeric_precision}.{x.numeric_scale}'
|
|
248
|
+
field["size"] = float(n)
|
|
249
|
+
fields.append(field)
|
|
250
|
+
qry = f"""select
|
|
251
|
+
x.constraint_name,
|
|
252
|
+
x.constraint_type,
|
|
253
|
+
x.column_name,
|
|
254
|
+
x.reference_schema,
|
|
255
|
+
x.reference_table,
|
|
256
|
+
r.column_name reference_column
|
|
257
|
+
from (
|
|
258
|
+
select ku.constraint_name, tc.constraint_type, ku.column_name,
|
|
259
|
+
rt.table_schema reference_schema, rt.table_name reference_table,
|
|
260
|
+
row_number() over (partition by ku.constraint_name order by ku.ordinal_position) column_position
|
|
261
|
+
from information_schema.key_column_usage ku
|
|
262
|
+
inner join information_schema.table_constraints tc on tc.constraint_name = ku.constraint_name and tc.table_name = ku.table_name and tc.table_schema = ku.table_schema
|
|
263
|
+
left join information_schema.referential_constraints rc on rc.constraint_name = ku.constraint_name
|
|
264
|
+
left join information_schema.table_constraints rt on rc.unique_constraint_name = rt.constraint_name
|
|
265
|
+
{fltr}
|
|
266
|
+
order by ku.constraint_name, ku.ordinal_position
|
|
267
|
+
) x
|
|
268
|
+
left join (
|
|
269
|
+
SELECT
|
|
270
|
+
tc.table_name, tc.table_schema, i2.COLUMN_NAME,
|
|
271
|
+
row_number() over (partition by tc.table_name, tc.table_schema order by i2.ordinal_position) column_position
|
|
272
|
+
FROM
|
|
273
|
+
information_schema.table_constraints tc
|
|
274
|
+
JOIN
|
|
275
|
+
information_schema.key_column_usage i2
|
|
276
|
+
ON tc.constraint_name = i2.constraint_name
|
|
277
|
+
WHERE
|
|
278
|
+
tc.constraint_type = 'PRIMARY KEY'
|
|
279
|
+
) r on x.reference_schema = r.table_schema and x.reference_table = r.table_name and x.column_position=r.column_position
|
|
280
|
+
order by x.constraint_name, x.column_position
|
|
281
|
+
"""
|
|
282
|
+
qry = await self.sql(qry, one_row=False, row_factory=RowFactory.NAMED_TUPLE)
|
|
283
|
+
constraints = []
|
|
284
|
+
item = {"name": None}
|
|
285
|
+
for x in qry:
|
|
286
|
+
if x.constraint_name!=item["name"]:
|
|
287
|
+
if item["name"] is not None:
|
|
288
|
+
constraints.append(item)
|
|
289
|
+
t = x.constraint_type[0].upper()
|
|
290
|
+
if t == "P":
|
|
291
|
+
item = {
|
|
292
|
+
"name": x.constraint_name,
|
|
293
|
+
"fields": [],
|
|
294
|
+
"type": "primary key"
|
|
295
|
+
}
|
|
296
|
+
elif t == "U":
|
|
297
|
+
item = {
|
|
298
|
+
"name": x.constraint_name,
|
|
299
|
+
"fields": [],
|
|
300
|
+
"type": "unique"
|
|
301
|
+
}
|
|
302
|
+
elif t == "F":
|
|
303
|
+
item = {
|
|
304
|
+
"name": x.constraint_name,
|
|
305
|
+
"fields": {},
|
|
306
|
+
"table": f"{self.ident_name(x.reference_schema)}.{self.ident_name(x.reference_table)}",
|
|
307
|
+
"type": "foreign key"
|
|
308
|
+
}
|
|
309
|
+
else:
|
|
310
|
+
item = {"name": None}
|
|
311
|
+
continue
|
|
312
|
+
if item["type"][0] == "f":
|
|
313
|
+
item["fields"][x.column_name] = x.reference_column
|
|
314
|
+
else:
|
|
315
|
+
item["fields"].append(x.column_name)
|
|
316
|
+
if item["name"]:
|
|
317
|
+
constraints.append(item)
|
|
318
|
+
return {
|
|
319
|
+
"table": f"{schema_name}.{table_name}" if schema_name else table_name,
|
|
320
|
+
"fields": fields,
|
|
321
|
+
"constraints": constraints
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async def page(self, query, limit, offset):
|
|
325
|
+
query = f"{query}\nlimit {limit}"
|
|
326
|
+
if offset:
|
|
327
|
+
query = f"{query}\noffset {offset}"
|
|
328
|
+
return query
|
|
329
|
+
|
|
330
|
+
async def get_position(self, table, key, filter):
|
|
331
|
+
"""
|
|
332
|
+
возвращает номер первой строки table, отсортированной по key, соответствующей фильтру filter
|
|
333
|
+
"""
|
|
334
|
+
n_field = "co_lu_mn__po_si_ti_on__by__ke_y"
|
|
335
|
+
qry = f"""select row_number() over (order by {key}) {n_field}, t.* from {table} t"""
|
|
336
|
+
qry = f"""select {n_field} from ({qry}) x where {filter} order by {n_field}"""
|
|
337
|
+
x = await self.sql(qry, one_row=True, row_factory=RowFactory.TUPLE)
|
|
338
|
+
if x:
|
|
339
|
+
return x[0]
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import sys, os
|
|
2
|
+
sys.path.append(os.path.dirname(__file__))
|
|
3
|
+
from driver import Driver as BaseDriver, RowFactory
|
|
4
|
+
try:
|
|
5
|
+
from mysql.connector.aio import connect
|
|
6
|
+
except ImportError:
|
|
7
|
+
raise Exception("For use MySQL driver, you need to install mysql.connector\n$ pip install mysql-connector-python")
|
|
8
|
+
from collections import namedtuple
|
|
9
|
+
|
|
10
|
+
def as_is(description, data):
|
|
11
|
+
return data
|
|
12
|
+
|
|
13
|
+
def as_dict(description, data):
|
|
14
|
+
fields = [column[0] for column in description]
|
|
15
|
+
if isinstance(data, list):
|
|
16
|
+
for n, row in enumerate(data):
|
|
17
|
+
data[n] = {key: value for key, value in zip(fields, row)}
|
|
18
|
+
return data
|
|
19
|
+
return {key: value for key, value in zip(fields, data)}
|
|
20
|
+
|
|
21
|
+
def as_namedtuple(description, data):
|
|
22
|
+
fields = [column[0].lower() for column in description]
|
|
23
|
+
cls = namedtuple("Row", fields)
|
|
24
|
+
if isinstance(data, list):
|
|
25
|
+
for n, row in enumerate(data):
|
|
26
|
+
data[n] = cls._make(row)
|
|
27
|
+
return data
|
|
28
|
+
return cls._make(data)
|
|
29
|
+
|
|
30
|
+
_FACTORY_ = {
|
|
31
|
+
RowFactory.ANY.value: as_is,
|
|
32
|
+
RowFactory.TUPLE.value: as_is,
|
|
33
|
+
RowFactory.DICT.value: as_dict,
|
|
34
|
+
RowFactory.NAMED_TUPLE.value: as_namedtuple,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Driver(BaseDriver):
|
|
39
|
+
|
|
40
|
+
def __init__(self, connection_string, on_open_close=None):
|
|
41
|
+
"""
|
|
42
|
+
localhost:3306/mysql?user=root&password=root
|
|
43
|
+
"""
|
|
44
|
+
if "{" in connection_string:
|
|
45
|
+
connection_string, options = connection_string.split("{", 1)
|
|
46
|
+
|
|
47
|
+
super().__init__(connection_string, on_open_close)
|
|
48
|
+
|
|
49
|
+
if "/" in self.connection_string:
|
|
50
|
+
x, options = self.connection_string.split("/", 1)
|
|
51
|
+
else:
|
|
52
|
+
x, options = self.connection_string.split("?", 1)
|
|
53
|
+
if ":" in x:
|
|
54
|
+
x, y = x.split(":")
|
|
55
|
+
self._conn_params = {
|
|
56
|
+
"host": x,
|
|
57
|
+
"port": int(y),
|
|
58
|
+
}
|
|
59
|
+
else:
|
|
60
|
+
self._conn_params = {
|
|
61
|
+
"host": x,
|
|
62
|
+
"port": 3306,
|
|
63
|
+
}
|
|
64
|
+
if "?" in options:
|
|
65
|
+
x, options = options.split("?",1)
|
|
66
|
+
self._conn_params["database"] = x
|
|
67
|
+
for x in options.split("&"):
|
|
68
|
+
k, v = x.split("=", 1)
|
|
69
|
+
self._conn_params[k] = v
|
|
70
|
+
|
|
71
|
+
async def begin(self):
|
|
72
|
+
await self._before_open()
|
|
73
|
+
self._conn = await connect(**self._conn_params)
|
|
74
|
+
|
|
75
|
+
async def sql(self, query, one_row=False, row_factory=RowFactory.DICT):
|
|
76
|
+
if self._conn is None:
|
|
77
|
+
await self.begin()
|
|
78
|
+
cur = await self._conn.cursor()
|
|
79
|
+
try:
|
|
80
|
+
await cur.execute(query)
|
|
81
|
+
descr = cur.description
|
|
82
|
+
if descr:
|
|
83
|
+
descr = descr.copy()
|
|
84
|
+
if one_row:
|
|
85
|
+
data = await cur.fetchone()
|
|
86
|
+
else:
|
|
87
|
+
data = await cur.fetchall()
|
|
88
|
+
else:
|
|
89
|
+
return cur.rowcount
|
|
90
|
+
finally:
|
|
91
|
+
try:
|
|
92
|
+
await cur.close()
|
|
93
|
+
except Exception as e:
|
|
94
|
+
pass
|
|
95
|
+
return _FACTORY_[row_factory.value](descr, data)
|
|
96
|
+
|
|
97
|
+
async def commit(self):
|
|
98
|
+
if not self._conn:
|
|
99
|
+
raise RuntimeError("Transaction is not open")
|
|
100
|
+
await self._conn.commit()
|
|
101
|
+
await self.rollback()
|
|
102
|
+
|
|
103
|
+
async def rollback(self):
|
|
104
|
+
if not self._conn:
|
|
105
|
+
return
|
|
106
|
+
if x := self._on_open_close:
|
|
107
|
+
await x(True)
|
|
108
|
+
await self._conn.close()
|
|
109
|
+
self._conn = None
|
|
110
|
+
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import sys, os
|
|
2
|
+
sys.path.append(os.path.dirname(__file__))
|
|
3
|
+
from driver import Driver as BaseDriver, RowFactory
|
|
4
|
+
from psycopg import AsyncConnection
|
|
5
|
+
import psycopg.rows as rows
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
_FACTORY_ = {
|
|
9
|
+
RowFactory.ANY.value: rows.tuple_row,
|
|
10
|
+
RowFactory.TUPLE.value: rows.tuple_row,
|
|
11
|
+
RowFactory.DICT.value: rows.dict_row,
|
|
12
|
+
RowFactory.NAMED_TUPLE.value: rows.namedtuple_row,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
def _col_type(col_type, type_cat):
|
|
16
|
+
if type_cat == "S":
|
|
17
|
+
return str
|
|
18
|
+
elif type_cat == "N":
|
|
19
|
+
if col_type in ("smallint", "integer", "bigint", "smallserial", "serial", "bigserial"):
|
|
20
|
+
return int
|
|
21
|
+
else:
|
|
22
|
+
return float
|
|
23
|
+
return str
|
|
24
|
+
|
|
25
|
+
class Driver(BaseDriver):
|
|
26
|
+
|
|
27
|
+
LIKE = "ILIKE"
|
|
28
|
+
|
|
29
|
+
def __init__(self, connection_string, on_open_close=None):
|
|
30
|
+
"""
|
|
31
|
+
localhost:5432/postgres?user=postgres&password=postgres
|
|
32
|
+
"""
|
|
33
|
+
super().__init__(connection_string, on_open_close)
|
|
34
|
+
host, options = self.connection_string.split("/", 1)
|
|
35
|
+
if ":" in host:
|
|
36
|
+
host, port = host.split(":", 1)
|
|
37
|
+
port = int(port.strip())
|
|
38
|
+
else:
|
|
39
|
+
port = None
|
|
40
|
+
options = options.split("?", 1)
|
|
41
|
+
dbname = options[0]
|
|
42
|
+
if len(options) == 2:
|
|
43
|
+
options = options[1].replace("&", " ")
|
|
44
|
+
else:
|
|
45
|
+
options = ""
|
|
46
|
+
self._conn_str = f"host={host} dbname={dbname}"
|
|
47
|
+
if port is not None:
|
|
48
|
+
self._conn_str += f" port={port}"
|
|
49
|
+
if options != "":
|
|
50
|
+
self._conn_str += " " + options
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def in_transaction(self):
|
|
54
|
+
return self._conn is not None
|
|
55
|
+
|
|
56
|
+
async def begin(self):
|
|
57
|
+
params = await self._before_open()
|
|
58
|
+
self._conn = await AsyncConnection.connect(self.connection_string)
|
|
59
|
+
for x in params:
|
|
60
|
+
if x == "TIMEZONE":
|
|
61
|
+
await self._conn.execute(f"set session timezone '{params[x]}'")
|
|
62
|
+
else:
|
|
63
|
+
await self._conn.execute(f"SET {x} = '{params[x]}'")
|
|
64
|
+
|
|
65
|
+
async def sql(self, query, one_row=False, row_factory=RowFactory.DICT):
|
|
66
|
+
if self._conn is None:
|
|
67
|
+
await self.begin()
|
|
68
|
+
async with self._conn.cursor() as acur:
|
|
69
|
+
|
|
70
|
+
acur.row_factory = _FACTORY_[row_factory.value]
|
|
71
|
+
|
|
72
|
+
await acur.execute(query)
|
|
73
|
+
descr = acur.description
|
|
74
|
+
if not descr:
|
|
75
|
+
return acur.rowcount
|
|
76
|
+
if one_row:
|
|
77
|
+
ret = await acur.fetchone()
|
|
78
|
+
else:
|
|
79
|
+
ret = await acur.fetchall()
|
|
80
|
+
if not ret:
|
|
81
|
+
ret = []
|
|
82
|
+
return ret
|
|
83
|
+
|
|
84
|
+
async def commit(self):
|
|
85
|
+
if not self._conn:
|
|
86
|
+
raise RuntimeError("Transaction is not open")
|
|
87
|
+
await self._conn.commit()
|
|
88
|
+
await self.rollback()
|
|
89
|
+
|
|
90
|
+
async def rollback(self):
|
|
91
|
+
if not self._conn:
|
|
92
|
+
return
|
|
93
|
+
await self._conn.close()
|
|
94
|
+
await super().rollback()
|
|
95
|
+
|
|
96
|
+
@staticmethod
|
|
97
|
+
def _specify_type(defs):
|
|
98
|
+
if defs.column_default and str(defs.column_default).startswith("nextval("):
|
|
99
|
+
if defs.data_type == "integer":
|
|
100
|
+
return BaseDriver.TypeSpec(type_name="serial", python_type=int, autoincrement=True)
|
|
101
|
+
elif defs.data_type == "bigint":
|
|
102
|
+
return BaseDriver.TypeSpec(type_name="bigserial", python_type=int, autoincrement=True)
|
|
103
|
+
elif defs.data_type == "smallint":
|
|
104
|
+
return BaseDriver.TypeSpec(type_name="smallserial", python_type=int, autoincrement=True)
|
|
105
|
+
return BaseDriver._specify_type(defs)
|
|
106
|
+
|
|
107
|
+
@staticmethod
|
|
108
|
+
def ident_name(name: str) -> str:
|
|
109
|
+
"""
|
|
110
|
+
Эмулирует поведение PostgreSQL функции quote_ident().
|
|
111
|
+
"""
|
|
112
|
+
if name is None:
|
|
113
|
+
return "NULL"
|
|
114
|
+
# Проверяем, является ли строка валидным идентификатором
|
|
115
|
+
if (name[0].isalpha() or name[0] == '_') \
|
|
116
|
+
and all(char.isalnum() or char == '_' for char in name) \
|
|
117
|
+
and name == name.lower():
|
|
118
|
+
return name
|
|
119
|
+
# Удваиваем существующие двойные кавычки
|
|
120
|
+
name = name.replace('"', '""')
|
|
121
|
+
# Заключаем в двойные кавычки
|
|
122
|
+
return f'"{name}"'
|