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.
Files changed (31) hide show
  1. {ab_engine-0.1.9/ab_engine.egg-info → ab_engine-0.2.1}/PKG-INFO +100 -9
  2. {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/__init__.py +1 -1
  3. {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/db/driver.py +5 -4
  4. {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/db/driver_sqlite.py +6 -2
  5. {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/db/driver_valkey.py +37 -1
  6. {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/db/option.py +50 -7
  7. {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/db/processor.py +16 -16
  8. {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/db/table.py +2 -2
  9. {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/env/config.py +31 -20
  10. {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/env/db_context.py +3 -3
  11. {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/error.yaml +11 -1
  12. ab_engine-0.2.1/ab_engine/rpc/__init__.py +2 -0
  13. {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/rpc/json_rpc.py +10 -10
  14. {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/rpc/rpc.py +116 -3
  15. ab_engine-0.1.9/readme.md → ab_engine-0.2.1/ab_engine.egg-info/PKG-INFO +112 -7
  16. {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine.egg-info/requires.txt +1 -1
  17. {ab_engine-0.1.9 → ab_engine-0.2.1}/pyproject.toml +2 -2
  18. ab_engine-0.1.9/PKG-INFO → ab_engine-0.2.1/readme.md +98 -21
  19. ab_engine-0.1.9/ab_engine/rpc/__init__.py +0 -2
  20. {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/class_tools.py +0 -0
  21. {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/db/__init__.py +0 -0
  22. {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/db/driver_mysql.py +0 -0
  23. {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/db/driver_postgresql.py +0 -0
  24. {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/env/__init__.py +0 -0
  25. {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/env/timer.py +0 -0
  26. {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/error.py +0 -0
  27. {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine/rpc/fnc.py +0 -0
  28. {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine.egg-info/SOURCES.txt +0 -0
  29. {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine.egg-info/dependency_links.txt +0 -0
  30. {ab_engine-0.1.9 → ab_engine-0.2.1}/ab_engine.egg-info/top_level.txt +0 -0
  31. {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.9
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: pyaml>=25.7.0
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
- Также можно задать несколько соединений, если использовать database как секцию:
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
- Для некоторых драйверо, например, для встроенного драйвера posstgresql строка сооединения с БД
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
- for x in query.split("$"):
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)[1][:-1].strip().split("\n")
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[row["name"]]
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 raise_error
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
- raise_error("NA_DB", connection=str(e))
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].split("&")
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
- raise_error("NOT_CONV_DICT_LIST", data=ret_data)
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
- raise_error("BAD_LIMIT_OFFSET")
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
- raise_error("ATTR_NOT_FOUND", name=item)
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 raise_error
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
- raise_error("NEQ_ROWS", src1=one_row.__class__.__name__, src2=arg.__class__.__name__)
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
- raise_error("DEF_STR_FABRIC", src = row_factory.__class__.__name__)
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 == DB:
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
- raise_error("DB_ALRDY_DEF")
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
- raise_error("PAGE_ALRDY_DEF")
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
- raise_error("TIMEOUT_ALRDY_DEF")
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
- raise_error("PMT_BEF_OPT")
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
- raise_error("PAGE_ITERATOR")
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 raise_error, error_msg
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(t), key
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
- raise_error("BAD_FORMAT", format_name=fn)
121
+ raise error("BAD_FORMAT", format_name=fn)
118
122
 
119
123
  self._log_level = LogLevel.INFO
120
124
  if path is None:
121
- raise_error("BAD_CONFIG_PATH")
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
- data[x] = item
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
- raise_error("ATTR_NOT_FOUND", name=item)
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__ == self.logging["handler"]:
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
- raise_error("BAD_LOG_HANDLER", handler=self.logging['handler'])
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
- raise_error("NA_FILE_NAME")
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
- lvl += 1
483
- x = log_to.findCaller(stacklevel=lvl)
484
- if '/venv/' in x[0]:
485
- break
486
- elif not x[0].startswith(p):
487
- info = x
488
- break
489
- else:
490
- info = x
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
- raise_error("NA_DB_IN_CONFIG")
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
- raise_error("NA_DB", connection=connection)
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