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.
@@ -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
- return dict(
5
- zip(
6
- [column[0] for column in cursor.description],
7
- result
8
- )
9
- )
9
+ return dict(
10
+ zip(
11
+ [column[0] for column in cursor.description],
12
+ result
13
+ )
14
+ )
10
15
 
11
- class Result(object):
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
- class Fetchone(object):
14
- def __init__(self, cursor, result, status_code = 200, message : str = ""):
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._message = message
17
- if result:
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
- self._list = result
20
- self._dict = dict(zip([column[0] for column in cursor.description], result))
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
- self._list = []
24
- self._dict = {}
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 message(self):
44
- return self._message
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
- class Fetchall:
192
+ @property
193
+ def query(self):
194
+ return self._fetchone.query
48
195
 
49
- def __init__(self, cursor, result, status_code = 200, message : str = ""):
50
- self._status_code = status_code
51
- self._message = message
52
- if result:
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
- self._list_dict = []
55
- for r in result:
56
- self._list_dict.append(dict(zip([column[0] for column in cursor.description], r)))
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 message(self):
79
- return self._message
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
- class Insert:
82
- def __init__(self, result : int | str, status_code = 200, message : str = ""):
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(result)
86
- self._message = message
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 message(self):
102
- return self._message
364
+ def error(self):
365
+ return self._error
103
366
 
104
- class Send:
105
- def __init__(self, result : bool, status_code = 200, message : str = ""):
106
- self._status_code = status_code
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 message(self):
120
- return self._message
389
+ def error(self):
390
+ return self._error
121
391
 
122
392
 
123
393
 
@@ -2,6 +2,7 @@ from .local_database import BaseLocalDatabase
2
2
  from .async_local_database import BaseAsyncLocalDatabase
3
3
  from .installer import DataBaseInstall
4
4
  from .models import BaseTables
5
+ from .decorators import safe_sqlite_operation, SqliteErrorType
5
6
 
6
7
 
7
8
 
@@ -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
- async with self.engine.connect() as conn:
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
@@ -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
- ins = insert(self.tables.system).values(App=app_name, Tema='light')
35
- conn.execute(ins)
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
- return all(table in existing_tables for table in required_tables)
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)