Initial Commit
This commit is contained in:
commit
4dbdc7d793
18 changed files with 2384 additions and 0 deletions
321
docs/README.ru.md
Normal file
321
docs/README.ru.md
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
# PyQt6 Scaffold
|
||||
Обёртка для PyQt6, делающая работу удобнее
|
||||
|
||||
[English documentation](../README.md)
|
||||
|
||||
## Лицензия
|
||||
|
||||
Этот проект лицензирован под лицензией LGPLv3.
|
||||
Для подробностей ознакомьтесь с файлом LICENSE.
|
||||
|
||||
# Описание
|
||||
Эта библиотека предоставляет набор готовых паттернов и инструментов, которые можно сразу использовать для разработки и избежать ошибок в сложных местах (вроде базы данных или навигации между окнами)
|
||||
|
||||
Библиотека делится на 2 больших модуля:
|
||||
- Core
|
||||
- Contrib
|
||||
|
||||
## Установка
|
||||
|
||||
Базовая установка (только ядро):
|
||||
```bash
|
||||
pip install pyqt6-scaffold
|
||||
```
|
||||
|
||||
С поддержкой MySQL:
|
||||
```bash
|
||||
pip install pyqt6-scaffold[mysql]
|
||||
```
|
||||
|
||||
С поддержкой PostgreSQL:
|
||||
```bash
|
||||
pip install pyqt6-scaffold[postgres]
|
||||
```
|
||||
|
||||
Все драйверы сразу:
|
||||
```bash
|
||||
pip install pyqt6-scaffold[all]
|
||||
```
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
### 1. Настройте переменные окружения
|
||||
|
||||
Создайте файл `start.sh` (Linux/macOS) или `start.cmd` (Windows):
|
||||
```bash
|
||||
# start.sh
|
||||
export MYSQL_HOST=localhost
|
||||
export MYSQL_PORT=3306
|
||||
export MYSQL_USER=root
|
||||
export MYSQL_DATABASE=mydb
|
||||
export MYSQL_PASSWORD=secret
|
||||
python main.py
|
||||
```
|
||||
```cmd
|
||||
:: start.cmd
|
||||
set MYSQL_HOST=localhost
|
||||
set MYSQL_PORT=3306
|
||||
set MYSQL_USER=root
|
||||
set MYSQL_DATABASE=mydb
|
||||
set MYSQL_PASSWORD=secret
|
||||
python main.py
|
||||
```
|
||||
|
||||
### 2. Реализуйте базу данных
|
||||
```python
|
||||
from pyqt6_scaffold import BaseUser
|
||||
from pyqt6_scaffold.contrib import MysqlDatabase
|
||||
|
||||
class AppDatabase(MysqlDatabase):
|
||||
def auth(self, login: str, password: str):
|
||||
with self.execute(
|
||||
f"SELECT id, name FROM users WHERE login = {self.placeholder} AND password = {self.placeholder}",
|
||||
(login, password)
|
||||
) as cursor:
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
return None
|
||||
return BaseUser(id=row[0], name=row[1], role=None)
|
||||
```
|
||||
|
||||
### 3. Создайте окна
|
||||
```python
|
||||
from pyqt6_scaffold import BaseWindow
|
||||
from PyQt6.QtWidgets import QLabel, QLineEdit, QPushButton, QVBoxLayout, QWidget
|
||||
|
||||
class LoginWindow(BaseWindow):
|
||||
def _define_widgets(self):
|
||||
self._login_input = QLineEdit()
|
||||
self._password_input = QLineEdit()
|
||||
self._pas3. Create Windowssword_input.setEchoMode(QLineEdit.EchoMode.Password)
|
||||
self._submit_btn = QPushButton("Войти")
|
||||
|
||||
def _tune_layouts(self):
|
||||
layout = QVBoxLayout()
|
||||
layout.addWidget(QLabel("Логин:"))
|
||||
layout.addWidget(self._login_input)
|
||||
layout.addWidget(QLabel("Пароль:"))
|
||||
layout.addWidget(self._password_input)
|
||||
layout.addWidget(self._submit_btn)
|
||||
container = QWidget()
|
||||
container.setLayout(layout)
|
||||
self.setCentralWidget(container)
|
||||
|
||||
def _connect_slots(self):
|
||||
self._submit_btn.clicked.connect(self._on_submit)
|
||||
|
||||
def _apply_windows_settings(self):
|
||||
self.setWindowTitle("Вход")
|
||||
|
||||
def _on_submit(self):
|
||||
user = self._db.auth(
|
||||
self._login_input.text(),
|
||||
self._password_input.text()
|
||||
)
|
||||
if user is None:
|
||||
return
|
||||
from pyqt6_scaffold import NavigateRequest, NavigationContext
|
||||
self._composer.navigate_request.emit(
|
||||
NavigateRequest(
|
||||
target="main",
|
||||
context=NavigationContext(data={"user": user})
|
||||
)
|
||||
)
|
||||
|
||||
class MainWindow(BaseWindow):
|
||||
def _apply_windows_settings(self):
|
||||
self.setWindowTitle("Главное окно")
|
||||
```
|
||||
|
||||
### 4. Соберите приложение в main.py
|
||||
```python
|
||||
import sys
|
||||
from PyQt6.QtWidgets import QApplication
|
||||
from pyqt6_scaffold import Composer
|
||||
|
||||
def main():
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
db = AppDatabase()
|
||||
db.connect()
|
||||
|
||||
composer = Composer(app=app, db=db)
|
||||
composer.register("login", LoginWindow)
|
||||
composer.register("main", MainWindow)
|
||||
|
||||
sys.exit(composer.run(start="login"))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
## Модуль Сore
|
||||
Модуль, предоставляющий основные инструменты для работы, как правило это абстрактные классы, реализующие удобный шаблон для разработки. Например, класс BaseWindow реализующий минимально приемлемый (по мнению автора) шаблон для окна приложения:
|
||||
|
||||
```python
|
||||
from PyQt6.QtWidgets import QMainWindow
|
||||
|
||||
class BaseWindow(QMainWindow):
|
||||
def __init__(self, composer, db):
|
||||
super().__init__()
|
||||
self._db = db
|
||||
self._composer = composer
|
||||
|
||||
self._define_widgets()
|
||||
self._tune_layouts()
|
||||
self._connect_slots()
|
||||
self._apply_windows_settings()
|
||||
|
||||
def _define_widgets(self): pass
|
||||
def _tune_layouts(self): pass
|
||||
def _connect_slots(self): pass
|
||||
def _apply_windows_settings(self): pass
|
||||
```
|
||||
|
||||
Главные составляюшие модуля Core это классы навигации и взаимодействия с базой данных.
|
||||
|
||||
### База Данных
|
||||
За управление базой данных в библиотеке отвечает класс ```AbstractDatabase```.
|
||||
Этот класс выглядит примерно так:
|
||||
|
||||

|
||||
|
||||
Это абстрактный класс который необходимо переопределить в классе наследнике. Разберём его подробнее:
|
||||
- Конструктор класса:
|
||||
- Содержит всего один аргумент: **strict**, он влияет на то, как реагировать на открытие пользователем нового соединения, так как по умолчанию предполагается что пользователь будет использовать только одно соедниение для взаимодействия с базой данных. Если **strict** = True, то в случае если класс уже хранит ссылку на существующее соедниение (_conn != None), то выполнение кода завершится с ошибкой. Если **strict** = False, то старое соединение закроется и класс создаст новое соедниение с базой данных.
|
||||
- *connect()* это контекстно независимый метод для создания соединения с базой данных. Внутри себя он вызывает приватный метод *_connect()*, который является контекстно зависимым от диалекта SQL и должен быть переопределён пользователем в классе наследнике
|
||||
- В методе *_connect()* пользователь реализует специфичную для используемого им диалекта SQL логику подключения к базе данных. Метод должен возвращать объект содениения, как правило он выглядит как: ```your_sql_dialect_connection.connect(**DB_CONFIG)```
|
||||
- *disconnect()* это контекстно независимый метод для ручного закрытия соединения с базой данных
|
||||
- *connection* это геттер для поля _conn, возвращает текущее соедниение с базой данных.
|
||||
- *execute()* это контекстно независимый метод для выполнения кода базы данных, состоит из открытия курсора соединения и выполнения запроса пользователя через курсор, состоит из нескольких компонентов:
|
||||
- *CursorContext* - контекстно независимый класс-обёртка над объектом курсора, необходим для совместимости и использовании курсора в контекстном менеджере, проксирует все методы курсора делая его программно эквивалентом объекта курсора
|
||||
- *placeholder* - контекстно зависимый геттер для placeholder'а специфичного диалекта SQL для подстановки значений, должен быть переопределён пользователем.
|
||||
- Аргументы метода: execute() принимает всего 2 аргумента: autocommit(bool) и params(tuple). Первый регулирует поведение базы данных при изменении данных - применять ли их сразу или просить пользтвателя сделать это вручную. Второй аргумент передаёт значения для подстановки.
|
||||
- В случае если выполнение запроса завершится с ошибкой, метод автоматически откатит изменения в базе данных (если они были)
|
||||
|
||||
#### Модели базы данных
|
||||
Как известно, взаимодействия между базой данных и фронтендом в PyQt6 реализовано через систему моделей (model) и представлений (view), в данной библиотеке заного определены модели для таблиц, списков и карточек, реализующие совместимый интерфейс для логики организации управления базами данных данной библиотеки, описанной выше.
|
||||
|
||||
Логика моделей в библиотеке представлена 3-мя классами модели и одним вспомогательным классом миксином:
|
||||
- *DataMixin*
|
||||
Миксин для моделей Qt (QAbstractTableModel / QAbstractListModel). Содержит общую логику хранения данных, обновления модели и стилизации строк. Миксин DataMixin используется совместно с Qt-моделями и не предназначен для самостоятельного использования. Он упрощает реализацию общих функций модели, чтобы конкретные классы сосредоточились на бизнес-логике.
|
||||
Предоставляет методы для:
|
||||
- хранения данных (_data)
|
||||
- перезагрузки модели (refresh)
|
||||
- доступа к строкам (row_data)
|
||||
- стилизации (row_background, row_foreground, row_font)
|
||||
- *BaseTableModel*
|
||||
Табличная модель, основанная на списке кортежей (row tuples). Поддерживает отображение данных в таблице с заголовками колонок, доступ к сырым данным и стилизацию ячеек через методы миксина. Предназначена для сценариев, где данные естественно представимы в виде таблицы (например, списки объектов с фиксированными полями).
|
||||
- *BaseListModel*
|
||||
Листовая модель, в которой каждая запись представлена как элемент списка. Отображение текста выполняется через метод row_display(). Подходит для простых списков, комбобоксов и других представлений, где не требуется табличная структура.
|
||||
- *BaseCardModel*
|
||||
Модель для карточных или кастомно отрисованных представлений.
|
||||
Не реализует DisplayRole — визуализация делегируется QStyledItemDelegate. Позволяет строить сложные UI-компоненты (карточки, плитки, кастомные списки), сохраняя доступ к данным через UserRole.
|
||||
|
||||
### Composer
|
||||
Класс-композитор — это маршрутизатор приложения и менеджер жизненного цикла окон. Он отвечает за регистрацию окон, навигацию между ними и хранение активного соединения с базой данных. Composer выступает центральным хабом приложения, через который происходит переключение представлений и передача контекста.
|
||||
|
||||

|
||||
|
||||
Разберём его устройство:
|
||||
- Конструктор класса:
|
||||
- Содержит 2 аргумента: **app** и **db**. **app** это ссылка на объект цикла событий PyQt6, **db** это ссылка на объект базы данных.
|
||||
- Внутри себя инициализирует 5 полей:
|
||||
- *_db* - хранит ссылку на объект базы данных
|
||||
- *_current* - хранит ссылку на активное окно
|
||||
- *_app* - хранит ссылку на объект цикла событий PyQt6
|
||||
- *_registry* - хранит ссылку регистр композитора, который в свою очередь хранит базу данных формата "ключ-значение" (словарь/dict python), содержащую сопоставление строкового названия окна и ссылку на класс окна.
|
||||
- Шаблон записи в регистре композитора определён в методе *register()* и выглядит так:
|
||||
```
|
||||
{
|
||||
"class": window,
|
||||
"instance": None,
|
||||
"lazy": lazy
|
||||
}
|
||||
```
|
||||
- Поле **class** это ссылка на класс окна
|
||||
- Поле **instance** это ссылка на объект окна (если он создан)
|
||||
- Поле **lazy** это настройка создания объекта окна. Если lazy = True, тогда объект окна создаётся в момент запроса на переключение на это окно. Если lazy = False, тогда объект окна создаётся сразу при регистрации окна в регистре композитора. Первый вариант больше подходит если содержимое окна контекстно зависимо.
|
||||
- *_current_ctx* - хранит контекст активного окна, представляет собой базу данных формата "ключ-значение" (словарь/dict python). Контекст уникален для каждого окна и перезаписывается при переключении окон. Может содержать любые записи "ключ-значение", информация из контекста используется для нужд окна (например, если пользователь использует RBAC как систему аутентификации, логично хранить объект с правами пользователя в контексте окна). Контекст задаётся во время вызова слота *navigate()*. Предполагается что стартовое окно контекст содержать не должно
|
||||
- *register()* - метод создающий новую запись в регистре композитора. Если класс передающийся в аргумент window не является наследником BaseWindow, произойдёт исключение Python.
|
||||
- *navigate_request* - единственный сигнал класса композитора, эмит сигнала должен происходить в случае если пользователю требуется переключить окно, передаёт ссылку на датакласс NavigationRequest.
|
||||
- *navigate()* - метод управляющий переключением окон. Является слотом PyQt6 и напрямую не должен вызываться, так как вызывается автоматически при эмитте сигнала *navigate_request*.
|
||||
- *NavigationRequest* это датакласс, который состоит из полей *target: str* и *context: NavigationContext*, и передаёт контекст запроса на переключение окон, состоящий из строкового названия окна и ссылки на класс окна (и возможно объекта класса окна). В свою очередь, датакласс *NavigationContext* состоит из одного поля *data*, которое хранит контекст окна.
|
||||
|
||||
Переключение между окнами осуществляется через программный интерфейс класса композитора, ссылку на объект которого по умолчанию додлжны получить все окна (Шаблон окна выше).
|
||||
|
||||
## Модуль Contrib
|
||||
|
||||
Модуль contrib содержит готовые реализации для типовых случаев.
|
||||
В отличие от core, contrib контекстно зависим и предполагает
|
||||
конкретные решения которые студент может использовать как есть
|
||||
или взять за основу.
|
||||
|
||||
### Backends
|
||||
|
||||
Готовые реализации AbstractDatabase для трёх диалектов SQL:
|
||||
```python
|
||||
from pyqt6_scaffold.contrib import PostgresqlDatabase, MysqlDatabase, SqliteDatabase
|
||||
```
|
||||
|
||||
Каждый backend читает конфигурацию из переменных окружения:
|
||||
|
||||
| Класс | Переменные окружения |
|
||||
|--------------------|--------------------------------------------------------------------|
|
||||
| PostgresqlDatabase | PG_HOST, PG_PORT, PG_USER, PG_DATABASE, PG_PASSWORD |
|
||||
| MysqlDatabase | MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_DATABASE, MYSQL_PASSWORD |
|
||||
| SqliteDatabase | SQLITE_PATH |
|
||||
|
||||
### Auth
|
||||
Система авторизации на основе RBAC (Role-Based Access Control):
|
||||
|
||||
```python
|
||||
from pyqt6_scaffold.contrib.auth import RBACMixin, Role, RoleLevel
|
||||
```
|
||||
|
||||
**Role** и **RoleLevel** — датакласс и enum для описания ролей пользователя:
|
||||
```python
|
||||
from pyqt6_scaffold.contrib.auth import Role, RoleLevel
|
||||
from pyqt6_scaffold import BaseUser
|
||||
|
||||
role = Role(name="Менеджер", level=RoleLevel.MANAGER.value)
|
||||
user = BaseUser(id=1, name="Иван", role=role)
|
||||
```
|
||||
|
||||
**RBACMixin** — миксин добавляющий метод `can()` в вашу базу данных.
|
||||
Предполагает наличие таблицы `permission_map` в базе данных:
|
||||
|
||||
```sql
|
||||
CREATE TABLE permission_map (
|
||||
perm VARCHAR(50),
|
||||
min_level INT
|
||||
);
|
||||
|
||||
INSERT INTO permission_map VALUES ('edit.products', 50);
|
||||
INSERT INTO permission_map VALUES ('delete.products', 100);
|
||||
INSERT INTO permission_map VALUES ('view.products', 0);
|
||||
...
|
||||
```
|
||||
|
||||
Использование:
|
||||
```python
|
||||
from pyqt6_scaffold.contrib import MysqlDatabase
|
||||
from pyqt6_scaffold.contrib.auth import RBACMixin
|
||||
|
||||
class AppDatabase(RBACMixin, MysqlDatabase):
|
||||
def auth(self, login, password): ...
|
||||
|
||||
db = AppDatabase()
|
||||
db.connect()
|
||||
|
||||
db.can(user, "edit.products") # True если user.role.level >= 50
|
||||
db.can(user, "delete.products") # False если user.role.level < 100
|
||||
```
|
||||
|
||||
Названия таблицы и полей можно переопределить:
|
||||
```python
|
||||
class AppDatabase(RBACMixin, MysqlDatabase):
|
||||
permission_table = "rights"
|
||||
permission_column = "action"
|
||||
level_column = "required_level"
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue