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 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 вручную.
@@ -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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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