py.lib.aw_db_tools
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