ab_engine 0.1.9__tar.gz → 0.2.1__tar.gz
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-0.1.9/ab_engine.egg-info → ab_engine-0.2.1}/PKG-INFO +100 -9
- {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/__init__.py +1 -1
- {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/db/driver.py +5 -4
- {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/db/driver_sqlite.py +6 -2
- {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/db/driver_valkey.py +37 -1
- {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/db/option.py +50 -7
- {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/db/processor.py +16 -16
- {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/db/table.py +2 -2
- {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/env/config.py +31 -20
- {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/env/db_context.py +3 -3
- {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/error.yaml +11 -1
- ab_engine-0.2.1/ab_engine/rpc/__init__.py +2 -0
- {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/rpc/json_rpc.py +10 -10
- {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/rpc/rpc.py +116 -3
- ab_engine-0.1.9/readme.md → ab_engine-0.2.1/ab_engine.egg-info/PKG-INFO +112 -7
- {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine.egg-info/requires.txt +1 -1
- {ab_engine-0.1.9 → ab_engine-0.2.1}/pyproject.toml +2 -2
- ab_engine-0.1.9/PKG-INFO → ab_engine-0.2.1/readme.md +98 -21
- ab_engine-0.1.9/ab_engine/rpc/__init__.py +0 -2
- {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/class_tools.py +0 -0
- {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/db/__init__.py +0 -0
- {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/db/driver_mysql.py +0 -0
- {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/db/driver_postgresql.py +0 -0
- {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/env/__init__.py +0 -0
- {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/env/timer.py +0 -0
- {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/error.py +0 -0
- {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/rpc/fnc.py +0 -0
- {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine.egg-info/SOURCES.txt +0 -0
- {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine.egg-info/dependency_links.txt +0 -0
- {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine.egg-info/top_level.txt +0 -0
- {ab_engine-0.1.9 → ab_engine-0.2.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ab_engine
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Library of standard modules
|
|
5
5
|
Author-email: Andrew Bulatov <BulatovAndrew@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -9,7 +9,7 @@ Requires-Python: >=3.10
|
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
10
|
Requires-Dist: json5>=0.12.0
|
|
11
11
|
Requires-Dist: psycopg>=3.2.9
|
|
12
|
-
Requires-Dist:
|
|
12
|
+
Requires-Dist: pyyaml>=6.0.3
|
|
13
13
|
Requires-Dist: toml>=0.10.2
|
|
14
14
|
|
|
15
15
|
Библиотека стандартных модулей
|
|
@@ -377,12 +377,39 @@ logging:
|
|
|
377
377
|
|
|
378
378
|
Можно описывать драйвера к другим БД. Для того чтобы использовать собственный драйвер, можно добавить класс драйвера
|
|
379
379
|
в словарь DRIVER_CLASSES, который находится в модуле ab_engine/db/option, либо положить драйвер в каталог драйверов.
|
|
380
|
+
|
|
380
381
|
Пример драйвера в DRIVER_CLASSES:
|
|
382
|
+
```python
|
|
383
|
+
from ab_engine.db import sql, ONE, DB
|
|
384
|
+
from ab_engine.db.option import DRIVER_CLASSES
|
|
385
|
+
from ab_engine.db.driver_postgresql import Driver
|
|
386
|
+
import asyncio
|
|
387
|
+
|
|
388
|
+
class MyPgDriver(Driver):
|
|
389
|
+
|
|
390
|
+
async def begin(self):
|
|
391
|
+
await super().begin()
|
|
392
|
+
print("BEGIN TRANSACTION", flush=True)
|
|
393
|
+
|
|
394
|
+
async def rollback(self):
|
|
395
|
+
await super().rollback()
|
|
396
|
+
print("END TRANSACTION", flush=True)
|
|
381
397
|
|
|
382
|
-
|
|
398
|
+
DRIVER_CLASSES["my_pg"] = MyPgDriver
|
|
399
|
+
|
|
400
|
+
async def main():
|
|
401
|
+
x = await sql("select version();", ONE, DB("my_pg://postgres:postgres@localhost:5432/postgres"))
|
|
402
|
+
print(x)
|
|
403
|
+
await DB.garbage_collect()
|
|
404
|
+
|
|
405
|
+
if __name__ == '__main__':
|
|
406
|
+
asyncio.run(main())
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
При необходимости задать путь поиска с собственным драйвером, можно в строке соединения, в
|
|
383
410
|
фигурных скобках задать параметр **driver_path**, определяющий путь для поиска драйвера.
|
|
384
411
|
|
|
385
|
-
Если нужно задать свой путь поиска для всех драйверов БД, можно задать параметр db_driver_path в defaults
|
|
412
|
+
Если нужно задать свой путь поиска для всех драйверов БД, можно задать параметр **db_driver_path** в секции конфигурации **defaults**.
|
|
386
413
|
|
|
387
414
|
Если приложению требуется только одна строка соединения, то ее можно задать в переменной database, например:
|
|
388
415
|
|
|
@@ -390,7 +417,7 @@ logging:
|
|
|
390
417
|
database: postgresql://localhost:5432/postgres?user=postgres&password=postgres
|
|
391
418
|
```
|
|
392
419
|
|
|
393
|
-
|
|
420
|
+
Чтобы задать несколько соединений, нужно использовать database как секцию:
|
|
394
421
|
|
|
395
422
|
```toml
|
|
396
423
|
[database]
|
|
@@ -401,7 +428,7 @@ mydb = "postgresql://localhost:5432/mydb?user=postgres&password=postgres"
|
|
|
401
428
|
database = "test"
|
|
402
429
|
```
|
|
403
430
|
|
|
404
|
-
При этом, в секции defaults можно задать имя соединения БД, которое будет использоваться по
|
|
431
|
+
При этом, в секции **defaults** можно задать имя соединения БД, которое будет использоваться по умолчанию. Для этого следует задать значение **database** в секции **defaults**.
|
|
405
432
|
|
|
406
433
|
Строка соединения с БД имеет следующую структуру:
|
|
407
434
|
<имя_драйвера>**://**<адрес сервера>**:**<номер порта>**/**<имя БД>**?**<параметры соединения>**{**дополнительные параметры**}**
|
|
@@ -418,10 +445,15 @@ database = "test"
|
|
|
418
445
|
который ограничит максимальное количество таких соединений. Т.е. по достижении данного числа соединений система
|
|
419
446
|
не станет выполнять очередную команду до тех пор, пока не освободится одно из текущих соединений с БД.
|
|
420
447
|
|
|
421
|
-
Для некоторых
|
|
448
|
+
Для некоторых драйверов, например, для встроенного драйвера posstgresql строка сооединения с БД
|
|
422
449
|
может иметь альтернативную структуру:
|
|
423
450
|
|
|
424
|
-
<имя_драйвера>**://**<имя
|
|
451
|
+
<имя_драйвера>**://**<имя пользователя>**:**<пароль>**@**<адрес сервера>**:**<номер порта>**/**<имя БД>**?**<параметры соединения>**{**дополнительные параметры**}**
|
|
452
|
+
|
|
453
|
+
Если нужно установить **peer** соединение c БД postgresql, то в строке соединения достаточно передать имя БД и доп. параметры (если нужно). Например:
|
|
454
|
+
```python
|
|
455
|
+
x = await sql("select version()", ONE, DB("postgresql://postgres?application_name=test"))
|
|
456
|
+
```
|
|
425
457
|
|
|
426
458
|
Драйвер БД должен иметь имя driver_<имя в строке соединения>.py и предоставлять следующий контракт:
|
|
427
459
|
|
|
@@ -655,6 +687,7 @@ async def sql(query:str, *args, **kwargs):
|
|
|
655
687
|
Передавать опции можно только в конце списка args, после позиционных параметров, либо вместо них.
|
|
656
688
|
В библиотеке реализованы следующие опции:
|
|
657
689
|
* DB - служит для передачи соединения в функцию sql. Передается как DB("строка соединения").
|
|
690
|
+
* NOTICE - то же, что DB, но запросы с такой опцией возвращают как результат список сообщений нотификации
|
|
658
691
|
* TIMEOUT- устанавливает максимальное время в секундах на выполнение запроса. точность - до сотых секунды.
|
|
659
692
|
По умолчанию вызывает ошибку, при превышении времени ожидания. TIMEOUT(time_sec:float, raise_error:bool=True)
|
|
660
693
|
* DICT - Указывает, что результат следует вернуть как список dict (RowFactory.DICT). Установлена по умолчанию
|
|
@@ -1217,7 +1250,65 @@ if __name__ == '__main__':
|
|
|
1217
1250
|
|
|
1218
1251
|
### Регистрация методов
|
|
1219
1252
|
|
|
1220
|
-
Методы RPC регистрируются c помощью
|
|
1253
|
+
Методы RPC регистрируются функцией register_rpc_list либо c помощью декоратора register_rpc, который также можно использовать как функцию.
|
|
1254
|
+
|
|
1255
|
+
Регистрация списка функций осуществляется с помощью register_rpc_list, которая принимает на вход параметры defs и self.
|
|
1256
|
+
* **defs** - содержит список или словарь с описаниями
|
|
1257
|
+
* **self** - необязательный параметр, в котором передается модуль или объект из которого будут регистрироваться функции. если в self ничего не передано, то будут регистрироваться функции из модуля, в котором вызвана register_rpc_list.
|
|
1258
|
+
|
|
1259
|
+
список или словарь с орписаниями содержит элементы следующей структуры:
|
|
1260
|
+
* **name**: имя функции в API (не нужно и игнорируется, если описание передается в словаре)
|
|
1261
|
+
* **function**: имя реально вызываемой функции, если оно отличается от name
|
|
1262
|
+
* **sql**: запрос sql, реализующий функцию, если передан sql, то не нужно передавать function и type
|
|
1263
|
+
* **help**: описание функции. если не задано - берется из функции
|
|
1264
|
+
* **type**: тип функции
|
|
1265
|
+
* **self** - функция из объекта, переданного в параметре self
|
|
1266
|
+
* **py** - функция из плагина python
|
|
1267
|
+
* **db** - функция БД
|
|
1268
|
+
* **ext**: дополнительные параметры регистрации, если нужны. для sql это могут быть опции или их имена
|
|
1269
|
+
|
|
1270
|
+
для функций с типом **self** для которых name==function возможна запись:
|
|
1271
|
+
***имя_функции: self***
|
|
1272
|
+
|
|
1273
|
+
Пример списка для регистрации:
|
|
1274
|
+
```python
|
|
1275
|
+
from ab_engine import register_rpc_list, call_rpc
|
|
1276
|
+
|
|
1277
|
+
def hello():
|
|
1278
|
+
return "Здравствуй мир!"
|
|
1279
|
+
|
|
1280
|
+
register_rpc_list({
|
|
1281
|
+
"hello":"self",
|
|
1282
|
+
"hello2":{
|
|
1283
|
+
"sql":"""
|
|
1284
|
+
do $$
|
|
1285
|
+
declare
|
|
1286
|
+
nm varchar;
|
|
1287
|
+
begin
|
|
1288
|
+
nm = $name::varchar;
|
|
1289
|
+
raise notice 'Hello % !!', nm;
|
|
1290
|
+
end $$
|
|
1291
|
+
""",
|
|
1292
|
+
"ext": ["NOTICE","ONE"]
|
|
1293
|
+
},
|
|
1294
|
+
"set":{
|
|
1295
|
+
"sql":"set $key $value",
|
|
1296
|
+
"ext":"DB(valkey)",
|
|
1297
|
+
"help": "устанавливает значение ключа key равным value"
|
|
1298
|
+
},
|
|
1299
|
+
"get":{
|
|
1300
|
+
"sql":"get $key",
|
|
1301
|
+
"ext":"DB(valkey)",
|
|
1302
|
+
"help": "возвращает значение ключа key"
|
|
1303
|
+
}
|
|
1304
|
+
})
|
|
1305
|
+
|
|
1306
|
+
async def test():
|
|
1307
|
+
await call_rpc("set", key="test", value=1234)
|
|
1308
|
+
```
|
|
1309
|
+
|
|
1310
|
+
|
|
1311
|
+
Регистрация с помощью декоратора:
|
|
1221
1312
|
```python
|
|
1222
1313
|
from ab_engine import register_rpc
|
|
1223
1314
|
from ab_engine.db import ONE
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
from .env import Config, LogLevel
|
|
2
2
|
from .env.timer import TimerInterval
|
|
3
|
-
from .rpc import register as register_rpc, call_rpc, call_json
|
|
3
|
+
from .rpc import register as register_rpc, call_rpc, call_json, register_list as register_rpc_list
|
|
4
4
|
from .error import raise_error, load_errors, error
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
2
|
from json import dumps
|
|
3
|
-
from datetime import datetime
|
|
3
|
+
from datetime import datetime, time
|
|
4
4
|
from abc import ABC, abstractmethod
|
|
5
5
|
from collections import namedtuple
|
|
6
6
|
from typing import Optional
|
|
@@ -158,7 +158,7 @@ class Driver(ABC):
|
|
|
158
158
|
elif isinstance(var, dict):
|
|
159
159
|
var = dumps(var, ensure_ascii=False).replace("'", "''")
|
|
160
160
|
return reencode(var)
|
|
161
|
-
elif isinstance(var, datetime):
|
|
161
|
+
elif isinstance(var, (datetime, time)):
|
|
162
162
|
return f" '{var}'"
|
|
163
163
|
else:
|
|
164
164
|
return f" {var}"
|
|
@@ -168,7 +168,8 @@ class Driver(ABC):
|
|
|
168
168
|
if callback:=kwargs.get("__PARAM_CALLBACK_GETTER"):
|
|
169
169
|
del kwargs["__PARAM_CALLBACK_GETTER"]
|
|
170
170
|
qs = None
|
|
171
|
-
|
|
171
|
+
|
|
172
|
+
for x in query.replace("$$", "🗿").split("$"):
|
|
172
173
|
if qs is None:
|
|
173
174
|
qs = x
|
|
174
175
|
continue
|
|
@@ -195,7 +196,7 @@ class Driver(ABC):
|
|
|
195
196
|
qs += f"{self.var_to_sql(n, x)}{x}"
|
|
196
197
|
|
|
197
198
|
if qs:
|
|
198
|
-
query = qs
|
|
199
|
+
query = qs.replace("🗿", "$$")
|
|
199
200
|
|
|
200
201
|
return query
|
|
201
202
|
|
|
@@ -19,7 +19,11 @@ _Info = namedtuple("Info", ["column_name", "data_type", "is_nullable", "characte
|
|
|
19
19
|
"numeric_precision", "numeric_scale", "column_default", "autoincrement", "pk"])
|
|
20
20
|
|
|
21
21
|
def _field_ifo_tuples(fields, str_fields):
|
|
22
|
-
str_fields = str_fields["sql"].split("\n", 1)
|
|
22
|
+
str_fields = str_fields["sql"].split("\n", 1)
|
|
23
|
+
if len(str_fields) > 1:
|
|
24
|
+
str_fields = str_fields[1][:-1].strip().split("\n")
|
|
25
|
+
else:
|
|
26
|
+
str_fields = []
|
|
23
27
|
str_def = {}
|
|
24
28
|
for x in str_fields:
|
|
25
29
|
n, x = x.strip().split(" ", 1)
|
|
@@ -30,7 +34,7 @@ def _field_ifo_tuples(fields, str_fields):
|
|
|
30
34
|
sz = int(sz)
|
|
31
35
|
else:
|
|
32
36
|
sz = None
|
|
33
|
-
ai = str_def
|
|
37
|
+
ai = str_def.get(row["name"],[])
|
|
34
38
|
fields[n] = _Info(column_name=row["name"], data_type=row["type"].lower(), character_maximum_length=sz,
|
|
35
39
|
column_default=row["dflt_value"], is_nullable=row["notnull"] == 0 and row["pk"] == 0,
|
|
36
40
|
autoincrement=" autoincrement" in ai, pk=row["pk"],
|
|
@@ -216,4 +216,40 @@ class Driver(BaseDriver):
|
|
|
216
216
|
await super().rollback()
|
|
217
217
|
|
|
218
218
|
async def table_struct(self, table_name) -> dict:
|
|
219
|
-
return {}
|
|
219
|
+
return {}
|
|
220
|
+
|
|
221
|
+
async def parse_query(self, query, *args, **kwargs):
|
|
222
|
+
if callback:=kwargs.get("__PARAM_CALLBACK_GETTER"):
|
|
223
|
+
del kwargs["__PARAM_CALLBACK_GETTER"]
|
|
224
|
+
qs = None
|
|
225
|
+
|
|
226
|
+
for x in query.split("$"):
|
|
227
|
+
if qs is None:
|
|
228
|
+
qs = x
|
|
229
|
+
continue
|
|
230
|
+
n, p = '', 0
|
|
231
|
+
if str(x[p]).isdigit():
|
|
232
|
+
while p < len(x) and x[p].isdigit():
|
|
233
|
+
n += x[p]
|
|
234
|
+
p += 1
|
|
235
|
+
n = int(n) - 1
|
|
236
|
+
if n < 0 or n >= len(args):
|
|
237
|
+
raise AttributeError(f"Attribute with index {n} is not exists")
|
|
238
|
+
n = args[n]
|
|
239
|
+
else:
|
|
240
|
+
while p < len(x) and not(x[p].isspace() or x[p] in "(:,)"):
|
|
241
|
+
n += x[p]
|
|
242
|
+
p += 1
|
|
243
|
+
if n in kwargs:
|
|
244
|
+
n = kwargs[n]
|
|
245
|
+
elif callback:
|
|
246
|
+
n = callback[n]
|
|
247
|
+
else:
|
|
248
|
+
raise AttributeError(f"Attribute with name {n} is not exists")
|
|
249
|
+
x = x[p:]
|
|
250
|
+
qs = f"{qs}{n}{x}"
|
|
251
|
+
|
|
252
|
+
if qs:
|
|
253
|
+
query = qs
|
|
254
|
+
|
|
255
|
+
return query
|
|
@@ -5,7 +5,7 @@ from pathlib import Path
|
|
|
5
5
|
from importlib.machinery import SourceFileLoader
|
|
6
6
|
from json5 import loads
|
|
7
7
|
from asyncio import BoundedSemaphore, wait_for, TimeoutError, sleep
|
|
8
|
-
from ..error import
|
|
8
|
+
from ..error import error
|
|
9
9
|
from inspect import iscoroutinefunction
|
|
10
10
|
from typing import Callable
|
|
11
11
|
from gc import collect as garbage_collect
|
|
@@ -66,6 +66,25 @@ class Option(ABC):
|
|
|
66
66
|
"""
|
|
67
67
|
return ret_data, row_factory
|
|
68
68
|
|
|
69
|
+
@staticmethod
|
|
70
|
+
def create(option:str, *args, **kwargs):
|
|
71
|
+
|
|
72
|
+
def find(parent_cls, name):
|
|
73
|
+
for x in parent_cls.__subclasses__():
|
|
74
|
+
if x.__name__ == name:
|
|
75
|
+
return x
|
|
76
|
+
else:
|
|
77
|
+
x = find(x, name)
|
|
78
|
+
if x:
|
|
79
|
+
return x
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
x = find(Option, option)
|
|
83
|
+
if x is None:
|
|
84
|
+
raise error("CLASS_NOT_FOUND", name=option)
|
|
85
|
+
return x(*args, **kwargs)
|
|
86
|
+
|
|
87
|
+
|
|
69
88
|
_set_is_option(Option.is_option)
|
|
70
89
|
|
|
71
90
|
class ALL(Option):
|
|
@@ -98,7 +117,7 @@ class DB(Option):
|
|
|
98
117
|
try:
|
|
99
118
|
_check_cfg()
|
|
100
119
|
except Exception as e:
|
|
101
|
-
|
|
120
|
+
raise error("NA_DB", connection=str(e))
|
|
102
121
|
connection_string = _CFG_.db_connection(connection_string)
|
|
103
122
|
driver_name, connection_string = connection_string.split("://", 1)
|
|
104
123
|
|
|
@@ -107,11 +126,15 @@ class DB(Option):
|
|
|
107
126
|
driver = DRIVER_CLASSES.get(driver_name)
|
|
108
127
|
driver_path = None
|
|
109
128
|
if conn_params:
|
|
110
|
-
conn_params = conn_params[:-1]
|
|
129
|
+
conn_params = conn_params[:-1]
|
|
130
|
+
mono_params = [x for x in conn_params.split("&") if "=" not in x ]
|
|
131
|
+
conn_params = {x[0].strip():x[1].strip() for x in [x.split("=", 1) for x in conn_params.split("&") if "=" in x]}
|
|
111
132
|
if "driver_path" in conn_params:
|
|
112
133
|
driver_path = conn_params["driver_path"]
|
|
113
134
|
del conn_params["driver_path"]
|
|
114
|
-
conn_params = "?" + "&".join(conn_params)
|
|
135
|
+
conn_params = "?" + "&".join([f"{x}={conn_params[x]}" for x in conn_params])
|
|
136
|
+
if mono_params:
|
|
137
|
+
conn_params +="&" + "&".join(mono_params)
|
|
115
138
|
connection_string += conn_params
|
|
116
139
|
if driver is None:
|
|
117
140
|
if driver_path is None and _CFG_ and _CFG_.initialized:
|
|
@@ -216,6 +239,22 @@ class TIMEOUT(Option):
|
|
|
216
239
|
raise e
|
|
217
240
|
return None
|
|
218
241
|
|
|
242
|
+
class NOTICE(DB):
|
|
243
|
+
|
|
244
|
+
def __init__(self, connection_string: str=""):
|
|
245
|
+
self._notify = []
|
|
246
|
+
super().__init__(connection_string, self._notify)
|
|
247
|
+
|
|
248
|
+
@classproperty
|
|
249
|
+
def can_process(cls):
|
|
250
|
+
return True
|
|
251
|
+
|
|
252
|
+
async def process(self, ret_data=None, connection=None, row_factory=None):
|
|
253
|
+
ret = self._notify.copy()
|
|
254
|
+
self._notify.clear()
|
|
255
|
+
return ret, None
|
|
256
|
+
|
|
257
|
+
|
|
219
258
|
class DICT(Option):
|
|
220
259
|
"""
|
|
221
260
|
Указывает, что результат следует вернуть как список dict (по умолчанию)
|
|
@@ -260,6 +299,10 @@ class ONE(ROW):
|
|
|
260
299
|
async def process(ret_data, connection, row_factory):
|
|
261
300
|
if ret_data is None or isinstance(ret_data, int):
|
|
262
301
|
return ret_data, None
|
|
302
|
+
elif row_factory is None and isinstance(ret_data, list):
|
|
303
|
+
if len(ret_data) == 0:
|
|
304
|
+
return None
|
|
305
|
+
return ret_data[0], RowFactory.ANY
|
|
263
306
|
match row_factory:
|
|
264
307
|
case RowFactory.TUPLE:
|
|
265
308
|
return ret_data[0], None
|
|
@@ -286,7 +329,7 @@ class JSON(ONE):
|
|
|
286
329
|
elif isinstance(ret_data, str):
|
|
287
330
|
return loads(ret_data), None
|
|
288
331
|
else:
|
|
289
|
-
|
|
332
|
+
raise error("NOT_CONV_DICT_LIST", data=ret_data)
|
|
290
333
|
|
|
291
334
|
class ROLLBACK(Option):
|
|
292
335
|
"""
|
|
@@ -325,7 +368,7 @@ class PAGE(Option):
|
|
|
325
368
|
:param offset: смещение от начала
|
|
326
369
|
"""
|
|
327
370
|
if not(isinstance(limit, int) and isinstance(offset, int)) or limit < 0 or offset < 0:
|
|
328
|
-
|
|
371
|
+
raise error("BAD_LIMIT_OFFSET")
|
|
329
372
|
self._limit = limit
|
|
330
373
|
self._offset = offset
|
|
331
374
|
|
|
@@ -361,7 +404,7 @@ class CALLBACK(Option):
|
|
|
361
404
|
|
|
362
405
|
def __getitem__(self, item):
|
|
363
406
|
if self._attrs is None:
|
|
364
|
-
|
|
407
|
+
raise error("ATTR_NOT_FOUND", name=item)
|
|
365
408
|
return self._attrs[item]
|
|
366
409
|
|
|
367
410
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
from .option import Option, DB, TIMEOUT, PAGE, CALLBACK, ITERATOR, RAW
|
|
1
|
+
from .option import Option, DB, TIMEOUT, PAGE, CALLBACK, ITERATOR, RAW, NOTICE
|
|
2
2
|
from .driver import RowFactory
|
|
3
|
-
from ..error import
|
|
3
|
+
from ..error import error
|
|
4
4
|
|
|
5
5
|
CONNECTION:str = ""
|
|
6
6
|
|
|
@@ -19,38 +19,38 @@ async def sql(query:str, *args, **kwargs):
|
|
|
19
19
|
if one_row is None and arg.one_row is not None:
|
|
20
20
|
one_row = arg
|
|
21
21
|
elif one_row and arg.one_row is not None and one_row.one_row!=arg.one_row:
|
|
22
|
-
|
|
22
|
+
raise error("NEQ_ROWS", src1=one_row.__class__.__name__, src2=arg.__class__.__name__)
|
|
23
23
|
if row_factory is None or row_factory.row_factory == RowFactory.ANY:
|
|
24
24
|
row_factory = arg
|
|
25
25
|
elif row_factory and arg.row_factory != RowFactory.ANY:
|
|
26
|
-
|
|
26
|
+
raise error("DEF_STR_FABRIC", src = row_factory.__class__.__name__)
|
|
27
|
+
if arg in (DB, NOTICE):
|
|
28
|
+
arg = arg()
|
|
29
|
+
if isinstance(arg, DB) and db is None:
|
|
30
|
+
db = arg
|
|
27
31
|
if arg.can_process:
|
|
28
32
|
process.append(arg)
|
|
29
33
|
continue
|
|
30
|
-
if arg
|
|
31
|
-
arg = DB()
|
|
32
|
-
if isinstance(arg, DB) and db is None:
|
|
33
|
-
db = arg
|
|
34
|
-
elif isinstance(arg, CALLBACK) and callback is None:
|
|
34
|
+
if isinstance(arg, CALLBACK) and callback is None:
|
|
35
35
|
callback = arg
|
|
36
36
|
elif isinstance(arg, DB):
|
|
37
|
-
|
|
37
|
+
continue #raise error("DB_ALRDY_DEF")
|
|
38
38
|
elif isinstance(arg, PAGE) and page is None:
|
|
39
39
|
page = arg
|
|
40
40
|
elif isinstance(arg, PAGE):
|
|
41
|
-
|
|
41
|
+
raise error("PAGE_ALRDY_DEF")
|
|
42
42
|
elif isinstance(arg, TIMEOUT) and tm is None:
|
|
43
43
|
tm = arg
|
|
44
44
|
elif isinstance(arg, TIMEOUT):
|
|
45
|
-
|
|
45
|
+
raise error("TIMEOUT_ALRDY_DEF")
|
|
46
46
|
elif arg == ITERATOR:
|
|
47
47
|
itr = ITERATOR()
|
|
48
48
|
elif isinstance(arg, ITERATOR):
|
|
49
49
|
itr = arg
|
|
50
|
-
elif arg == RAW:
|
|
50
|
+
elif arg == RAW or isinstance(arg, RAW):
|
|
51
51
|
parse = False
|
|
52
52
|
elif process:
|
|
53
|
-
|
|
53
|
+
raise error("PMT_BEF_OPT")
|
|
54
54
|
if process:
|
|
55
55
|
args = args[:process.pop(0)]
|
|
56
56
|
if db is None:
|
|
@@ -61,7 +61,7 @@ async def sql(query:str, *args, **kwargs):
|
|
|
61
61
|
await db.garbage_collect(False)
|
|
62
62
|
one_row = one_row.one_row if one_row else False
|
|
63
63
|
row_factory = row_factory.row_factory if row_factory and row_factory.row_factory != RowFactory.ANY else RowFactory.DICT
|
|
64
|
-
if callback:
|
|
64
|
+
if callback is not None:
|
|
65
65
|
kwargs["__PARAM_CALLBACK_GETTER"] = callback
|
|
66
66
|
if parse:
|
|
67
67
|
query = await db.connection.parse_query(query, *args, **kwargs)
|
|
@@ -71,7 +71,7 @@ async def sql(query:str, *args, **kwargs):
|
|
|
71
71
|
query = await callback(query)
|
|
72
72
|
if itr:
|
|
73
73
|
if page:
|
|
74
|
-
|
|
74
|
+
raise error("PAGE_ITERATOR")
|
|
75
75
|
return itr(query, db, row_factory, process)
|
|
76
76
|
try:
|
|
77
77
|
if tm is None:
|
|
@@ -193,7 +193,7 @@ class Field:
|
|
|
193
193
|
q = f"{q} where {self.table._filter}"
|
|
194
194
|
ret = await sql(q, self.table._db, ONE)
|
|
195
195
|
if self.table._auto_close_conn:
|
|
196
|
-
self.table._db.rollback()
|
|
196
|
+
await self.table._db.connection.rollback()
|
|
197
197
|
return ret
|
|
198
198
|
|
|
199
199
|
async def min(self):
|
|
@@ -648,7 +648,7 @@ class EnvTable(Table):
|
|
|
648
648
|
|
|
649
649
|
@classmethod
|
|
650
650
|
async def create(cls, table_name, env, page_size=100, async_delay=0.0001):
|
|
651
|
-
table_struct = await env.sql("\d "+table_name)
|
|
651
|
+
table_struct = await env.sql(r"\d "+table_name)
|
|
652
652
|
if not table_struct:
|
|
653
653
|
raise_error("NA_TABLE", name=table_name)
|
|
654
654
|
t = cls(table_struct, env, page_size, async_delay)
|
|
@@ -10,7 +10,7 @@ import logging as LOG
|
|
|
10
10
|
from enum import Enum
|
|
11
11
|
from .timer import TimerList
|
|
12
12
|
from ..class_tools import classproperty
|
|
13
|
-
from ..error import
|
|
13
|
+
from ..error import error, error_msg
|
|
14
14
|
import asyncio
|
|
15
15
|
|
|
16
16
|
|
|
@@ -60,7 +60,7 @@ def typed_val(key, values: dict):
|
|
|
60
60
|
case 'STR)':
|
|
61
61
|
return str(v), key
|
|
62
62
|
case 'FLOAT)':
|
|
63
|
-
return float(
|
|
63
|
+
return float(v), key
|
|
64
64
|
case _:
|
|
65
65
|
return v
|
|
66
66
|
return v, key
|
|
@@ -74,6 +74,10 @@ def from_env(key:str, key_map:dict):
|
|
|
74
74
|
eval_key = "x" + "".join(eval_key)
|
|
75
75
|
else:
|
|
76
76
|
eval_key = f"x['{key}']"
|
|
77
|
+
|
|
78
|
+
if any(char in eval_key for char in ("+", "-", "*", "/", "(", ")", "%", " ")):
|
|
79
|
+
raise error("BAD_KEY", key=eval_key)
|
|
80
|
+
|
|
77
81
|
node = eval(eval_key, {"x": Config._settings}) if key else Config._settings[key]
|
|
78
82
|
node.update(ret)
|
|
79
83
|
|
|
@@ -114,11 +118,11 @@ class Config:
|
|
|
114
118
|
case ".toml":
|
|
115
119
|
return toml_load(data)
|
|
116
120
|
case _:
|
|
117
|
-
|
|
121
|
+
raise error("BAD_FORMAT", format_name=fn)
|
|
118
122
|
|
|
119
123
|
self._log_level = LogLevel.INFO
|
|
120
124
|
if path is None:
|
|
121
|
-
|
|
125
|
+
raise error("BAD_CONFIG_PATH")
|
|
122
126
|
elif isinstance(path, dict):
|
|
123
127
|
Config._settings = path
|
|
124
128
|
elif isinstance(path, str):
|
|
@@ -133,7 +137,9 @@ class Config:
|
|
|
133
137
|
if isinstance(dest, dict):
|
|
134
138
|
dest.update(item)
|
|
135
139
|
else:
|
|
136
|
-
|
|
140
|
+
Config._settings[x] = item
|
|
141
|
+
else:
|
|
142
|
+
raise error("BAD_CONFIG_TYPE")
|
|
137
143
|
|
|
138
144
|
if not Config._settings:
|
|
139
145
|
Config._settings = {}
|
|
@@ -145,6 +151,7 @@ class Config:
|
|
|
145
151
|
f = Config._settings.get(x)
|
|
146
152
|
if isinstance(f, str):
|
|
147
153
|
Config._settings[x] = load(f)
|
|
154
|
+
self.__log_timers = {}
|
|
148
155
|
if "logging" in Config._settings:
|
|
149
156
|
self._logger = ...
|
|
150
157
|
if self.logger:
|
|
@@ -262,7 +269,7 @@ class Config:
|
|
|
262
269
|
|
|
263
270
|
def __getattr__(self, item):
|
|
264
271
|
if not(item in Config._settings):
|
|
265
|
-
|
|
272
|
+
raise error("ATTR_NOT_FOUND", name=item)
|
|
266
273
|
return Config._settings[item]
|
|
267
274
|
|
|
268
275
|
@property
|
|
@@ -359,7 +366,7 @@ class Config:
|
|
|
359
366
|
|
|
360
367
|
hndl = self.logging["handler"]["class"]
|
|
361
368
|
for x in LOG.Handler.__subclasses__():
|
|
362
|
-
if x.__name__ ==
|
|
369
|
+
if x.__name__ == hndl:
|
|
363
370
|
hndl = x
|
|
364
371
|
break
|
|
365
372
|
|
|
@@ -373,7 +380,7 @@ class Config:
|
|
|
373
380
|
hndl = x
|
|
374
381
|
break
|
|
375
382
|
if isinstance(hndl, str):
|
|
376
|
-
|
|
383
|
+
raise error("BAD_LOG_HANDLER", handler=self.logging['handler'])
|
|
377
384
|
|
|
378
385
|
self._logger = logging.getLogger(self._name)
|
|
379
386
|
|
|
@@ -407,7 +414,7 @@ class Config:
|
|
|
407
414
|
if self.logging["handler"]["class"] == "TimedRotatingFileHandler":
|
|
408
415
|
x = params.get('filename')
|
|
409
416
|
if x is None:
|
|
410
|
-
|
|
417
|
+
raise error("NA_FILE_NAME")
|
|
411
418
|
Path(x).parents[0].mkdir(parents=True, exist_ok=True)
|
|
412
419
|
with open(x, 'a'):
|
|
413
420
|
...
|
|
@@ -451,7 +458,9 @@ class Config:
|
|
|
451
458
|
|
|
452
459
|
if isinstance(msg, Exception):
|
|
453
460
|
level = LogLevel.ERROR
|
|
461
|
+
sql = False
|
|
454
462
|
else:
|
|
463
|
+
sql = level == LogLevel.SQL
|
|
455
464
|
level = self._parse_log_level(level)
|
|
456
465
|
msg = str(msg)
|
|
457
466
|
if level.value < self.log_level.value:
|
|
@@ -479,15 +488,15 @@ class Config:
|
|
|
479
488
|
info = log_to.findCaller(stacklevel=lvl)
|
|
480
489
|
p = str(Path(__file__).parent.parent)
|
|
481
490
|
while info[0].startswith(p):
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
+
lvl += 1
|
|
492
|
+
x = log_to.findCaller(stacklevel=lvl)
|
|
493
|
+
if '/venv/' in x[0]:
|
|
494
|
+
break
|
|
495
|
+
elif not x[0].startswith(p):
|
|
496
|
+
info = x
|
|
497
|
+
break
|
|
498
|
+
else:
|
|
499
|
+
info = x
|
|
491
500
|
else:
|
|
492
501
|
info = log_to.findCaller(stacklevel=kwargs.get('stacklevel', 2))
|
|
493
502
|
|
|
@@ -510,6 +519,8 @@ class Config:
|
|
|
510
519
|
|
|
511
520
|
params["msg"] = f"{msg}"
|
|
512
521
|
info = log_to.makeRecord(**params)
|
|
522
|
+
if sql:
|
|
523
|
+
info.levelname = "SQL"
|
|
513
524
|
log_to.handle(info)
|
|
514
525
|
except Exception as e:
|
|
515
526
|
# Больше вывести некуда - выводим на стандартный вывод
|
|
@@ -518,7 +529,7 @@ class Config:
|
|
|
518
529
|
|
|
519
530
|
def db_connection(self, connection:str="")->str:
|
|
520
531
|
if not Config.hasattr("database"):
|
|
521
|
-
|
|
532
|
+
raise error("NA_DB_IN_CONFIG")
|
|
522
533
|
db = self.database
|
|
523
534
|
if isinstance(db, str) and connection=="":
|
|
524
535
|
return db
|
|
@@ -526,5 +537,5 @@ class Config:
|
|
|
526
537
|
dflt = self.defaults if self.hasattr("defaults") else {}
|
|
527
538
|
db = db.get(connection, db.get(dflt.get("database", "main")))
|
|
528
539
|
if not db:
|
|
529
|
-
|
|
540
|
+
raise error("NA_DB", connection=connection)
|
|
530
541
|
return db
|
|
@@ -88,11 +88,11 @@ class DB_ENV(UserDict):
|
|
|
88
88
|
return ret.value if isinstance(ret, Property) else ret
|
|
89
89
|
|
|
90
90
|
def __setitem__(self, key, value):
|
|
91
|
-
x = self.get(key)
|
|
91
|
+
x = self.data.get(key)
|
|
92
92
|
if isinstance(x, Property):
|
|
93
93
|
x.value = value
|
|
94
94
|
else:
|
|
95
|
-
super.__setitem__(key, value)
|
|
95
|
+
super().__setitem__(key, value)
|
|
96
96
|
|
|
97
97
|
def has_item(self, key):
|
|
98
98
|
# возвращает True если в данном DB_ENV хранится переменная с именем key
|
|
@@ -140,7 +140,7 @@ class DB_ENV(UserDict):
|
|
|
140
140
|
query = await self._context.connection.parse_func(query, **kwargs)
|
|
141
141
|
elif cmd==r"\d":
|
|
142
142
|
return await self._context.connection.table_struct(query.strip())
|
|
143
|
-
elif cmd=="\json":
|
|
143
|
+
elif cmd==r"\json":
|
|
144
144
|
query, params = query.split("(", 1)
|
|
145
145
|
if params.lower() in ("json)", "jsonb)"):
|
|
146
146
|
args = (kwargs,) + args
|