ab_engine 0.1.3__tar.gz → 0.2.0__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.3 → ab_engine-0.2.0}/PKG-INFO +165 -28
  2. {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine/__init__.py +1 -1
  3. {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine/db/__init__.py +1 -1
  4. {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine/db/driver.py +57 -7
  5. {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine/db/driver_mysql.py +1 -1
  6. {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine/db/driver_postgresql.py +39 -20
  7. {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine/db/driver_sqlite.py +2 -2
  8. ab_engine-0.2.0/ab_engine/db/driver_valkey.py +255 -0
  9. {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine/db/option.py +66 -9
  10. {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine/db/processor.py +20 -17
  11. {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine/env/config.py +13 -9
  12. {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine/env/db_context.py +4 -2
  13. {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine/error.py +5 -2
  14. ab_engine-0.2.0/ab_engine/error.yaml +123 -0
  15. ab_engine-0.2.0/ab_engine/rpc/__init__.py +2 -0
  16. {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine/rpc/fnc.py +9 -6
  17. {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine/rpc/json_rpc.py +1 -1
  18. {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine/rpc/rpc.py +121 -8
  19. {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine.egg-info/PKG-INFO +165 -28
  20. {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine.egg-info/SOURCES.txt +2 -0
  21. {ab_engine-0.1.3 → ab_engine-0.2.0}/pyproject.toml +3 -1
  22. {ab_engine-0.1.3 → ab_engine-0.2.0}/readme.md +165 -28
  23. ab_engine-0.1.3/ab_engine/rpc/__init__.py +0 -2
  24. {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine/class_tools.py +0 -0
  25. {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine/db/table.py +0 -0
  26. {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine/env/__init__.py +0 -0
  27. {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine/env/timer.py +0 -0
  28. {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine.egg-info/dependency_links.txt +0 -0
  29. {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine.egg-info/requires.txt +0 -0
  30. {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine.egg-info/top_level.txt +0 -0
  31. {ab_engine-0.1.3 → ab_engine-0.2.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ab_engine
3
- Version: 0.1.3
3
+ Version: 0.2.0
4
4
  Summary: Library of standard modules
5
5
  Author-email: Andrew Bulatov <BulatovAndrew@gmail.com>
6
6
  License-Expression: MIT
@@ -45,7 +45,7 @@ Requires-Dist: toml>=0.10.2
45
45
  **env_map** - задает соответствие ключей словаря переменным окружения. если такое соответствие заддано, то после
46
46
  загрузки параметров из файлов, система заменяет значения в словаре значениями из переменныых окружения.
47
47
 
48
- Если в окружении нет таких переменных, то в качестве значения остается то значение, которое было загружено из файла.
48
+ Если в окружении нет таких переменных, то в качестве значения остается, то значение, которое было загружено из файла.
49
49
  Структура для задания соответствия переменным окружения является словарем вида:
50
50
 
51
51
  ```json
@@ -373,16 +373,16 @@ logging:
373
373
  ## Доступ к БД
374
374
 
375
375
  Библиотека поддерживает хранение настроек соединений и выполнение команд SQL на этих соединениях с БД.
376
- Из коробки реализованы драйвер Postgresql, Sqlite, Mysql (но надо установить зависимость mysql-connector-python)
376
+ Из коробки реализованы драйвер Postgresql, Sqlite и Valkey (Redis). Также реализован драйвер Mysql но для его работы дополнительно надо установить зависимость mysql-connector-python.
377
377
 
378
378
  Можно описывать драйвера к другим БД. Для того чтобы использовать собственный драйвер, можно добавить класс драйвера
379
379
  в словарь DRIVER_CLASSES, который находится в модуле ab_engine/db/option, либо положить драйвер в каталог драйверов.
380
380
  Пример драйвера в DRIVER_CLASSES:
381
381
 
382
- необходимости использовать собственный драйвер, можно в встроке соединения в
383
- фигурных скобках задать параметр **driver_path**, определяющий путь для поиска драйвра.
382
+ необходимости использовать собственный драйвер, можно в строке соединения в
383
+ фигурных скобках задать параметр **driver_path**, определяющий путь для поиска драйвера.
384
384
 
385
- Если нужно задать свой путь поиска для всех драйверов БД, можэно задать параметр db_driver_path в defaults конфигурации.
385
+ Если нужно задать свой путь поиска для всех драйверов БД, можно задать параметр db_driver_path в defaults конфигурации.
386
386
 
387
387
  Если приложению требуется только одна строка соединения, то ее можно задать в переменной database, например:
388
388
 
@@ -411,12 +411,17 @@ database = "test"
411
411
  помимо параметров, определяемых драйвером, можно передать параметр driver_path, который
412
412
  определяет путь поиска драйвера
413
413
 
414
- * **дополнительные параметры** - параметры, которые будут переданы в сессию соединения с БД (в фигуреых скобках).
414
+ * **дополнительные параметры** - параметры, которые будут переданы в сессию соединения с БД (в фигурных скобках).
415
415
  передаются как **"**ключ**"="**значение**"**
416
416
  в качестве разделителя параметров используется запятая.
417
417
  Для любого сочетания драйвер+строка соединения-доп.параметры, можно задать дополнительный параметр LIMIT,
418
418
  который ограничит максимальное количество таких соединений. Т.е. по достижении данного числа соединений система
419
- не станет выполнять очередную команду до тех пор, пока не освободится одно из текущихз соединений с БД.
419
+ не станет выполнять очередную команду до тех пор, пока не освободится одно из текущих соединений с БД.
420
+
421
+ Для некоторых драйверо, например, для встроенного драйвера posstgresql строка сооединения с БД
422
+ может иметь альтернативную структуру:
423
+
424
+ <имя_драйвера>**://**<имя пользователя>**:**<пароль>**"**<адрес сервера>**:**<номер порта>**/**<имя БД>**?**<параметры соединения>**{**дополнительные параметры**}**
420
425
 
421
426
  Драйвер БД должен иметь имя driver_<имя в строке соединения>.py и предоставлять следующий контракт:
422
427
 
@@ -424,16 +429,20 @@ database = "test"
424
429
  class Driver:
425
430
 
426
431
  @abstractmethod
427
- def __init__(self, connection_string, on_open_close=None):
432
+ def __init__(self, connection_string, on_open_close=None, notify=None, out_params:Optional[dict]=None):
428
433
  """
429
434
  констркуктор драйвера.
430
435
  должен разобрать строку соединения (описана выше) и инициализировать экземпляр драйвера.
431
436
  дополнительные параметры строки соединения (те что в фигурных скобках) не передаются в строку
432
437
  соединения.
433
438
  on_open_close функция, передаваемая драйверу, описываемая как
434
- async def on_open_close(self, close=False)->dict:
435
- данная функция возвращает драйверу дополнительные параметры, которые могут быть использованы
436
- при создании и закрытии соединения (если драйвер это поддерживает)
439
+ async def on_open_close(self, close=False)->dict:
440
+ данная функция возвращает драйверу дополнительные параметры, которые могут быть использованы
441
+ при создании и закрытии соединения (если драйвер это поддерживает)
442
+ notify список, в который будут записаны уведомления о событиях, либо метод, который будет
443
+ вызван для каждого события. метод может быть как синхронным, так и асинхронным, на его входе строка.
444
+ out_params словарь выходных параметров. используется в конструкторе драйверов для получения
445
+ разобранного connection_string
437
446
  """
438
447
  ...
439
448
 
@@ -469,7 +478,7 @@ class Driver:
469
478
  если one_row=False и выборка пуста
470
479
  Если one_row=True, то вернет только первую строку результата, либо Null,
471
480
  если соответствующая запись не найдена.
472
- В случае, если query яляется запросом, не возвращающим выборку, вернет
481
+ В случае, если query является запросом, не возвращающим выборку, вернет
473
482
  целое число - количество записей, на которые повлиял запрос.
474
483
  """
475
484
  #if self._conn is None:
@@ -559,7 +568,7 @@ class Driver:
559
568
  Функция **parse_func** позволяет унифицировать вызов функций. Подставляет необходимый для вызова префикс и вызывает
560
569
  parse_query для подстановки параметров.
561
570
 
562
- Функция **parse_query** обеспечивает безопасную подстановку значений параметров в запрос. Для того, чтобы указать
571
+ Функция **parse_query** обеспечивает безопасную подстановку значений параметров в запрос. Для того чтобы указать
563
572
  в тексте запроса параметр, следует указать $<номер параметра> и тогда запрос будет взят из args с номером на единицу меньшим.
564
573
  Либо указать $<имя параметра> в этом случае значение будет взято из kwargs с тем же именем. Номер всегда состоит только из цифр.
565
574
  Имя начинается с буквы и заканчивается перед пробельным символом или перед скобкой, или двоеточием, или запятой.
@@ -630,6 +639,11 @@ if __name__ == '__main__':
630
639
  В этом примере стало необязательно указывать опцию DB, тк если она не указана, то система берет ту строку соединения,
631
640
  которая указана в файле конфигурации.
632
641
 
642
+ Для того чтобы установить PEER соединение с postgresql от имени текущего пользователя, достаточно указать имя драйвера
643
+ и имя базы данных:
644
+ ```yaml config.yaml
645
+ database: postgresql://postgres
646
+ ```
633
647
  Для выполнения запроса, в обоих примерах, мы использовали функцию sql из ab_engine.db, которая определяется как:
634
648
  ```python
635
649
  async def sql(query:str, *args, **kwargs):
@@ -641,6 +655,7 @@ async def sql(query:str, *args, **kwargs):
641
655
  Передавать опции можно только в конце списка args, после позиционных параметров, либо вместо них.
642
656
  В библиотеке реализованы следующие опции:
643
657
  * DB - служит для передачи соединения в функцию sql. Передается как DB("строка соединения").
658
+ * NOTICE - то же, что DB, но запросы с такой опцией возвращают как результат список сообщений нотификации
644
659
  * TIMEOUT- устанавливает максимальное время в секундах на выполнение запроса. точность - до сотых секунды.
645
660
  По умолчанию вызывает ошибку, при превышении времени ожидания. TIMEOUT(time_sec:float, raise_error:bool=True)
646
661
  * DICT - Указывает, что результат следует вернуть как список dict (RowFactory.DICT). Установлена по умолчанию
@@ -649,11 +664,12 @@ async def sql(query:str, *args, **kwargs):
649
664
  * ROW - Указывает, что нужно вернуть только первую строку из выборки
650
665
  * ONE - Указывает, что нужно вернуть только значение первого поля из первой строки выборки
651
666
  * JSON - Указывает, что значение первого поля в первой строке следует привести к dict или list
652
- * ROLLBACK - казывает, что после выполнения запроса соединение должно быть закрыто с откатом транзакции
667
+ * ROLLBACK - указывает, что после выполнения запроса соединение должно быть закрыто с откатом транзакции
653
668
  * COMMIT - Указывает, что после выполнения запроса соединение должно быть закрыто с подтверждением транзакции
654
669
  * PAGE - Позволяет задать максимальный размер страницы и её смещение от начала выборки (limit, offset)
655
670
  * ITERATOR - Если опция задана, то результатом функции sql будет итератор для работы с большими таблицами в async for
656
671
  * ALL - Специальная опция, добавлена для принудительного отключения фильтров в функциях, которые это понимают (например у экземпляра Table)
672
+ * RAW - Отключает предобработку запроса с подстановкой параметров
657
673
  * CALLBACK - Из функции sql отдает в callback_function запрос с подставленными параметрами. Что позволяет доработать запрос или вывести в лог.
658
674
  Eсли callback_function возвращает строку, то именно эта строка станет запросом. Функция sql вызывает только первый callback из args
659
675
 
@@ -706,11 +722,13 @@ DB_ENV реализует следующий контракт:
706
722
  ```python
707
723
  class DB_ENV:
708
724
 
709
- def __init__(self, connection: str="", db_params:Optional[set]=None, **kwargs):
725
+ def __init__(self, connection: str="", db_params:Optional[set]=None, notify=None, **kwargs):
710
726
  """
711
727
  Окружение для работы с БД
712
- :param connection: строка соеддинения или экземпляр DB_ENV, на основе которого нужно создать данный
713
- :param db_params: список переменных, которые должны передаваться в в окружение соединения с БД
728
+ :param connection: строка соединения или экземпляр DB_ENV, на основе которого нужно создать данный
729
+ :param db_params: список переменных, которые должны передаваться в окружение соединения с БД
730
+ :param notify: список или функция, в которую будут переданы извещения о событиях.
731
+ функция может быть синхронной или асинхронной и должна принимать на вход один параметр.
714
732
  :param kwargs: значения переменных
715
733
  """
716
734
 
@@ -819,7 +837,7 @@ if __name__ == '__main__':
819
837
  контекст завершит транзакцию и откатит её, т.е. результат выполнения insert не будет сохранен.
820
838
  В данном случае DB_ENV создан без параметров, но он может принять на вход:
821
839
  * connection - строку соединения с БД. это позволяет задать соединение отличное от заданного файла конфигурации. но,
822
- тем не менее, окружение не может работать без конфигурации потому что троебует и других настроек, например настроек ведения лога.
840
+ тем не менее, окружение не может работать без конфигурации потому что требует и других настроек, например настроек ведения лога.
823
841
  * params - словарь с дополнительными параметрами соединения. например, тут можно передать id текущего пользователя или
824
842
  еще какие-то параметры сессии.
825
843
 
@@ -853,10 +871,10 @@ if __name__ == '__main__':
853
871
  |----------------------------|-------------------------------|----------------|
854
872
  | * имя_таблицы | select * from имя_таблицы |
855
873
  | \\JSON имя_функции(параметры)| select имя_функции(параметры) | вместо select может быть другая команда, в зависимости от дроайвера СУБД|
856
- | \\JSON имя_функции(JSON) | select имя_функции($1::json) | selecn и преобразование ::jsonb завист от СУБД, **kwargs функции sql будут переданы как параметр типа json|
874
+ | \\JSON имя_функции(JSON) | select имя_функции($1::json) | selecn и преобразование ::jsonb зависит от СУБД, **kwargs функции sql будут переданы как параметр типа json|
857
875
  | \\JSON имя_функции(JSONB) | select имя_функции($1::json) | как и выше, но параметр приводится к jsonb (если это поддерживает СУБД, иначе к json)|
858
876
  | \\d имя_таблицы | | возвращает json, содержащий структуру таблицы |
859
- | \\call имя_функции | | вызывает функцию, полставляя *args, **kwargs как параметры |
877
+ | \\call имя_функции | | вызывает функцию, подставляя *args, **kwargs как параметры |
860
878
 
861
879
  При вызове запросов из DB_ENV можно использовать параметры, например:
862
880
  ```python
@@ -876,6 +894,62 @@ async def test():
876
894
  print(x) # также выведет 10
877
895
  ```
878
896
 
897
+ #### Особенности работы с Valkey
898
+
899
+ В случае подключения драйвера Valkey, работа с библиотекой отличается от работы с sql базами данных:
900
+ 1) В качестве входного языка для функций sql используется язык redis-cli с которым можно ознакомиться, например, в [The Little Book](https://github.com/akandratovich/the-little-redis-book/blob/master/ru/redis.md)
901
+ 2) Поскольку это не язык SQL, то и описанные выше расширения синтаксиса неприменимы к Valkey
902
+ 3) Опции позволяющие менять обработку sql, например, ONE не имеют смысла для Valkey
903
+
904
+ Проиллюстрируем работу с Valkey на примере.
905
+ Для этого создадим файл test.toml следующего содержания.
906
+ ```toml
907
+ [database]
908
+ valkey = "valkey://localhost:6379/0"
909
+ ```
910
+ вместо localhost:6379 в файле должны быть заданы адрес хоста и порт Valkey.
911
+
912
+ Далее напишем следующий код на python:
913
+ ```python
914
+ import asyncio
915
+ from ab_engine.db import ONE
916
+ from ab_engine import Config
917
+ from ab_engine.env import DB_ENV
918
+
919
+ async def main():
920
+ notify = [] # массив для получения уведомлений
921
+ env = DB_ENV("valkey", notify=notify)
922
+ # запишем 123 по ключу x и проверим значение ключа
923
+ await env.sql("set x 123")
924
+ x = await env.sql("get x")
925
+ print(x)
926
+ # теперь запишем 321, но с использованием параметров
927
+ await env.sql("set x $1", 321)
928
+ x = await env.sql("get x")
929
+ print(x)
930
+ # запустим цикл для получения уведомлений от Valkey
931
+ # в данном случае будем получать уведомления в ранее объявленный список notify, но можно было бы передать в
932
+ # параметр notify конструктора DB_ENV ссылку на функцию с одним обязательным аргументом, тогда уведомления
933
+ # будут передаваться в эту функцию
934
+
935
+ # подписываемся на уведомления публикаций темы test
936
+ await env.sql("subscribe test")
937
+
938
+ # теперь, пока открыто наше соедингение мы будем получать уведомления, если кто-то выполнит в Valkey команду:
939
+ # publish test "какой-то текст"
940
+ while True:
941
+ if notify:
942
+ print("Уведомления:")
943
+ while len(notify) > 0:
944
+ print(" ", notify.pop(0))
945
+ await asyncio.sleep(0.2)
946
+
947
+ if __name__ == '__main__':
948
+ Config("test.toml")
949
+ asyncio.run(main())
950
+ ```
951
+
952
+
879
953
  ## Работа с курсорами таблиц БД
880
954
 
881
955
  Курсор таблицы может быть получен из DB_ENV посредством функции table.
@@ -1144,7 +1218,65 @@ if __name__ == '__main__':
1144
1218
 
1145
1219
  ### Регистрация методов
1146
1220
 
1147
- Методы RPC регистрируются c помощью декоратора:
1221
+ Методы RPC регистрируются функцией register_rpc_list либо c помощью декоратора register_rpc, который также можно использовать как функцию.
1222
+
1223
+ Регистрация списка функций осуществляется с помощью register_rpc_list, которая принимает на вход парамектры defs и self.
1224
+ * **defs** - содержит список или словарь с описаниями
1225
+ * **self** - необязательный параметр, в котором передается модуль или объект из которого будут регистрироваться функции. если в self ничего не передано, то будут регистрироваться функции из модуля, в котором вызвана register_rpc_list.
1226
+
1227
+ список или словарь с орписаниями содержит элементы следующей структуры:
1228
+ * **name**: имя функции в API (не нужно и игнорируется, если описание передается в словаре)
1229
+ * **function**: имя реально вызываемой функции, если оно отличается от name
1230
+ * **sql**: запрос sql, реализующий функцию, если передан sql, то не нужно передавать function и type
1231
+ * **help**: описание функции. если не задано - берется из функции
1232
+ * **type**: тип функции
1233
+ * **self** - функция из объекта, переданного в параметре self
1234
+ * **py** - функция из плагина python
1235
+ * **db** - функция БД
1236
+ * **ext**: дополнительные параметры регистрации, если нужны. для sql это могут быть опции или их имена
1237
+
1238
+ для функций с типом **self** для которых name==function возможна запись:
1239
+ ***имя_функции: self***
1240
+
1241
+ Пример списка для регистрации:
1242
+ ```python
1243
+ from ab_engine import register_rpc_list, call_rpc
1244
+
1245
+ def hello():
1246
+ return "Здравствуй мир!"
1247
+
1248
+ register_rpc_list({
1249
+ "hello":"self",
1250
+ "hello2":{
1251
+ "sql":"""
1252
+ do $$
1253
+ declare
1254
+ nm varchar;
1255
+ begin
1256
+ nm = $name::varchar;
1257
+ raise notice 'Hello % !!', nm;
1258
+ end $$
1259
+ """,
1260
+ "ext": ["NOTICE","ONE"]
1261
+ },
1262
+ "set":{
1263
+ "sql":"set $key $value",
1264
+ "ext":"DB(valkey)",
1265
+ "help": "устанавливает значение ключа key равным value"
1266
+ },
1267
+ "get":{
1268
+ "sql":"get $key",
1269
+ "ext":"DB(valkey)",
1270
+ "help": "возвращает значение ключа key"
1271
+ }
1272
+ })
1273
+
1274
+ async def test():
1275
+ await call_rpc("set", key="test", value=1234)
1276
+ ```
1277
+
1278
+
1279
+ Регистрация с помощью декоратора:
1148
1280
  ```python
1149
1281
  from ab_engine import register_rpc
1150
1282
  from ab_engine.db import ONE
@@ -1299,7 +1431,7 @@ if __name__ == '__main__':
1299
1431
  ## Поддержка плагинов
1300
1432
 
1301
1433
  Плагины, как и sql-запросы и функции python регистрируются с помощью функции register_rpc.
1302
- Создадим plugin.py с примером плагина:
1434
+ Создадим plugin.py с примером плагина:
1303
1435
  ```python
1304
1436
  def test(**kwargs):
1305
1437
  if kwargs:
@@ -1325,7 +1457,7 @@ async def main():
1325
1457
  if __name__ == '__main__':
1326
1458
  asyncio.run(main())
1327
1459
  ```
1328
- Из этого примера можно увидеть как регистрируются функци плагинов.
1460
+ Из этого примера можно увидеть как регистрируются функции плагинов.
1329
1461
  Также, из примера следует, что функция call_rpc умеет вызывать как асинхронные, так и обычные функции.
1330
1462
 
1331
1463
  Функции плагинов могут получать экземпляр текущего окружения (DB_ENV).
@@ -1337,13 +1469,13 @@ async def with_env(env, **kwargs):
1337
1469
  x.update(kwargs)
1338
1470
  return x
1339
1471
  ```
1340
- также нам понадобится файл test.toml следующего содержания:
1472
+ Также нам понадобится файл test.toml следующего содержания:
1341
1473
  ```toml
1342
1474
  [defaults]
1343
1475
  plugin_path = "./"
1344
1476
  ```
1345
1477
 
1346
- и основной код примера:
1478
+ И основной код примера:
1347
1479
  ```python
1348
1480
  from ab_engine import register_rpc, call_rpc, Config
1349
1481
  from ab_engine.env import DB_ENV
@@ -1351,7 +1483,7 @@ import asyncio
1351
1483
 
1352
1484
  async def main():
1353
1485
  Config("test.toml")
1354
- register_rpc("plugin", "_plugin.py:with_env")
1486
+ register_rpc("plugin", "plugin.py:with_env") # не указан путь
1355
1487
  env = DB_ENV(c=3, d=4)
1356
1488
  x = await call_rpc("plugin", env, a=1, b=2)
1357
1489
  print(x)
@@ -1359,5 +1491,10 @@ async def main():
1359
1491
  if __name__ == '__main__':
1360
1492
  asyncio.run(main())
1361
1493
  ```
1362
- в данном примере в функцию плагина будет передан экземпляр окружения, из которого она фвозьмет параметры и объединит
1494
+ В данном примере в функцию плагина будет передан экземпляр окружения, из которого она возьмет параметры и объединит
1363
1495
  их с параметрами, переданными в функцию.
1496
+
1497
+ В этом примере следует отметить, что путь к коду плагина не указан в параметре функции register_rpc.
1498
+ Путь поиска плагинов в этом примере задан в файле test.toml в параметре plugin_path.
1499
+ В параметре plugin_path можно указать несколько путей поиска, разделенных ; (точкой с запятой), поиск
1500
+ файла в этом случае будет идти в том порядке, в котором перечислены пути.
@@ -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,3 +1,3 @@
1
- from .option import DB, TUPLE, OBJECT, ONE, ROW, ROLLBACK, COMMIT, JSON, TIMEOUT, PAGE, CALLBACK, ITERATOR
1
+ from .option import DB, TUPLE, OBJECT, ONE, ROW, ROLLBACK, COMMIT, JSON, TIMEOUT, PAGE, CALLBACK, ITERATOR, RAW
2
2
  from .processor import sql, set_connection
3
3
  from .table import Table
@@ -3,6 +3,9 @@ from json import dumps
3
3
  from datetime import datetime
4
4
  from abc import ABC, abstractmethod
5
5
  from collections import namedtuple
6
+ from typing import Optional
7
+ from urllib.parse import unquote
8
+ from inspect import iscoroutinefunction
6
9
 
7
10
  _is_option = None
8
11
 
@@ -31,16 +34,61 @@ def reencode(s):
31
34
 
32
35
  class Driver(ABC):
33
36
 
34
- LIKE = "LIKE"
37
+ LIKE = "LIKE" # что использовать как like, например для postgresql это ILIKE
35
38
 
36
- def __init__(self, connection_string, on_open_close=None):
39
+ def __init__(self, connection_string, on_open_close=None, notify=None, out_params:Optional[dict]=None):
37
40
  self._conn = None
38
41
  if isinstance(connection_string, Driver):
39
42
  self._conn_str = connection_string.connection_string
40
43
  self._on_open_close = on_open_close or connection_string._on_open_close
41
44
  return
45
+ self._notify = notify
42
46
  self._on_open_close = on_open_close
47
+ connection_string = connection_string.strip()
48
+ connection_string, options = connection_string.split("{",1) if "{" in connection_string and connection_string.endswith("}") else connection_string, "}"
49
+ options = options[:-1]
43
50
  self._conn_str = connection_string
51
+ if out_params is None:
52
+ return
53
+ out_params["options"]=options
54
+ connection_string, options = f"{connection_string}?".split("?",1)
55
+ options = options[:-1]
56
+ if options!="":
57
+ options=options.split("&")
58
+ for n, x in enumerate(options):
59
+ x = x.split("=",1)
60
+ if len(x)==1:
61
+ x.append("")
62
+ options[n] = (unquote(x[0]),unquote(x[1]),)
63
+ out_params["params"] = {x[0]:x[1] for x in options}
64
+ if "@" in connection_string:
65
+ options, connection_string = connection_string.split("@", 1)
66
+ if ":" in options:
67
+ options = options.split(":", 1)
68
+ out_params["user"] = unquote(options[0])
69
+ out_params["password"] = unquote(options[1])
70
+ else:
71
+ out_params["user"] = unquote(options)
72
+ if "/"in connection_string:
73
+ connection_string, options = connection_string.split("/", 1)
74
+ out_params["database"] = unquote(options)
75
+ if ":" in connection_string:
76
+ connection_string, options = connection_string.rsplit(":", 1)
77
+ try:
78
+ out_params["port"] = int(options)
79
+ except Exception as e:
80
+ raise ValueError("The port number must be an integer")
81
+ out_params["host"] = unquote(connection_string)
82
+ else:
83
+ out_params["database"] = unquote(connection_string)
84
+
85
+ async def _notify_callback(self, notify):
86
+ if isinstance(self._notify, list):
87
+ self._notify.append(notify)
88
+ elif iscoroutinefunction(self._notify):
89
+ await self._notify(notify)
90
+ elif callable(self._notify):
91
+ self._notify(notify)
44
92
 
45
93
  @property
46
94
  def connection_string(self):
@@ -103,10 +151,10 @@ class Driver(ABC):
103
151
  return reencode(var)
104
152
  else:
105
153
  var = ','.join(['NULL' if x is None else str(x) for x in var])
106
- return reencode(var)
154
+ return reencode(f"{{{var}}}")
107
155
  elif isinstance(var, set):
108
156
  var = ','.join('NULL' if x is None else str(x) for x in var)
109
- return reencode(var)
157
+ return reencode(f"{{{var}}}")
110
158
  elif isinstance(var, dict):
111
159
  var = dumps(var, ensure_ascii=False).replace("'", "''")
112
160
  return reencode(var)
@@ -120,7 +168,8 @@ class Driver(ABC):
120
168
  if callback:=kwargs.get("__PARAM_CALLBACK_GETTER"):
121
169
  del kwargs["__PARAM_CALLBACK_GETTER"]
122
170
  qs = None
123
- for x in query.split("$"):
171
+
172
+ for x in query.replace("$$", "🗿").split("$"):
124
173
  if qs is None:
125
174
  qs = x
126
175
  continue
@@ -143,10 +192,11 @@ class Driver(ABC):
143
192
  n = callback[n]
144
193
  else:
145
194
  raise AttributeError(f"Attribute with name {n} is not exists")
146
- qs += f"{self.var_to_sql(n, x)}{x[p:]}"
195
+ x = x[p:]
196
+ qs += f"{self.var_to_sql(n, x)}{x}"
147
197
 
148
198
  if qs:
149
- query = qs
199
+ query = qs.replace("🗿", "$$")
150
200
 
151
201
  return query
152
202
 
@@ -37,7 +37,7 @@ _FACTORY_ = {
37
37
 
38
38
  class Driver(BaseDriver):
39
39
 
40
- def __init__(self, connection_string, on_open_close=None):
40
+ def __init__(self, connection_string, on_open_close=None, notify=None):
41
41
  """
42
42
  localhost:3306/mysql?user=root&password=root
43
43
  """
@@ -26,28 +26,25 @@ class Driver(BaseDriver):
26
26
 
27
27
  LIKE = "ILIKE"
28
28
 
29
- def __init__(self, connection_string, on_open_close=None):
29
+ def __init__(self, connection_string, on_open_close=None, notify=None):
30
30
  """
31
31
  localhost:5432/postgres?user=postgres&password=postgres
32
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
33
+ drv_params = {}
34
+ super().__init__(connection_string, on_open_close, out_params=drv_params, notify=notify)
35
+ if "user" in drv_params:
36
+ drv_params["params"]["user"] = drv_params["user"]
37
+ if "password" in drv_params:
38
+ drv_params["params"]["password"] = drv_params["password"]
39
+ con = ""
40
+ if "host" in drv_params:
41
+ con += f"host={drv_params['host']} "
42
+ con += f"dbname={drv_params['database']} "
43
+ if "port" in drv_params:
44
+ con += f"port={drv_params['port']} "
45
+ for x in drv_params["params"]:
46
+ con += f"{x}={drv_params['params'][x]} "
47
+ self._conn_str = con.strip()
51
48
 
52
49
  @property
53
50
  def in_transaction(self):
@@ -61,6 +58,12 @@ class Driver(BaseDriver):
61
58
  await self._conn.execute(f"set session timezone '{params[x]}'")
62
59
  else:
63
60
  await self._conn.execute(f"SET {x} = '{params[x]}'")
61
+ if self._notify:
62
+ self._conn.add_notify_handler(self._notify_callback)
63
+
64
+ async def _notify_callback(self, notify):
65
+ notify = notify if isinstance(notify, str) else f"[{notify.pid}: {notify.channel}] {notify.payload}"
66
+ await super()._notify_callback(notify)
64
67
 
65
68
  async def sql(self, query, one_row=False, row_factory=RowFactory.DICT):
66
69
  if self._conn is None:
@@ -69,7 +72,23 @@ class Driver(BaseDriver):
69
72
 
70
73
  acur.row_factory = _FACTORY_[row_factory.value]
71
74
 
72
- await acur.execute(query)
75
+ if self._notify is not None:
76
+ notices = []
77
+
78
+ def add_notice(notify):
79
+ nonlocal notices
80
+ notices.append(f"[{notify.severity}] {notify.message_primary}")
81
+
82
+ try:
83
+ self._conn.add_notice_handler(add_notice)
84
+ await acur.execute(query)
85
+ finally:
86
+ self._conn.remove_notice_handler(add_notice)
87
+ for x in notices:
88
+ await self._notify_callback(x)
89
+ else:
90
+ await acur.execute(query)
91
+
73
92
  descr = acur.description
74
93
  if not descr:
75
94
  return acur.rowcount
@@ -46,11 +46,11 @@ _FACTORY_ = {
46
46
 
47
47
  class Driver(BaseDriver):
48
48
 
49
- def __init__(self, connection_string, on_open_close=None):
49
+ def __init__(self, connection_string, on_open_close=None, notify=None):
50
50
  """
51
51
  test_db.sqlite
52
52
  """
53
- super().__init__(connection_string, on_open_close)
53
+ super().__init__(connection_string, on_open_close, notify)
54
54
 
55
55
  async def begin(self):
56
56
  await self._before_open()