ab-engine 0.1.2
Library of standard modules
Requires Python: >=3.10

Библиотека стандартных модулей
==========================================================

Позволяет не делать каждый раз, для каждого приложения, такие типовые вещи как:

* управление настройками
* управление таймерами
* обработка ошибок с возможностью легко поменять текст или язык сообщений
* вывод сообщений в лог
* доступ к БД и безопасное выполнение параметризованных запросов
* работа с курсорами таблиц БД
* единообразное выполнение функций python и sql, а также поддержка плагинов

## Управление настройками

Для управления настройками используется класс Config, я вляющийся синглтоном.
В конструктор Config передаются следующие параметры:

**path** - строка с именем файла, либо список строк с именами файлов, содержащих настройки
система определяет тип файла по расширению и загружает его
допустимы следующие расширения: .json, .json5, .yaml, .yml, .toml
в случае, если указан список файлов, каждый последующий файл меняет или дополняет настройки, загруженные из предыдущего.
результатом загрузки является словарь в памяти, который используется в дальнейшем, как источник настроек.
кроме того, в path можно передать словарь, который Config будет воспринимать как набор уже прочитанных настроек

**can_include** - список ключей словаря, значения которых могут быть прочитаны из включаемого файла (include).
если значение такого ключа является строкой, то система считает эту строку путем к включаемому файлу.

**env_map** - задает соответствие ключей словаря переменным окружения.  если такое соответствие заддано, то после
загрузки параметров из файлов, система заменяет значения в словаре значениями из переменныых окружения.

Если в окружении нет таких переменных, то в качестве значения остается то значение, которое было загружено из файла.
Структура для задания соответствия переменным окружения является словарем вида:

```json
{
  "имя ключа верхнего уровня": {
    "имя переменной окружения": "имя ключа, значение которого заменить из окружения"
    ...
  }
  ...
}
```

### Пример использования конфигурации

создадим файл настроек test.toml:

```toml
[auth]
user = "123" # id пользователя по умолчанию
```

и тестовый пример:

```python
import asyncio
from ab_engine import Config

async def test():
    Config("test.toml", env_map={"auth":{"USER":"user"}})
    Config().log(Config().auth['user'])

if __name__ == '__main__':
    asyncio.run(test())
```

в результате выполнения данной программы в лог будет выведено имя текущего пользователя, либо 123,
если в системе нет переменной окружения USER :)

вместо toml файла можно было использовать yaml:

```yaml
auth:
  user: 123 # id пользователя по умолчанию
```

или json:

```json
{
  "auth": {
    "user": 123
  }
}
```

или json5:

```json5
{
  auth: {
    user: 123 // id пользователя по умолчанию
  }
}
```

### Хранение конфигурации в БД

Для считывания конфигурации из БД, в конструктор Config следует передать не имя файла, а строку соединения: 
```python
import asyncio
from ab_engine import Config

async def test():
    cfg=Config()
    cfg.log(cfg.database)

if __name__ == '__main__':
    Config('postgresql://localhost:5432/test?user=postgres&password=postgres{"query":"select k, v, id, parent_id from system.config"}')
    asyncio.run(test())
```
**ВАЖНО!!** Конструктор Config, читающий из БД должен быть вызван **до** запуска асинхронного цикла.

В параметрах соединения можно задать (внутри фигурных скобок, одной строкой без переносов, маленькими буквами)
query - запрос для получения настроек, в запросе должны быть указаны поля в следующем порядке:
1. поле ключа
2. поле значения
3. идентификатор записи
4. идентификатор родительской записи

также возможен вариант, когда
4. тип значения
5. идентификатор родительской записи

в этом случае конфигуратор понимает следующие типы:
* I - int
* F - float
* S - str
* B - bool

Преобразование типа происходит в функции Config._cast(value, to_type) т.е. наследники Config могут обрабатывать
другие типы.

Если в запросе указано where, то это условие будет добавлено во все запросы
при построении конфигурации.

По умолчанию запрос:
select k, v, id, parent_id from config

соответственно самый простой вариант таблицы:
create table config(
    id serial not null primary key,
    parent_id integer references config(id),
    k varchar(150) not null,
    v varchar,
    constraint config_level_uk unique (parent_id, k)
)

### Специальные ключи конфигурации

нресколько имен ключей файла конфигурации зарезервированы и являются специальными:

