dapper-sqls 1.1.3__py3-none-any.whl → 1.2.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.
- dapper_sqls/__init__.py +3 -1
- dapper_sqls/_types.py +25 -2
- dapper_sqls/async_dapper/async_executors.py +127 -52
- dapper_sqls/builders/model/model.py +418 -33
- dapper_sqls/builders/model/utils.py +334 -42
- dapper_sqls/builders/query.py +164 -43
- dapper_sqls/builders/stored.py +15 -5
- dapper_sqls/builders/stp.py +6 -2
- dapper_sqls/config.py +40 -31
- dapper_sqls/dapper/executors.py +130 -55
- dapper_sqls/http/__init__.py +4 -0
- dapper_sqls/http/aiohttp.py +155 -0
- dapper_sqls/http/decorators.py +123 -0
- dapper_sqls/http/models.py +58 -0
- dapper_sqls/http/request.py +140 -0
- dapper_sqls/models/__init__.py +3 -5
- dapper_sqls/models/base.py +246 -20
- dapper_sqls/models/connection.py +1 -1
- dapper_sqls/models/query_field.py +214 -0
- dapper_sqls/models/result.py +314 -44
- dapper_sqls/sqlite/__init__.py +1 -0
- dapper_sqls/sqlite/async_local_database.py +69 -5
- dapper_sqls/sqlite/decorators.py +69 -0
- dapper_sqls/sqlite/installer.py +8 -4
- dapper_sqls/sqlite/local_database.py +39 -5
- dapper_sqls/sqlite/models.py +25 -1
- dapper_sqls/sqlite/utils.py +2 -1
- dapper_sqls/utils.py +16 -4
- dapper_sqls-1.2.1.dist-info/METADATA +41 -0
- dapper_sqls-1.2.1.dist-info/RECORD +40 -0
- {dapper_sqls-1.1.3.dist-info → dapper_sqls-1.2.1.dist-info}/WHEEL +1 -1
- dapper_sqls-1.1.3.dist-info/METADATA +0 -10
- dapper_sqls-1.1.3.dist-info/RECORD +0 -33
- {dapper_sqls-1.1.3.dist-info → dapper_sqls-1.2.1.dist-info}/top_level.txt +0 -0
dapper_sqls/models/result.py
CHANGED
@@ -1,27 +1,168 @@
|
|
1
1
|
# coding: utf-8
|
2
|
+
from typing import Generic, Any
|
3
|
+
from .._types import SqlErrorType, SQL_ERROR_HTTP_CODES, T
|
4
|
+
from .base import SensitiveFields
|
5
|
+
import json
|
6
|
+
from collections import defaultdict
|
2
7
|
|
3
8
|
def result_dict(cursor, result):
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
9
|
+
return dict(
|
10
|
+
zip(
|
11
|
+
[column[0] for column in cursor.description],
|
12
|
+
result
|
13
|
+
)
|
14
|
+
)
|
10
15
|
|
11
|
-
|
16
|
+
def classify_error(message: str) -> SqlErrorType:
|
17
|
+
msg = message.lower()
|
18
|
+
|
19
|
+
if "unique key constraint" in msg or "duplicate key" in msg:
|
20
|
+
return SqlErrorType.UNIQUE_VIOLATION
|
21
|
+
if "foreign key constraint" in msg:
|
22
|
+
return SqlErrorType.FOREIGN_KEY_VIOLATION
|
23
|
+
if "check constraint" in msg:
|
24
|
+
return SqlErrorType.CHECK_CONSTRAINT_VIOLATION
|
25
|
+
if "permission denied" in msg or "permission violation" in msg:
|
26
|
+
return SqlErrorType.PERMISSION_DENIED
|
27
|
+
if "syntax error" in msg:
|
28
|
+
return SqlErrorType.SYNTAX_ERROR
|
29
|
+
if "timeout" in msg:
|
30
|
+
return SqlErrorType.TIMEOUT
|
31
|
+
if any(kw in msg for kw in [
|
32
|
+
"could not connect",
|
33
|
+
"connection failed",
|
34
|
+
"server not found",
|
35
|
+
"network-related",
|
36
|
+
"login failed",
|
37
|
+
"connection timeout",
|
38
|
+
"transport-level error",
|
39
|
+
"communication link failure"
|
40
|
+
]):
|
41
|
+
return SqlErrorType.CONNECTION_ERROR
|
12
42
|
|
13
|
-
|
14
|
-
|
43
|
+
return SqlErrorType.UNKNOWN
|
44
|
+
|
45
|
+
class Error(object):
|
46
|
+
def __init__(self, exception: Exception = None):
|
47
|
+
self.message = str(exception) if isinstance(exception, Exception) else ""
|
48
|
+
self.type = classify_error(self.message)
|
49
|
+
|
50
|
+
class BaseResult(object):
|
51
|
+
def __init__(self, query : str | tuple):
|
52
|
+
if isinstance(query, tuple):
|
53
|
+
q_str, *params = query
|
54
|
+
stored_procedure = {
|
55
|
+
"query": q_str,
|
56
|
+
"params": [list(p) if isinstance(p, tuple) else p for p in params]
|
57
|
+
}
|
58
|
+
self._query = json.dumps(stored_procedure)
|
59
|
+
else:
|
60
|
+
self._query = query
|
61
|
+
|
62
|
+
@property
|
63
|
+
def query(self):
|
64
|
+
return self._query
|
65
|
+
|
66
|
+
class Result(object):
|
67
|
+
|
68
|
+
class Count(BaseResult):
|
69
|
+
def __init__(self, query : str | tuple, result : int | str, status_code : int, error: Error):
|
70
|
+
super().__init__(query)
|
71
|
+
self._count = result
|
15
72
|
self._status_code = status_code
|
16
|
-
self.
|
17
|
-
|
73
|
+
self._success = bool(status_code == 200)
|
74
|
+
self._error = error
|
75
|
+
|
76
|
+
def model_dump(self):
|
77
|
+
if self.success:
|
78
|
+
return {'status_code': self.status_code, 'count': self.count}
|
79
|
+
else:
|
80
|
+
return {'status_code': self.status_code, 'message': self.error.message}
|
81
|
+
|
82
|
+
@property
|
83
|
+
def count(self):
|
84
|
+
return self._count
|
85
|
+
|
86
|
+
@property
|
87
|
+
def status_code(self):
|
88
|
+
return self._status_code
|
89
|
+
|
90
|
+
@property
|
91
|
+
def success(self):
|
92
|
+
return self._success
|
93
|
+
|
94
|
+
@property
|
95
|
+
def error(self):
|
96
|
+
return self._error
|
97
|
+
|
98
|
+
class Fetchone(BaseResult):
|
99
|
+
def __init__(self, query : str | tuple, cursor, result, exception: Exception = None):
|
100
|
+
super().__init__(query)
|
101
|
+
self._error = Error(exception)
|
102
|
+
self._list = []
|
103
|
+
self._dict : dict[str, Any] = {}
|
104
|
+
if cursor != None:
|
105
|
+
self._status_code = 200
|
18
106
|
self._success = True
|
19
|
-
|
20
|
-
|
107
|
+
if result:
|
108
|
+
sensitive_fields = SensitiveFields.get()
|
109
|
+
columns = [column[0] for column in cursor.description]
|
110
|
+
raw_dict = dict(zip(columns, result))
|
111
|
+
self._dict = {
|
112
|
+
k: v for k, v in raw_dict.items()
|
113
|
+
if k not in sensitive_fields
|
114
|
+
}
|
115
|
+
self._list = result
|
21
116
|
else:
|
117
|
+
self._status_code = SQL_ERROR_HTTP_CODES.get(self._error.type, 500)
|
22
118
|
self._success = False
|
23
|
-
|
24
|
-
|
119
|
+
|
120
|
+
def _organize_joined_tables(self, joins: list):
|
121
|
+
alias_to_table_name = {
|
122
|
+
join.model.TABLE_ALIAS: join.model.__class__.__name__ for join in joins
|
123
|
+
}
|
124
|
+
|
125
|
+
if not self._dict:
|
126
|
+
return
|
127
|
+
|
128
|
+
alias_data = defaultdict(dict)
|
129
|
+
keys_to_remove = []
|
130
|
+
|
131
|
+
for key, value in self._dict.items():
|
132
|
+
for alias_table, table_name in alias_to_table_name.items():
|
133
|
+
if alias_table in key:
|
134
|
+
column_name = key.replace(alias_table, '')
|
135
|
+
alias_data[table_name][column_name] = value
|
136
|
+
keys_to_remove.append(key)
|
137
|
+
break
|
138
|
+
|
139
|
+
for key in keys_to_remove:
|
140
|
+
self._dict.pop(key)
|
141
|
+
|
142
|
+
if alias_data:
|
143
|
+
self._dict['joined_tables'] = dict(alias_data)
|
144
|
+
|
145
|
+
if self._list:
|
146
|
+
columns = [col for col in self._dict.keys() if col]
|
147
|
+
self._list = [self._dict[col] for col in columns]
|
148
|
+
|
149
|
+
def model_dump(self, *, include: set[str] = None):
|
150
|
+
if not self.success:
|
151
|
+
return {
|
152
|
+
'status_code': self.status_code,
|
153
|
+
'message': self.error.message
|
154
|
+
}
|
155
|
+
|
156
|
+
result_dict = self._dict.copy()
|
157
|
+
|
158
|
+
if include is not None:
|
159
|
+
include = set(include)
|
160
|
+
result_dict = {k: v for k, v in result_dict.items() if k in include or k == 'joined_tables'}
|
161
|
+
|
162
|
+
return {
|
163
|
+
'status_code': self.status_code,
|
164
|
+
'data': result_dict
|
165
|
+
}
|
25
166
|
|
26
167
|
@property
|
27
168
|
def status_code(self):
|
@@ -40,24 +181,110 @@ class Result(object):
|
|
40
181
|
return self._success
|
41
182
|
|
42
183
|
@property
|
43
|
-
def
|
44
|
-
return self.
|
184
|
+
def error(self):
|
185
|
+
return self._error
|
45
186
|
|
187
|
+
class FetchoneModel(Generic[T]):
|
188
|
+
def __init__(self, model_instance: T, fetchone_result: 'Result.Fetchone'):
|
189
|
+
self._model = model_instance
|
190
|
+
self._fetchone = fetchone_result
|
46
191
|
|
47
|
-
|
192
|
+
@property
|
193
|
+
def query(self):
|
194
|
+
return self._fetchone.query
|
48
195
|
|
49
|
-
|
50
|
-
|
51
|
-
self.
|
52
|
-
|
196
|
+
@property
|
197
|
+
def model(self) -> T:
|
198
|
+
return self._model
|
199
|
+
|
200
|
+
@property
|
201
|
+
def success(self):
|
202
|
+
return self._fetchone.success
|
203
|
+
|
204
|
+
@property
|
205
|
+
def dict(self):
|
206
|
+
return self._fetchone.dict
|
207
|
+
|
208
|
+
@property
|
209
|
+
def list(self):
|
210
|
+
return self._fetchone.list
|
211
|
+
|
212
|
+
@property
|
213
|
+
def status_code(self):
|
214
|
+
return self._fetchone.status_code
|
215
|
+
|
216
|
+
@property
|
217
|
+
def error(self):
|
218
|
+
return self._fetchone.error
|
219
|
+
|
220
|
+
def model_dump(self, *, include: set[str] = None):
|
221
|
+
return self._fetchone.model_dump(include=include)
|
222
|
+
|
223
|
+
|
224
|
+
class Fetchall(BaseResult):
|
225
|
+
|
226
|
+
def __init__(self, query : str | tuple, cursor, result, exception: Exception = None):
|
227
|
+
super().__init__(query)
|
228
|
+
self._error = Error(exception)
|
229
|
+
self._list_dict : list[dict[str, Any]] = []
|
230
|
+
if cursor != None:
|
231
|
+
self._status_code = 200
|
53
232
|
self._success = True
|
54
|
-
|
55
|
-
|
56
|
-
|
233
|
+
if result:
|
234
|
+
sensitive_fields = SensitiveFields.get()
|
235
|
+
columns = [column[0] for column in cursor.description]
|
236
|
+
for r in result:
|
237
|
+
raw_dict = dict(zip(columns, r))
|
238
|
+
clean_dict = {
|
239
|
+
k: v for k, v in raw_dict.items()
|
240
|
+
if k not in sensitive_fields
|
241
|
+
}
|
242
|
+
self._list_dict.append(clean_dict)
|
57
243
|
else:
|
244
|
+
self._status_code = SQL_ERROR_HTTP_CODES.get(self._error.type, 500)
|
58
245
|
self._success = False
|
59
|
-
self._list_dict = []
|
60
246
|
|
247
|
+
def _organize_joined_tables(self, joins: list):
|
248
|
+
alias_to_table_name = {
|
249
|
+
join.model.TABLE_ALIAS: join.model.__class__.__name__ for join in joins
|
250
|
+
}
|
251
|
+
|
252
|
+
for item in self._list_dict:
|
253
|
+
alias_data = defaultdict(dict)
|
254
|
+
keys_to_remove = []
|
255
|
+
|
256
|
+
for key, value in item.items():
|
257
|
+
for alias_table, table_name in alias_to_table_name.items():
|
258
|
+
if alias_table in key:
|
259
|
+
column_name = key.replace(alias_table, '')
|
260
|
+
alias_data[table_name][column_name] = value
|
261
|
+
keys_to_remove.append(key)
|
262
|
+
break
|
263
|
+
|
264
|
+
for key in keys_to_remove:
|
265
|
+
del item[key]
|
266
|
+
|
267
|
+
if alias_data:
|
268
|
+
item['joined_tables'] = dict(alias_data)
|
269
|
+
|
270
|
+
def model_dump(self, *, include: set[str] = None):
|
271
|
+
if not self.success:
|
272
|
+
return {
|
273
|
+
'status_code': self.status_code,
|
274
|
+
'message': self.error.message
|
275
|
+
}
|
276
|
+
|
277
|
+
data = self._list_dict
|
278
|
+
|
279
|
+
if include is not None:
|
280
|
+
include = set(include)
|
281
|
+
data = [{k: v for k, v in d.items() if k in include or k == 'joined_tables'} for d in data]
|
282
|
+
|
283
|
+
return {
|
284
|
+
'status_code': self.status_code,
|
285
|
+
'data': data
|
286
|
+
}
|
287
|
+
|
61
288
|
@property
|
62
289
|
def status_code(self):
|
63
290
|
return self._status_code
|
@@ -66,24 +293,60 @@ class Result(object):
|
|
66
293
|
def list_dict(self):
|
67
294
|
return self._list_dict
|
68
295
|
|
69
|
-
@property
|
70
|
-
def dict(self):
|
71
|
-
return self._dict
|
72
|
-
|
73
296
|
@property
|
74
297
|
def success(self):
|
75
298
|
return self._success
|
76
299
|
|
77
300
|
@property
|
78
|
-
def
|
79
|
-
return self.
|
301
|
+
def error(self):
|
302
|
+
return self._error
|
303
|
+
|
304
|
+
class FetchallModel(Generic[T]):
|
305
|
+
def __init__(self, model_list: list[T], fetchall_result: 'Result.Fetchall'):
|
306
|
+
self._models = model_list
|
307
|
+
self._fetchall = fetchall_result
|
308
|
+
|
309
|
+
@property
|
310
|
+
def query(self):
|
311
|
+
return self._fetchall.query
|
312
|
+
|
313
|
+
@property
|
314
|
+
def models(self) -> list[T]:
|
315
|
+
return self._models
|
316
|
+
|
317
|
+
@property
|
318
|
+
def success(self):
|
319
|
+
return self._fetchall.success
|
320
|
+
|
321
|
+
@property
|
322
|
+
def list_dict(self):
|
323
|
+
return self._fetchall.list_dict
|
324
|
+
|
325
|
+
@property
|
326
|
+
def status_code(self):
|
327
|
+
return self._fetchall.status_code
|
328
|
+
|
329
|
+
@property
|
330
|
+
def error(self):
|
331
|
+
return self._fetchall.error
|
80
332
|
|
81
|
-
|
82
|
-
|
333
|
+
def model_dump(self, *, include: set[str] = None):
|
334
|
+
include = set(include)
|
335
|
+
return self._fetchall.model_dump(include=include)
|
336
|
+
|
337
|
+
class Insert(BaseResult):
|
338
|
+
def __init__(self, query : str | tuple, result : int | str, status_code : int, error: Error):
|
339
|
+
super().__init__(query)
|
83
340
|
self._id = result
|
84
341
|
self._status_code = status_code
|
85
|
-
self._success = bool(
|
86
|
-
self.
|
342
|
+
self._success = bool(status_code == 200)
|
343
|
+
self._error = error
|
344
|
+
|
345
|
+
def model_dump(self):
|
346
|
+
if self.success:
|
347
|
+
return {'status_code': self.status_code, 'id': self.id}
|
348
|
+
else:
|
349
|
+
return {'status_code': self.status_code, 'message': self.error.message}
|
87
350
|
|
88
351
|
@property
|
89
352
|
def id(self):
|
@@ -98,15 +361,22 @@ class Result(object):
|
|
98
361
|
return self._success
|
99
362
|
|
100
363
|
@property
|
101
|
-
def
|
102
|
-
return self.
|
364
|
+
def error(self):
|
365
|
+
return self._error
|
103
366
|
|
104
|
-
class Send:
|
105
|
-
def __init__(self,
|
106
|
-
|
367
|
+
class Send(BaseResult):
|
368
|
+
def __init__(self, query : str | tuple, result : bool, exception: Exception = None):
|
369
|
+
super().__init__(query)
|
370
|
+
self._error = Error(exception)
|
371
|
+
self._status_code = 200 if result else SQL_ERROR_HTTP_CODES.get(self._error.type, 500)
|
107
372
|
self._success = result
|
108
|
-
self._message = message
|
109
373
|
|
374
|
+
def model_dump(self):
|
375
|
+
if self.success:
|
376
|
+
return {'status_code': self.status_code}
|
377
|
+
else:
|
378
|
+
return {'status_code': self.status_code, 'message': self.error.message}
|
379
|
+
|
110
380
|
@property
|
111
381
|
def status_code(self):
|
112
382
|
return self._status_code
|
@@ -116,8 +386,8 @@ class Result(object):
|
|
116
386
|
return self._success
|
117
387
|
|
118
388
|
@property
|
119
|
-
def
|
120
|
-
return self.
|
389
|
+
def error(self):
|
390
|
+
return self._error
|
121
391
|
|
122
392
|
|
123
393
|
|
dapper_sqls/sqlite/__init__.py
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
# coding: utf-8
|
2
2
|
from sqlalchemy.ext.asyncio import create_async_engine, AsyncEngine
|
3
|
-
from sqlalchemy import text
|
4
|
-
from .models import BaseTables, Path, System, EnvVar
|
3
|
+
from sqlalchemy import text, insert, delete, select, Connection
|
4
|
+
from .models import BaseTables, Path, System, EnvVar, NotificationData
|
5
5
|
from .utils import get_value
|
6
6
|
|
7
7
|
class BaseAsyncLocalDatabase:
|
8
8
|
|
9
|
-
def __init__(self, app_name: str, path : str, is_new_database : bool):
|
9
|
+
def __init__(self, app_name: str, path : str, is_new_database : bool, insistent_tables : list[str]):
|
10
10
|
self._app_name = app_name
|
11
11
|
self.is_new_database = is_new_database
|
12
|
+
self.insistent_tables = insistent_tables
|
12
13
|
self._engine: AsyncEngine = create_async_engine(f'sqlite+aiosqlite:///{path}')
|
13
14
|
|
14
15
|
@property
|
@@ -18,6 +19,11 @@ class BaseAsyncLocalDatabase:
|
|
18
19
|
@property
|
19
20
|
def app_name(self):
|
20
21
|
return self._app_name
|
22
|
+
|
23
|
+
async def close(self):
|
24
|
+
await self._engine.dispose()
|
25
|
+
self._engine.pool.dispose()
|
26
|
+
self._engine = None
|
21
27
|
|
22
28
|
async def init_db(self):
|
23
29
|
async with self.engine.begin() as conn:
|
@@ -25,8 +31,15 @@ class BaseAsyncLocalDatabase:
|
|
25
31
|
await conn.execute(BaseTables.system.insert().values(App=self.app_name, Tema='light'))
|
26
32
|
await conn.commit()
|
27
33
|
|
28
|
-
async def select(self, table: str, where: str = None):
|
29
|
-
|
34
|
+
async def select(self, table: str, where: str = None, conn : Connection = None):
|
35
|
+
if not conn:
|
36
|
+
async with self.engine.connect() as conn:
|
37
|
+
query = f"SELECT * FROM {table} WHERE App = :app_name"
|
38
|
+
if where:
|
39
|
+
query += f" AND {where}"
|
40
|
+
result = await conn.execute(text(query), {'app_name': self.app_name})
|
41
|
+
return [row._mapping for row in result]
|
42
|
+
else:
|
30
43
|
query = f"SELECT * FROM {table} WHERE App = :app_name"
|
31
44
|
if where:
|
32
45
|
query += f" AND {where}"
|
@@ -102,3 +115,54 @@ class BaseAsyncLocalDatabase:
|
|
102
115
|
BaseTables.system.update().where(BaseTables.system.c.App == self.app_name).values(Tema=theme)
|
103
116
|
)
|
104
117
|
|
118
|
+
async def insert_notification(self, data: NotificationData):
|
119
|
+
async with self.engine.begin() as conn:
|
120
|
+
ins = insert(BaseTables.notification).values(
|
121
|
+
App=self.app_name,
|
122
|
+
guid=data.guid,
|
123
|
+
local=data.local,
|
124
|
+
title=data.title,
|
125
|
+
message=data.message,
|
126
|
+
type=data.type,
|
127
|
+
date=data.date
|
128
|
+
)
|
129
|
+
await conn.execute(ins)
|
130
|
+
|
131
|
+
async def delete_notification(self, guid: str):
|
132
|
+
async with self.engine.begin() as conn:
|
133
|
+
stmt = delete(BaseTables.notification).where(
|
134
|
+
(BaseTables.notification.c.guid == guid) & (BaseTables.notification.c.App == self.app_name)
|
135
|
+
)
|
136
|
+
await conn.execute(stmt)
|
137
|
+
|
138
|
+
async def clear_notification(self):
|
139
|
+
async with self.engine.begin() as conn:
|
140
|
+
stmt = delete(BaseTables.notification).where(BaseTables.notification.c.App == self.app_name)
|
141
|
+
await conn.execute(stmt)
|
142
|
+
|
143
|
+
async def get_notifications(self):
|
144
|
+
async with self.engine.connect() as conn:
|
145
|
+
stmt = select(BaseTables.notification).where(BaseTables.notification.c.App == self.app_name)
|
146
|
+
result = await conn.execute(stmt)
|
147
|
+
notifications = result.fetchall()
|
148
|
+
return [NotificationData(**notification._mapping) for notification in notifications]
|
149
|
+
|
150
|
+
async def insert_notification(self, data : NotificationData):
|
151
|
+
async with self.engine.connect() as conn:
|
152
|
+
ins = insert(BaseTables.notification).values(App=self.app_name, guid=data.guid, local=data.local, title=data.title, message=data.message, type=data.type,date=data.date)
|
153
|
+
await conn.execute(ins)
|
154
|
+
await conn.commit()
|
155
|
+
|
156
|
+
async def delete_notification(self, guid : str):
|
157
|
+
async with self.engine.connect() as conn:
|
158
|
+
await conn.execute(delete(BaseTables.notification).where((BaseTables.notification.c.guid == guid) & (BaseTables.notification.c.App == self.app_name)))
|
159
|
+
await conn.commit()
|
160
|
+
|
161
|
+
async def clear_notification(self):
|
162
|
+
async with self.engine.connect() as conn:
|
163
|
+
await conn.execute(delete(BaseTables.notification).where(BaseTables.notification.c.App == self.app_name))
|
164
|
+
await conn.commit()
|
165
|
+
|
166
|
+
async def get_notifications(self):
|
167
|
+
notifications = await self.select('notification')
|
168
|
+
return [NotificationData(**notification) for notification in notifications]
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
from functools import wraps
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from typing import Any, Callable, TypeVar
|
5
|
+
from enum import Enum
|
6
|
+
|
7
|
+
T = TypeVar("T")
|
8
|
+
|
9
|
+
# Tipos de erro padronizados
|
10
|
+
class SqliteErrorType(Enum):
|
11
|
+
UNIQUE_VIOLATION = "Unique violation"
|
12
|
+
FOREIGN_KEY_VIOLATION = "Foreign key violation"
|
13
|
+
CHECK_CONSTRAINT_VIOLATION = "Check constraint violation"
|
14
|
+
PERMISSION_DENIED = "Permission denied"
|
15
|
+
SYNTAX_ERROR = "Syntax error"
|
16
|
+
TIMEOUT = "Timeout"
|
17
|
+
CONNECTION_ERROR = "Connection error"
|
18
|
+
UNKNOWN = "Unknown"
|
19
|
+
|
20
|
+
# Classificador específico para SQLite
|
21
|
+
def classify_sqlite_error(message: str) -> SqliteErrorType:
|
22
|
+
msg = message.lower()
|
23
|
+
|
24
|
+
if "unique constraint failed" in msg:
|
25
|
+
return SqliteErrorType.UNIQUE_VIOLATION
|
26
|
+
if "foreign key constraint failed" in msg:
|
27
|
+
return SqliteErrorType.FOREIGN_KEY_VIOLATION
|
28
|
+
if "check constraint failed" in msg:
|
29
|
+
return SqliteErrorType.CHECK_CONSTRAINT_VIOLATION
|
30
|
+
if "permission denied" in msg:
|
31
|
+
return SqliteErrorType.PERMISSION_DENIED
|
32
|
+
if "syntax error" in msg or ("near" in msg and "syntax error" in msg):
|
33
|
+
return SqliteErrorType.SYNTAX_ERROR
|
34
|
+
if "database is locked" in msg or "timeout" in msg:
|
35
|
+
return SqliteErrorType.TIMEOUT
|
36
|
+
if any(kw in msg for kw in [
|
37
|
+
"unable to open database file",
|
38
|
+
"disk i/o error",
|
39
|
+
"not a database",
|
40
|
+
"file is encrypted",
|
41
|
+
"file is not a database"
|
42
|
+
]):
|
43
|
+
return SqliteErrorType.CONNECTION_ERROR
|
44
|
+
|
45
|
+
return SqliteErrorType.UNKNOWN
|
46
|
+
|
47
|
+
# Classe Error
|
48
|
+
class Error:
|
49
|
+
def __init__(self, exception: Exception = None):
|
50
|
+
self.message = str(exception) if isinstance(exception, Exception) else ""
|
51
|
+
self.type = classify_sqlite_error(self.message) if self.message.strip() else None
|
52
|
+
|
53
|
+
# Classe de retorno do decorador
|
54
|
+
@dataclass
|
55
|
+
class OperationResult:
|
56
|
+
success: bool
|
57
|
+
error: Error | None
|
58
|
+
result: Any = None
|
59
|
+
|
60
|
+
# Decorador
|
61
|
+
def safe_sqlite_operation(func: Callable[..., T]) -> Callable[..., OperationResult]:
|
62
|
+
@wraps(func)
|
63
|
+
def wrapper(*args, **kwargs) -> OperationResult:
|
64
|
+
try:
|
65
|
+
result = func(*args, **kwargs)
|
66
|
+
return OperationResult(success=True, error=None, result=result)
|
67
|
+
except Exception as e:
|
68
|
+
return OperationResult(success=False, error=Error(e), result=None)
|
69
|
+
return wrapper
|
dapper_sqls/sqlite/installer.py
CHANGED
@@ -21,6 +21,7 @@ class DataBaseInstall(object):
|
|
21
21
|
self.tables = tables if tables else BaseTables
|
22
22
|
self._engine : Engine = None
|
23
23
|
self.new_database = not path.isfile(self._path_database)
|
24
|
+
self.insistent_tables = []
|
24
25
|
if not path.isfile(self._path_database):
|
25
26
|
if not path.exists(path.join(path_local_database,database_folder_name)):
|
26
27
|
makedirs(path.join(path_local_database,database_folder_name))
|
@@ -29,10 +30,12 @@ class DataBaseInstall(object):
|
|
29
30
|
conn.execute(text("PRAGMA encoding = 'UTF-8'"))
|
30
31
|
conn.commit()
|
31
32
|
|
33
|
+
|
32
34
|
with self.engine.connect() as conn:
|
33
35
|
self.tables.meta_data.create_all(self.engine)
|
34
|
-
|
35
|
-
|
36
|
+
if hasattr(self.tables, 'system'):
|
37
|
+
ins = insert(self.tables.system).values(App=app_name, Tema='light')
|
38
|
+
conn.execute(ins)
|
36
39
|
conn.commit()
|
37
40
|
else:
|
38
41
|
if not self.are_tables_existing(self.engine):
|
@@ -47,7 +50,7 @@ class DataBaseInstall(object):
|
|
47
50
|
...
|
48
51
|
|
49
52
|
def instance(self, obj : T) -> T:
|
50
|
-
return obj(self._app_name, self._path_database, self.new_database)
|
53
|
+
return obj(self._app_name, self._path_database, self.new_database, self.insistent_tables)
|
51
54
|
|
52
55
|
@property
|
53
56
|
def engine(self):
|
@@ -74,7 +77,8 @@ class DataBaseInstall(object):
|
|
74
77
|
inspector = inspect(engine)
|
75
78
|
existing_tables = inspector.get_table_names()
|
76
79
|
required_tables = self.tables.meta_data.tables.keys()
|
77
|
-
|
80
|
+
self.insistent_tables = [table for table in required_tables if table not in existing_tables]
|
81
|
+
return not bool(self.insistent_tables)
|
78
82
|
|
79
83
|
def synchronize_columns(self, engine):
|
80
84
|
inspector = inspect(engine)
|