Приветствую!

Я хочу вам рассказать о моей разработке — JSON БД на Python

Не все проекты нуждаются в медленных и сложных SQL базах данных, например ТГ‑Боты и парсеры. Конечно не буду спорить, что SQL — вероятно лучшее решение для бизнеса, но иногда они могут быть избыточными для небольших проектов. NoSQL базы данных — это отличный выбор для таких случаев.

Если вы уже знакомы с MongoDB или Redis, то принцип работы моей библиотеки вам будет понятен.

Ну что, начнём?

Основы

from jsoner import Database

# инициализация объекта БД
db = Database('db.json')

 # добавление данных
db.add('key', 'value')

# получение
db.get('key') # 'value'

# запись изменений в файл
db.commit()

# изменение значения
db.update('key', 100)

# увеличение значения на 5
db.incr(key, 5)

# уменьшение значения на 15
db.decr(key, 15)

# удаление
db.delete('key')

Теги

Теги создаются путем добавления словаря из пар Тег: значение

from jsoner import Database
from jsoner.tags import const_tag
from jsoner.errors import ValueIsConstant

db = Database('db.json', autocommit=True)

# тег неизменяемого значения
db.add('pi', 3.14, {const_tag: True})

try:
    db.update('pi', 4)
except ValueIsConstant:
     print('Ключ "pi" - константа')

>>> 'Ключ "pi" - константа'

Теги и их значения записываются в словарь tags

db.json

{
    "__settings__": {
        "__version__": "0.1", 
        "default": null,
        "tags": {
            "pi": {
                "const": true
            }
        }, 
        "global_tags": {}
    },
    "pi": 3.14
}

Создание своих тегов

С помощью класса NewTag можно создавать свои теги

from jsoner import Database
from jsoner.tags import NewTag

db = Database('db.json')

class MyTag(NewTag):

    # создании тега, метод должен вернуть преобразованный аргумент тега
    def create(db: Database, value, tag_arg):
        return tag_arg

    # изменение значения
    def update(db: Database, key: str, old_value, new_value, tag_arg):
        return new_value

    # чтение значения
    def read(db: Database, key: str, value, tag_arg):
        return value

Пример создания своего тега

from jsoner import Database
from jsoner.tags import NewTag
import time

class ttl_tag(NewTag):
    'Time to life - время жизни ключа'

    def create(db: Database, value, tag_arg) -> str:

        # tag_arg в данном случае - время действия ключа
        # в аргументе тега сохранится время, до которого действителен ключ
        
        return tag_arg + time.time()

    def read(db: Database, key, value, tag_arg):

        # если текущее время больше аргумента тега, ключ следует удалить, и вернуть значение по умолчанию
        
        if time.time() <= tag_arg:
            return value
        else:
            db.delete(key)
            return db.data[db.settings]['default'] # тут хранится значение по умолчанию

Установка значения по умолчанию

from jsoner import Database

db = Database('data.json')

# установка значения по умолчанию
db.set_default(0)

print(db.get('Unknown key'))
>>> 0

Добавление глобальных тегов

Глобальные теги действительны и одинаковы для всех ключей

from jsoner import Database
from jsoner.tags import const_tag
from jsoner.errors import ValueIsConstant

db = Database('data.json')

# добавление глобального тега
db.set_global_tag(const_tag, True)

db.add('num', 123)
db.update('num', 0)
>>> raise ValueIsConstant

Работа с оператором with

При входе в оператор with вызывается метод db.discard(), который стирает несохраненные в файл данные

Если атрибут autocommit у db равен True, то он заменится на False, а при выходе из with вернется обратно

При выходе из with вызывается метод db.commit()

from jsoner import Database

db = Database('data.json')

db.add('num', 0)

with db:
    db.add('key', 'value')

print(db.items())
>>> [('key', 'value')]

Другие полезные методы

  • discard - стереть несохраненные в файле данные

  • get_many - получить несколько значений по ключам

  • set - автоматически либо добавляет, либо изменяет данные. Т. к. при добавлении существующего ключа или обновлении несуществующего вызовется исключение

  • keys, values, items - схожи с методами из обычных словарей

  • db['key'] = 'value' - установить значение

  • db['key'] - прочитать значение

Полное описание и код можете увидеть на моём гитхабе

Послесловие

Приму конструктивную критику в свой адрес, но дяденьки-программисты, хочу вам сказать, что мне 15 годиков, так что не судите строго

