bpappbuilder 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,7 @@
1
+ import bpappbuilder.app
2
+ import bpappbuilder.fields
3
+ import bpappbuilder.reports
4
+ import bpappbuilder.tabs
5
+
6
+ # __all__ = ["App", "Fields", "Reports", "Tabs"]
7
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ from .app import App
2
+ from .db import DataBase, SQLiteDB
3
+ # from .themes import *
4
+
5
+ __all__ = ["App", "DataBase", "SQLiteDB"]
@@ -0,0 +1,134 @@
1
+ from PySide6.QtWidgets import QApplication, QMainWindow, QTabWidget, QWidget, QVBoxLayout, QLabel
2
+ from PySide6.QtGui import QIcon
3
+
4
+ from ..tabs.tab import Tab
5
+ from ..tabs.group import TabGroup
6
+ from .db import DataBase
7
+ from .themes import apply_styles
8
+
9
+ from typing import Optional
10
+ import logging
11
+ import sys
12
+ import os
13
+
14
+
15
+ logging.basicConfig(level=logging.DEBUG,
16
+ format="%(asctime)s %(module)s %(funcName)s: %(lineno)d - %(levelname)s - %(message)s",
17
+ datefmt='%H:%M:%S')
18
+
19
+
20
+ class App:
21
+ """
22
+ Класс приложения с таблицами
23
+ """
24
+
25
+ def __init__(self, name: str, db: DataBase, window_width: int, window_height: int, icon_path: Optional[str] = None):
26
+ """
27
+ Args:
28
+ name (str): Отображаемое название приложения
29
+ db (DataBase): Объект базы данных
30
+ window_width (int): Начальная ширина окна приложения в пикселях
31
+ window_height (int): Начальная ширина окна приложения в пикселях
32
+ icon_path (str, optional): Путь к файлу изображения, который будет использоваться как иконка приложения
33
+ """
34
+
35
+ self.tabs: list[Tab] = []
36
+ self.groups: list[TabGroup] = []
37
+ self.icon_path = icon_path
38
+
39
+ self.db = db
40
+ self.name = name
41
+ self.window_width = window_width
42
+ self.window_height = window_height
43
+
44
+ logging._defaultLastResort
45
+
46
+ def add_tab(self, tab: Tab):
47
+ """
48
+ Добавление вкладки в приложение
49
+
50
+ Args:
51
+ tab (Tab): Объект вкладки
52
+ """
53
+
54
+ tab.app_add_tab(self.db)
55
+
56
+ self.tabs.append(tab)
57
+
58
+ def add_group(self, group: TabGroup):
59
+ """
60
+ Добавление группы вкладок в приложение.
61
+ После добавления хотя бы одной группы, добавленные вне групп вкладки отображаться не будут.
62
+ Args:
63
+ group (TabGroup): Объект группы вкладок
64
+ """
65
+
66
+ for tab in group.tabs:
67
+ tab.app_add_tab(self.db)
68
+
69
+ self.groups.append(group)
70
+
71
+ def run(self):
72
+ """
73
+ Запуск приложения
74
+ """
75
+
76
+ # TODO: Добавить поддержку тёмной темы
77
+
78
+ # Создание базы данных
79
+ if len(self.groups) > 0: # Если есть группы, то используем только вкладки из них
80
+ self.tabs = []
81
+ for group in self.groups:
82
+ self.tabs.extend(group.tabs)
83
+
84
+ # Создание объекта приложения
85
+ app = QApplication(sys.argv)
86
+
87
+ # Создание главного окна
88
+ window_widget = QMainWindow()
89
+ window_widget.setWindowTitle(self.name)
90
+ if self.icon_path is not None:
91
+ window_widget.setWindowIcon(QIcon(self.icon_path))
92
+ window_widget.resize(self.window_width, self.window_height)
93
+
94
+ container = QWidget()
95
+ container.setProperty("class", "main-container")
96
+ container_layout = QVBoxLayout()
97
+ container_layout.setContentsMargins(0, 0, 0, 0)
98
+ container.setLayout(container_layout)
99
+
100
+ apply_styles(window_widget)
101
+ QApplication.instance().paletteChanged.connect(lambda: apply_styles(window_widget))
102
+
103
+ if len(self.groups) > 0:
104
+ groups_tab_widget = QTabWidget()
105
+ groups_tab_widget.setProperty("class", "groups-tab")
106
+
107
+ tab_bar = groups_tab_widget.tabBar()
108
+ tab_bar.setProperty("class", "groups-tab-bar")
109
+
110
+ for group in self.groups:
111
+ tabs_tab_widget = QTabWidget()
112
+ for tab in group.tabs:
113
+ tabs_tab_widget.addTab(tab.create_tab_widget(), tab.name)
114
+ tabs_tab_widget.setCurrentIndex(0)
115
+
116
+ groups_tab_widget.addTab(tabs_tab_widget, group.name)
117
+
118
+ groups_tab_widget.setCurrentIndex(0)
119
+ container_layout.addWidget(groups_tab_widget)
120
+ else:
121
+ tabs_widget = QTabWidget()
122
+
123
+ for tab in self.tabs:
124
+ tabs_widget.addTab(tab.create_tab_widget(), tab.name)
125
+
126
+ tabs_widget.setCurrentIndex(0)
127
+ container_layout.addWidget(tabs_widget)
128
+
129
+ window_widget.setCentralWidget(container)
130
+
131
+ window_widget.show()
132
+
133
+ app.exec()
134
+ self.db.close()
bpappbuilder/app/db.py ADDED
@@ -0,0 +1,47 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Optional
3
+ import sqlite3
4
+ import logging
5
+
6
+ class DataBase(ABC):
7
+ """
8
+ Базовый класс базы данных, имеющий поля conn и cur
9
+ """
10
+
11
+ @abstractmethod
12
+ def __init__(self):
13
+ self.conn = None
14
+ self.cur = None
15
+
16
+ @abstractmethod
17
+ def close(self):
18
+ """
19
+ Закрытие соединения с БД при завершении работы приложения
20
+ """
21
+ pass
22
+
23
+ class SQLiteDB(DataBase):
24
+ """
25
+ База данных SQLite
26
+ """
27
+
28
+ def __init__(self, path: str):
29
+ """
30
+ Args:
31
+ path (str): Путь к .db файлу
32
+ """
33
+
34
+ self.conn = sqlite3.connect(path, check_same_thread = False)
35
+ self.conn.row_factory = sqlite3.Row
36
+ self.conn.set_trace_callback(self.log) # Логирование всех запросов к БД
37
+ self.cur = self.conn.cursor()
38
+
39
+ self.cur.execute("PRAGMA foreign_keys = ON")
40
+ self.conn.commit()
41
+
42
+ def close(self):
43
+ self.cur.close()
44
+ self.conn.close()
45
+
46
+ def log(self, statement: str):
47
+ logging.info(f"SQlite: \033[2;32m{statement}\033[0m")
@@ -0,0 +1,243 @@
1
+ .main-container {
2
+ margin: 0;
3
+ padding: 0;
4
+ background-color: var(--groups-bar-bg);
5
+ }
6
+
7
+ QTabWidget QGroupBox {
8
+ border: 1px solid var(--default-widget-border);
9
+ border-radius: 1px;
10
+ }
11
+
12
+ QTabWidget::pane {
13
+ border-top: 1px solid var(--tab-border);
14
+ background-color: var(--app-bg);
15
+ margin: 0;
16
+ }
17
+
18
+ QTabWidget:tab-bar {
19
+ left: 1px;
20
+ }
21
+
22
+ QTabBar::tab {
23
+ background: var(--unselected-tab-bg);
24
+ border: 1px solid var(--tab-border);
25
+ border-bottom-color: var(--app-bg);
26
+ border-top-left-radius: 4px;
27
+ border-top-right-radius: 4px;
28
+ min-width: 8ex;
29
+ padding: 3px;
30
+ }
31
+
32
+ QTabBar::tab:selected, QTabBar::tab:hover {
33
+ background: var(--app-bg);
34
+ }
35
+
36
+ QTabBar::tab:selected {
37
+ border-width: 2px;
38
+ border-color: var(--selected-tab-border);
39
+ border-bottom-color: var(--app-bg);
40
+ }
41
+
42
+ .groups-tab::pane {
43
+ border: 0;
44
+ }
45
+
46
+ .groups-tab-bar::tab {
47
+ border: 0;
48
+ border-radius: 0;
49
+ padding: 15px 25px;
50
+ font-size: 14px;
51
+ background-color: var(--groups-bar-bg);
52
+ }
53
+
54
+ .groups-tab-bar::tab:selected, .groups-tab-bar::tab:hover {
55
+ background: var(--app-bg);
56
+ }
57
+
58
+ QDateTimeEdit {
59
+ border: 1px solid var(--default-widget-border);
60
+ border-radius: 3px;
61
+ padding: 3px;
62
+ }
63
+
64
+ QDateTimeEdit:focus {
65
+ border: 2px solid var(--default-selected-widget-border);
66
+ }
67
+
68
+ QTimeEdit {
69
+ border: 1px solid var(--default-widget-border);
70
+ border-radius: 3px;
71
+ padding: 3px;
72
+ padding-top: 2px;
73
+ height: 22px;
74
+ }
75
+
76
+ QTimeEdit:focus {
77
+ border: 2px solid var(--default-selected-widget-border);
78
+ }
79
+
80
+ QComboBox {
81
+ /*background-color: white;*/
82
+ border: 1px solid var(--default-widget-border);
83
+ border-radius: 3px;
84
+ padding: 1px 5px;
85
+ }
86
+
87
+ QComboBox:on {
88
+ border: 2px solid var(--default-selected-widget-border);
89
+ }
90
+
91
+ QSpinBox {
92
+ border: 1px solid var(--default-widget-border);
93
+ border-radius: 3px;
94
+ /*background-color: white;*/
95
+ }
96
+
97
+ QSpinBox:focus {
98
+ border: 2px solid var(--default-selected-widget-border);
99
+ }
100
+
101
+ QDoubleSpinBox {
102
+ border: 1px solid var(--default-widget-border);
103
+ border-radius: 3px;
104
+ /*background-color: white;*/
105
+ }
106
+
107
+ QDoubleSpinBox:focus {
108
+ border: 2px solid var(--default-selected-widget-border);
109
+ }
110
+
111
+ QTextEdit {
112
+ border: 1px solid var(--default-widget-border);
113
+ border-radius: 3px;
114
+ /*background-color: white;*/
115
+ }
116
+
117
+ QTextEdit:focus {
118
+ border: 2px solid var(--default-selected-widget-border);
119
+ }
120
+
121
+ QLineEdit {
122
+ border: 1px solid var(--default-widget-border);
123
+ border-radius: 3px;
124
+ padding: 2px;
125
+ }
126
+
127
+ QLineEdit:focus {
128
+ border: 2px solid var(--default-selected-widget-border);
129
+ }
130
+
131
+ QPushButton {
132
+ border: 1px solid var(--default-widget-border);
133
+ border-radius: 4px;
134
+ background-color: var(--gray-btn-bg);
135
+ padding: 3px 8px 3px 8px;
136
+ }
137
+
138
+ QPushButton:hover {
139
+ background-color: var(--hover-gray-btn-bg);
140
+ }
141
+
142
+ QPushButton:focus {
143
+ border: 2px solid var(--default-selected-widget-border);
144
+ }
145
+
146
+ QPushButton:pressed {
147
+ background-color: var(--pressed-gray-btn-bg);
148
+ }
149
+
150
+ .table-line-edit {
151
+ border: 0;
152
+ }
153
+
154
+ .form-add {
155
+ margin: 0px;
156
+ padding: 0px;
157
+ }
158
+
159
+ .add-button {
160
+ border: 0;
161
+ height: 25px;
162
+ background-color: var(--add-btn-bg);
163
+ color: white;
164
+ font-weight: 700;
165
+ font-size: 13px;
166
+ }
167
+
168
+ .add-button:hover {
169
+ background-color: var(--hover-add-btn-bg);
170
+ }
171
+
172
+ .add-button:pressed {
173
+ background-color: var(--pressed-add-btn-bg);
174
+ }
175
+
176
+ .red-button {
177
+ background-color: var(--red-btn-bg);
178
+ }
179
+
180
+ .red-button:hover {
181
+ background-color: var(--hover-red-btn);
182
+ }
183
+
184
+ .red-button:pressed {
185
+ background-color: var(--pressed-red-btn-bg);
186
+ }
187
+
188
+ /*.red-button::focus {
189
+ border-color: #d10000;
190
+ }*/
191
+
192
+ .green-button {
193
+ background-color: var(--green-btn-bg);
194
+ }
195
+
196
+ .green-button:hover {
197
+ background-color: var(--hover-green-btn-bg);
198
+ }
199
+
200
+ .green-button:pressed {
201
+ background-color: var(--pressed-green-btn-bg);
202
+ }
203
+
204
+ /*.green-button::focus {
205
+ border-color: #059100;
206
+ }*/
207
+
208
+ .gray-button {
209
+ background-color: var(--gray-btn-bg);
210
+ }
211
+
212
+ .gray-button:hover {
213
+ background-color: var(--hover-gray-btn-bg);
214
+ }
215
+
216
+ .gray-button:pressed {
217
+ background-color: var(--pressed-gray-btn-bg);
218
+ }
219
+
220
+ .gray-button::focus {
221
+ border-color: var(--default-selected-widget-border);
222
+ }
223
+
224
+ .form-label {
225
+ font-size: 17pt;
226
+ }
227
+
228
+ .rowdatadialog-label {
229
+ font-size: 13pt;
230
+ }
231
+
232
+ .submitdialog-label {
233
+ font-size: 10pt;
234
+ }
235
+
236
+ .report-section {
237
+ border: 1px solid var(--report-section-border);
238
+ border-radius: 5px;
239
+ }
240
+
241
+ .report-widget {
242
+ background-color: var(--report-widget-bg);
243
+ }
@@ -0,0 +1,72 @@
1
+ from PySide6.QtWidgets import QWidget, QApplication
2
+
3
+ from typing import Optional
4
+ import os
5
+
6
+ THEMES = {
7
+ "light": {
8
+ "--app-bg": "#eee",
9
+ "--tab-border": "#C2C7CB",
10
+ "--selected-tab-border": "#679dcf",
11
+ "--unselected-tab-bg": "#e4e4e4",
12
+ "--groups-bar-bg": "#e3e3e3",
13
+ "--default-widget-border": "gray",
14
+ "--default-selected-widget-border": "#3484cf",
15
+ "--gray-btn-bg": "#e3e3e3",
16
+ "--hover-gray-btn-bg": "#dbdbdb",
17
+ "--pressed-gray-btn-bg": "#d4d4d4",
18
+ "--add-btn-bg": "#3484cf",
19
+ "--hover-add-btn-bg": "#2679c7",
20
+ "--pressed-add-btn-bg": "#1f6eb8",
21
+ "--red-btn-bg": "#ff4d4d",
22
+ "--hover-red-btn": "#f74444",
23
+ "--pressed-red-btn-bg": "#f13737",
24
+ "--green-btn-bg": "#30c72b",
25
+ "--hover-green-btn-bg": "#30be2b",
26
+ "--pressed-green-btn-bg": "#33af2e",
27
+ "--report-section-border": "#E6E6E6",
28
+ "--report-widget-bg": "white",
29
+ },
30
+ "dark": {
31
+ "--app-bg": "#111215",
32
+ "--tab-border": "#545454",
33
+ "--selected-tab-border": "#2472d1",
34
+ "--unselected-tab-bg": "#1c1e23",
35
+ "--groups-bar-bg": "#1c1e23",
36
+ "--default-widget-border": "#545454",
37
+ "--default-selected-widget-border": "#3484cf",
38
+ "--gray-btn-bg": "#24262c",
39
+ "--hover-gray-btn-bg": "#282a30",
40
+ "--pressed-gray-btn-bg": "#31333b",
41
+ "--add-btn-bg": "#3484cf",
42
+ "--hover-add-btn-bg": "#2679c7",
43
+ "--pressed-add-btn-bg": "#1f6eb8",
44
+ "--red-btn-bg": "#ff2b2f",
45
+ "--hover-red-btn": "#f74444",
46
+ "--pressed-red-btn-bg": "#e62026",
47
+ "--green-btn-bg": "#35b62e",
48
+ "--hover-green-btn-bg": "#30be2b",
49
+ "--pressed-green-btn-bg": "#33af2e",
50
+ "--report-section-border": "#414141",
51
+ "--report-widget-bg": "#15161a",
52
+ }
53
+ }
54
+
55
+ def apply_styles(widget: QWidget, previous_theme: Optional[str] = None) -> str:
56
+ app = QApplication.instance()
57
+ palette = app.palette()
58
+
59
+ is_dark = palette.color(palette.ColorRole.Window).lightness() < 128
60
+ theme = "dark" if is_dark else "light"
61
+
62
+ if previous_theme == theme:
63
+ return theme
64
+
65
+ with open(os.path.dirname(os.path.realpath(__file__)) + "/styles.qss", "r") as f:
66
+ style = f.read()
67
+ for var, data in THEMES[theme].items():
68
+ style = style.replace(f"var({var})", data)
69
+ widget.setStyleSheet(style)
70
+
71
+ return theme
72
+
@@ -0,0 +1,29 @@
1
+ from .combo_box_items import (
2
+ NameGenerator,
3
+ StandardNameGenerators,
4
+ StaticItems,
5
+ ComboBoxItems,
6
+ TableFieldItems,
7
+ TableItems
8
+ )
9
+ from .field import (
10
+ OnChangeHandler,
11
+ Field,
12
+ CheckField,
13
+ IntField,
14
+ FloatField,
15
+ TextLineField,
16
+ TextAreaField,
17
+ ComboBoxField,
18
+ DateTimeField,
19
+ DateField,
20
+ TimeField,
21
+ FileField,
22
+ TableField,
23
+ LabelField
24
+ )
25
+
26
+ __all__ = ["NameGenerator", "StandardNameGenerators", "ComboBoxItems", "StaticItems", "TableFieldItems",
27
+ "TableItems", "OnChangeHandler", "Field", "CheckField", "IntField", "FloatField",
28
+ "TextLineField", "TextAreaField", "ComboBoxField", "DateTimeField", "DateField",
29
+ "TimeField", "FileField", "TableField", "LabelField"]
@@ -0,0 +1,93 @@
1
+ from ..app.db import DataBase
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Optional, Callable, Sequence, Any, Iterable
5
+
6
+
7
+ NameGenerator = Callable[[Sequence[Any]], str]
8
+
9
+ class StandardNameGenerators():
10
+ def Id(data: dict[str, str]) -> str:
11
+ return "#" + data["id"]
12
+ def DateTime(data: dict[str, str]) -> str:
13
+ return "от " + data["date"]
14
+ def IdDateTime(data: dict[str, str]) -> str:
15
+ return "#" + data["id"] + " от " + data["date"]
16
+
17
+ class ComboBoxItems(ABC):
18
+ """
19
+ Класс данных, из которых будут формироваться элементы выбора в ComboBoxField
20
+ """
21
+
22
+ @abstractmethod
23
+ def get_items(self) -> dict[int, str]:
24
+ """
25
+ Получает списки с элементами для ComboBoxField
26
+ Returns:
27
+ dict Словарь, где ключи - данные элемента, которые будут сохранены в БД,
28
+ а значения - отображаемый текст элемента
29
+ """
30
+ pass
31
+
32
+ class StaticItems(ComboBoxItems):
33
+ """
34
+ Статичный неизменяемый список элементов для ComboBox
35
+ """
36
+
37
+ def __init__(self, items: list[str]):
38
+ """
39
+ Args:
40
+ items (list[str]): Список строк для элементов
41
+ """
42
+
43
+ self.items = items
44
+
45
+ def get_items(self) -> dict[int, str]:
46
+ return {i: value for i, value in enumerate(self.items)}
47
+
48
+ class TableFieldItems(ComboBoxItems):
49
+ """
50
+ Список элементов генерируется из конкретного поля одной из таблиц
51
+ """
52
+
53
+ def __init__(self, table_sql_name: str, field_sql_name: str, db: DataBase) -> dict[int, str]:
54
+ """
55
+ Args:
56
+ table_sql_name (str): SQL название таблицы, из которой будут браться данные
57
+ field_sql_name (str): SQL название поля, данные которого будут составлять элементы списка
58
+ db (DataBase): База данных
59
+ """
60
+
61
+ self.table = table_sql_name
62
+ self.field = field_sql_name
63
+ self.db = db
64
+
65
+ def get_items(self) -> tuple[list[int], list[str]]:
66
+ result = self.db.cur.execute(f"SELECT id, {self.field} FROM {self.table}").fetchall()
67
+ return {int(x["id"]): x[self.field] for x in result}
68
+
69
+ class TableItems(ComboBoxItems):
70
+ """
71
+ Список элементов формируется по названиям строк определённой таблицы
72
+ """
73
+
74
+ def __init__(self, table_sql_name: str, db: DataBase, name_generator: NameGenerator, columns: Optional[Iterable[str]] = None):
75
+ """
76
+ Args:
77
+ table_sql_name (str): SQL название таблицы, из которой будут браться данные
78
+ db (DataBase): база данных
79
+ name_generator ((row_data) -> row_name): Функция создания названия строки таблицы, в которой:
80
+ - row_data (Sequence[Any]): Данные из строки таблицы в виде массива пар ключ-значения (ключи - названия полей)
81
+ - row_name (str): Название строки таблицы, которое будет добавлено в список элементов
82
+ columns (list[str], optional): Список SQL названий столбцов, которые будут получены из БД; если не указано,
83
+ будут извлекаться все столбцы
84
+ """
85
+
86
+ self.table = table_sql_name
87
+ self.name_generator = name_generator
88
+ self.columns = columns
89
+ self.db = db
90
+
91
+ def get_items(self) -> dict[int, str]:
92
+ result = self.db.cur.execute(f"SELECT {'*' if self.columns is None else ', '.join(self.columns)} FROM {self.table}").fetchall()
93
+ return {int(x["id"]): self.name_generator(x) for x in result}