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.
- {ab_engine-0.1.3 → ab_engine-0.2.0}/PKG-INFO +165 -28
- {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine/__init__.py +1 -1
- {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine/db/__init__.py +1 -1
- {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine/db/driver.py +57 -7
- {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine/db/driver_mysql.py +1 -1
- {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine/db/driver_postgresql.py +39 -20
- {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine/db/driver_sqlite.py +2 -2
- ab_engine-0.2.0/ab_engine/db/driver_valkey.py +255 -0
- {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine/db/option.py +66 -9
- {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine/db/processor.py +20 -17
- {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine/env/config.py +13 -9
- {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine/env/db_context.py +4 -2
- {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine/error.py +5 -2
- ab_engine-0.2.0/ab_engine/error.yaml +123 -0
- ab_engine-0.2.0/ab_engine/rpc/__init__.py +2 -0
- {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine/rpc/fnc.py +9 -6
- {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine/rpc/json_rpc.py +1 -1
- {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine/rpc/rpc.py +121 -8
- {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine.egg-info/PKG-INFO +165 -28
- {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine.egg-info/SOURCES.txt +2 -0
- {ab_engine-0.1.3 → ab_engine-0.2.0}/pyproject.toml +3 -1
- {ab_engine-0.1.3 → ab_engine-0.2.0}/readme.md +165 -28
- ab_engine-0.1.3/ab_engine/rpc/__init__.py +0 -2
- {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine/class_tools.py +0 -0
- {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine/db/table.py +0 -0
- {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine/env/__init__.py +0 -0
- {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine/env/timer.py +0 -0
- {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine.egg-info/dependency_links.txt +0 -0
- {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine.egg-info/requires.txt +0 -0
- {ab_engine-0.1.3 → ab_engine-0.2.0}/ab_engine.egg-info/top_level.txt +0 -0
- {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.
|
|
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
|
|
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
|
-
Если нужно задать свой путь поиска для всех драйверов БД,
|
|
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
|
-
|
|
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: строка
|
|
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
|
|
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 имя_функции | | вызывает функцию,
|
|
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
|
-
|
|
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", "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -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
|
-
|
|
34
|
-
|
|
35
|
-
if "
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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()
|