**timers**  - определяет интервалы для таймеров системы
**logging** - позволяет задать настройки логирования
**database**- содержит строки соединения с одной или несколькими БД
**defaults**- содержит значения по умолчанию

#### Значения по умолчанию

**defaults.name** - имя, подставляемое в лог как имя сервиса
**defaults.database** - имя соединения БД по умолчанию
**defaults.plugin_path** - путь к каталогу плагинов
**defaults.db_driver_path** - путь к драйверам БД (по умолчанию ab_engine/db)

## Управление таймерами

Таймеры определяются в секции timers файла конфигурации. Для управления таймеры доступны в свойстве Config().timers

Чтобы интервалы использовались в программе, таймеры должны быть проинициализированы методом Config().timers["имя таймера"].init:
init(callback, interval_type = TimerInterval.SECOND, interval: int = 0, immediately_start=True)

> callbac       - обработчик таймера, может быть как async, так и синхронным
> 
> interval_type - тип интервала (единица времени) MILLISECOND, SECOND, MINUTE, HOUR, DAY по умолчанию SECOND
> 
> interval      - количество единиц времени, если задан, то перекроет считанное из конфига
> 
> immediately_start - означает, что calback будет запущен сразу после инициализации, иначе через интервал

например:
Config().timers["X"].init(callback, interval_type = TimerInterval.MINUTE):

callbac должен быть функцией вида:

> def callback(name, prev_time):
> 
> ...

или:

> async def callback(name, prev_time):
> 
> ...

в параметр **name** передается в имя таймера, в **prev_time** предыдущее время запуска, первый раз **prev_time = None**

Предусмотрены следующие типы интервалов работы таймера:

* TimerInterval.MILLISECOND
* TimerInterval.SECOND
* TimerInterval.MINUTE
* TimerInterval.HOUR
* TimerInterval.DAY

Для управления таймерами, также, используются:

* **start**(immediately_start=True) - запускает таймер, если он остановлен. Если immediately_start=False, то таймер ждет свой
    интервал и только потом запускает callback, иначе callback будет запущен сразу после вызова start()
* **stop**()  - останавливает таймер. Остановленный таймер можно запустить методом start()
* **started** - возвращает True если таймер запущен

### Пример работы с таймерами

Создадим файл настроек test.toml:

```toml
[timers]
A = 1
B = 15
```

и программу:

```python
import asyncio
from ab_engine import Config

def timer_test(name, prev_time):
    cfg = Config()
    cfg.log(f"timer test {name} {prev_time}")
    if name == "A":
        if cfg.timers["B"].started:
            cfg.log("STOP timer B")
            cfg.timers["B"].stop()
        else:
            cfg.log("START timer B")
            cfg.timers["B"].start()
  
async def timers():
    Config("test.toml")
    Config().timers["A"].init(timer_test, "minute", immediately_start=False)
    Config().timers["B"].init(timer_test)
    Config().timers["A"].start(immediately_start=False)
    while True:
        await asyncio.sleep(1)


if __name__ == '__main__':
    asyncio.run(timers())
```

В результате таймер A будет вызываться раз в 1 минуту, а таймер В раз в 15 секунд (по умолчанию тип интервала - секунды)
При этом, таймер А будет вызван только через минуту после старта и будет включать/выключать таймер B

## Обработка ошибок

Библиотека предлагает хранение текстов и кодов ошибок в отдельном файле yaml, который имеет следующую структуру:

```yaml
- code: ERROR # код для вызова из функции raise_error
  http: 444 # код для возврата для HTTP. можно не указывать
  msg: В {place} у объекта {name} случилось {what} # Сообщение в которое будут подставлены kwargs из raise_error
  class: Error # задает класс исключения. По умолчанию это Error из библиотеки (т.е. Error можно было нре указывать)
- code: OTHER_ERROR
  class: Exception 
  msg: Еще ошибка
```

сохраним этот yaml как errors.yaml и в файле рядом напишем код:

```python
from ab_engine import error, load_errors

load_errors("./errors.yaml")

try:
    raise error("ERROR", place="программе", name="в коде", what="страшное")
except Exception as e:
    print(type(e))
    print(e)
    print("CODE:", e.code)
    print("HTTP CODE:",e.http_code)

try:
    raise error("OTHER_ERROR")
except Exception as e:
    print(type(e))
    print(e)
```

