sqlite3-client 1.0.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.
- sqlite3_client/client.py +642 -0
- sqlite3_client/query_by_key/query.py +52 -0
- sqlite3_client/query_by_key/query_util.py +164 -0
- sqlite3_client/query_by_key/settings.py +23 -0
- sqlite3_client/settings.py +65 -0
- sqlite3_client-1.0.0.dist-info/METADATA +422 -0
- sqlite3_client-1.0.0.dist-info/RECORD +10 -0
- sqlite3_client-1.0.0.dist-info/WHEEL +5 -0
- sqlite3_client-1.0.0.dist-info/licenses/LICENSE +21 -0
- sqlite3_client-1.0.0.dist-info/top_level.txt +1 -0
sqlite3_client/client.py
ADDED
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
"""database client"""
|
|
2
|
+
|
|
3
|
+
import atexit
|
|
4
|
+
import csv
|
|
5
|
+
import io
|
|
6
|
+
import queue
|
|
7
|
+
import sqlite3
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
from collections.abc import AsyncGenerator, Generator
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
|
|
13
|
+
from .query_by_key.query import Query
|
|
14
|
+
|
|
15
|
+
# pylint: disable=relative-beyond-top-level
|
|
16
|
+
from .query_by_key.query_util import (
|
|
17
|
+
get_query_with_value,
|
|
18
|
+
)
|
|
19
|
+
from .query_by_key.settings import Settings as QrySettings
|
|
20
|
+
from .settings import Settings
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class RealDictRow(dict):
|
|
24
|
+
"""RealDictRow mimicking psycopg2.extras.RealDictRow"""
|
|
25
|
+
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class RealDictCursor(sqlite3.Cursor):
|
|
30
|
+
"""RealDictCursor mimicking psycopg2.extras.RealDictCursor"""
|
|
31
|
+
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
connection = sqlite3.Connection
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def dict_factory(cursor: sqlite3.Cursor, row: tuple) -> RealDictRow:
|
|
39
|
+
"""Row factory that returns rows as RealDictRow (dict subclass)"""
|
|
40
|
+
fields = [col[0] for col in cursor.description]
|
|
41
|
+
return RealDictRow(zip(fields, row))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ClientPool:
|
|
45
|
+
"""database connection pool"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, db_settings_pool: Settings):
|
|
48
|
+
self.db_settings_pool = db_settings_pool
|
|
49
|
+
self._lock = threading.Lock()
|
|
50
|
+
self._pool: queue.Queue[sqlite3.Connection] = queue.Queue(
|
|
51
|
+
maxsize=db_settings_pool.maxconn
|
|
52
|
+
)
|
|
53
|
+
self._all_conns: list[sqlite3.Connection] = []
|
|
54
|
+
self._closed = False
|
|
55
|
+
|
|
56
|
+
for _ in range(db_settings_pool.minconn):
|
|
57
|
+
conn = self._create_connection()
|
|
58
|
+
self._pool.put(conn)
|
|
59
|
+
self._all_conns.append(conn)
|
|
60
|
+
|
|
61
|
+
print(datetime.now(), self.__class__.__name__, self.__init__.__name__)
|
|
62
|
+
|
|
63
|
+
def _create_connection(self) -> sqlite3.Connection:
|
|
64
|
+
uri = (
|
|
65
|
+
self.db_settings_pool.database.startswith("file:")
|
|
66
|
+
or "?" in self.db_settings_pool.database
|
|
67
|
+
)
|
|
68
|
+
timeout = getattr(
|
|
69
|
+
self.db_settings_pool,
|
|
70
|
+
"timeout",
|
|
71
|
+
self.db_settings_pool.connect_timeout,
|
|
72
|
+
)
|
|
73
|
+
conn = sqlite3.connect(
|
|
74
|
+
self.db_settings_pool.database,
|
|
75
|
+
timeout=timeout,
|
|
76
|
+
check_same_thread=False,
|
|
77
|
+
uri=uri,
|
|
78
|
+
)
|
|
79
|
+
conn.row_factory = dict_factory
|
|
80
|
+
if not self.db_settings_pool.database.startswith(":memory:"):
|
|
81
|
+
try:
|
|
82
|
+
conn.execute("PRAGMA journal_mode=WAL;")
|
|
83
|
+
except Exception:
|
|
84
|
+
pass
|
|
85
|
+
return conn
|
|
86
|
+
|
|
87
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
88
|
+
"""Close the shared connection pool."""
|
|
89
|
+
self.closeall()
|
|
90
|
+
print(datetime.now(), self.__class__.__name__, self.__exit__.__name__)
|
|
91
|
+
|
|
92
|
+
def getconn(self) -> sqlite3.Connection:
|
|
93
|
+
"""return conn_pool"""
|
|
94
|
+
with self._lock:
|
|
95
|
+
if self._closed:
|
|
96
|
+
raise RuntimeError("Connection pool is closed")
|
|
97
|
+
try:
|
|
98
|
+
return self._pool.get_nowait()
|
|
99
|
+
except queue.Empty:
|
|
100
|
+
if len(self._all_conns) < self.db_settings_pool.maxconn:
|
|
101
|
+
conn = self._create_connection()
|
|
102
|
+
self._all_conns.append(conn)
|
|
103
|
+
return conn
|
|
104
|
+
try:
|
|
105
|
+
return self._pool.get(timeout=self.db_settings_pool.connect_timeout)
|
|
106
|
+
except queue.Empty:
|
|
107
|
+
raise TimeoutError("Timeout waiting for connection from pool") from None
|
|
108
|
+
|
|
109
|
+
def putconn(self, conn: sqlite3.Connection):
|
|
110
|
+
"""putconn"""
|
|
111
|
+
with self._lock:
|
|
112
|
+
if self._closed:
|
|
113
|
+
try:
|
|
114
|
+
conn.close()
|
|
115
|
+
except Exception:
|
|
116
|
+
pass
|
|
117
|
+
return
|
|
118
|
+
try:
|
|
119
|
+
self._pool.put_nowait(conn)
|
|
120
|
+
except queue.Full:
|
|
121
|
+
try:
|
|
122
|
+
conn.close()
|
|
123
|
+
except Exception:
|
|
124
|
+
pass
|
|
125
|
+
if conn in self._all_conns:
|
|
126
|
+
self._all_conns.remove(conn)
|
|
127
|
+
|
|
128
|
+
def closeall(self):
|
|
129
|
+
"""close all connections in pool"""
|
|
130
|
+
with self._lock:
|
|
131
|
+
self._closed = True
|
|
132
|
+
for conn in self._all_conns:
|
|
133
|
+
try:
|
|
134
|
+
conn.close()
|
|
135
|
+
except Exception:
|
|
136
|
+
pass
|
|
137
|
+
self._all_conns.clear()
|
|
138
|
+
while not self._pool.empty():
|
|
139
|
+
try:
|
|
140
|
+
self._pool.get_nowait()
|
|
141
|
+
except queue.Empty:
|
|
142
|
+
break
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
db_set_and_pool: dict[str, ClientPool] = {}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class Client:
|
|
149
|
+
"""database client"""
|
|
150
|
+
|
|
151
|
+
# Class-level shared connection pool
|
|
152
|
+
_conn_pool: ClientPool
|
|
153
|
+
|
|
154
|
+
def __init__(self, db_settings: Settings):
|
|
155
|
+
# pylint:disable=global-statement,global-variable-not-assigned
|
|
156
|
+
global db_set_and_pool
|
|
157
|
+
|
|
158
|
+
self.conn: connection
|
|
159
|
+
self.in_with_block = False
|
|
160
|
+
self.db_settings = db_settings
|
|
161
|
+
self.qry = Query(
|
|
162
|
+
qry_settings=QrySettings(
|
|
163
|
+
use_en_ko_column_alias=db_settings.use_en_ko_column_alias,
|
|
164
|
+
use_conditional=db_settings.use_conditional,
|
|
165
|
+
all_query=db_settings.all_query,
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
self.query_recent = ""
|
|
169
|
+
|
|
170
|
+
db_set_key = db_settings.key
|
|
171
|
+
if db_set_key not in db_set_and_pool:
|
|
172
|
+
client_pool = ClientPool(db_settings)
|
|
173
|
+
db_set_and_pool[db_set_key] = client_pool
|
|
174
|
+
Client._conn_pool = client_pool
|
|
175
|
+
|
|
176
|
+
def __enter__(self):
|
|
177
|
+
# Called when entering the 'with' block
|
|
178
|
+
self.conn = Client._conn_pool.getconn()
|
|
179
|
+
self.in_with_block = True
|
|
180
|
+
return self
|
|
181
|
+
|
|
182
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
183
|
+
# Called when exiting the 'with' block
|
|
184
|
+
try:
|
|
185
|
+
if exc_type is None:
|
|
186
|
+
# No exception, commit the transaction
|
|
187
|
+
self.conn.commit()
|
|
188
|
+
else:
|
|
189
|
+
# Exception occurred, rollback the transaction
|
|
190
|
+
self.conn.rollback()
|
|
191
|
+
finally:
|
|
192
|
+
if self.conn:
|
|
193
|
+
self._conn_pool.putconn(self.conn)
|
|
194
|
+
|
|
195
|
+
self.in_with_block = False
|
|
196
|
+
|
|
197
|
+
def read_rows(
|
|
198
|
+
self,
|
|
199
|
+
qry_key: str,
|
|
200
|
+
params: dict,
|
|
201
|
+
*,
|
|
202
|
+
en: bool = False,
|
|
203
|
+
fetchone: bool = False,
|
|
204
|
+
) -> list[RealDictRow]:
|
|
205
|
+
"""Returns all rows
|
|
206
|
+
|
|
207
|
+
Arguments:
|
|
208
|
+
qry_key: Key of the Dictionary registered in the clients/queries folder
|
|
209
|
+
params: Key, Value pairs to pass as parameters to the SQL query.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
a List of Dictionaries;
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
def read_rows_by_param(
|
|
216
|
+
qry_key: str,
|
|
217
|
+
params: dict,
|
|
218
|
+
*,
|
|
219
|
+
en: bool = False,
|
|
220
|
+
fetchone: bool = False,
|
|
221
|
+
cursor: sqlite3.Cursor,
|
|
222
|
+
):
|
|
223
|
+
if not isinstance(params, dict):
|
|
224
|
+
params = vars(params)
|
|
225
|
+
|
|
226
|
+
qry_str = self.qry.get_query_by_key(qry_key, params, "read", en)
|
|
227
|
+
|
|
228
|
+
start = 0
|
|
229
|
+
if self.db_settings.before_read_execute:
|
|
230
|
+
self.db_settings.before_read_execute(
|
|
231
|
+
qry_key,
|
|
232
|
+
params,
|
|
233
|
+
qry_str,
|
|
234
|
+
get_query_with_value(qry_str, params),
|
|
235
|
+
)
|
|
236
|
+
start = time.time()
|
|
237
|
+
|
|
238
|
+
rows: list[RealDictRow] = []
|
|
239
|
+
cursor.execute(qry_str, params)
|
|
240
|
+
|
|
241
|
+
if not fetchone:
|
|
242
|
+
rows = cursor.fetchall()
|
|
243
|
+
else:
|
|
244
|
+
row = cursor.fetchone()
|
|
245
|
+
if row:
|
|
246
|
+
rows.append(row)
|
|
247
|
+
|
|
248
|
+
if self.db_settings.after_read_execute:
|
|
249
|
+
duration = int(round((time.time() - start) * 1000))
|
|
250
|
+
self.db_settings.after_read_execute(qry_key, duration)
|
|
251
|
+
|
|
252
|
+
if not rows:
|
|
253
|
+
return rows
|
|
254
|
+
|
|
255
|
+
return rows
|
|
256
|
+
|
|
257
|
+
rows: list[RealDictRow] = []
|
|
258
|
+
if self.in_with_block:
|
|
259
|
+
cursor = self.conn.cursor()
|
|
260
|
+
rows = read_rows_by_param(
|
|
261
|
+
qry_key,
|
|
262
|
+
params,
|
|
263
|
+
en=en,
|
|
264
|
+
fetchone=fetchone,
|
|
265
|
+
cursor=cursor,
|
|
266
|
+
)
|
|
267
|
+
else:
|
|
268
|
+
conn_pool = Client._conn_pool
|
|
269
|
+
conn = conn_pool.getconn()
|
|
270
|
+
cursor = conn.cursor()
|
|
271
|
+
try:
|
|
272
|
+
rows = read_rows_by_param(
|
|
273
|
+
qry_key,
|
|
274
|
+
params,
|
|
275
|
+
en=en,
|
|
276
|
+
fetchone=fetchone,
|
|
277
|
+
cursor=cursor,
|
|
278
|
+
)
|
|
279
|
+
finally:
|
|
280
|
+
cursor.close()
|
|
281
|
+
conn_pool.putconn(conn)
|
|
282
|
+
|
|
283
|
+
return rows
|
|
284
|
+
|
|
285
|
+
def read_row(
|
|
286
|
+
self,
|
|
287
|
+
qry_key: str,
|
|
288
|
+
params: dict,
|
|
289
|
+
*,
|
|
290
|
+
en: bool = False,
|
|
291
|
+
) -> RealDictRow | None:
|
|
292
|
+
"""call read_rows"""
|
|
293
|
+
|
|
294
|
+
rows = self.read_rows(
|
|
295
|
+
qry_key,
|
|
296
|
+
params,
|
|
297
|
+
en=en,
|
|
298
|
+
fetchone=True,
|
|
299
|
+
)
|
|
300
|
+
if not rows:
|
|
301
|
+
return None
|
|
302
|
+
|
|
303
|
+
return rows[0]
|
|
304
|
+
|
|
305
|
+
async def read_csv_partial_async(
|
|
306
|
+
self,
|
|
307
|
+
qry_key: str,
|
|
308
|
+
params: dict,
|
|
309
|
+
*,
|
|
310
|
+
row_count_partial: int = 100,
|
|
311
|
+
en: bool = False,
|
|
312
|
+
) -> AsyncGenerator[bytes]:
|
|
313
|
+
"""Return rows partially in batches with async
|
|
314
|
+
|
|
315
|
+
Arguments:
|
|
316
|
+
qry_key: key of the Dictionary registered in the clients/queries folder
|
|
317
|
+
params: key, value pairs to pass as parameters to the SQL query.
|
|
318
|
+
row_count_partial: Number of rows to return at a time
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
CSV format converted to UTF-8-BOM
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
async def read_csv_partial_async_by_param(
|
|
325
|
+
qry_key: str,
|
|
326
|
+
params: dict,
|
|
327
|
+
*,
|
|
328
|
+
row_count_partial: int = 100,
|
|
329
|
+
en: bool = False,
|
|
330
|
+
cursor: sqlite3.Cursor,
|
|
331
|
+
) -> AsyncGenerator[bytes]:
|
|
332
|
+
if not isinstance(params, dict):
|
|
333
|
+
params = vars(params)
|
|
334
|
+
|
|
335
|
+
qry_str = self.qry.get_query_by_key(qry_key, params, "csv", en)
|
|
336
|
+
|
|
337
|
+
is_second = False
|
|
338
|
+
|
|
339
|
+
# without UTF-8 BOM, hangul will be broken.
|
|
340
|
+
utf8_bom = b"\xef\xbb\xbf"
|
|
341
|
+
yield utf8_bom
|
|
342
|
+
|
|
343
|
+
start = 0
|
|
344
|
+
if self.db_settings.before_read_execute:
|
|
345
|
+
self.db_settings.before_read_execute(
|
|
346
|
+
qry_key,
|
|
347
|
+
params,
|
|
348
|
+
qry_str,
|
|
349
|
+
get_query_with_value(qry_str, params),
|
|
350
|
+
)
|
|
351
|
+
start = time.time()
|
|
352
|
+
|
|
353
|
+
cursor.execute(qry_str, params)
|
|
354
|
+
while True:
|
|
355
|
+
rows = (
|
|
356
|
+
cursor.fetchmany(row_count_partial)
|
|
357
|
+
if hasattr(cursor, "fetchmany")
|
|
358
|
+
else cursor.fetchall()
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
if not is_second:
|
|
362
|
+
if self.db_settings.after_read_execute:
|
|
363
|
+
duration = int(round((time.time() - start) * 1000))
|
|
364
|
+
self.db_settings.after_read_execute(qry_key, duration)
|
|
365
|
+
|
|
366
|
+
if not rows:
|
|
367
|
+
break
|
|
368
|
+
|
|
369
|
+
csv_out = io.StringIO()
|
|
370
|
+
csv_w = csv.writer(csv_out)
|
|
371
|
+
if not is_second and cursor.description:
|
|
372
|
+
column_names = [desc[0] for desc in cursor.description]
|
|
373
|
+
csv_w.writerow(column_names)
|
|
374
|
+
csv_w.writerows(
|
|
375
|
+
[list(r.values()) if isinstance(r, dict) else r for r in rows]
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
yield csv_out.getvalue().encode("utf-8")
|
|
379
|
+
|
|
380
|
+
is_second = True
|
|
381
|
+
|
|
382
|
+
if self.in_with_block:
|
|
383
|
+
cursor = self.conn.cursor()
|
|
384
|
+
async for value in read_csv_partial_async_by_param(
|
|
385
|
+
qry_key,
|
|
386
|
+
params,
|
|
387
|
+
row_count_partial=row_count_partial,
|
|
388
|
+
en=en,
|
|
389
|
+
cursor=cursor,
|
|
390
|
+
):
|
|
391
|
+
yield value
|
|
392
|
+
else:
|
|
393
|
+
conn_pool = Client._conn_pool
|
|
394
|
+
conn = conn_pool.getconn()
|
|
395
|
+
cursor = conn.cursor()
|
|
396
|
+
try:
|
|
397
|
+
async for value in read_csv_partial_async_by_param(
|
|
398
|
+
qry_key,
|
|
399
|
+
params,
|
|
400
|
+
row_count_partial=row_count_partial,
|
|
401
|
+
en=en,
|
|
402
|
+
cursor=cursor,
|
|
403
|
+
):
|
|
404
|
+
yield value
|
|
405
|
+
finally:
|
|
406
|
+
cursor.close()
|
|
407
|
+
conn_pool.putconn(conn)
|
|
408
|
+
|
|
409
|
+
def read_csv_partial(
|
|
410
|
+
self,
|
|
411
|
+
qry_key: str,
|
|
412
|
+
params: dict,
|
|
413
|
+
*,
|
|
414
|
+
row_count_partial: int = 100,
|
|
415
|
+
en: bool = False,
|
|
416
|
+
) -> Generator[bytes]:
|
|
417
|
+
"""Return rows partially in batches
|
|
418
|
+
|
|
419
|
+
Arguments:
|
|
420
|
+
qry_key: key of the Dictionary registered in the clients/queries folder
|
|
421
|
+
params: key, value pairs to pass as parameters to the SQL query.
|
|
422
|
+
row_count_partial: Number of rows to return at a time
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
CSV format converted to UTF-8-BOM
|
|
426
|
+
"""
|
|
427
|
+
|
|
428
|
+
def read_csv_partial_by_param(
|
|
429
|
+
qry_key: str,
|
|
430
|
+
params: dict,
|
|
431
|
+
*,
|
|
432
|
+
row_count_partial: int = 100,
|
|
433
|
+
en: bool = False,
|
|
434
|
+
cursor: sqlite3.Cursor,
|
|
435
|
+
) -> Generator[bytes]:
|
|
436
|
+
if not isinstance(params, dict):
|
|
437
|
+
params = vars(params)
|
|
438
|
+
|
|
439
|
+
qry_str = self.qry.get_query_by_key(qry_key, params, "csv", en)
|
|
440
|
+
|
|
441
|
+
is_second = False
|
|
442
|
+
|
|
443
|
+
# without UTF-8 BOM, hangul will be broken.
|
|
444
|
+
utf8_bom = b"\xef\xbb\xbf"
|
|
445
|
+
yield utf8_bom
|
|
446
|
+
|
|
447
|
+
start = 0
|
|
448
|
+
if self.db_settings.before_read_execute:
|
|
449
|
+
self.db_settings.before_read_execute(
|
|
450
|
+
qry_key,
|
|
451
|
+
params,
|
|
452
|
+
qry_str,
|
|
453
|
+
get_query_with_value(qry_str, params),
|
|
454
|
+
)
|
|
455
|
+
start = time.time()
|
|
456
|
+
|
|
457
|
+
cursor.execute(qry_str, params)
|
|
458
|
+
while True:
|
|
459
|
+
rows = (
|
|
460
|
+
cursor.fetchmany(row_count_partial)
|
|
461
|
+
if hasattr(cursor, "fetchmany")
|
|
462
|
+
else cursor.fetchall()
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
if not is_second:
|
|
466
|
+
if self.db_settings.after_read_execute:
|
|
467
|
+
duration = int(round((time.time() - start) * 1000))
|
|
468
|
+
self.db_settings.after_read_execute(qry_key, duration)
|
|
469
|
+
if not rows:
|
|
470
|
+
break
|
|
471
|
+
|
|
472
|
+
csv_out = io.StringIO()
|
|
473
|
+
csv_w = csv.writer(csv_out)
|
|
474
|
+
if not is_second and cursor.description:
|
|
475
|
+
column_names = [desc[0] for desc in cursor.description]
|
|
476
|
+
csv_w.writerow(column_names)
|
|
477
|
+
csv_w.writerows(
|
|
478
|
+
[list(r.values()) if isinstance(r, dict) else r for r in rows]
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
yield csv_out.getvalue().encode("utf-8")
|
|
482
|
+
|
|
483
|
+
is_second = True
|
|
484
|
+
|
|
485
|
+
if self.in_with_block:
|
|
486
|
+
cursor = self.conn.cursor()
|
|
487
|
+
yield from read_csv_partial_by_param(
|
|
488
|
+
qry_key,
|
|
489
|
+
params,
|
|
490
|
+
row_count_partial=row_count_partial,
|
|
491
|
+
en=en,
|
|
492
|
+
cursor=cursor,
|
|
493
|
+
)
|
|
494
|
+
else:
|
|
495
|
+
conn_pool = Client._conn_pool
|
|
496
|
+
conn = conn_pool.getconn()
|
|
497
|
+
cursor = conn.cursor()
|
|
498
|
+
try:
|
|
499
|
+
yield from read_csv_partial_by_param(
|
|
500
|
+
qry_key,
|
|
501
|
+
params,
|
|
502
|
+
row_count_partial=row_count_partial,
|
|
503
|
+
en=en,
|
|
504
|
+
cursor=cursor,
|
|
505
|
+
)
|
|
506
|
+
finally:
|
|
507
|
+
cursor.close()
|
|
508
|
+
conn_pool.putconn(conn)
|
|
509
|
+
|
|
510
|
+
def updates(
|
|
511
|
+
self,
|
|
512
|
+
qry_key_params_list: list[tuple[str, dict, dict]] | list[tuple[str, dict]],
|
|
513
|
+
) -> list[int]:
|
|
514
|
+
"""Executes a list of SQL statements within a single transaction.
|
|
515
|
+
If all SQL commands succeed, returns a list of the number of rows affected by each qry_key.
|
|
516
|
+
If any command fails, an error is raised.
|
|
517
|
+
|
|
518
|
+
Arguments:
|
|
519
|
+
qry_key_params_list: A list of tuples, each containing the following two values:
|
|
520
|
+
qry_key: key of the dictionary registered in the clients/queries folder
|
|
521
|
+
params: key, value pairs to pass as parameters to the SQL query.
|
|
522
|
+
|
|
523
|
+
Returns:
|
|
524
|
+
A list of the number of rows affected
|
|
525
|
+
"""
|
|
526
|
+
|
|
527
|
+
def normalize_qry_key_params_list(
|
|
528
|
+
qry_key_params_list: list[any], # type: ignore
|
|
529
|
+
) -> list[tuple[str, dict, dict]]:
|
|
530
|
+
"""normalize all item from parameter of Sqlite3Client.updates"""
|
|
531
|
+
|
|
532
|
+
qry_key_params_list_new: list[tuple[str, dict, dict]] = []
|
|
533
|
+
for item in qry_key_params_list:
|
|
534
|
+
# append params_out if not exists
|
|
535
|
+
item_new: tuple[str, dict, dict] = (
|
|
536
|
+
item if len(item) == 3 else (item[0], item[1], {})
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
qry_key, params, params_out = item_new
|
|
540
|
+
if not isinstance(params, dict):
|
|
541
|
+
params: dict = vars(params)
|
|
542
|
+
|
|
543
|
+
if params_out is None:
|
|
544
|
+
params_out = {}
|
|
545
|
+
if not isinstance(params_out, dict):
|
|
546
|
+
params_out: dict = vars(params_out)
|
|
547
|
+
|
|
548
|
+
qry_key_params_list_new.append((qry_key, params, params_out))
|
|
549
|
+
|
|
550
|
+
return qry_key_params_list_new
|
|
551
|
+
|
|
552
|
+
def updates_by_param(
|
|
553
|
+
qry_key_params_list: list[tuple[str, dict, dict]] | list[tuple[str, dict]],
|
|
554
|
+
cursor: sqlite3.Cursor,
|
|
555
|
+
) -> list[int]:
|
|
556
|
+
row_counts: list[int] = []
|
|
557
|
+
qry_strs: list[str] = []
|
|
558
|
+
|
|
559
|
+
qry_key_params_list_new = normalize_qry_key_params_list(qry_key_params_list)
|
|
560
|
+
|
|
561
|
+
for item in qry_key_params_list_new:
|
|
562
|
+
qry_key, params, params_out = item
|
|
563
|
+
|
|
564
|
+
qry_str = self.qry.get_query_by_key(qry_key, params, "update")
|
|
565
|
+
|
|
566
|
+
start = 0
|
|
567
|
+
if self.db_settings.before_update_execute:
|
|
568
|
+
self.db_settings.before_update_execute(
|
|
569
|
+
qry_key,
|
|
570
|
+
params,
|
|
571
|
+
params_out,
|
|
572
|
+
qry_str,
|
|
573
|
+
get_query_with_value(qry_str, params),
|
|
574
|
+
)
|
|
575
|
+
start = time.time()
|
|
576
|
+
|
|
577
|
+
cursor.execute(qry_str, params)
|
|
578
|
+
|
|
579
|
+
if cursor.description:
|
|
580
|
+
rows = cursor.fetchall()
|
|
581
|
+
if params_out and rows:
|
|
582
|
+
row = rows[0]
|
|
583
|
+
for k, v in row.items():
|
|
584
|
+
if k in params_out:
|
|
585
|
+
params_out[k] = v
|
|
586
|
+
|
|
587
|
+
row_count = max(0, cursor.rowcount)
|
|
588
|
+
|
|
589
|
+
if self.db_settings.after_update_execute:
|
|
590
|
+
duration = int(round((time.time() - start) * 1000))
|
|
591
|
+
self.db_settings.after_update_execute(
|
|
592
|
+
qry_key, row_count, params_out, duration
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
row_counts.append(row_count)
|
|
596
|
+
qry_strs.append(qry_str)
|
|
597
|
+
|
|
598
|
+
return row_counts
|
|
599
|
+
|
|
600
|
+
row_counts: list[int] = []
|
|
601
|
+
if self.in_with_block:
|
|
602
|
+
cursor = self.conn.cursor()
|
|
603
|
+
row_counts = updates_by_param(qry_key_params_list, cursor)
|
|
604
|
+
else:
|
|
605
|
+
conn_pool = Client._conn_pool
|
|
606
|
+
conn = conn_pool.getconn()
|
|
607
|
+
try:
|
|
608
|
+
with conn:
|
|
609
|
+
cursor = conn.cursor()
|
|
610
|
+
row_counts = updates_by_param(qry_key_params_list, cursor)
|
|
611
|
+
cursor.close()
|
|
612
|
+
finally:
|
|
613
|
+
conn_pool.putconn(conn)
|
|
614
|
+
|
|
615
|
+
return row_counts
|
|
616
|
+
|
|
617
|
+
def update(
|
|
618
|
+
self,
|
|
619
|
+
qry_key: str,
|
|
620
|
+
params: dict,
|
|
621
|
+
# pylint: disable=dangerous-default-value
|
|
622
|
+
params_out: dict = {},
|
|
623
|
+
) -> int:
|
|
624
|
+
"""call updates"""
|
|
625
|
+
|
|
626
|
+
row_counts = self.updates([(qry_key, params, params_out)])
|
|
627
|
+
return row_counts[0] if row_counts else 0
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
@atexit.register
|
|
631
|
+
def close_all_connection():
|
|
632
|
+
"""call when python exits"""
|
|
633
|
+
|
|
634
|
+
# pylint:disable=global-statement
|
|
635
|
+
global db_set_and_pool
|
|
636
|
+
|
|
637
|
+
for v in db_set_and_pool.values():
|
|
638
|
+
if v:
|
|
639
|
+
if hasattr(v, "closeall"):
|
|
640
|
+
v.closeall()
|
|
641
|
+
v = None
|
|
642
|
+
db_set_and_pool = {}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""client_util"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import date, datetime
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
# pylint: disable=relative-beyond-top-level
|
|
8
|
+
from .query_util import get_conditional, replace_en_ko_column_alias
|
|
9
|
+
from .settings import Settings
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Query:
|
|
13
|
+
"""query"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, qry_settings: Settings):
|
|
16
|
+
self.qry_settings = qry_settings
|
|
17
|
+
|
|
18
|
+
def get_query_by_key(
|
|
19
|
+
self,
|
|
20
|
+
qry_key: str,
|
|
21
|
+
params: dict,
|
|
22
|
+
func_type: Literal["update", "read", "csv"],
|
|
23
|
+
en: bool = False,
|
|
24
|
+
) -> str:
|
|
25
|
+
"""get query string by qry_key"""
|
|
26
|
+
|
|
27
|
+
def serial_date(obj):
|
|
28
|
+
"""JSON serializer for objects not serializable by default json code"""
|
|
29
|
+
|
|
30
|
+
if isinstance(obj, (datetime, date)):
|
|
31
|
+
return obj.isoformat()
|
|
32
|
+
return str(obj)
|
|
33
|
+
|
|
34
|
+
query = self.qry_settings.all_query.get(qry_key)
|
|
35
|
+
if not query:
|
|
36
|
+
raise KeyError(f"{qry_key} not exists")
|
|
37
|
+
|
|
38
|
+
info = {
|
|
39
|
+
"qry_key": qry_key,
|
|
40
|
+
"params": params,
|
|
41
|
+
"func_type": func_type,
|
|
42
|
+
"en": en,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if self.qry_settings.use_en_ko_column_alias and isinstance(en, bool):
|
|
46
|
+
query = replace_en_ko_column_alias(query, en)
|
|
47
|
+
if self.qry_settings.use_conditional and "#if" in query:
|
|
48
|
+
query = get_conditional(query, params)
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
f"/* {json.dumps(info, ensure_ascii=False, default=serial_date)} */{query}"
|
|
52
|
+
)
|