Комментарии (14)


  1. TerraV
    06.08.2024 09:24
    +4

    Больше велосипедов богу велосипедов


    1. Andrey_Solomatin
      06.08.2024 09:24
      +1

      Не на pypi и то хорошо.

      Нормальный учебный проект, автор научится чему-то новому.


  1. fire64
    06.08.2024 09:24
    +4

    А чем sqlite3 не угодил, если хотите использовать файловое решение и встроенную BD?


  1. Andrey_Solomatin
    06.08.2024 09:24
    +3

    В статье нет сравнения с аналогами. А их много. Например diskcache.
    SQL тоже может быть лёгким, например sqlite.

    Не плохо бы замутить какое-нибудь сравнение по производительности.


    1. remzalp
      06.08.2024 09:24
      +2

      Случай, когда комментарий делает статью в разы лучше :)
      https://pypi.org/project/diskcache/ - раньше не встречал, но и не искал
      sqlite3 хватает


      1. Andrey_Solomatin
        06.08.2024 09:24

        У меня скриптик скачивает джейсоны и потом их процессит, diskcashe для этого удобнее, так как не надо менять формат данных. А sqlite там внутри работает.


  1. little-brother
    06.08.2024 09:24
    +6

    Одна из проблем текстовых форматов хранения, таких как json, XML или csv в том, что при изменении хотя бы одного символа в середине вам требуется переписывать весь файл на диске, который и так являются узким местом. Базы данных, как и файловые системы, работают с блоками данных и при сохранении изменения вносятся только в них.

    в медленных и сложных SQL базах данных

    Так что медленным будет именно ваше решение. А сложность - это понятие относительное. Реализовать json-базу данных можно и на SQLite. Все необходимое есть уже из коробки: не только функции для работы с данными по пути, но и даже индексация и бинарное хранение для экономии места/времени обработки. Но даже у SQLite есть свои ограничения, напр. плохо подходит, когда в базу пишут несколько потоков или невозможность работать с ней по сети.

    Использовать Mongo или Posgres для чат-ботов это конечно из пушки по воробьям, но с другой стороны если у вас много ботов, возможно расположенных на разных машинах/контейнерах Docker, то разумно писать всё в одну базу, чтобы иметь возможность собирать по ним статистику, к примеру.

    И просто стоит упомянуть, что базы данных - это не только таблицы, но и дополнительные плюшки, такие как консистетность данных, триггеры, индексы, иногда хранимые процедуры, планировщики запросов и различные утилиты. У вашего решения ничего этого нет.


    1. dima-doroshenko Автор
      06.08.2024 09:24

      Консистентность данных и триггеры можно реализовать с помощью глобальных тегов. И зачем нужны хранимые процедуры когда база данных ключ-значение?


      1. little-brother
        06.08.2024 09:24

        Реализовать можно практически что угодно. Другое дело, что обычно хочется чтобы это уже было. Так некоторые отказываются от SQLite, поскольку хранимых в ней нет. С другой стороны, многие проекты используют далеко даже не половину возможностей предоставляемых базой данных, особенно те, которые заявляют поддержку сразу нескольких СУБД.

        Лично я бы базу данных реализовать не стал, т. к. проще взять уже готовую СУБД, коих сейчас на любой вкус и цвет, а для маленьких проектов достаточно использовать json напрямую, т. е. загрузить данные в память, обработать их и записать результат в тот же или другой json.

        Для JavaScript было что-то подобное вашей базе - NeDB. Проект весьма ожидаемо был свернут, ибо конкурировать за разработчиков на файловых базах данных с SQLite очень сложно: вы должны не просто быть сопоставимы по функционалу, а предложить что-то такое, что SQLite не может, и тогда может быть появится шанс.


  1. Derfirm
    06.08.2024 09:24

    Спасибо за интересный взгляд на работу с жсон/хранилищем для него. Радость и боль жсона в его структуре и ограниченность поддерживаемых типов данных. С одной стороны можно скормить что угодно из любых источников, но как только возникает необходимость отмодифицировать какой-то кусок данных, тотчас находится тысяча и один способ сделать это неэффективно. Начиная от переписывания файла с жсоном при изменении одного значения до втягивания монго с ее пусть редкими но всплывающими ограничениями (речь про in-place update). Например, нужно всегда валидировать тип и диапазон для чисел при вставке/обновлении, всегда таскать жсонсхему в довесок ?

    Если жсон как контракт или нечто фиксированное для обмена, то с этим можно жить, а если хочется большего?


  1. CrazyElf
    06.08.2024 09:24

    А у меня другая претензия. Утвердительное предложение, само по себе не несущее негативной коннотации - плохое название для исключения. Ну значение константа - и что? CanNotChangeConstant было бы гораздо понятнее. А просто "значение - константа" - и что? Сразу непонятно что )


    1. Andrey_Solomatin
      06.08.2024 09:24

      Мои настройки линтеров будет ругаться на такое имя. https://docs.astral.sh/ruff/rules/error-suffix-on-exception-name/

      И я стараюсь избегать Not в именах.

      Может быть: ConstantChangeError


      1. CrazyElf
        06.08.2024 09:24
        +1

        Да мне самому моё не очень нравится, но не придумал лучше. Ваше предложение хорошее, одобряю :)


  1. Dominux
    06.08.2024 09:24
    +3

    медленных и сложных SQL базах данных

    NoSQL базы данных — это отличный выбор для таких случаев.

    Если вы уже знакомы с MongoDB

    Сильное заявление, настолько сильное, что я даже не знаю, где новички берут такие неверные данные. Самое глупое - потом писать об этом всему миру в своих статьях, к сожалению, продвигая ложную информацию далее

    Очень много наплодилось новичков в индустрии, которые не так поняли, зачем нужна та же монга и другие NoSQL, и юзают ее без разбора, а потом мучаются с тем, что нет никакой схемы данных. Данные вставить без наличия ограничений - изи, а как потом строить запросы - уже попаболь, о которых маслята, как им и принято, не задумываются заранее!

    Если конкретно по монге, то она все же хороша для хранения больших вложенностей разнообразной структуры и глубины и в простоте организации кластера распределенных нод, где не нужно делать шардирование/партиционирование более сложными путями. Ее популяризация в стеках MEAN, MERN, MEVN и тд несколько лет назад, очевидно, и привела к тому, что каждый вкатун так и наравит использовать именно ее из-за простоты использования по сравнению со стандартными РСУБД