pyflashdb 1.0.1__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.
- flash/__init__.py +10 -0
- flash/adapters/__init__.py +0 -0
- flash/adapters/mongo_adapter.py +247 -0
- flash/adapters/mysql_adapter.py +217 -0
- flash/adapters/postgres_adapter.py +226 -0
- flash/core.py +306 -0
- flash/exceptions.py +33 -0
- flash/filters.py +140 -0
- flash/triggers.py +148 -0
- pyflashdb-1.0.1.dist-info/METADATA +34 -0
- pyflashdb-1.0.1.dist-info/RECORD +13 -0
- pyflashdb-1.0.1.dist-info/WHEEL +5 -0
- pyflashdb-1.0.1.dist-info/top_level.txt +1 -0
flash/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Flash - A unified database interface for MySQL, PostgreSQL, and MongoDB.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .core import FlashDB
|
|
6
|
+
from .exceptions import FlashError, ConnectionError, QueryError
|
|
7
|
+
|
|
8
|
+
__version__ = "1.0.0"
|
|
9
|
+
__author__ = "Flash Team"
|
|
10
|
+
__all__ = ["FlashDB", "FlashError", "ConnectionError", "QueryError"]
|
|
File without changes
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MongoDB adapter for Flash.
|
|
3
|
+
Requires: pip install pymongo
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
from ..filters import filters_to_mongo
|
|
8
|
+
from ..exceptions import ConnectionError, QueryError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MongoAdapter:
|
|
12
|
+
"""
|
|
13
|
+
MongoDB backend adapter for FlashDB.
|
|
14
|
+
Uses pymongo under the hood.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, config: dict):
|
|
18
|
+
self.config = config
|
|
19
|
+
self.client = None
|
|
20
|
+
self.db = None
|
|
21
|
+
self._connect()
|
|
22
|
+
|
|
23
|
+
def _connect(self):
|
|
24
|
+
try:
|
|
25
|
+
from pymongo import MongoClient
|
|
26
|
+
from pymongo.errors import ConnectionFailure
|
|
27
|
+
|
|
28
|
+
uri = self.config.get("uri")
|
|
29
|
+
if uri:
|
|
30
|
+
self.client = MongoClient(uri)
|
|
31
|
+
else:
|
|
32
|
+
host = self.config.get("host", "localhost")
|
|
33
|
+
port = self.config.get("port", 27017)
|
|
34
|
+
user = self.config.get("user")
|
|
35
|
+
password = self.config.get("password")
|
|
36
|
+
|
|
37
|
+
if user and password:
|
|
38
|
+
self.client = MongoClient(
|
|
39
|
+
host=host, port=port, username=user, password=password
|
|
40
|
+
)
|
|
41
|
+
else:
|
|
42
|
+
self.client = MongoClient(host=host, port=port)
|
|
43
|
+
|
|
44
|
+
db_name = self.config.get("database", "flash_db")
|
|
45
|
+
self.db = self.client[db_name]
|
|
46
|
+
|
|
47
|
+
# Test connection
|
|
48
|
+
self.client.admin.command("ping")
|
|
49
|
+
|
|
50
|
+
except ImportError:
|
|
51
|
+
raise ConnectionError(
|
|
52
|
+
"pymongo is not installed. Run: pip install pymongo"
|
|
53
|
+
)
|
|
54
|
+
except Exception as e:
|
|
55
|
+
raise ConnectionError(f"MongoDB connection failed: {e}")
|
|
56
|
+
|
|
57
|
+
def _col(self, table: str):
|
|
58
|
+
"""Return the pymongo Collection for the given table/collection name."""
|
|
59
|
+
return self.db[table]
|
|
60
|
+
|
|
61
|
+
def _clean(self, doc) -> dict:
|
|
62
|
+
"""Convert ObjectId to string for clean output."""
|
|
63
|
+
if doc and "_id" in doc:
|
|
64
|
+
doc["_id"] = str(doc["_id"])
|
|
65
|
+
return doc
|
|
66
|
+
|
|
67
|
+
# ── Core CRUD ──────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
def all(self, table: str) -> List[dict]:
|
|
70
|
+
return [self._clean(d) for d in self._col(table).find()]
|
|
71
|
+
|
|
72
|
+
def select(self, table: str, fields: List[str] = None, filters: dict = None,
|
|
73
|
+
limit: int = None, offset: int = None, order_by: str = None) -> List[dict]:
|
|
74
|
+
query = filters_to_mongo(filters) if filters else {}
|
|
75
|
+
projection = {f: 1 for f in fields} if fields else None
|
|
76
|
+
|
|
77
|
+
cursor = self._col(table).find(query, projection)
|
|
78
|
+
|
|
79
|
+
if order_by:
|
|
80
|
+
# Support "field" (asc) or "-field" (desc)
|
|
81
|
+
if order_by.startswith("-"):
|
|
82
|
+
cursor = cursor.sort(order_by[1:], -1)
|
|
83
|
+
else:
|
|
84
|
+
cursor = cursor.sort(order_by, 1)
|
|
85
|
+
|
|
86
|
+
if offset:
|
|
87
|
+
cursor = cursor.skip(int(offset))
|
|
88
|
+
if limit:
|
|
89
|
+
cursor = cursor.limit(int(limit))
|
|
90
|
+
|
|
91
|
+
return [self._clean(d) for d in cursor]
|
|
92
|
+
|
|
93
|
+
def add(self, table: str, data: dict) -> str:
|
|
94
|
+
try:
|
|
95
|
+
result = self._col(table).insert_one(data.copy())
|
|
96
|
+
return str(result.inserted_id)
|
|
97
|
+
except Exception as e:
|
|
98
|
+
raise QueryError(f"MongoDB insert failed: {e}")
|
|
99
|
+
|
|
100
|
+
def bulk_insert(self, table: str, records: List[dict]) -> int:
|
|
101
|
+
if not records:
|
|
102
|
+
return 0
|
|
103
|
+
try:
|
|
104
|
+
result = self._col(table).insert_many([r.copy() for r in records])
|
|
105
|
+
return len(result.inserted_ids)
|
|
106
|
+
except Exception as e:
|
|
107
|
+
raise QueryError(f"MongoDB bulk insert failed: {e}")
|
|
108
|
+
|
|
109
|
+
def update(self, table: str, filters: dict, data: dict) -> int:
|
|
110
|
+
query = filters_to_mongo(filters)
|
|
111
|
+
try:
|
|
112
|
+
result = self._col(table).update_many(query, {"$set": data})
|
|
113
|
+
return result.modified_count
|
|
114
|
+
except Exception as e:
|
|
115
|
+
raise QueryError(f"MongoDB update failed: {e}")
|
|
116
|
+
|
|
117
|
+
def delete(self, table: str, filters: dict = None) -> int:
|
|
118
|
+
query = filters_to_mongo(filters) if filters else {}
|
|
119
|
+
try:
|
|
120
|
+
result = self._col(table).delete_many(query)
|
|
121
|
+
return result.deleted_count
|
|
122
|
+
except Exception as e:
|
|
123
|
+
raise QueryError(f"MongoDB delete failed: {e}")
|
|
124
|
+
|
|
125
|
+
# ── Schema / Collection Operations ─────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
def create_table(self, table: str, schema: dict = None, primary_key: str = "id") -> bool:
|
|
128
|
+
"""
|
|
129
|
+
In MongoDB, collections are created implicitly.
|
|
130
|
+
This optionally applies a JSON Schema validator.
|
|
131
|
+
"""
|
|
132
|
+
if schema:
|
|
133
|
+
props = {}
|
|
134
|
+
for field, typ in schema.items():
|
|
135
|
+
bson_type = self._py_to_bson(typ)
|
|
136
|
+
props[field] = {"bsonType": bson_type}
|
|
137
|
+
|
|
138
|
+
validator = {
|
|
139
|
+
"$jsonSchema": {
|
|
140
|
+
"bsonType": "object",
|
|
141
|
+
"properties": props,
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
try:
|
|
145
|
+
self.db.create_collection(table, validator=validator)
|
|
146
|
+
except Exception:
|
|
147
|
+
# Collection may already exist; apply collMod instead
|
|
148
|
+
self.db.command("collMod", table, validator=validator)
|
|
149
|
+
else:
|
|
150
|
+
if table not in self.db.list_collection_names():
|
|
151
|
+
self.db.create_collection(table)
|
|
152
|
+
return True
|
|
153
|
+
|
|
154
|
+
def drop_table(self, table: str) -> bool:
|
|
155
|
+
self._col(table).drop()
|
|
156
|
+
return True
|
|
157
|
+
|
|
158
|
+
def truncate(self, table: str) -> bool:
|
|
159
|
+
self._col(table).delete_many({})
|
|
160
|
+
return True
|
|
161
|
+
|
|
162
|
+
def show_tables(self) -> List[str]:
|
|
163
|
+
return self.db.list_collection_names()
|
|
164
|
+
|
|
165
|
+
def describe(self, table: str) -> dict:
|
|
166
|
+
"""Return collection stats as schema info."""
|
|
167
|
+
return self.db.command("collStats", table)
|
|
168
|
+
|
|
169
|
+
def create_index(self, table: str, field: str, unique: bool = False) -> str:
|
|
170
|
+
from pymongo import ASCENDING
|
|
171
|
+
result = self._col(table).create_index([(field, ASCENDING)], unique=unique)
|
|
172
|
+
return result
|
|
173
|
+
|
|
174
|
+
# ── Transactions (MongoDB 4.0+ with replica set) ───────────────────────
|
|
175
|
+
|
|
176
|
+
def begin(self):
|
|
177
|
+
self._session = self.client.start_session()
|
|
178
|
+
self._session.start_transaction()
|
|
179
|
+
|
|
180
|
+
def commit(self):
|
|
181
|
+
if hasattr(self, "_session"):
|
|
182
|
+
self._session.commit_transaction()
|
|
183
|
+
self._session.end_session()
|
|
184
|
+
|
|
185
|
+
def rollback(self):
|
|
186
|
+
if hasattr(self, "_session"):
|
|
187
|
+
self._session.abort_transaction()
|
|
188
|
+
self._session.end_session()
|
|
189
|
+
|
|
190
|
+
# ── Raw Query ──────────────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
def raw(self, command: dict, table: str = None) -> Any:
|
|
193
|
+
"""
|
|
194
|
+
Execute a raw MongoDB command.
|
|
195
|
+
For collection-level ops pass table; otherwise it runs on the DB.
|
|
196
|
+
"""
|
|
197
|
+
try:
|
|
198
|
+
if table:
|
|
199
|
+
return list(self._col(table).find(command))
|
|
200
|
+
return self.db.command(command)
|
|
201
|
+
except Exception as e:
|
|
202
|
+
raise QueryError(f"MongoDB raw command failed: {e}")
|
|
203
|
+
|
|
204
|
+
# ── Count ──────────────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
def count(self, table: str, filters: dict = None) -> int:
|
|
207
|
+
query = filters_to_mongo(filters) if filters else {}
|
|
208
|
+
return self._col(table).count_documents(query)
|
|
209
|
+
|
|
210
|
+
# ── Aggregation ────────────────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
def aggregate(self, table: str, pipeline: list) -> List[dict]:
|
|
213
|
+
return [self._clean(d) for d in self._col(table).aggregate(pipeline)]
|
|
214
|
+
|
|
215
|
+
# ── Lifecycle ──────────────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
def close(self):
|
|
218
|
+
if self.client:
|
|
219
|
+
self.client.close()
|
|
220
|
+
|
|
221
|
+
def ping(self) -> bool:
|
|
222
|
+
try:
|
|
223
|
+
self.client.admin.command("ping")
|
|
224
|
+
return True
|
|
225
|
+
except Exception:
|
|
226
|
+
return False
|
|
227
|
+
|
|
228
|
+
# ── Helpers ────────────────────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
def _py_to_bson(self, type_str: str) -> str:
|
|
231
|
+
mapping = {
|
|
232
|
+
"int": "int",
|
|
233
|
+
"integer": "int",
|
|
234
|
+
"str": "string",
|
|
235
|
+
"string": "string",
|
|
236
|
+
"text": "string",
|
|
237
|
+
"float": "double",
|
|
238
|
+
"double": "double",
|
|
239
|
+
"bool": "bool",
|
|
240
|
+
"boolean": "bool",
|
|
241
|
+
"date": "date",
|
|
242
|
+
"datetime": "date",
|
|
243
|
+
"list": "array",
|
|
244
|
+
"dict": "object",
|
|
245
|
+
"json": "object",
|
|
246
|
+
}
|
|
247
|
+
return mapping.get(type_str.lower(), "string")
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MySQL adapter for Flash.
|
|
3
|
+
Requires: pip install mysql-connector-python
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
from ..filters import filters_to_sql, build_update_sql, python_type_to_sql
|
|
8
|
+
from ..exceptions import ConnectionError, QueryError, TransactionError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MySQLAdapter:
|
|
12
|
+
"""
|
|
13
|
+
MySQL backend adapter for FlashDB.
|
|
14
|
+
Uses mysql-connector-python under the hood.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, config: dict):
|
|
18
|
+
self.config = config
|
|
19
|
+
self.conn = None
|
|
20
|
+
self.cursor = None
|
|
21
|
+
self._in_transaction = False
|
|
22
|
+
self._connect()
|
|
23
|
+
|
|
24
|
+
def _connect(self):
|
|
25
|
+
try:
|
|
26
|
+
import mysql.connector
|
|
27
|
+
self.conn = mysql.connector.connect(
|
|
28
|
+
host=self.config.get("host", "localhost"),
|
|
29
|
+
port=self.config.get("port", 3306),
|
|
30
|
+
user=self.config.get("user", "root"),
|
|
31
|
+
password=self.config.get("password", ""),
|
|
32
|
+
database=self.config.get("database"),
|
|
33
|
+
autocommit=True,
|
|
34
|
+
)
|
|
35
|
+
self.cursor = self.conn.cursor(dictionary=True)
|
|
36
|
+
except ImportError:
|
|
37
|
+
raise ConnectionError(
|
|
38
|
+
"mysql-connector-python is not installed. Run: pip install mysql-connector-python"
|
|
39
|
+
)
|
|
40
|
+
except Exception as e:
|
|
41
|
+
raise ConnectionError(f"MySQL connection failed: {e}")
|
|
42
|
+
|
|
43
|
+
def _execute(self, sql: str, params: list = None) -> Any:
|
|
44
|
+
try:
|
|
45
|
+
self.cursor.execute(sql, params or [])
|
|
46
|
+
return self.cursor
|
|
47
|
+
except Exception as e:
|
|
48
|
+
raise QueryError(f"MySQL query failed: {e}\nSQL: {sql}\nParams: {params}")
|
|
49
|
+
|
|
50
|
+
# ── Core CRUD ──────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
def all(self, table: str) -> List[dict]:
|
|
53
|
+
self._execute(f"SELECT * FROM `{table}`")
|
|
54
|
+
return self.cursor.fetchall()
|
|
55
|
+
|
|
56
|
+
def select(self, table: str, fields: List[str] = None, filters: dict = None,
|
|
57
|
+
limit: int = None, offset: int = None, order_by: str = None) -> List[dict]:
|
|
58
|
+
cols = ", ".join(f"`{f}`" for f in fields) if fields else "*"
|
|
59
|
+
sql = f"SELECT {cols} FROM `{table}`"
|
|
60
|
+
params = []
|
|
61
|
+
|
|
62
|
+
if filters:
|
|
63
|
+
clause, params = filters_to_sql(filters)
|
|
64
|
+
sql += f" WHERE {clause}"
|
|
65
|
+
|
|
66
|
+
if order_by:
|
|
67
|
+
sql += f" ORDER BY {order_by}"
|
|
68
|
+
|
|
69
|
+
if limit is not None:
|
|
70
|
+
sql += f" LIMIT {int(limit)}"
|
|
71
|
+
if offset is not None:
|
|
72
|
+
sql += f" OFFSET {int(offset)}"
|
|
73
|
+
|
|
74
|
+
self._execute(sql, params)
|
|
75
|
+
return self.cursor.fetchall()
|
|
76
|
+
|
|
77
|
+
def add(self, table: str, data: dict) -> int:
|
|
78
|
+
fields = ", ".join(f"`{k}`" for k in data.keys())
|
|
79
|
+
placeholders = ", ".join(["%s"] * len(data))
|
|
80
|
+
sql = f"INSERT INTO `{table}` ({fields}) VALUES ({placeholders})"
|
|
81
|
+
self._execute(sql, list(data.values()))
|
|
82
|
+
if not self._in_transaction:
|
|
83
|
+
self.conn.commit()
|
|
84
|
+
return self.cursor.lastrowid
|
|
85
|
+
|
|
86
|
+
def bulk_insert(self, table: str, records: List[dict]) -> int:
|
|
87
|
+
if not records:
|
|
88
|
+
return 0
|
|
89
|
+
fields = ", ".join(f"`{k}`" for k in records[0].keys())
|
|
90
|
+
placeholders = ", ".join(["%s"] * len(records[0]))
|
|
91
|
+
sql = f"INSERT INTO `{table}` ({fields}) VALUES ({placeholders})"
|
|
92
|
+
rows = [list(r.values()) for r in records]
|
|
93
|
+
try:
|
|
94
|
+
self.cursor.executemany(sql, rows)
|
|
95
|
+
if not self._in_transaction:
|
|
96
|
+
self.conn.commit()
|
|
97
|
+
return self.cursor.rowcount
|
|
98
|
+
except Exception as e:
|
|
99
|
+
raise QueryError(f"MySQL bulk insert failed: {e}")
|
|
100
|
+
|
|
101
|
+
def update(self, table: str, filters: dict, data: dict) -> int:
|
|
102
|
+
set_clause, set_params = build_update_sql(data)
|
|
103
|
+
where_clause, where_params = filters_to_sql(filters)
|
|
104
|
+
sql = f"UPDATE `{table}` SET {set_clause}"
|
|
105
|
+
if where_clause:
|
|
106
|
+
sql += f" WHERE {where_clause}"
|
|
107
|
+
self._execute(sql, set_params + where_params)
|
|
108
|
+
if not self._in_transaction:
|
|
109
|
+
self.conn.commit()
|
|
110
|
+
return self.cursor.rowcount
|
|
111
|
+
|
|
112
|
+
def delete(self, table: str, filters: dict = None) -> int:
|
|
113
|
+
sql = f"DELETE FROM `{table}`"
|
|
114
|
+
params = []
|
|
115
|
+
if filters:
|
|
116
|
+
clause, params = filters_to_sql(filters)
|
|
117
|
+
sql += f" WHERE {clause}"
|
|
118
|
+
self._execute(sql, params)
|
|
119
|
+
if not self._in_transaction:
|
|
120
|
+
self.conn.commit()
|
|
121
|
+
return self.cursor.rowcount
|
|
122
|
+
|
|
123
|
+
# ── Schema Operations ──────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
def create_table(self, table: str, schema: dict, primary_key: str = "id") -> bool:
|
|
126
|
+
col_defs = []
|
|
127
|
+
has_pk = False
|
|
128
|
+
for col, typ in schema.items():
|
|
129
|
+
sql_type = python_type_to_sql(typ, "mysql")
|
|
130
|
+
if col == primary_key:
|
|
131
|
+
col_defs.append(f"`{col}` {sql_type} AUTO_INCREMENT PRIMARY KEY")
|
|
132
|
+
has_pk = True
|
|
133
|
+
else:
|
|
134
|
+
col_defs.append(f"`{col}` {sql_type}")
|
|
135
|
+
if not has_pk:
|
|
136
|
+
col_defs.insert(0, f"`{primary_key}` INT AUTO_INCREMENT PRIMARY KEY")
|
|
137
|
+
|
|
138
|
+
sql = f"CREATE TABLE IF NOT EXISTS `{table}` ({', '.join(col_defs)})"
|
|
139
|
+
self._execute(sql)
|
|
140
|
+
return True
|
|
141
|
+
|
|
142
|
+
def drop_table(self, table: str) -> bool:
|
|
143
|
+
self._execute(f"DROP TABLE IF EXISTS `{table}`")
|
|
144
|
+
return True
|
|
145
|
+
|
|
146
|
+
def truncate(self, table: str) -> bool:
|
|
147
|
+
self._execute(f"TRUNCATE TABLE `{table}`")
|
|
148
|
+
return True
|
|
149
|
+
|
|
150
|
+
def show_tables(self) -> List[str]:
|
|
151
|
+
self._execute("SHOW TABLES")
|
|
152
|
+
rows = self.cursor.fetchall()
|
|
153
|
+
return [list(r.values())[0] for r in rows]
|
|
154
|
+
|
|
155
|
+
def describe(self, table: str) -> List[dict]:
|
|
156
|
+
self._execute(f"DESCRIBE `{table}`")
|
|
157
|
+
return self.cursor.fetchall()
|
|
158
|
+
|
|
159
|
+
# ── Transactions ───────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
def begin(self):
|
|
162
|
+
self.conn.autocommit = False
|
|
163
|
+
self._in_transaction = True
|
|
164
|
+
|
|
165
|
+
def commit(self):
|
|
166
|
+
try:
|
|
167
|
+
self.conn.commit()
|
|
168
|
+
except Exception as e:
|
|
169
|
+
raise TransactionError(f"Commit failed: {e}")
|
|
170
|
+
finally:
|
|
171
|
+
self.conn.autocommit = True
|
|
172
|
+
self._in_transaction = False
|
|
173
|
+
|
|
174
|
+
def rollback(self):
|
|
175
|
+
try:
|
|
176
|
+
self.conn.rollback()
|
|
177
|
+
except Exception as e:
|
|
178
|
+
raise TransactionError(f"Rollback failed: {e}")
|
|
179
|
+
finally:
|
|
180
|
+
self.conn.autocommit = True
|
|
181
|
+
self._in_transaction = False
|
|
182
|
+
|
|
183
|
+
# ── Raw Query ──────────────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
def raw(self, sql: str, params: list = None) -> Any:
|
|
186
|
+
self._execute(sql, params)
|
|
187
|
+
try:
|
|
188
|
+
return self.cursor.fetchall()
|
|
189
|
+
except Exception:
|
|
190
|
+
return self.cursor.rowcount
|
|
191
|
+
|
|
192
|
+
# ── Count ──────────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
def count(self, table: str, filters: dict = None) -> int:
|
|
195
|
+
sql = f"SELECT COUNT(*) as cnt FROM `{table}`"
|
|
196
|
+
params = []
|
|
197
|
+
if filters:
|
|
198
|
+
clause, params = filters_to_sql(filters)
|
|
199
|
+
sql += f" WHERE {clause}"
|
|
200
|
+
self._execute(sql, params)
|
|
201
|
+
row = self.cursor.fetchone()
|
|
202
|
+
return row["cnt"] if row else 0
|
|
203
|
+
|
|
204
|
+
# ── Lifecycle ──────────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
def close(self):
|
|
207
|
+
if self.cursor:
|
|
208
|
+
self.cursor.close()
|
|
209
|
+
if self.conn:
|
|
210
|
+
self.conn.close()
|
|
211
|
+
|
|
212
|
+
def ping(self) -> bool:
|
|
213
|
+
try:
|
|
214
|
+
self.conn.ping(reconnect=True)
|
|
215
|
+
return True
|
|
216
|
+
except Exception:
|
|
217
|
+
return False
|