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 ADDED
@@ -0,0 +1,4 @@
1
+ from .env import Config, LogLevel
2
+ from .env.timer import TimerInterval
3
+ from .rpc import register as register_rpc, call_rpc, call_json
4
+ from .error import raise_error, load_errors
@@ -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))
@@ -0,0 +1,3 @@
1
+ from .option import DB, TUPLE, OBJECT, ONE, ROW, ROLLBACK, COMMIT, JSON, TIMEOUT, PAGE, CALLBACK, ITERATOR
2
+ from .processor import sql, set_connection
3
+ from .table import Table
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}"'