py.lib.aw_db_tools

Yohn Y. 2024-02-27 Child:e90eb3d2fd01

0:4b0d10bfa023 Browse Files

..init

docs/db-migrator.md pyproject.toml setup.py src/aw_db_tools/__init__.py src/aw_db_tools/migrator.py src/aw_db_tools/sqlite.py tools/make_pkg.sh

     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/docs/db-migrator.md	Tue Feb 27 21:06:11 2024 +0300
     1.3 @@ -0,0 +1,83 @@
     1.4 +# db_migrator
     1.5 +
     1.6 +## Окружение
     1.7 +
     1.8 +Для обеспечения миграции должен быть развёрнут каталог с данными миграции и в БД присутствовать отношение, хранящее версию текущей схемы.
     1.9 +
    1.10 +Для организации миграции необходимо создать и поддерживать директорию определённой структуры:
    1.11 +
    1.12 +```yaml
    1.13 +- schema.sql: Файл с начальной схемой данных
    1.14 +  patch: # Каталог с изменениями
    1.15 +  - 0.sql
    1.16 +  - 1.sql
    1.17 +  # ...
    1.18 +```
    1.19 +
    1.20 +Схема таблицы с версией:
    1.21 +
    1.22 +```sql
    1.23 +CREATE TABLE app_version_info (
    1.24 +   version integer default -1
    1.25 +);
    1.26 +```
    1.27 +
    1.28 +Имя таблицы значения не имеет, так как задаётся при вызове класса миграции. Однако **схема должна** соответствовать.
    1.29 +
    1.30 +В методах принимающих `<tt class="remarkup-monospaced">db</tt>`, ему нужно передавать объект <tt class="remarkup-monospaced">connection</tt> `<tt class="remarkup-monospaced">DB API 2</tt>` стандартный для <tt class="remarkup-monospaced">Python</tt>.
    1.31 +
    1.32 +БД, за версией которой мы следим, естественно должна взаимодействовать посредством простого <tt class="remarkup-monospaced">SQL</tt>. Модуль не выполняет каких-то сложных манипуляций, а простым `<tt class="remarkup-monospaced">SELECT</tt>` запрашивает данные, и исполняет `<tt class="remarkup-monospaced">UPDATE</tt>` при обновлении данных о версии
    1.33 +
    1.34 +#### Структура файла схемы и файлов изменений
    1.35 +
    1.36 +Файл схемы должен состоять из отдельных команд на изменение схемы разделённых маркетом `<tt class="remarkup-monospaced">--</tt>`, который должен идти первым не пробельным символом. По данному маркеру происходит разбиение файла на отдельные команды. После этих символов содержание остальной строки игнорируется, а сама строка в команду не входит.
    1.37 +
    1.38 +Каждая команда исполняется отдельно и после её выполнения происходит <tt class="remarkup-monospaced">commit</tt>. Поэтому необходимо **внимательно** следить за согласованностью данных после применения команды.
    1.39 +
    1.40 +**Имена файлов изменений** должны оканчиваться на <tt class="remarkup-monospaced">.sql</tt> (регистр не важен) и первые символы до точки должны приводится к числу. Данное число будет версией, к торовой данный файл приводит схему.
    1.41 +
    1.42 +То есть имя файла рекомендуется составлять из 3-х компонентов, разделённых точками:
    1.43 +
    1.44 +```
    1.45 +00001.some_comment.sql
    1.46 +```
    1.47 +
    1.48 +**где:**
    1.49 +
    1.50 +- `<tt class="remarkup-monospaced">00001</tt>` - версия, количество нулей роли не играет
    1.51 +- `<tt class="remarkup-monospaced">some_comment</tt>` - пояснение к изменению, не является обязательным
    1.52 +- `<tt class="remarkup-monospaced">sql</tt>` - расширение файла, относящее его к <tt class="remarkup-monospaced">SQL</tt>.
    1.53 +
    1.54 +### Документация
    1.55 +
    1.56 +#### Классы
    1.57 +
    1.58 +<div class="remarkup-table-wrap" id="bkmrk-"></div><div class="remarkup-table-wrap" id="bkmrk-%D0%98%D0%BC%D1%8F-%D0%BA%D0%BB%D0%B0%D1%81%D1%81%D0%B0-%D0%9E%D0%BF%D0%B8%D1%81%D0%B0%D0%BD%D0%B8%D0%B5-"><div class="remarkup-table-wrap"><table border="1" style="border-collapse: collapse; width: 100%;"><colgroup><col style="width: 21.3818%;"></col><col style="width: 78.5935%;"></col></colgroup><tbody><tr><th class="align-center" style="background-color: rgb(236, 240, 241); vertical-align: bottom;">**Имя класса**</th><th class="align-center" style="background-color: rgb(236, 240, 241); vertical-align: bottom;">**Описание**</th></tr><tr><td>`<tt class="remarkup-monospaced">MigrateManager</tt>`</td><td>Менеджер миграции. Выполняет всю работу</td></tr><tr><td>`<tt class="remarkup-monospaced">MigrateError</tt>`</td><td>Класс собственных исключений модуля</td></tr></tbody></table>
    1.59 +
    1.60 +</div></div>#### Методы класса `<tt class="remarkup-monospaced">MigrateManager</tt>`
    1.61 +
    1.62 +##### Конструктор
    1.63 +
    1.64 +**Параметры:**
    1.65 +
    1.66 +<table border="1" id="bkmrk-control_table-str-o-" style="border-collapse: collapse; width: 100%;"><colgroup><col style="width: 14.953%;"></col><col style="width: 10.8749%;"></col><col style="width: 4.36944%;"></col><col style="width: 69.6543%;"></col></colgroup><tbody><tr><td>`<tt class="remarkup-monospaced">control_table</tt>`</td><td>`<tt class="remarkup-monospaced">str</tt>`</td><td>`<tt class="remarkup-monospaced">O</tt>`</td><td>Имя таблицы, хранящей версию (схема её дана выше)</td></tr><tr><td>`<tt class="remarkup-monospaced">migrate_env</tt>`</td><td>`<tt class="remarkup-monospaced">str(path)</tt>`</td><td>`<tt class="remarkup-monospaced">O</tt>`</td><td>Каталог с файлами миграции. Структура описана выше</td></tr></tbody></table>
    1.67 +
    1.68 +**Выполняет:** общие проверки среды, команд на БД не вызывает
    1.69 +
    1.70 +##### `<tt class="remarkup-monospaced">init_db</tt>`
    1.71 +
    1.72 +**Параметры:**
    1.73 +
    1.74 +<table border="1" id="bkmrk-db-%D0%9E%D0%B1%D1%8A%D0%B5%D0%BA%D1%82-%D0%91%D0%94-o-%D0%9E%D0%B1%D1%8A%D0%B5%D0%BA" style="border-collapse: collapse; width: 100%;"><colgroup><col style="width: 7.78547%;"></col><col style="width: 11.6164%;"></col><col style="width: 4.44811%;"></col><col style="width: 76.1253%;"></col></colgroup><tbody><tr><td>`<tt class="remarkup-monospaced">db</tt>`</td><td>`<tt class="remarkup-monospaced">Объект БД</tt>`</td><td>`<tt class="remarkup-monospaced">O</tt>`</td><td>Объект БД, должен соответствовать описанному выше интерфейсу</td></tr></tbody></table>
    1.75 +
    1.76 +**Выполняет:** Исполнение на БД команд из файла начальной схемы
    1.77 +
    1.78 +##### `<tt class="remarkup-monospaced">check</tt>`
    1.79 +
    1.80 +**Параметры:**
    1.81 +
    1.82 +<table border="1" id="bkmrk-db-%D0%9E%D0%B1%D1%8A%D0%B5%D0%BA%D1%82-%D0%91%D0%94-o-%D0%9E%D0%B1%D1%8A%D0%B5%D0%BA-1" style="border-collapse: collapse; width: 100%;"><colgroup><col style="width: 4.81957%;"></col><col style="width: 12.4815%;"></col><col style="width: 4.50132%;"></col><col style="width: 78.0493%;"></col></colgroup><tbody><tr><td>`<tt class="remarkup-monospaced"></tt>db`</td><td>`Объект БД`</td><td>`O`</td><td>Объект БД, должен соответствовать описанному выше интерфейсу</td></tr></tbody></table>
    1.83 +
    1.84 +**Выполняет:** Проверку версию схемы БД, и исполнение на БД команд из файлов версий выше текущей на БД.
    1.85 +
    1.86 +<p class="callout warning">При отсутствии таблицы модуль просто возбудит исключение, причём это исключение возбудит сама база, поскольку запрошенной таблицы в ней не окажется.</p>
    1.87 \ No newline at end of file
     2.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     2.2 +++ b/pyproject.toml	Tue Feb 27 21:06:11 2024 +0300
     2.3 @@ -0,0 +1,11 @@
     2.4 +[build-system]
     2.5 +# Minimum requirements for the build system to execute.
     2.6 +requires = ["setuptools", "wheel"]  # PEP 508 specifications.
     2.7 +
     2.8 +[project]
     2.9 +name = "aw_db_tools"
    2.10 +version = "0.202402.1"
    2.11 +requires-python = ">=3.8"
    2.12 +
    2.13 +[project.urls]
    2.14 +src = "https://devel.a0fs.ru/py.lib.aw_db_tools/"
    2.15 \ No newline at end of file
     3.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     3.2 +++ b/setup.py	Tue Feb 27 21:06:11 2024 +0300
     3.3 @@ -0,0 +1,9 @@
     3.4 +from setuptools import setup
     3.5 +
     3.6 +setup(
     3.7 +    name='aw_db_tools',
     3.8 +    version='0.202402.1',
     3.9 +    packages=['aw_db_tools'],
    3.10 +    package_dir={'aw_db_tools': 'src/aw_db_tools'},
    3.11 +    author='awgur',
    3.12 +)
     4.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     4.2 +++ b/src/aw_db_tools/__init__.py	Tue Feb 27 21:06:11 2024 +0300
     4.3 @@ -0,0 +1,6 @@
     4.4 +# coding: utf-8
     4.5 +
     4.6 +class Error(Exception):
     4.7 +    """\
     4.8 +    Общий класс ошибок
     4.9 +    """
     5.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     5.2 +++ b/src/aw_db_tools/migrator.py	Tue Feb 27 21:06:11 2024 +0300
     5.3 @@ -0,0 +1,147 @@
     5.4 +# coding: utf-8
     5.5 +"""\
     5.6 +Инструмент миграции схемы БД.
     5.7 +
     5.8 +"""
     5.9 +
    5.10 +from os.path import exists, join as p_join, isdir
    5.11 +from os import listdir
    5.12 +
    5.13 +from . import Error
    5.14 +
    5.15 +
    5.16 +class MigrateError(Error):
    5.17 +    """\
    5.18 +    Общий класс ошибок миграции
    5.19 +    """
    5.20 +
    5.21 +
    5.22 +class MigrateManager(object):
    5.23 +    """\
    5.24 +    Менеджер миграции
    5.25 +    """
    5.26 +    def __init__(self, control_table: str, migrate_env: str):
    5.27 +        """
    5.28 +        :param control_table: Имя таблицы, хранящей метаданные миграции
    5.29 +        :param migrate_env: Директория, хранящая SQL-скрипты миграции
    5.30 +        """
    5.31 +        self.control_table = control_table
    5.32 +
    5.33 +        if not exists(migrate_env):
    5.34 +            raise MigrateError('Migrate enviroment not found')
    5.35 +
    5.36 +        self.schema = p_join(migrate_env, 'schema.sql')
    5.37 +        if not exists(self.schema):
    5.38 +            raise MigrateError(f'Schema file not found: {self.schema}')
    5.39 +
    5.40 +        self.patch_dir = p_join(migrate_env, 'patch')
    5.41 +        if not isdir(self.patch_dir):
    5.42 +            raise MigrateError(f'Patch dir not found or not directory: {self.patch_dir}')
    5.43 +
    5.44 +    def get_patch_files(self, ver: int):
    5.45 +        """\
    5.46 +        Получение из директории файлов миграции списка применяемых к данному экземпляру БД
    5.47 +        """
    5.48 +        res = {}
    5.49 +        for f in listdir(self.patch_dir):
    5.50 +            if not f.lower().endswith('.sql'):
    5.51 +                continue
    5.52 +
    5.53 +            _f = f.strip().split('.')
    5.54 +
    5.55 +            try:
    5.56 +                _ver = int(_f[0])
    5.57 +
    5.58 +            except (TypeError, ValueError) as e:
    5.59 +                raise MigrateError(f'Error on parse version "{_f[0]}" of file "{f}": {e}')
    5.60 +
    5.61 +            except IndexError:
    5.62 +                raise MigrateError(f'Error on get version from filename: {f}')
    5.63 +
    5.64 +            if _ver in res:
    5.65 +                raise MigrateError(f'Version duplicates on parse file: {f}')
    5.66 +
    5.67 +            res[_ver] = p_join(self.patch_dir, f)
    5.68 +
    5.69 +        for i in sorted(res.keys()):
    5.70 +            if i > ver:
    5.71 +                yield i, res[i]
    5.72 +
    5.73 +    @staticmethod
    5.74 +    def get_commands(file: str):
    5.75 +        """\
    5.76 +        Получение из файлов серий команд, которые необходимо применять на БД
    5.77 +        """
    5.78 +        buf = []
    5.79 +        with open(file) as IN:
    5.80 +            for l in IN:
    5.81 +                if l.lstrip().startswith('--'):
    5.82 +                    if buf:
    5.83 +                        yield '\n'.join(buf)
    5.84 +                        buf[:] = []
    5.85 +
    5.86 +                else:
    5.87 +                    buf.append(l)
    5.88 +
    5.89 +        if buf:
    5.90 +            yield '\n'.join(buf)
    5.91 +
    5.92 +    def init_db(self, db):
    5.93 +        """\
    5.94 +        Инициализация БД.
    5.95 +
    5.96 +        :param db: Объект-подключения, представляющий нужную БД и при этом поддерживающий DB API Python
    5.97 +        """
    5.98 +        cursor = db.cursor()
    5.99 +        for c in self.get_commands(self.schema):
   5.100 +            cursor.execute(c)
   5.101 +            db.commit()
   5.102 +
   5.103 +        db.commit()
   5.104 +
   5.105 +    def check(self, db):
   5.106 +        """\
   5.107 +        Проверка БД на соответствие.
   5.108 +
   5.109 +        :param db: Объект-подключения, представляющий нужную БД и при этом поддерживающий DB API Python
   5.110 +        """
   5.111 +        cursor = db.cursor()
   5.112 +        cursor.execute(f"SELECT version FROM {self.control_table}")
   5.113 +        q = cursor.fetchone()
   5.114 +        del cursor
   5.115 +
   5.116 +        if q is None:
   5.117 +            ver = -1
   5.118 +        else:
   5.119 +            ver = int(q[0])
   5.120 +
   5.121 +        new_ver = ver
   5.122 +        cursor = db.cursor()
   5.123 +        for up_ver, patch_file in self.get_patch_files(ver):
   5.124 +            new_ver = up_ver
   5.125 +            for cmd in self.get_commands(patch_file):
   5.126 +                cursor.execute(cmd)
   5.127 +                db.commit()
   5.128 +
   5.129 +        cursor.execute(f"DELETE FROM {self.control_table}")
   5.130 +
   5.131 +        cursor.execute(f"""
   5.132 +            INSERT INTO {self.control_table} (version)
   5.133 +            VALUES ({new_ver})
   5.134 +        """)
   5.135 +        db.commit()
   5.136 +
   5.137 +    @staticmethod
   5.138 +    def get_conn_from_my_obj(obj: object):
   5.139 +        """\
   5.140 +        Получиение объекта соединения из обёрток, которые я сам себе пишу для работы с DB-API
   5.141 +
   5.142 +        :param obj: Получение объекта-подключения из объектов БД своего стиля оформления.
   5.143 +        :return:
   5.144 +        """
   5.145 +        if hasattr(obj, '_conn'):
   5.146 +            return obj._conn
   5.147 +        elif hasattr(obj, '_con'):
   5.148 +            return obj._con
   5.149 +        else:
   5.150 +            raise TypeError('No known connection object in given database object found')
     6.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     6.2 +++ b/src/aw_db_tools/sqlite.py	Tue Feb 27 21:06:11 2024 +0300
     6.3 @@ -0,0 +1,44 @@
     6.4 +# coding: utf-8
     6.5 +"""\
     6.6 +Обёртка вокруг стандартного модуля работы с СУБД SQLite. Создана исключительно из собственных представлений
     6.7 +о прекрасном.
     6.8 +"""
     6.9 +
    6.10 +import sqlite3
    6.11 +from sqlite3 import Error as DBError, IntegrityError
    6.12 +
    6.13 +
    6.14 +class DB:
    6.15 +    def __init__(self, db_file):
    6.16 +        self._conn = sqlite3.connect(db_file)
    6.17 +        self._ex = self._conn.execute
    6.18 +        self.commit = self._conn.commit
    6.19 +        self.rollback = self._conn.rollback
    6.20 +
    6.21 +        # DB PREP
    6.22 +        self._ex("PRAGMA journal=WAL")
    6.23 +        self._ex("PRAGMA foreign_keys=ON")
    6.24 +        self.commit()
    6.25 +
    6.26 +    def __del__(self):
    6.27 +        try:
    6.28 +            self.rollback()
    6.29 +            self._conn.close()
    6.30 +
    6.31 +        except:
    6.32 +            pass
    6.33 +
    6.34 +    def __call__(self, *a, **kwa):
    6.35 +        cur = self._conn.cursor()
    6.36 +        cur.execute(*a, **kwa)
    6.37 +        return cur
    6.38 +
    6.39 +    def cq(self, *a, **wa):
    6.40 +        try:
    6.41 +            res = self(*a, **wa)
    6.42 +            self.commit()
    6.43 +            return res
    6.44 +
    6.45 +        except DBError as e:
    6.46 +            self.rollback()
    6.47 +            raise e
     7.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     7.2 +++ b/tools/make_pkg.sh	Tue Feb 27 21:06:11 2024 +0300
     7.3 @@ -0,0 +1,11 @@
     7.4 +#!/bin/sh
     7.5 +# devel.a0fs.ru -- devel:python.tools::make_pkg.sh -- v0.r202402.2
     7.6 +this_dir="$(dirname "$(readlink -f "$0")")"
     7.7 +this_pkg="$(dirname "$this_dir")"
     7.8 +
     7.9 +cd "${this_pkg}" || exit
    7.10 +if [ -d "${this_pkg}/.e" ] ; then
    7.11 +  source ${this_pkg}/.e/bin/activate
    7.12 +fi
    7.13 +
    7.14 +python3 setup.py bdist_wheel
    7.15 \ No newline at end of file