При выполнении этого кода, сначала получим исключение с типом Error в свойствах которого можно увидеть код ошибки и код http,
а затем получим ошибку с классом Exception и сообщением, считанным из файла

При выполнении load_errors, если в систему уже загружены описания ошибок с кодами, которые есть в загруженном файле, то
описания ошибок будут заменены новыми описаниями.

Если при вызове raise_error не передать какие-либо параметры, которые есть в шаблоне сообщения об ошибке, то вместо них 
в шаблон будут вставлены пустые строки.

## Вывод сообщений в лог

Библиотека предоставляет возможность хранения настроек логирования в файле конфигурации и применение этих настроек
при создании экземпляра класса Config.
Настройки логирования задаются в секции logging. Ниже приведен пример настроек в нотации yaml c комментариями:

```yaml
logging:
  level: debug # уровень лога
  format: [%(asctime)s | %(levelname)s]: %(message)s # формат лога
  datefmt: %m.%d.%Y %H:%M:%S # формат даты в логе

  # секция handler позволяет явно указать класс и параметры логера
  handler:
    class: TimedRotatingFileHandler
    # рядом с именем класса можно указать значения параметров для передачи в конструктор
    # если параметр нужно пеедать определенным типом, например как str, как int или float, то требуемый тип нужно указать после имени
    # в скобках без пробелов, например, iunterval(int)
    filename: /home/abulatov/logs/log.log
    when: m
    interval(int): 1
    backupCount(int): 5
    # если нужно назначить какие-то свойства логгера после создания его экземпляра - используется секция property
    # в свойствах, как  и в параметрах конструктора можно указать тип
    property:
      suffix: "%Y%m%d"
```

Таким образом, библиотека позволяет использовать любой handler реализованный для стандартной библиотеки logging.
Однако уровни лога несколько отличаются от logging и задаются в перечислении ab_engine.LogLevel.
Уровни лога:

* OFF      - логирование выключено
* CRITICAL - выводятся только критические ошибки
* FATAL    - фатальные ошибки (то же, что CRITICAL)
* ERROR    - ошибки
* WARNING  - предупрежения
* WARN     - то же, что WARNING
* INFO     - информационные сообщения
* DEBUG    - отладочные сообщения
* SQL      - то же что DEBUG, но в лог добавляются тексты sql-запросов
* NOTSET   - не задан = в лог выводится всё

Для вывода в лог используется функция Config().log:

```python
    def log(self, msg, level:LogLevel | str | int=LogLevel.INFO, *args, **kwargs):
        """
        запись в лог
        :param msg: сообщение об ошибке. может быть экземпляром Exception
        :param level: уровень сообщения, для msg = Exception автомаптически вставляется ERROR. Может быть передан как строка
        :param args:  --/- зарезервировано для дальнейшего использования
        :param kwargs:-/
        """
```

## Доступ к БД

Библиотека поддерживает хранение настроек соединений и выполнение команд SQL на этих соединениях с БД.
Из коробки реализованы драйвер Postgresql, Sqlite, Mysql (но надо установить зависимость mysql-connector-python)

Можно описывать драйвера к другим БД. Для того чтобы использовать собственный драйвер, можно добавить класс драйвера
в словарь DRIVER_CLASSES, который находится в модуле ab_engine/db/option, либо положить драйвер в каталог драйверов.
Пример драйвера в DRIVER_CLASSES:

необходимости использовать собственный драйвер, можно в встроке соединения в 
фигурных скобках задать параметр **driver_path**, определяющий путь для поиска драйвра.

Если нужно задать свой путь поиска для всех драйверов БД, можэно задать параметр db_driver_path в defaults конфигурации.

Если приложению требуется только одна строка соединения, то ее можно задать в переменной database, например:

```yaml
database: postgresql://localhost:5432/postgres?user=postgres&password=postgres
```

Также можно задать несколько соединений, если использовать database как секцию:

```toml
[database]
test = "postgresql://localhost:5432/test?user=postgres&password=postgres"
mydb = "postgresql://localhost:5432/mydb?user=postgres&password=postgres"

[defaults]
database = "test"
```

При этом, в секции defaults можно задать имя соединения БД, которое будет использоваться по умолчанию (см пример выше)

