deskkit 1.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.
deskkit/__init__.py ADDED
@@ -0,0 +1,44 @@
1
+ """DeskKit — простые десктоп-приложения на Qt и MySQL."""
2
+
3
+ from deskkit.app import ShopApp, run_app, run_from_json
4
+ from deskkit.auth import AuthService, make_guest, user_display_name
5
+ from deskkit.catalog import apply_catalog_filters, search_products, sort_products
6
+ from deskkit.config import AppConfig
7
+ from deskkit.database import Database
8
+ from deskkit.exceptions import AuthError, DatabaseError, DeskKitError, ValidationError
9
+ from deskkit.generator import export_project, run_generated
10
+ from deskkit.images import load_pixmap, save_product_image, validate_product_data
11
+ from deskkit.qt_backend import backend_name, init_qt
12
+ from deskkit.roles import ADMIN, CLIENT, GUEST, MANAGER
13
+
14
+ __version__ = "1.1.0"
15
+
16
+ __all__ = [
17
+ "ShopApp",
18
+ "AppConfig",
19
+ "Database",
20
+ "AuthService",
21
+ "export_project",
22
+ "run_generated",
23
+ "run_app",
24
+ "run_from_json",
25
+ "apply_catalog_filters",
26
+ "search_products",
27
+ "sort_products",
28
+ "load_pixmap",
29
+ "save_product_image",
30
+ "validate_product_data",
31
+ "make_guest",
32
+ "user_display_name",
33
+ "init_qt",
34
+ "backend_name",
35
+ "ADMIN",
36
+ "CLIENT",
37
+ "GUEST",
38
+ "MANAGER",
39
+ "DeskKitError",
40
+ "DatabaseError",
41
+ "AuthError",
42
+ "ValidationError",
43
+ "__version__",
44
+ ]
deskkit/app.py ADDED
@@ -0,0 +1,114 @@
1
+ """Точка входа — запуск готового приложения."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from typing import Callable
7
+
8
+ from deskkit import qt_backend
9
+ from deskkit.auth import AuthService
10
+ from deskkit.config import AppConfig
11
+ from deskkit.database import Database
12
+ from deskkit.exceptions import DatabaseError
13
+ from deskkit.images import ensure_images_dir
14
+ from deskkit.ui.login_window import LoginWindow
15
+ from deskkit.ui.main_window import MainWindow
16
+ from deskkit.ui.styles import APP_STYLE
17
+
18
+ qt_backend.init_qt()
19
+ QtWidgets = qt_backend.QtWidgets
20
+
21
+
22
+ class ShopApp:
23
+ """
24
+ Готовое приложение «из коробки».
25
+
26
+ Пример:
27
+ from deskkit import ShopApp, AppConfig
28
+
29
+ ShopApp(AppConfig(
30
+ title="Обувной магазин",
31
+ database="shoe_store",
32
+ password="root",
33
+ )).run()
34
+ """
35
+
36
+ def __init__(self, config: AppConfig | None = None):
37
+ self.config = config or AppConfig()
38
+ qt_backend.init_qt(self.config.qt_backend)
39
+ self.db = Database(self.config)
40
+ self.auth = AuthService(self.db)
41
+ self._app: QtWidgets.QApplication | None = None
42
+ self._main_window: MainWindow | None = None
43
+
44
+ def run(self) -> int:
45
+ """Запустить приложение или сгенерировать автономный проект."""
46
+ if self.config.export_project and not self.config.library_runtime:
47
+ from deskkit.generator import export_project, run_generated
48
+
49
+ out = export_project(self.config)
50
+ print(f"Проект создан: {out}")
51
+ if self.config.run_generated:
52
+ return run_generated(self.config, out)
53
+ return 0
54
+
55
+ return self._run_library()
56
+
57
+ def _run_library(self) -> int:
58
+ """Запуск через deskkit (режим разработки)."""
59
+ ensure_images_dir(self.config)
60
+
61
+ self._app = QtWidgets.QApplication.instance()
62
+ if self._app is None:
63
+ self._app = QtWidgets.QApplication(sys.argv)
64
+ self._app.setStyleSheet(APP_STYLE)
65
+
66
+ try:
67
+ if not self.db.test_connection():
68
+ QtWidgets.QMessageBox.critical(
69
+ None,
70
+ "Ошибка подключения",
71
+ "Не удалось подключиться к базе данных.\n"
72
+ "Проверьте настройки AppConfig.",
73
+ )
74
+ return 1
75
+ except DatabaseError as exc:
76
+ QtWidgets.QMessageBox.critical(None, "Ошибка подключения", str(exc))
77
+ return 1
78
+
79
+ if not self._show_login():
80
+ return 0
81
+ return qt_backend.exec_app(self._app)
82
+
83
+ def _show_login(self) -> bool:
84
+ def on_success(user: dict) -> None:
85
+ self._show_main()
86
+
87
+ login = LoginWindow(self.config, self.auth, on_success)
88
+ result = login.exec()
89
+ return result == QtWidgets.QDialog.DialogCode.Accepted
90
+
91
+ def _show_main(self) -> None:
92
+ if self._main_window:
93
+ self._main_window.close()
94
+
95
+ def on_logout() -> None:
96
+ self._show_login()
97
+
98
+ self._main_window = MainWindow(
99
+ self.config,
100
+ self.db,
101
+ self.auth,
102
+ on_logout=on_logout,
103
+ )
104
+ self._main_window.show()
105
+
106
+
107
+ def run_app(config: AppConfig | None = None) -> int:
108
+ """Быстрый запуск приложения одной функцией."""
109
+ return ShopApp(config).run()
110
+
111
+
112
+ def run_from_json(path: str) -> int:
113
+ """Запуск по JSON-файлу настроек."""
114
+ return ShopApp(AppConfig.from_json(path)).run()
deskkit/auth.py ADDED
@@ -0,0 +1,72 @@
1
+ """Авторизация пользователей."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from deskkit.database import Database
8
+ from deskkit.exceptions import AuthError
9
+ from deskkit.roles import ADMIN, CLIENT, GUEST, MANAGER
10
+
11
+
12
+ def make_guest() -> dict[str, Any]:
13
+ return {
14
+ "id_user": 0,
15
+ "login": "guest",
16
+ "first_name": "Гость",
17
+ "last_name": "",
18
+ "middle_name": None,
19
+ "id_role": 0,
20
+ "role_name": GUEST,
21
+ }
22
+
23
+
24
+ def user_display_name(user: dict[str, Any]) -> str:
25
+ parts = [user.get("last_name", ""), user.get("first_name", "")]
26
+ middle = user.get("middle_name")
27
+ if middle:
28
+ parts.append(middle)
29
+ name = " ".join(p for p in parts if p).strip()
30
+ return name or user.get("login", "Пользователь")
31
+
32
+
33
+ class AuthService:
34
+ def __init__(self, db: Database):
35
+ self.db = db
36
+ self.current_user: dict[str, Any] | None = None
37
+
38
+ @property
39
+ def role(self) -> str:
40
+ if not self.current_user:
41
+ return GUEST
42
+ return str(self.current_user.get("role_name", GUEST))
43
+
44
+ def login(self, login: str, password: str) -> dict[str, Any]:
45
+ if not login.strip() or not password.strip():
46
+ raise AuthError("Введите логин и пароль")
47
+
48
+ user = self.db.authenticate(login.strip(), password)
49
+ if not user:
50
+ raise AuthError("Неверный логин или пароль")
51
+
52
+ self.current_user = user
53
+ return user
54
+
55
+ def login_as_guest(self) -> dict[str, Any]:
56
+ self.current_user = make_guest()
57
+ return self.current_user
58
+
59
+ def logout(self) -> None:
60
+ self.current_user = None
61
+
62
+ def is_admin(self) -> bool:
63
+ return self.role == ADMIN
64
+
65
+ def is_manager(self) -> bool:
66
+ return self.role == MANAGER
67
+
68
+ def is_client(self) -> bool:
69
+ return self.role == CLIENT
70
+
71
+ def is_guest(self) -> bool:
72
+ return self.role == GUEST
deskkit/catalog.py ADDED
@@ -0,0 +1,103 @@
1
+ """Поиск, фильтрация и сортировка товаров."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Callable
6
+
7
+
8
+ def _text(value: Any) -> str:
9
+ if value is None:
10
+ return ""
11
+ return str(value).lower()
12
+
13
+
14
+ def search_products(
15
+ products: list[dict[str, Any]],
16
+ query: str,
17
+ fields: list[str] | None = None,
18
+ ) -> list[dict[str, Any]]:
19
+ """Поиск в реальном времени по текстовым полям."""
20
+ if not query.strip():
21
+ return list(products)
22
+
23
+ q = query.strip().lower()
24
+ if fields is None:
25
+ fields = [
26
+ "article_number",
27
+ "product_name",
28
+ "description",
29
+ "category_name",
30
+ "manufacturer_name",
31
+ "supplier_name",
32
+ "unit",
33
+ "availability_status",
34
+ ]
35
+
36
+ result = []
37
+ for product in products:
38
+ for field in fields:
39
+ if q in _text(product.get(field)):
40
+ result.append(product)
41
+ break
42
+ return result
43
+
44
+
45
+ def filter_by_supplier(
46
+ products: list[dict[str, Any]],
47
+ supplier_id: int | None,
48
+ ) -> list[dict[str, Any]]:
49
+ if supplier_id is None or supplier_id <= 0:
50
+ return list(products)
51
+ return [p for p in products if int(p.get("id_supplier") or 0) == supplier_id]
52
+
53
+
54
+ def filter_by_category(
55
+ products: list[dict[str, Any]],
56
+ category_id: int | None,
57
+ ) -> list[dict[str, Any]]:
58
+ if category_id is None or category_id <= 0:
59
+ return list(products)
60
+ return [p for p in products if int(p.get("id_category") or 0) == category_id]
61
+
62
+
63
+ def sort_products(
64
+ products: list[dict[str, Any]],
65
+ field: str = "stock_quantity",
66
+ ascending: bool = True,
67
+ ) -> list[dict[str, Any]]:
68
+ def key_fn(item: dict[str, Any]) -> Any:
69
+ value = item.get(field, 0)
70
+ if value is None:
71
+ return 0
72
+ return value
73
+
74
+ return sorted(products, key=key_fn, reverse=not ascending)
75
+
76
+
77
+ def apply_catalog_filters(
78
+ products: list[dict[str, Any]],
79
+ *,
80
+ search: str = "",
81
+ supplier_id: int | None = None,
82
+ category_id: int | None = None,
83
+ sort_field: str = "product_name",
84
+ sort_asc: bool = True,
85
+ ) -> list[dict[str, Any]]:
86
+ """Комбинирует поиск, фильтры и сортировку."""
87
+ result = search_products(products, search)
88
+ result = filter_by_supplier(result, supplier_id)
89
+ result = filter_by_category(result, category_id)
90
+ return sort_products(result, sort_field, sort_asc)
91
+
92
+
93
+ def row_style(product: dict[str, Any], rules: list[dict[str, Any]]) -> dict[str, str]:
94
+ """Возвращает стили строки по правилам условного форматирования."""
95
+ styles: dict[str, str] = {}
96
+ for rule in rules:
97
+ condition: Callable[[dict[str, Any]], bool] = rule["when"]
98
+ if condition(product):
99
+ if "background" in rule:
100
+ styles["background"] = rule["background"]
101
+ if "foreground" in rule:
102
+ styles["foreground"] = rule["foreground"]
103
+ return styles
deskkit/config.py ADDED
@@ -0,0 +1,130 @@
1
+ """Конфигурация приложения."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import dataclass, field
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+
11
+ @dataclass
12
+ class AppConfig:
13
+ """Настройки приложения — достаточно заполнить и передать в ShopApp."""
14
+
15
+ title: str = "Каталог товаров"
16
+ host: str = "localhost"
17
+ port: int = 3306
18
+ user: str = "root"
19
+ password: str = ""
20
+ database: str = "shoe_store"
21
+
22
+ images_dir: str = "images"
23
+ placeholder_image: str = "picture.png"
24
+
25
+ qt_backend: str | None = None # 'pyside6' | 'pyqt6' | None
26
+
27
+ # Поля каталога (9 полей для отображения)
28
+ catalog_columns: list[str] = field(
29
+ default_factory=lambda: [
30
+ "photo",
31
+ "article_number",
32
+ "product_name",
33
+ "category_name",
34
+ "manufacturer_name",
35
+ "supplier_name",
36
+ "price",
37
+ "unit",
38
+ "stock_quantity",
39
+ "discount_percent",
40
+ ]
41
+ )
42
+
43
+ column_labels: dict[str, str] = field(
44
+ default_factory=lambda: {
45
+ "photo": "Фото",
46
+ "article_number": "Артикул",
47
+ "product_name": "Наименование",
48
+ "category_name": "Категория",
49
+ "manufacturer_name": "Производитель",
50
+ "supplier_name": "Поставщик",
51
+ "price": "Цена",
52
+ "unit": "Ед. изм.",
53
+ "stock_quantity": "Кол-во",
54
+ "discount_percent": "Скидка %",
55
+ "description": "Описание",
56
+ "availability_status": "Наличие",
57
+ }
58
+ )
59
+
60
+ # Условное форматирование строк каталога
61
+ format_rules: list[dict[str, Any]] = field(
62
+ default_factory=lambda: [
63
+ {
64
+ "name": "discount",
65
+ "when": lambda p: float(p.get("discount_percent") or 0) > 0,
66
+ "background": "#FFF5F5",
67
+ },
68
+ {
69
+ "name": "out_of_stock",
70
+ "when": lambda p: int(p.get("stock_quantity") or 0) == 0,
71
+ "background": "#ECECEC",
72
+ "foreground": "#888888",
73
+ },
74
+ {
75
+ "name": "big_discount",
76
+ "when": lambda p: float(p.get("discount_percent") or 0) >= 15,
77
+ "background": "#E8F5E9",
78
+ "foreground": "#2E8B57",
79
+ },
80
+ ]
81
+ )
82
+
83
+ image_width: int = 300
84
+ image_height: int = 200
85
+
86
+ # --- Экспорт автономного проекта (по умолчанию включён) ---
87
+ export_project: bool = True
88
+ """True: сгенерировать готовый проект без импортов deskkit."""
89
+ output_dir: str = ""
90
+ """Папка результата. Пусто — имя из title."""
91
+ project_name: str = ""
92
+ """Имя папки проекта (slug)."""
93
+ include_seed_data: bool = True
94
+ """Добавить тестовые данные в sql/database.sql."""
95
+ run_generated: bool = True
96
+ """После генерации запустить main.py сгенерированного проекта."""
97
+ library_runtime: bool = False
98
+ """True: запуск через deskkit (без генерации). Приоритет над export_project."""
99
+
100
+ @classmethod
101
+ def from_json(cls, path: str | Path) -> "AppConfig":
102
+ data = json.loads(Path(path).read_text(encoding="utf-8"))
103
+ known = {f.name for f in cls.__dataclass_fields__.values()} # type: ignore[attr-defined]
104
+ filtered = {k: v for k, v in data.items() if k in known and k not in ("format_rules",)}
105
+ return cls(**filtered)
106
+
107
+ def save_json(self, path: str | Path) -> None:
108
+ data = {
109
+ "title": self.title,
110
+ "host": self.host,
111
+ "port": self.port,
112
+ "user": self.user,
113
+ "password": self.password,
114
+ "database": self.database,
115
+ "images_dir": self.images_dir,
116
+ "placeholder_image": self.placeholder_image,
117
+ "qt_backend": self.qt_backend,
118
+ "image_width": self.image_width,
119
+ "image_height": self.image_height,
120
+ "export_project": self.export_project,
121
+ "output_dir": self.output_dir,
122
+ "project_name": self.project_name,
123
+ "include_seed_data": self.include_seed_data,
124
+ "run_generated": self.run_generated,
125
+ "library_runtime": self.library_runtime,
126
+ }
127
+ Path(path).write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
128
+
129
+ def images_path(self) -> Path:
130
+ return Path(self.images_dir)
deskkit/database.py ADDED
@@ -0,0 +1,211 @@
1
+ """Работа с MySQL через mysql.connector."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from contextlib import contextmanager
6
+ from decimal import Decimal
7
+ from typing import Any, Generator, Iterable
8
+
9
+ import mysql.connector
10
+ from mysql.connector import Error as MySQLError
11
+
12
+ from deskkit.config import AppConfig
13
+ from deskkit.exceptions import DatabaseError
14
+
15
+
16
+ def _row_to_dict(cursor: Any, row: tuple) -> dict[str, Any]:
17
+ columns = [desc[0] for desc in cursor.description]
18
+ result: dict[str, Any] = {}
19
+ for key, value in zip(columns, row):
20
+ if isinstance(value, Decimal):
21
+ value = float(value)
22
+ result[key] = value
23
+ return result
24
+
25
+
26
+ class Database:
27
+ """Простой слой доступа к MySQL."""
28
+
29
+ PRODUCTS_QUERY = """
30
+ SELECT
31
+ p.id_product,
32
+ p.article_number,
33
+ p.product_name,
34
+ p.description,
35
+ p.price,
36
+ p.discount_percent,
37
+ p.unit,
38
+ p.stock_quantity,
39
+ p.photo_url,
40
+ p.id_category,
41
+ p.id_supplier,
42
+ p.id_manufacturer,
43
+ c.category_name,
44
+ m.manufacturer_name,
45
+ s.supplier_name,
46
+ CASE
47
+ WHEN p.stock_quantity = 0 THEN 'Нет в наличии'
48
+ WHEN p.stock_quantity <= 5 THEN 'Мало в наличии'
49
+ ELSE 'В наличии'
50
+ END AS availability_status
51
+ FROM products p
52
+ LEFT JOIN categories c ON p.id_category = c.id_category
53
+ LEFT JOIN manufacturers m ON p.id_manufacturer = m.id_manufacturer
54
+ LEFT JOIN suppliers s ON p.id_supplier = s.id_supplier
55
+ """
56
+
57
+ def __init__(self, config: AppConfig):
58
+ self.config = config
59
+
60
+ def _connect(self) -> mysql.connector.MySQLConnection:
61
+ try:
62
+ return mysql.connector.connect(
63
+ host=self.config.host,
64
+ port=self.config.port,
65
+ user=self.config.user,
66
+ password=self.config.password,
67
+ database=self.config.database,
68
+ charset="utf8mb4",
69
+ use_unicode=True,
70
+ )
71
+ except MySQLError as exc:
72
+ raise DatabaseError(f"Не удалось подключиться к MySQL: {exc}") from exc
73
+
74
+ @contextmanager
75
+ def connection(self) -> Generator[mysql.connector.MySQLConnection, None, None]:
76
+ conn = self._connect()
77
+ try:
78
+ yield conn
79
+ finally:
80
+ conn.close()
81
+
82
+ @contextmanager
83
+ def cursor(self, dictionary: bool = False) -> Generator[Any, None, None]:
84
+ with self.connection() as conn:
85
+ cur = conn.cursor(dictionary=dictionary)
86
+ try:
87
+ yield cur
88
+ conn.commit()
89
+ except Exception:
90
+ conn.rollback()
91
+ raise
92
+ finally:
93
+ cur.close()
94
+
95
+ def test_connection(self) -> bool:
96
+ with self.connection() as conn:
97
+ return conn.is_connected()
98
+
99
+ def fetch_all(self, query: str, params: Iterable[Any] | None = None) -> list[dict[str, Any]]:
100
+ with self.cursor() as cur:
101
+ cur.execute(query, params or ())
102
+ rows = cur.fetchall()
103
+ return [_row_to_dict(cur, row) for row in rows]
104
+
105
+ def fetch_one(self, query: str, params: Iterable[Any] | None = None) -> dict[str, Any] | None:
106
+ rows = self.fetch_all(query, params)
107
+ return rows[0] if rows else None
108
+
109
+ def execute(self, query: str, params: Iterable[Any] | None = None) -> int:
110
+ with self.cursor() as cur:
111
+ cur.execute(query, params or ())
112
+ return cur.rowcount
113
+
114
+ # --- Пользователи ---
115
+
116
+ def authenticate(self, login: str, password: str) -> dict[str, Any] | None:
117
+ query = """
118
+ SELECT
119
+ u.id_user, u.login, u.first_name, u.last_name, u.middle_name,
120
+ r.id_role, r.role_name
121
+ FROM users u
122
+ JOIN roles r ON u.id_role = r.id_role
123
+ WHERE u.login = %s AND u.password_hash = %s
124
+ """
125
+ return self.fetch_one(query, (login, password))
126
+
127
+ # --- Товары ---
128
+
129
+ def get_products(self) -> list[dict[str, Any]]:
130
+ return self.fetch_all(self.PRODUCTS_QUERY + " ORDER BY p.product_name")
131
+
132
+ def get_product(self, product_id: int) -> dict[str, Any] | None:
133
+ return self.fetch_one(
134
+ self.PRODUCTS_QUERY + " WHERE p.id_product = %s",
135
+ (product_id,),
136
+ )
137
+
138
+ def create_product(self, data: dict[str, Any]) -> int:
139
+ query = """
140
+ INSERT INTO products (
141
+ article_number, product_name, description, price,
142
+ discount_percent, unit, stock_quantity, photo_url,
143
+ id_category, id_manufacturer, id_supplier
144
+ ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
145
+ """
146
+ params = (
147
+ data["article_number"],
148
+ data["product_name"],
149
+ data.get("description"),
150
+ data["price"],
151
+ data.get("discount_percent", 0),
152
+ data.get("unit", "шт."),
153
+ data["stock_quantity"],
154
+ data.get("photo_url"),
155
+ data["id_category"],
156
+ data["id_manufacturer"],
157
+ data["id_supplier"],
158
+ )
159
+ with self.cursor() as cur:
160
+ cur.execute(query, params)
161
+ return int(cur.lastrowid)
162
+
163
+ def update_product(self, product_id: int, data: dict[str, Any]) -> bool:
164
+ query = """
165
+ UPDATE products SET
166
+ article_number = %s, product_name = %s, description = %s,
167
+ price = %s, discount_percent = %s, unit = %s,
168
+ stock_quantity = %s, photo_url = %s,
169
+ id_category = %s, id_manufacturer = %s, id_supplier = %s
170
+ WHERE id_product = %s
171
+ """
172
+ params = (
173
+ data["article_number"],
174
+ data["product_name"],
175
+ data.get("description"),
176
+ data["price"],
177
+ data.get("discount_percent", 0),
178
+ data.get("unit", "шт."),
179
+ data["stock_quantity"],
180
+ data.get("photo_url"),
181
+ data["id_category"],
182
+ data["id_manufacturer"],
183
+ data["id_supplier"],
184
+ product_id,
185
+ )
186
+ return self.execute(query, params) > 0
187
+
188
+ def delete_product(self, product_id: int) -> bool:
189
+ return self.execute("DELETE FROM products WHERE id_product = %s", (product_id,)) > 0
190
+
191
+ def product_in_orders(self, product_id: int) -> bool:
192
+ row = self.fetch_one(
193
+ "SELECT COUNT(*) AS cnt FROM order_items WHERE id_product = %s",
194
+ (product_id,),
195
+ )
196
+ return bool(row and int(row["cnt"]) > 0)
197
+
198
+ # --- Справочники ---
199
+
200
+ def get_categories(self) -> list[dict[str, Any]]:
201
+ return self.fetch_all("SELECT id_category, category_name FROM categories ORDER BY category_name")
202
+
203
+ def get_manufacturers(self) -> list[dict[str, Any]]:
204
+ return self.fetch_all(
205
+ "SELECT id_manufacturer, manufacturer_name FROM manufacturers ORDER BY manufacturer_name"
206
+ )
207
+
208
+ def get_suppliers(self) -> list[dict[str, Any]]:
209
+ return self.fetch_all(
210
+ "SELECT id_supplier, supplier_name FROM suppliers ORDER BY supplier_name"
211
+ )
deskkit/exceptions.py ADDED
@@ -0,0 +1,17 @@
1
+ """Исключения библиотеки."""
2
+
3
+
4
+ class DeskKitError(Exception):
5
+ """Базовая ошибка DeskKit."""
6
+
7
+
8
+ class DatabaseError(DeskKitError):
9
+ """Ошибка работы с базой данных."""
10
+
11
+
12
+ class ValidationError(DeskKitError):
13
+ """Ошибка валидации данных."""
14
+
15
+
16
+ class AuthError(DeskKitError):
17
+ """Ошибка авторизации."""
@@ -0,0 +1,3 @@
1
+ from deskkit.generator.export import export_project, run_generated
2
+
3
+ __all__ = ["export_project", "run_generated"]