fastqt6 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastqt6-0.1.0/LICENSE +21 -0
- fastqt6-0.1.0/PKG-INFO +179 -0
- fastqt6-0.1.0/README.md +155 -0
- fastqt6-0.1.0/pyproject.toml +42 -0
- fastqt6-0.1.0/setup.cfg +4 -0
- fastqt6-0.1.0/src/fastqt6/__init__.py +22 -0
- fastqt6-0.1.0/src/fastqt6/cli.py +71 -0
- fastqt6-0.1.0/src/fastqt6/db.py +191 -0
- fastqt6-0.1.0/src/fastqt6/designer.py +204 -0
- fastqt6-0.1.0/src/fastqt6/fields.py +98 -0
- fastqt6-0.1.0/src/fastqt6/forms.py +187 -0
- fastqt6-0.1.0/src/fastqt6/scaffold.py +50 -0
- fastqt6-0.1.0/src/fastqt6/widgets.py +156 -0
- fastqt6-0.1.0/src/fastqt6.egg-info/PKG-INFO +179 -0
- fastqt6-0.1.0/src/fastqt6.egg-info/SOURCES.txt +17 -0
- fastqt6-0.1.0/src/fastqt6.egg-info/dependency_links.txt +1 -0
- fastqt6-0.1.0/src/fastqt6.egg-info/entry_points.txt +2 -0
- fastqt6-0.1.0/src/fastqt6.egg-info/requires.txt +2 -0
- fastqt6-0.1.0/src/fastqt6.egg-info/top_level.txt +1 -0
fastqt6-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Leevandr
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
fastqt6-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastqt6
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Convenient PyQt6 templates for dynamic forms, CRUD windows, Designer .ui files and SQL helpers
|
|
5
|
+
Author: Leevandr
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Leevandr/fastQT6
|
|
8
|
+
Project-URL: Repository, https://github.com/Leevandr/fastQT6
|
|
9
|
+
Keywords: pyqt6,qt,designer,mysql,crud,forms
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Database
|
|
17
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: PyQt6>=6.6
|
|
22
|
+
Requires-Dist: PyMySQL>=1.1
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# fastqt6
|
|
26
|
+
|
|
27
|
+
`fastqt6` - это небольшая библиотека-шаблон для PyQt6-проектов: динамические формы,
|
|
28
|
+
CRUD-окна, SQL-хелперы и генерация `.ui` файлов для Qt Designer.
|
|
29
|
+
|
|
30
|
+
Установка после публикации:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install fastqt6
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Локальная установка из репозитория:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
python -m pip install -e .
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Быстрый пример
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from PyQt6.QtWidgets import QApplication
|
|
46
|
+
from fastqt6 import SQLDatabase, field
|
|
47
|
+
from fastqt6.widgets import CrudWindow
|
|
48
|
+
|
|
49
|
+
SCHEMA = """
|
|
50
|
+
create table if not exists products (
|
|
51
|
+
id integer primary key autoincrement,
|
|
52
|
+
article text not null,
|
|
53
|
+
title text not null,
|
|
54
|
+
price real not null default 0,
|
|
55
|
+
stock integer not null default 0
|
|
56
|
+
);
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
app = QApplication([])
|
|
60
|
+
db = SQLDatabase.sqlite("app.db")
|
|
61
|
+
db.run_script(SCHEMA)
|
|
62
|
+
|
|
63
|
+
window = CrudWindow(
|
|
64
|
+
db,
|
|
65
|
+
table="products",
|
|
66
|
+
fields=[
|
|
67
|
+
field("article", "Артикул", required=True),
|
|
68
|
+
field("title", "Название", required=True),
|
|
69
|
+
field("price", "Цена", "float", min_value=0),
|
|
70
|
+
field("stock", "Остаток", "int", min_value=0),
|
|
71
|
+
],
|
|
72
|
+
)
|
|
73
|
+
window.show()
|
|
74
|
+
app.exec()
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## SQL-хелперы
|
|
78
|
+
|
|
79
|
+
SQLite:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from fastqt6 import SQLDatabase
|
|
83
|
+
|
|
84
|
+
db = SQLDatabase.sqlite("app.db")
|
|
85
|
+
db.insert("products", {"article": "A-1", "title": "Мяч", "price": 1000})
|
|
86
|
+
rows = db.select("products", where="price > ?", params=(500,), order_by="title")
|
|
87
|
+
db.update("products", {"price": 1200}, "id=?", (1,))
|
|
88
|
+
db.delete("products", "id=?", (1,))
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
MySQL:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from fastqt6 import SQLDatabase
|
|
95
|
+
|
|
96
|
+
db = SQLDatabase.mysql("sportplus_kvalik", user="root", password="")
|
|
97
|
+
user = db.login("users", "admin", "admin")
|
|
98
|
+
rows = db.fetch_all("select * from products where title like ?", ("%мяч%",))
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
В запросах можно писать `?` как универсальный placeholder. Для MySQL библиотека
|
|
102
|
+
сама заменит его на `%s`.
|
|
103
|
+
|
|
104
|
+
## Динамические формы
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from fastqt6 import field
|
|
108
|
+
from fastqt6.forms import DynamicFormDialog
|
|
109
|
+
|
|
110
|
+
fields = [
|
|
111
|
+
field("article", "Артикул", required=True),
|
|
112
|
+
field("title", "Название", required=True),
|
|
113
|
+
field("price", "Цена", "float", min_value=0),
|
|
114
|
+
field("category_id", "Категория", "combo", choices=[("Мячи", 1), ("Обувь", 2)]),
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
dialog = DynamicFormDialog(fields, title="Товар")
|
|
118
|
+
if dialog.exec():
|
|
119
|
+
data = dialog.get_data()
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Генерация файлов Qt Designer
|
|
123
|
+
|
|
124
|
+
Создать `auth.ui`:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
fastqt6 ui-auth ui/auth.ui
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Создать главное окно с вкладками:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
fastqt6 ui-main ui/main.ui --tabs "Каталог,Мои заказы,Все заказы,Статистика"
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Создать форму:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
fastqt6 ui-form ui/product.ui \
|
|
140
|
+
--title "Товар" \
|
|
141
|
+
--class-name "ProductDialog" \
|
|
142
|
+
--field article:text:Артикул \
|
|
143
|
+
--field title:text:Название \
|
|
144
|
+
--field price:float:Цена \
|
|
145
|
+
--field stock:int:Остаток
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
После этого файл можно открыть в Qt Designer или конвертировать:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
pyuic6 ui/product.ui -o gen/product.py
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## CLI
|
|
155
|
+
|
|
156
|
+
```text
|
|
157
|
+
fastqt6 scaffold my_app
|
|
158
|
+
fastqt6 ui-auth ui/auth.ui
|
|
159
|
+
fastqt6 ui-main ui/main.ui
|
|
160
|
+
fastqt6 ui-form ui/form.ui --field title:text:Название
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Что вводить на PyPI Trusted Publisher
|
|
164
|
+
|
|
165
|
+
Для репозитория `git@github.com:Leevandr/fastQT6.git` заполни форму так:
|
|
166
|
+
|
|
167
|
+
```text
|
|
168
|
+
PyPI Project Name: fastqt6
|
|
169
|
+
Owner: Leevandr
|
|
170
|
+
Repository name: fastQT6
|
|
171
|
+
Workflow name: publish.yml
|
|
172
|
+
Environment name: pypi
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Workflow уже лежит в `.github/workflows/publish.yml`. В GitHub желательно создать
|
|
176
|
+
environment с названием `pypi`: `Settings -> Environments -> New environment`.
|
|
177
|
+
|
|
178
|
+
Публикация пойдет через GitHub Actions без API-токена: после настройки Trusted
|
|
179
|
+
Publisher создай GitHub Release или запусти workflow вручную.
|
fastqt6-0.1.0/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# fastqt6
|
|
2
|
+
|
|
3
|
+
`fastqt6` - это небольшая библиотека-шаблон для PyQt6-проектов: динамические формы,
|
|
4
|
+
CRUD-окна, SQL-хелперы и генерация `.ui` файлов для Qt Designer.
|
|
5
|
+
|
|
6
|
+
Установка после публикации:
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install fastqt6
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Локальная установка из репозитория:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
python -m pip install -e .
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Быстрый пример
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from PyQt6.QtWidgets import QApplication
|
|
22
|
+
from fastqt6 import SQLDatabase, field
|
|
23
|
+
from fastqt6.widgets import CrudWindow
|
|
24
|
+
|
|
25
|
+
SCHEMA = """
|
|
26
|
+
create table if not exists products (
|
|
27
|
+
id integer primary key autoincrement,
|
|
28
|
+
article text not null,
|
|
29
|
+
title text not null,
|
|
30
|
+
price real not null default 0,
|
|
31
|
+
stock integer not null default 0
|
|
32
|
+
);
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
app = QApplication([])
|
|
36
|
+
db = SQLDatabase.sqlite("app.db")
|
|
37
|
+
db.run_script(SCHEMA)
|
|
38
|
+
|
|
39
|
+
window = CrudWindow(
|
|
40
|
+
db,
|
|
41
|
+
table="products",
|
|
42
|
+
fields=[
|
|
43
|
+
field("article", "Артикул", required=True),
|
|
44
|
+
field("title", "Название", required=True),
|
|
45
|
+
field("price", "Цена", "float", min_value=0),
|
|
46
|
+
field("stock", "Остаток", "int", min_value=0),
|
|
47
|
+
],
|
|
48
|
+
)
|
|
49
|
+
window.show()
|
|
50
|
+
app.exec()
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## SQL-хелперы
|
|
54
|
+
|
|
55
|
+
SQLite:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from fastqt6 import SQLDatabase
|
|
59
|
+
|
|
60
|
+
db = SQLDatabase.sqlite("app.db")
|
|
61
|
+
db.insert("products", {"article": "A-1", "title": "Мяч", "price": 1000})
|
|
62
|
+
rows = db.select("products", where="price > ?", params=(500,), order_by="title")
|
|
63
|
+
db.update("products", {"price": 1200}, "id=?", (1,))
|
|
64
|
+
db.delete("products", "id=?", (1,))
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
MySQL:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from fastqt6 import SQLDatabase
|
|
71
|
+
|
|
72
|
+
db = SQLDatabase.mysql("sportplus_kvalik", user="root", password="")
|
|
73
|
+
user = db.login("users", "admin", "admin")
|
|
74
|
+
rows = db.fetch_all("select * from products where title like ?", ("%мяч%",))
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
В запросах можно писать `?` как универсальный placeholder. Для MySQL библиотека
|
|
78
|
+
сама заменит его на `%s`.
|
|
79
|
+
|
|
80
|
+
## Динамические формы
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from fastqt6 import field
|
|
84
|
+
from fastqt6.forms import DynamicFormDialog
|
|
85
|
+
|
|
86
|
+
fields = [
|
|
87
|
+
field("article", "Артикул", required=True),
|
|
88
|
+
field("title", "Название", required=True),
|
|
89
|
+
field("price", "Цена", "float", min_value=0),
|
|
90
|
+
field("category_id", "Категория", "combo", choices=[("Мячи", 1), ("Обувь", 2)]),
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
dialog = DynamicFormDialog(fields, title="Товар")
|
|
94
|
+
if dialog.exec():
|
|
95
|
+
data = dialog.get_data()
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Генерация файлов Qt Designer
|
|
99
|
+
|
|
100
|
+
Создать `auth.ui`:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
fastqt6 ui-auth ui/auth.ui
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Создать главное окно с вкладками:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
fastqt6 ui-main ui/main.ui --tabs "Каталог,Мои заказы,Все заказы,Статистика"
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Создать форму:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
fastqt6 ui-form ui/product.ui \
|
|
116
|
+
--title "Товар" \
|
|
117
|
+
--class-name "ProductDialog" \
|
|
118
|
+
--field article:text:Артикул \
|
|
119
|
+
--field title:text:Название \
|
|
120
|
+
--field price:float:Цена \
|
|
121
|
+
--field stock:int:Остаток
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
После этого файл можно открыть в Qt Designer или конвертировать:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
pyuic6 ui/product.ui -o gen/product.py
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## CLI
|
|
131
|
+
|
|
132
|
+
```text
|
|
133
|
+
fastqt6 scaffold my_app
|
|
134
|
+
fastqt6 ui-auth ui/auth.ui
|
|
135
|
+
fastqt6 ui-main ui/main.ui
|
|
136
|
+
fastqt6 ui-form ui/form.ui --field title:text:Название
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Что вводить на PyPI Trusted Publisher
|
|
140
|
+
|
|
141
|
+
Для репозитория `git@github.com:Leevandr/fastQT6.git` заполни форму так:
|
|
142
|
+
|
|
143
|
+
```text
|
|
144
|
+
PyPI Project Name: fastqt6
|
|
145
|
+
Owner: Leevandr
|
|
146
|
+
Repository name: fastQT6
|
|
147
|
+
Workflow name: publish.yml
|
|
148
|
+
Environment name: pypi
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Workflow уже лежит в `.github/workflows/publish.yml`. В GitHub желательно создать
|
|
152
|
+
environment с названием `pypi`: `Settings -> Environments -> New environment`.
|
|
153
|
+
|
|
154
|
+
Публикация пойдет через GitHub Actions без API-токена: после настройки Trusted
|
|
155
|
+
Publisher создай GitHub Release или запусти workflow вручную.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "fastqt6"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Convenient PyQt6 templates for dynamic forms, CRUD windows, Designer .ui files and SQL helpers"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Leevandr" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["pyqt6", "qt", "designer", "mysql", "crud", "forms"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Topic :: Database",
|
|
24
|
+
"Topic :: Software Development :: User Interfaces",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"PyQt6>=6.6",
|
|
28
|
+
"PyMySQL>=1.1",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/Leevandr/fastQT6"
|
|
33
|
+
Repository = "https://github.com/Leevandr/fastQT6"
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
fastqt6 = "fastqt6.cli:main"
|
|
37
|
+
|
|
38
|
+
[tool.setuptools]
|
|
39
|
+
package-dir = { "" = "src" }
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.packages.find]
|
|
42
|
+
where = ["src"]
|
fastqt6-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Fast helpers for PyQt6 training and small database applications."""
|
|
2
|
+
|
|
3
|
+
from .db import DBConfig, SQLDatabase
|
|
4
|
+
from .designer import DesignerForm, write_auth_ui, write_form_ui, write_main_window_ui
|
|
5
|
+
from .fields import Choice, FieldSpec, field
|
|
6
|
+
from .forms import DynamicForm, DynamicFormDialog
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"Choice",
|
|
12
|
+
"DBConfig",
|
|
13
|
+
"DesignerForm",
|
|
14
|
+
"DynamicForm",
|
|
15
|
+
"DynamicFormDialog",
|
|
16
|
+
"FieldSpec",
|
|
17
|
+
"SQLDatabase",
|
|
18
|
+
"field",
|
|
19
|
+
"write_auth_ui",
|
|
20
|
+
"write_form_ui",
|
|
21
|
+
"write_main_window_ui",
|
|
22
|
+
]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Command-line entrypoint for fastqt6."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .designer import write_auth_ui, write_form_ui, write_main_window_ui
|
|
9
|
+
from .fields import field
|
|
10
|
+
from .scaffold import scaffold_basic_app
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main(argv: list[str] | None = None) -> int:
|
|
14
|
+
parser = argparse.ArgumentParser(prog="fastqt6")
|
|
15
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
16
|
+
|
|
17
|
+
scaffold_parser = subparsers.add_parser("scaffold", help="create a small starter app")
|
|
18
|
+
scaffold_parser.add_argument("target")
|
|
19
|
+
|
|
20
|
+
auth_parser = subparsers.add_parser("ui-auth", help="create auth.ui")
|
|
21
|
+
auth_parser.add_argument("path")
|
|
22
|
+
|
|
23
|
+
main_parser = subparsers.add_parser("ui-main", help="create main.ui with tabs")
|
|
24
|
+
main_parser.add_argument("path")
|
|
25
|
+
main_parser.add_argument("--tabs", default="Каталог,Мои заказы,Все заказы,Статистика")
|
|
26
|
+
|
|
27
|
+
form_parser = subparsers.add_parser("ui-form", help="create a form .ui from field declarations")
|
|
28
|
+
form_parser.add_argument("path")
|
|
29
|
+
form_parser.add_argument("--title", default="Форма")
|
|
30
|
+
form_parser.add_argument("--class-name", default="Form")
|
|
31
|
+
form_parser.add_argument(
|
|
32
|
+
"--field",
|
|
33
|
+
action="append",
|
|
34
|
+
default=[],
|
|
35
|
+
help="field as name:kind:label, for example article:text:Артикул",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
args = parser.parse_args(argv)
|
|
39
|
+
|
|
40
|
+
if args.command == "scaffold":
|
|
41
|
+
scaffold_basic_app(args.target)
|
|
42
|
+
print(f"Created {args.target}")
|
|
43
|
+
return 0
|
|
44
|
+
if args.command == "ui-auth":
|
|
45
|
+
write_auth_ui(args.path)
|
|
46
|
+
print(args.path)
|
|
47
|
+
return 0
|
|
48
|
+
if args.command == "ui-main":
|
|
49
|
+
tabs = [tab.strip() for tab in args.tabs.split(",") if tab.strip()]
|
|
50
|
+
write_main_window_ui(args.path, tabs=tabs)
|
|
51
|
+
print(args.path)
|
|
52
|
+
return 0
|
|
53
|
+
if args.command == "ui-form":
|
|
54
|
+
specs = [_parse_field(raw) for raw in args.field]
|
|
55
|
+
write_form_ui(args.path, specs, title=args.title, class_name=args.class_name)
|
|
56
|
+
print(args.path)
|
|
57
|
+
return 0
|
|
58
|
+
return 1
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _parse_field(raw: str):
|
|
62
|
+
parts = raw.split(":", 2)
|
|
63
|
+
if len(parts) == 1:
|
|
64
|
+
return field(parts[0])
|
|
65
|
+
if len(parts) == 2:
|
|
66
|
+
return field(parts[0], kind=parts[1])
|
|
67
|
+
return field(parts[0], parts[2], parts[1])
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
if __name__ == "__main__":
|
|
71
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Small SQL helpers for PyQt6 apps."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sqlite3
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Iterable, Iterator, Mapping, Sequence
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(slots=True)
|
|
13
|
+
class DBConfig:
|
|
14
|
+
driver: str = "mysql"
|
|
15
|
+
host: str = "localhost"
|
|
16
|
+
ports: tuple[int, ...] = (3308, 3306)
|
|
17
|
+
user: str = "root"
|
|
18
|
+
password: str = ""
|
|
19
|
+
database: str = ""
|
|
20
|
+
charset: str = "utf8mb4"
|
|
21
|
+
sqlite_path: str | Path = "app.db"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SQLDatabase:
|
|
25
|
+
"""A tiny DAO wrapper that works with MySQL and SQLite."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, config: DBConfig | None = None, connection: Any | None = None):
|
|
28
|
+
self.config = config or DBConfig()
|
|
29
|
+
self.connection = connection or self._connect()
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def mysql(
|
|
33
|
+
cls,
|
|
34
|
+
database: str,
|
|
35
|
+
*,
|
|
36
|
+
host: str = "localhost",
|
|
37
|
+
ports: tuple[int, ...] = (3308, 3306),
|
|
38
|
+
user: str = "root",
|
|
39
|
+
password: str = "",
|
|
40
|
+
) -> "SQLDatabase":
|
|
41
|
+
return cls(DBConfig(driver="mysql", host=host, ports=ports, user=user, password=password, database=database))
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def sqlite(cls, path: str | Path = "app.db") -> "SQLDatabase":
|
|
45
|
+
return cls(DBConfig(driver="sqlite", sqlite_path=path))
|
|
46
|
+
|
|
47
|
+
def close(self) -> None:
|
|
48
|
+
self.connection.close()
|
|
49
|
+
|
|
50
|
+
def execute(self, sql: str, params: Sequence[Any] | Mapping[str, Any] = ()) -> Any:
|
|
51
|
+
with self.cursor() as cursor:
|
|
52
|
+
cursor.execute(self._sql(sql), params)
|
|
53
|
+
return cursor
|
|
54
|
+
|
|
55
|
+
def fetch_one(self, sql: str, params: Sequence[Any] | Mapping[str, Any] = ()) -> dict[str, Any] | None:
|
|
56
|
+
with self.cursor() as cursor:
|
|
57
|
+
cursor.execute(self._sql(sql), params)
|
|
58
|
+
row = cursor.fetchone()
|
|
59
|
+
return dict(row) if row is not None else None
|
|
60
|
+
|
|
61
|
+
def fetch_all(self, sql: str, params: Sequence[Any] | Mapping[str, Any] = ()) -> list[dict[str, Any]]:
|
|
62
|
+
with self.cursor() as cursor:
|
|
63
|
+
cursor.execute(self._sql(sql), params)
|
|
64
|
+
return [dict(row) for row in cursor.fetchall()]
|
|
65
|
+
|
|
66
|
+
def scalar(self, sql: str, params: Sequence[Any] | Mapping[str, Any] = (), default: Any = None) -> Any:
|
|
67
|
+
row = self.fetch_one(sql, params)
|
|
68
|
+
if not row:
|
|
69
|
+
return default
|
|
70
|
+
return next(iter(row.values()))
|
|
71
|
+
|
|
72
|
+
def select(
|
|
73
|
+
self,
|
|
74
|
+
table: str,
|
|
75
|
+
*,
|
|
76
|
+
columns: str | Iterable[str] = "*",
|
|
77
|
+
where: str = "",
|
|
78
|
+
params: Sequence[Any] = (),
|
|
79
|
+
order_by: str = "",
|
|
80
|
+
limit: int | None = None,
|
|
81
|
+
) -> list[dict[str, Any]]:
|
|
82
|
+
cols = columns if isinstance(columns, str) else ", ".join(columns)
|
|
83
|
+
sql = f"select {cols} from {table}"
|
|
84
|
+
if where:
|
|
85
|
+
sql += f" where {where}"
|
|
86
|
+
if order_by:
|
|
87
|
+
sql += f" order by {order_by}"
|
|
88
|
+
if limit is not None:
|
|
89
|
+
sql += f" limit {int(limit)}"
|
|
90
|
+
return self.fetch_all(sql, params)
|
|
91
|
+
|
|
92
|
+
def insert(self, table: str, data: Mapping[str, Any]) -> int:
|
|
93
|
+
columns = list(data)
|
|
94
|
+
placeholders = ", ".join(["?"] * len(columns))
|
|
95
|
+
sql = f"insert into {table} ({', '.join(columns)}) values ({placeholders})"
|
|
96
|
+
with self.cursor() as cursor:
|
|
97
|
+
cursor.execute(self._sql(sql), tuple(data[column] for column in columns))
|
|
98
|
+
return int(getattr(cursor, "lastrowid", 0) or 0)
|
|
99
|
+
|
|
100
|
+
def update(self, table: str, data: Mapping[str, Any], where: str, params: Sequence[Any] = ()) -> None:
|
|
101
|
+
columns = list(data)
|
|
102
|
+
assignments = ", ".join(f"{column}=?" for column in columns)
|
|
103
|
+
sql = f"update {table} set {assignments} where {where}"
|
|
104
|
+
values = tuple(data[column] for column in columns) + tuple(params)
|
|
105
|
+
self.execute(sql, values)
|
|
106
|
+
|
|
107
|
+
def delete(self, table: str, where: str, params: Sequence[Any] = ()) -> None:
|
|
108
|
+
self.execute(f"delete from {table} where {where}", params)
|
|
109
|
+
|
|
110
|
+
def login(
|
|
111
|
+
self,
|
|
112
|
+
table: str,
|
|
113
|
+
username: str,
|
|
114
|
+
password: str,
|
|
115
|
+
*,
|
|
116
|
+
username_col: str = "username",
|
|
117
|
+
password_col: str = "password",
|
|
118
|
+
) -> dict[str, Any] | None:
|
|
119
|
+
return self.fetch_one(
|
|
120
|
+
f"select * from {table} where {username_col}=? and {password_col}=?",
|
|
121
|
+
(username, password),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def run_script(self, sql: str) -> None:
|
|
125
|
+
statements = [part.strip() for part in sql.split(";") if part.strip()]
|
|
126
|
+
with self.cursor() as cursor:
|
|
127
|
+
for statement in statements:
|
|
128
|
+
cursor.execute(self._sql(statement))
|
|
129
|
+
|
|
130
|
+
@contextmanager
|
|
131
|
+
def transaction(self) -> Iterator["SQLDatabase"]:
|
|
132
|
+
old_autocommit = None
|
|
133
|
+
if self.config.driver == "mysql":
|
|
134
|
+
old_autocommit = self.connection.get_autocommit()
|
|
135
|
+
self.connection.autocommit(False)
|
|
136
|
+
try:
|
|
137
|
+
yield self
|
|
138
|
+
self.connection.commit()
|
|
139
|
+
except Exception:
|
|
140
|
+
self.connection.rollback()
|
|
141
|
+
raise
|
|
142
|
+
finally:
|
|
143
|
+
if old_autocommit is not None:
|
|
144
|
+
self.connection.autocommit(old_autocommit)
|
|
145
|
+
|
|
146
|
+
@contextmanager
|
|
147
|
+
def cursor(self) -> Iterator[Any]:
|
|
148
|
+
cursor = self.connection.cursor()
|
|
149
|
+
try:
|
|
150
|
+
yield cursor
|
|
151
|
+
if self.config.driver == "sqlite":
|
|
152
|
+
self.connection.commit()
|
|
153
|
+
finally:
|
|
154
|
+
cursor.close()
|
|
155
|
+
|
|
156
|
+
def _connect(self) -> Any:
|
|
157
|
+
if self.config.driver == "sqlite":
|
|
158
|
+
connection = sqlite3.connect(self.config.sqlite_path)
|
|
159
|
+
connection.row_factory = sqlite3.Row
|
|
160
|
+
return connection
|
|
161
|
+
|
|
162
|
+
if self.config.driver != "mysql":
|
|
163
|
+
raise ValueError("driver must be 'mysql' or 'sqlite'")
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
import pymysql
|
|
167
|
+
from pymysql.cursors import DictCursor
|
|
168
|
+
except ModuleNotFoundError as exc:
|
|
169
|
+
raise RuntimeError("Install PyMySQL: pip install PyMySQL") from exc
|
|
170
|
+
|
|
171
|
+
last_error: Exception | None = None
|
|
172
|
+
for port in self.config.ports:
|
|
173
|
+
try:
|
|
174
|
+
return pymysql.connect(
|
|
175
|
+
host=self.config.host,
|
|
176
|
+
port=port,
|
|
177
|
+
user=self.config.user,
|
|
178
|
+
password=self.config.password,
|
|
179
|
+
database=self.config.database,
|
|
180
|
+
charset=self.config.charset,
|
|
181
|
+
cursorclass=DictCursor,
|
|
182
|
+
autocommit=True,
|
|
183
|
+
)
|
|
184
|
+
except Exception as exc: # pragma: no cover - depends on local server
|
|
185
|
+
last_error = exc
|
|
186
|
+
raise last_error or RuntimeError("Could not connect to MySQL")
|
|
187
|
+
|
|
188
|
+
def _sql(self, sql: str) -> str:
|
|
189
|
+
if self.config.driver == "mysql":
|
|
190
|
+
return sql.replace("?", "%s")
|
|
191
|
+
return sql
|