Строка соединения с БД имеет следующую структуру:
<имя_драйвера>**://**<адрес сервера>**:**<номер порта>**/**<имя БД>**?**<параметры соединения>**{**дополнительные параметры**}**

* **параметры соединения**  - могут зависеть от СУБД и параметров соединения, поддерживаемых драйвером. 
                          для большинства БД нужны такие параметры как **user** и **password**.
                          помимо параметров, определяемых драйвером, можно передать параметр driver_path, который 
                          определяет путь поиска драйвера

* **дополнительные параметры**  - параметры, которые будут переданы в сессию соединения с БД (в фигуреых скобках). 
    передаются как **"**ключ**"="**значение**"**
    в качестве разделителя параметров используется запятая. 
    Для любого сочетания драйвер+строка соединения-доп.параметры, можно задать дополнительный параметр LIMIT, 
    который ограничит максимальное количество таких соединений. Т.е. по достижении данного числа соединений система
    не станет выполнять очередную команду до тех пор, пока не освободится одно из текущихз соединений с БД.

Драйвер БД должен иметь имя driver_<имя в строке соединения>.py и предоставлять следующий контракт:

```python
class Driver:

    @abstractmethod
    def __init__(self, connection_string, on_open_close=None):
        """
        констркуктор драйвера.
        должен разобрать строку соединения (описана выше) и инициализировать экземпляр драйвера.
        дополнительные параметры строки соединения (те что в фигурных скобках) не передаются в строку 
        соединения.
        on_open_close функция, передаваемая драйверу, описываемая как 
        async def on_open_close(self, close=False)->dict:
        данная функция возвращает драйверу дополнительные параметры, которые могут быть использованы 
        при создании и закрытии соединения (если драйвер это поддерживает) 
        """
        ...

    @property
    def in_transaction(self)->bool:
        """
        сообщает открыта ли сейчаст транзакция на соединении поддерживаемом экземпляром драйвера
        """
        ...

    @abstractmethod
    async def begin(self):
        """
        может быть вызвана методом sql для создания соединения с БД, если это соединение не открыто
        тут же могут быть использованы параметры, возвращаемые функцией on_open_close
        """
        #self._conn = None
        ...

    @abstractmethod
    async def sql(self, query, one_row=False, row_factory=RowFactory.DICT):
        """
        выполняет запрос к БД
        query - текст запроса
        one_row - переключатель метода получения запроса между feth и fethall
        row_factory - задает фабрику для получения данных драйвером.
            перечисление RowFactory допускает следующие значения:
                ANY   - значение, используемое чтобы показать, что фабрика не задана
                TUPLE - возврат значений строки в tuple
                DICT  - возвращает словарь, где ключи имена полей, а значения - значения полей
                NAMED_TUPLE - возвращает строки как namedtuple
        Для выборки возвращает результат с применением соответствующего RowFactory, либо [], 
        если one_row=False и выборка пуста
        Если one_row=True, то вернет только первую строку результата, либо Null, 
        если соответствующая запись не найдена.
        В случае, если query яляется запросом, не возвращающим выборку, вернет 
        целое число - количество записей, на которые повлиял запрос.
        """
        #if self._conn is None:
        #    await self.begin()
        ...

    @abstractmethod
    async def commit(self):
        """
        подтверждает выполнение транзакции и закрывает соединение
        """
        ...

    @abstractmethod
    async def rollback(self):
        """
        отменяет выполнение транзакции и закрывает соединение
        """
        ...
    
    @abstractmethod
    async def table_struct(self, table_name)->dict:
        """
        возвращает структуру заданной таблицы в формате:
        {
            "table": "имя таблицы",
            "description": "описание, если есть",
            "fields": [
                {
                    "name": "имя поля для использования в python",
                    "field": "имя поля для использования в запросах (например в двойных кавычках)",
                    "type": "имя типа поля из БД",
                    "python_type": "тип python для хранения поля",
                    "size": "размер поля",
                    "not_null": "True, если значение не может быть null"
                }
            ],
            "constraints": [    
                {
                    "type": "primary key" | "unique",
                    "fields": ["список полей"]
                },
                {
                    "type": "foreign key",
                    "table": "имя таблицы, на которую ссылка",
                    "fields": {
                      "поле лакальной таблицы": "поле таблицы по ссылке"
                    }
                }
            ]
        }
        """
        ...

    async def parse_query(self, query, *args, **kwargs):
        """подставляет параметры в текст запроса"""
        ...

    async def parse_func(self, func_name, *args, **kwargs):
        """
        Возвращает запрос для вызова функции func_name
        :param args: неименованные параметры
        :param kwargs: именованные параметры
        :return: запрос
        """
        ...
    
    async def cast(self, param_name, to_type):
        """
        возвращает часть запроса для приведения типа параметра с заданным именем
        """
        ...

    async def page(self, query, limit, offset):
        """
        возвращает запроса query с дописанными в него ограничением количества записей и отступом 
        от начала выборки
        """
        ...
```
Класс Driver, как и перечисление RowFactory можно найти в ab_engine/db/driver.py
Функции cast, parse_func и parse_query уже реализованы в Driver и могут быть повторно использованы.

Функция **cast** принимает на вход имя параметра и имя типа, к которому параметр нужно привести.
Синтаксис приведения параметра к типу зависит от СУБД, данная функция позволяет унифицировать преобразование типов.

Функция **parse_func** позволяет унифицировать вызов функций. Подставляет необходимый для вызова префикс и вызывает
parse_query для подстановки параметров.

Функция **parse_query** обеспечивает безопасную подстановку значений параметров в запрос. Для того, чтобы указать
в тексте запроса параметр, следует указать $<номер параметра> и тогда запрос будет взят из args с номером на единицу меньшим.
Либо указать $<имя параметра> в этом случае значение будет взято из kwargs с тем же именем. Номер всегда состоит только из цифр.
Имя начинается с буквы и заканчивается перед пробельным символом или перед скобкой, или двоеточием, или запятой. 
В примере ниже все параметры имеют имя **name**:
```sql
select $name, t.value from t;

select t.id from t where t.name = $name and t.ok;

select t.x 
from t
inner join t2 on t2.t_id=t.id and t2.name=$name
where t.date < now()::date;
```
система автоматически создает подстроку со значением параметра и, при необходимости, заключает это значение в кавычки.
|**Тип python**|**Тип БД**|**В кавычках**|
|--------|---------|-----|
| None   | NULL    | [ ] |
| str    | varchar | [x] |
| int    | bigint  | [ ] |
| float  | double precision  | [ ] |
| dict   | json    | [ ] |
| list, tuple | array*  | [x*] |
| set    | array   | [x] |
| date   | date    | [x] |
| time   | timestamp| [x] |

\* если указано преобразование в json ( $x::json ), то список будет преобразован в json, а не в массив

В системе есть несколько способов выполнения sql запросов:
* функция sql из модуля ab_engine.db
* функция sql, как функция член класса DB_ENV из модуля ab_engine.env
* sql запрос, зарегистрированный как метод RPC

### Функция sql из модуля ab_engine.db
Пример:
```python main.py
import asyncio
from ab_engine.db import sql, DB, ONE

async def main():
    x = await sql("SELECT sqlite_version()", DB("sqlite://:memory:"), ONE)
    print(x)

if __name__ == '__main__':
    asyncio.run(main())
```
Это минимальный пример, в котором для вызова sql-запроса не требуется создавать файл конфигурации.
Для передачи строки соединения с БД используется опция DB (об опциях см. ниже) 
Если же убрать строку соединения в файл конфигурации, например, в config.yaml :
```yaml config.yaml
database: postgresql://localhost:5432/postgres?user=postgres&password=postgres
```
то код на python, аналогичный примеру выше, будет выглядеть так:
```python main.py
import asyncio
from ab_engine import Config
from ab_engine.db import sql, ONE

async def main():
    Config("config.yaml")
    x = await sql("select version()", ONE)
    print(x)

if __name__ == '__main__':
    asyncio.run(main())
```
В этом примере стало необязательно указывать опцию DB, тк если она не указана, то система берет ту строку соединения,
которая указана в файле конфигурации.

Для выполнения запроса, в обоих примерах, мы использовали функцию sql из ab_engine.db, которая определяется как:
```python
async def sql(query:str, *args, **kwargs):
    ...
```
На вход эта функция принимает текст запроса, позиционные параметры, опции и именованные параметры.

Опции, это специальные параметры, которые влияют на выполнение запроса в целом.
Передавать опции можно только в конце списка args, после позиционных параметров, либо вместо них.
В библиотеке реализованы следующие опции:
* DB    - служит для передачи соединения в функцию sql. Передается как DB("строка соединения").
* TIMEOUT- устанавливает максимальное время в секундах на выполнение запроса. точность - до сотых секунды. 
        По умолчанию вызывает ошибку, при превышении времени ожидания. TIMEOUT(time_sec:float, raise_error:bool=True)
* DICT   - Указывает, что результат следует вернуть как список dict (RowFactory.DICT). Установлена по умолчанию
* TUPLE  - Указывает, что результат следует вернуть как список tuple (RowFactory.TUPLE)
* OBJECT - Указывает, что результат следует вернуть как список namedtuple (RowFactory.NAMED_TUPLE)
* ROW    - Указывает, что нужно вернуть только первую строку из выборки
* ONE    - Указывает, что нужно вернуть только значение первого поля из первой строки выборки
* JSON   - Указывает, что значение первого поля в первой строке следует привести к dict или list
* ROLLBACK - казывает, что после выполнения запроса соединение должно быть закрыто с откатом транзакции
* COMMIT - Указывает, что после выполнения запроса соединение должно быть закрыто с подтверждением транзакции
* PAGE   - Позволяет задать максимальный размер страницы и её смещение от начала выборки (limit, offset)
* ITERATOR - Если опция задана, то результатом функции sql будет итератор для работы с большими таблицами в async for
* ALL    - Специальная опция, добавлена для принудительного отключения фильтров в функциях, которые это понимают (например у экземпляра Table)
* CALLBACK - Из функции sql отдает в callback_function запрос с подставленными параметрами. Что позволяет доработать запрос или вывести в лог.
        Eсли callback_function возвращает строку, то именно эта строка станет запросом. Функция sql вызывает только первый callback из args

CALLBACK, TIMEOUT и PAGE это опции, в которых нужно вызывать конструктор. 
В опциях DB и ITERATOR можно вызвать конструктор дял передачи доп. параметров этих опций.
Другие опции можно указывать без вызова конструктора (Без круглых скобок после имени опции).

Соединение, созданное в опции DB можно использовать в нескольких запросах:
```python main.py
import asyncio
from ab_engine.db import sql, DB, ONE, ROLLBACK, COMMIT, TIMEOUT

async def main():
    db = DB("postgresql://localhost:5432/postgres?user=postgres&password=postgres")
    x = await sql("select version()", db, ONE, TIMEOUT(0.5))
    print(x)
    x = await sql("select '{1,2,3}'::bigint[]", db, ROLLBACK) # в этом месте соединение закроется с откатом
    print(x)
    await sql("create table if not exists t1(id serial primary key, v varchar)", db) # тут снова откроется соединение с теми же параметрами
    await sql("insert into t1(v) values ('test')", db, COMMIT) # тут соединение закроется с COMMIT двух последних запросов
    
if __name__ == '__main__':
    asyncio.run(main())
```

Опцию TIMEOUT можно использовать для вызова любых асинхронных функций:
```python main.py
import asyncio
from ab_engine.db import TIMEOUT

async def main():
    await TIMEOUT(2)(asyncio.sleep(5))
    print("OK!")
    
if __name__ == '__main__':
    asyncio.run(main())
```

## Окружение и контекст для работы с БД (DB_ENV)

DB_ENV и его наследники предоставляют окружение, использующую и используемую различными компонентами библиотеки.
Такое окружение предполагает обязательную настройку параметров с использованием конфигурационных файлов и переменных окружения ОС.
Класс DB_ENV находится в модуле ab_engine.env и позволяет:
* хранить переменные локального окружения
* управлять состоянием транзакции
* выполнять запросы в контексте и вне его
* генерировать курсоры для таблиц БД

DB_ENV реализует следующий контракт:
```python
class DB_ENV:

    def __init__(self, connection: str="", db_params:Optional[set]=None, **kwargs):
        """
        Окружение для работы с БД
        :param connection: строка соеддинения или экземпляр DB_ENV, на основе которого нужно создать данный
        :param db_params: список переменных, которые должны передаваться в в окружение соединения с БД
        :param kwargs: значения переменных
        """
        
    def has_item(self, key):
        """
        Возвращает True если в данном DB_ENV хранится переменная с именем key
        Переменные передаются в kwargs, либо экземпляру DB_ENV оператором [], например:
        db = DB_ENV()
        db["x"] = 1
        print(db["x"])