tina4-python 0.2.122__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.
- tina4_python/Auth.py +222 -0
- tina4_python/Constant.py +43 -0
- tina4_python/Database.py +591 -0
- tina4_python/DatabaseResult.py +107 -0
- tina4_python/DatabaseTypes.py +15 -0
- tina4_python/Debug.py +126 -0
- tina4_python/Env.py +37 -0
- tina4_python/Localization.py +42 -0
- tina4_python/Messages.py +30 -0
- tina4_python/MiddleWare.py +90 -0
- tina4_python/Migration.py +107 -0
- tina4_python/ORM.py +639 -0
- tina4_python/Queue.py +615 -0
- tina4_python/Request.py +19 -0
- tina4_python/Response.py +121 -0
- tina4_python/Router.py +423 -0
- tina4_python/Session.py +342 -0
- tina4_python/ShellColors.py +20 -0
- tina4_python/Swagger.py +228 -0
- tina4_python/Template.py +107 -0
- tina4_python/Webserver.py +429 -0
- tina4_python/Websocket.py +49 -0
- tina4_python/__init__.py +392 -0
- tina4_python/messages.pot +83 -0
- tina4_python/public/css/readme.md +0 -0
- tina4_python/public/favicon.ico +0 -0
- tina4_python/public/images/403.png +0 -0
- tina4_python/public/images/404.png +0 -0
- tina4_python/public/images/500.png +0 -0
- tina4_python/public/images/logo.png +0 -0
- tina4_python/public/images/readme.md +0 -0
- tina4_python/public/js/readme.md +0 -0
- tina4_python/public/js/reconnecting-websocket.js +365 -0
- tina4_python/public/js/tina4helper.js +397 -0
- tina4_python/public/swagger/index.html +90 -0
- tina4_python/public/swagger/oauth2-redirect.html +63 -0
- tina4_python/templates/errors/403.twig +10 -0
- tina4_python/templates/errors/404.twig +10 -0
- tina4_python/templates/errors/500.twig +11 -0
- tina4_python/templates/readme.md +1 -0
- tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- tina4_python/translations/en/LC_MESSAGES/messages.po +80 -0
- tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- tina4_python/translations/fr/LC_MESSAGES/messages.po +84 -0
- tina4_python-0.2.122.dist-info/METADATA +465 -0
- tina4_python-0.2.122.dist-info/RECORD +47 -0
- tina4_python-0.2.122.dist-info/WHEEL +4 -0
tina4_python/Database.py
ADDED
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Tina4 - This is not a 4ramework.
|
|
3
|
+
# Copy-right 2007 - current Tina4
|
|
4
|
+
# License: MIT https://opensource.org/licenses/MIT
|
|
5
|
+
#
|
|
6
|
+
# flake8: noqa: E501
|
|
7
|
+
import base64
|
|
8
|
+
import sys
|
|
9
|
+
import importlib
|
|
10
|
+
import datetime
|
|
11
|
+
import json
|
|
12
|
+
from tina4_python import Debug, Constant
|
|
13
|
+
from tina4_python.Constant import TINA4_LOG_ERROR
|
|
14
|
+
from tina4_python.DatabaseResult import DatabaseResult
|
|
15
|
+
from tina4_python.DatabaseTypes import *
|
|
16
|
+
|
|
17
|
+
class Database:
|
|
18
|
+
|
|
19
|
+
def __init__(self, _connection_string, _username="", _password=""):
|
|
20
|
+
"""
|
|
21
|
+
Initializes a database connection
|
|
22
|
+
:param _connection_string:
|
|
23
|
+
"""
|
|
24
|
+
# split out the connection string
|
|
25
|
+
# driver:host/port:schema/path
|
|
26
|
+
params = _connection_string.split(":", 1)
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
self.database_module = importlib.import_module(params[0])
|
|
30
|
+
except Exception:
|
|
31
|
+
install_message = "Please implement "+params[0]+" in Database.py and make a pull request!"
|
|
32
|
+
if params[0] == SQLITE:
|
|
33
|
+
install_message = "Your python is missing the sqlite3 module, please reinstall or update"
|
|
34
|
+
elif params[0] == MYSQL:
|
|
35
|
+
install_message = "Your python is missing the mysql module, please install with "+MYSQL_INSTALL
|
|
36
|
+
elif params[0] == POSTGRES:
|
|
37
|
+
install_message = "Your python is missing the postgres module, please install with "+POSTGRES_INSTALL
|
|
38
|
+
elif params[0] == FIREBIRD:
|
|
39
|
+
install_message = "Your python is missing the firebird module, please install with "+FIREBIRD_INSTALL
|
|
40
|
+
elif params[0] == MSSQL:
|
|
41
|
+
install_message = "Your python is missing the mssql module, please install with "+MSSQL_INSTALL
|
|
42
|
+
|
|
43
|
+
sys.exit("Could not load database driver for "+params[0]+"\n"+install_message)
|
|
44
|
+
|
|
45
|
+
self.database_engine = params[0]
|
|
46
|
+
self.database_path = params[1]
|
|
47
|
+
self.username = _username
|
|
48
|
+
self.password = _password
|
|
49
|
+
|
|
50
|
+
if self.database_engine == SQLITE:
|
|
51
|
+
self.dba = self.database_module.connect(self.database_path)
|
|
52
|
+
self.port = None
|
|
53
|
+
self.host = None
|
|
54
|
+
|
|
55
|
+
# we need to register data adapters for sqlite3 due to deprecations in python3.12
|
|
56
|
+
def adapt_date_iso(val):
|
|
57
|
+
"""Adapt datetime.date to ISO 8601 date."""
|
|
58
|
+
return val.isoformat()
|
|
59
|
+
|
|
60
|
+
def adapt_datetime_iso(val):
|
|
61
|
+
"""Adapt datetime.datetime to timezone-naive ISO 8601 date."""
|
|
62
|
+
return val.isoformat()
|
|
63
|
+
|
|
64
|
+
def adapt_datetime_epoch(val):
|
|
65
|
+
"""Adapt datetime.datetime to Unix timestamp."""
|
|
66
|
+
return int(val.timestamp())
|
|
67
|
+
|
|
68
|
+
self.database_module.register_adapter(datetime.date, adapt_date_iso)
|
|
69
|
+
self.database_module.register_adapter(datetime.datetime, adapt_datetime_iso)
|
|
70
|
+
self.database_module.register_adapter(datetime.datetime, adapt_datetime_epoch)
|
|
71
|
+
|
|
72
|
+
def convert_date(val):
|
|
73
|
+
"""Convert ISO 8601 date to datetime.date object."""
|
|
74
|
+
return datetime.date.fromisoformat(val.decode())
|
|
75
|
+
|
|
76
|
+
def convert_datetime(val):
|
|
77
|
+
"""Convert ISO 8601 datetime to datetime.datetime object."""
|
|
78
|
+
return datetime.datetime.fromisoformat(val.decode())
|
|
79
|
+
|
|
80
|
+
def convert_timestamp(val):
|
|
81
|
+
"""Convert Unix epoch timestamp to datetime.datetime object."""
|
|
82
|
+
return datetime.datetime.fromtimestamp(int(val))
|
|
83
|
+
|
|
84
|
+
self.database_module.register_converter("date", convert_date)
|
|
85
|
+
self.database_module.register_converter("datetime", convert_datetime)
|
|
86
|
+
self.database_module.register_converter("timestamp", convert_timestamp)
|
|
87
|
+
else:
|
|
88
|
+
# <host>/<port>:<file>
|
|
89
|
+
temp_params = self.database_path.split(":", 1)
|
|
90
|
+
host_port = temp_params[0].split("/", 1)
|
|
91
|
+
self.host = host_port[0]
|
|
92
|
+
if len(host_port) > 1:
|
|
93
|
+
self.port = int(host_port[1])
|
|
94
|
+
else:
|
|
95
|
+
self.port = 3050
|
|
96
|
+
|
|
97
|
+
self.database_path = temp_params[1]
|
|
98
|
+
|
|
99
|
+
if self.database_engine == FIREBIRD:
|
|
100
|
+
self.dba = self.database_module.connect(
|
|
101
|
+
self.host + "/" + str(self.port) + ":" + self.database_path,
|
|
102
|
+
user=self.username,
|
|
103
|
+
password=self.password
|
|
104
|
+
)
|
|
105
|
+
elif self.database_engine == MYSQL:
|
|
106
|
+
self.dba = self.database_module.connect(
|
|
107
|
+
database=self.database_path,
|
|
108
|
+
port=self.port,
|
|
109
|
+
host=self.host,
|
|
110
|
+
user=self.username,
|
|
111
|
+
password=self.password,
|
|
112
|
+
consume_results=True
|
|
113
|
+
)
|
|
114
|
+
elif self.database_engine == POSTGRES:
|
|
115
|
+
self.dba = self.database_module.connect(
|
|
116
|
+
dbname=self.database_path,
|
|
117
|
+
port=self.port,
|
|
118
|
+
host=self.host,
|
|
119
|
+
user=self.username,
|
|
120
|
+
password=self.password
|
|
121
|
+
)
|
|
122
|
+
elif self.database_engine == MSSQL:
|
|
123
|
+
self.dba = self.database_module.connect(
|
|
124
|
+
server=self.host,
|
|
125
|
+
port=self.port,
|
|
126
|
+
user=self.username,
|
|
127
|
+
password=self.password,
|
|
128
|
+
database=self.database_path
|
|
129
|
+
)
|
|
130
|
+
self.dba.autocommit(False)
|
|
131
|
+
else:
|
|
132
|
+
sys.exit("Could not load database driver for "+params[0])
|
|
133
|
+
|
|
134
|
+
Debug("DATABASE:", self.database_module.__name__, self.host, self.port, self.database_path, self.username,
|
|
135
|
+
Constant.TINA4_LOG_DEBUG)
|
|
136
|
+
|
|
137
|
+
def table_exists(self, table_name):
|
|
138
|
+
"""
|
|
139
|
+
Checks if a table exists in the database
|
|
140
|
+
:param str table_name: Name of the table
|
|
141
|
+
:return: bool : True if table exists, else False
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
if self.database_engine == MSSQL:
|
|
145
|
+
sql = "select count(*) as count_table from sys.tables WHERE name = '"+table_name.upper()+"'"
|
|
146
|
+
elif self.database_engine == SQLITE:
|
|
147
|
+
sql = "SELECT count(*) as count_table FROM sqlite_master WHERE type='table' AND name='"+table_name+"'"
|
|
148
|
+
elif self.database_engine == MYSQL:
|
|
149
|
+
sql = "SELECT count(*) as count_table FROM information_schema.tables WHERE table_schema = '"+self.database_path+"' AND table_name = '"+table_name+"'"
|
|
150
|
+
elif self.database_engine == POSTGRES:
|
|
151
|
+
sql = """SELECT count(*) as count_table FROM pg_catalog.pg_class c
|
|
152
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
153
|
+
WHERE c.relname = '"""+table_name+"""'
|
|
154
|
+
AND c.relkind = 'r' """
|
|
155
|
+
elif self.database_engine == FIREBIRD:
|
|
156
|
+
sql = "SELECT count(*) as count_table FROM RDB$RELATIONS WHERE RDB$RELATION_NAME = upper('"+table_name+"')"
|
|
157
|
+
else:
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
record = self.fetch_one(sql)
|
|
162
|
+
except Exception as e:
|
|
163
|
+
raise Exception (f"Error checking if table {table_name} exists: "+str(e))
|
|
164
|
+
|
|
165
|
+
if record:
|
|
166
|
+
if record["count_table"] > 0:
|
|
167
|
+
return True
|
|
168
|
+
else:
|
|
169
|
+
return False
|
|
170
|
+
else:
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
def get_next_id(self, table_name, column_name="id"):
|
|
174
|
+
"""
|
|
175
|
+
Gets the next id using max method in sql for databases which don't have good sequences
|
|
176
|
+
:param str table_name: Name of the table
|
|
177
|
+
:param str column_name: Name of the column in that table to increment
|
|
178
|
+
:return: int : The next id in the sequence
|
|
179
|
+
"""
|
|
180
|
+
try:
|
|
181
|
+
sql = "select max(" + column_name + ") as \"max_id\" from " + table_name
|
|
182
|
+
record = self.fetch_one(sql)
|
|
183
|
+
if record["max_id"] is None:
|
|
184
|
+
record = {"max_id": 0}
|
|
185
|
+
|
|
186
|
+
next_id = int(record["max_id"]) + 1
|
|
187
|
+
return next_id
|
|
188
|
+
except Exception as e:
|
|
189
|
+
Debug("Get next id", str(e), TINA4_LOG_ERROR)
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
def database_exists(self, database_name):
|
|
193
|
+
|
|
194
|
+
return True
|
|
195
|
+
|
|
196
|
+
def current_timestamp(self):
|
|
197
|
+
"""
|
|
198
|
+
Gets the current timestamp based on the database being used
|
|
199
|
+
:return:
|
|
200
|
+
"""
|
|
201
|
+
if self.database_engine == FIREBIRD:
|
|
202
|
+
return datetime.datetime.now().strftime("%d/%m/%Y %H:%M:%S")
|
|
203
|
+
elif self.database_engine == SQLITE:
|
|
204
|
+
return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
205
|
+
else:
|
|
206
|
+
return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
207
|
+
|
|
208
|
+
def get_database_result(self, cursor, counter, limit, skip):
|
|
209
|
+
"""
|
|
210
|
+
Get database results
|
|
211
|
+
:param cursor:
|
|
212
|
+
:param counter:
|
|
213
|
+
:param limit:
|
|
214
|
+
:param skip:
|
|
215
|
+
:return:
|
|
216
|
+
"""
|
|
217
|
+
columns = [column[0].lower() for column in cursor.description]
|
|
218
|
+
records = cursor.fetchall()
|
|
219
|
+
rows = [dict(zip(columns, row)) for row in records]
|
|
220
|
+
cursor.close()
|
|
221
|
+
return DatabaseResult(rows, columns, None, counter, limit, skip)
|
|
222
|
+
|
|
223
|
+
def is_json(self, myjson):
|
|
224
|
+
"""
|
|
225
|
+
Checks if a JSON string is valid
|
|
226
|
+
:param myjson:
|
|
227
|
+
:return:
|
|
228
|
+
"""
|
|
229
|
+
try:
|
|
230
|
+
json.loads(myjson)
|
|
231
|
+
except Exception:
|
|
232
|
+
return False
|
|
233
|
+
return True
|
|
234
|
+
|
|
235
|
+
def check_connected(self):
|
|
236
|
+
"""
|
|
237
|
+
Checks if the database connection is established
|
|
238
|
+
:return:
|
|
239
|
+
"""
|
|
240
|
+
if self.database_engine == MYSQL:
|
|
241
|
+
self.dba.ping(reconnect=True, attempts=1, delay=0)
|
|
242
|
+
else:
|
|
243
|
+
# implement other database requirements if needed
|
|
244
|
+
pass
|
|
245
|
+
|
|
246
|
+
def fetch(self, sql, params=[], limit=10, skip=0):
|
|
247
|
+
"""
|
|
248
|
+
Fetch records based on a sql statement
|
|
249
|
+
:param str sql: A plain SQL statement or one with params in it designated by ?
|
|
250
|
+
:param list params: A list of params in order of precedence
|
|
251
|
+
:param int limit: Number of records to fetch
|
|
252
|
+
:param int skip: Offset of records to skip
|
|
253
|
+
:return: DatabaseResult
|
|
254
|
+
"""
|
|
255
|
+
self.check_connected()
|
|
256
|
+
# make a statement to count the records
|
|
257
|
+
# select top * from table
|
|
258
|
+
sql_count = f"select count(*) as \"count_records\" from ({sql}) as t"
|
|
259
|
+
|
|
260
|
+
# modify the select statement for limit and skip
|
|
261
|
+
if self.database_engine == FIREBIRD:
|
|
262
|
+
sql = f"select first {limit} skip {skip} * from ({sql}) as t"
|
|
263
|
+
elif self.database_engine == SQLITE or self.database_engine == MYSQL:
|
|
264
|
+
sql = f"select * from ({sql}) as t limit {skip},{limit}"
|
|
265
|
+
elif self.database_engine == POSTGRES:
|
|
266
|
+
sql = f"select * from ({sql}) as t limit {limit} offset {skip}"
|
|
267
|
+
elif self.database_engine == MSSQL:
|
|
268
|
+
sql_check = sql.upper().rsplit("ORDER BY")[0]
|
|
269
|
+
sql_count = f"select count(*) as \"count_records\" from ({sql_check}) as t"
|
|
270
|
+
if "ORDER BY" in sql.upper():
|
|
271
|
+
sql = f"select * from ({sql} offset {skip} rows FETCH NEXT {limit} ROWS ONLY) as t"
|
|
272
|
+
else:
|
|
273
|
+
sql = f"select * from ({sql} order by 1 OFFSET {skip} ROWS FETCH NEXT {limit} ROWS ONLY) as t"
|
|
274
|
+
else:
|
|
275
|
+
sql = f"select * from ({sql}) as t limit {limit} offset {skip}"
|
|
276
|
+
try:
|
|
277
|
+
cursor = self.dba.cursor()
|
|
278
|
+
try:
|
|
279
|
+
counter_cursor = self.dba.cursor()
|
|
280
|
+
if "?" in sql_count:
|
|
281
|
+
sql_count = self.parse_place_holders(sql_count)
|
|
282
|
+
counter_cursor.execute(sql_count, params)
|
|
283
|
+
else:
|
|
284
|
+
counter_cursor.execute(sql_count)
|
|
285
|
+
count_records = counter_cursor.fetchall()
|
|
286
|
+
|
|
287
|
+
if len(count_records) > 0:
|
|
288
|
+
count_records = count_records[0][0]
|
|
289
|
+
else:
|
|
290
|
+
count_records = 0
|
|
291
|
+
except Exception as e:
|
|
292
|
+
Debug("FETCH ERROR:", sql_count, str(e), TINA4_LOG_ERROR)
|
|
293
|
+
finally:
|
|
294
|
+
counter_cursor.close()
|
|
295
|
+
|
|
296
|
+
sql = self.parse_place_holders(sql)
|
|
297
|
+
|
|
298
|
+
cursor.execute(sql, params)
|
|
299
|
+
return self.get_database_result(cursor, count_records, limit, skip)
|
|
300
|
+
except Exception as e:
|
|
301
|
+
Debug("FETCH ERROR:", sql, str(e), "params", params, "limit", limit, "skip", skip, Constant.TINA4_LOG_DEBUG)
|
|
302
|
+
return DatabaseResult(None, [], str(e))
|
|
303
|
+
|
|
304
|
+
def fetch_one(self, sql, params=[], skip=0):
|
|
305
|
+
"""
|
|
306
|
+
Fetch a single record based on a sql statement, take note that BLOB and byte record data is converted into base64 automatically
|
|
307
|
+
:param str sql: A plain SQL statement or one with params in it designated by ?
|
|
308
|
+
:param list params: A list of params in order of precedence
|
|
309
|
+
:param int skip: Offset of records to skip
|
|
310
|
+
:return: dict : A dictionary containing the single record
|
|
311
|
+
"""
|
|
312
|
+
# Calling the fetch method with limit as 1 and returning the result
|
|
313
|
+
record = self.fetch(sql, params=params, limit=1, skip=skip)
|
|
314
|
+
if record.error is None and record.count == 1:
|
|
315
|
+
data = {}
|
|
316
|
+
for key in record.records[0]:
|
|
317
|
+
if isinstance(record.records[0][key], (datetime.date, datetime.datetime)):
|
|
318
|
+
data[key] = record.records[0][key].isoformat()
|
|
319
|
+
if isinstance(record.records[0][key], bytes):
|
|
320
|
+
data[key] = base64.b64encode(record.records[0][key]).decode('utf-8')
|
|
321
|
+
else:
|
|
322
|
+
if isinstance(record.records[0][key], str) and self.is_json(record.records[0][key]):
|
|
323
|
+
data[key] = json.loads(record.records[0][key])
|
|
324
|
+
else:
|
|
325
|
+
data[key] = record.records[0][key]
|
|
326
|
+
return data
|
|
327
|
+
else:
|
|
328
|
+
return None
|
|
329
|
+
|
|
330
|
+
def parse_place_holders(self, sql):
|
|
331
|
+
"""
|
|
332
|
+
Sanitizes a sql statement to replace param chars with the appropriate placeholders
|
|
333
|
+
MYSQL expects %s and firebird, posgres and sqlite expect ?
|
|
334
|
+
:param sql:
|
|
335
|
+
:return:
|
|
336
|
+
"""
|
|
337
|
+
if self.database_engine == MYSQL or self.database_engine == POSTGRES or self.database_engine == MSSQL:
|
|
338
|
+
return sql.replace("?", "%s")
|
|
339
|
+
else:
|
|
340
|
+
return sql.replace("%s", "?")
|
|
341
|
+
|
|
342
|
+
def execute(self, sql, params=[]):
|
|
343
|
+
"""
|
|
344
|
+
Execute a query based on a sql statement
|
|
345
|
+
:param str sql: A plain SQL statement or one with params in it designated by ?
|
|
346
|
+
:param list params: A list of params in order of precedence
|
|
347
|
+
:return: DatabaseResult
|
|
348
|
+
"""
|
|
349
|
+
self.check_connected()
|
|
350
|
+
sql = self.parse_place_holders(sql)
|
|
351
|
+
cursor = self.dba.cursor()
|
|
352
|
+
# Running an execute statement and committing any changes to the database
|
|
353
|
+
try:
|
|
354
|
+
cursor.execute(sql, params)
|
|
355
|
+
if "returning" in sql.lower():
|
|
356
|
+
return self.get_database_result(cursor, 1, 1, 0)
|
|
357
|
+
else:
|
|
358
|
+
# see if we are mysql and if we are insert statement to get the last record
|
|
359
|
+
if "insert" in sql.lower() and (self.database_engine == MYSQL or self.database_engine == MSSQL):
|
|
360
|
+
return DatabaseResult([{"id": cursor.lastrowid}], [], None, 1, 1, 0)
|
|
361
|
+
|
|
362
|
+
# On success return an empty result set with no error
|
|
363
|
+
return DatabaseResult(None, [], None)
|
|
364
|
+
except Exception as e:
|
|
365
|
+
Debug("EXECUTE ERROR:", sql, str(e), Constant.TINA4_LOG_ERROR)
|
|
366
|
+
# Return the error in the result
|
|
367
|
+
return DatabaseResult(None, [], str(e))
|
|
368
|
+
finally:
|
|
369
|
+
cursor.close()
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def execute_many(self, sql, params=[]):
|
|
373
|
+
"""
|
|
374
|
+
Execute a query based on a single sql statement with a different number of params
|
|
375
|
+
:param sql: A plain SQL statement or one with params in it designated by ?
|
|
376
|
+
:param params: A list of params in order of precedence
|
|
377
|
+
:return: DatabaseResult
|
|
378
|
+
"""
|
|
379
|
+
self.check_connected()
|
|
380
|
+
sql = self.parse_place_holders(sql)
|
|
381
|
+
cursor = self.dba.cursor()
|
|
382
|
+
# Running an execute statement and committing any changes to the database
|
|
383
|
+
try:
|
|
384
|
+
cursor.executemany(sql, params)
|
|
385
|
+
# On success return an empty result set with no error
|
|
386
|
+
return DatabaseResult(None, [], None)
|
|
387
|
+
except Exception as e:
|
|
388
|
+
Debug("EXECUTE MANY ERROR:", sql, str(e), Constant.TINA4_LOG_ERROR)
|
|
389
|
+
# Return the error in the result
|
|
390
|
+
return DatabaseResult(None, [], str(e))
|
|
391
|
+
finally:
|
|
392
|
+
cursor.close()
|
|
393
|
+
|
|
394
|
+
def start_transaction(self):
|
|
395
|
+
"""
|
|
396
|
+
Starts a transaction
|
|
397
|
+
:return:
|
|
398
|
+
"""
|
|
399
|
+
try:
|
|
400
|
+
self.check_connected()
|
|
401
|
+
if self.database_engine == SQLITE:
|
|
402
|
+
self.dba.execute("BEGIN TRANSACTION")
|
|
403
|
+
elif self.database_engine == FIREBIRD:
|
|
404
|
+
self.dba.begin()
|
|
405
|
+
elif self.database_engine == MYSQL:
|
|
406
|
+
self.dba.start_transaction()
|
|
407
|
+
elif self.database_engine == MSSQL:
|
|
408
|
+
self.dba.execute("BEGIN TRANSACTION")
|
|
409
|
+
elif self.database_engine == POSTGRES:
|
|
410
|
+
self.dba.rollback() #start fresh
|
|
411
|
+
else:
|
|
412
|
+
Debug("START TRANSACTION ERROR:", "Database engine unrecognised/not supported",
|
|
413
|
+
Constant.TINA4_LOG_ERROR)
|
|
414
|
+
except Exception as e:
|
|
415
|
+
Debug("START TRANSACTION ERROR:", str(e), Constant.TINA4_LOG_ERROR)
|
|
416
|
+
|
|
417
|
+
def commit(self):
|
|
418
|
+
"""
|
|
419
|
+
Commit transaction
|
|
420
|
+
:return:
|
|
421
|
+
"""
|
|
422
|
+
try:
|
|
423
|
+
self.dba.commit()
|
|
424
|
+
except Exception as e:
|
|
425
|
+
Debug("COMMIT TRANSACTION ERROR:", str(e), Constant.TINA4_LOG_ERROR)
|
|
426
|
+
|
|
427
|
+
def rollback(self):
|
|
428
|
+
"""
|
|
429
|
+
Rollback transaction
|
|
430
|
+
:return:
|
|
431
|
+
"""
|
|
432
|
+
try:
|
|
433
|
+
self.dba.rollback()
|
|
434
|
+
except Exception as e:
|
|
435
|
+
Debug("ROLLBACK TRANSACTION ERROR:", str(e), Constant.TINA4_LOG_ERROR)
|
|
436
|
+
|
|
437
|
+
def close(self):
|
|
438
|
+
"""
|
|
439
|
+
Close database connection
|
|
440
|
+
:return:
|
|
441
|
+
"""
|
|
442
|
+
try:
|
|
443
|
+
self.dba.close()
|
|
444
|
+
except Exception as e:
|
|
445
|
+
Debug("DATABASE CLOSE ERROR:", str(e), Constant.TINA4_LOG_ERROR)
|
|
446
|
+
|
|
447
|
+
def sanitize(self, record):
|
|
448
|
+
"""
|
|
449
|
+
Changes dictionaries and list values into json for updating and inserting
|
|
450
|
+
:param record:
|
|
451
|
+
:return:
|
|
452
|
+
"""
|
|
453
|
+
for key in record:
|
|
454
|
+
if isinstance(record[key], list) or isinstance(record[key], dict):
|
|
455
|
+
record[key] = json.dumps(record[key])
|
|
456
|
+
return record
|
|
457
|
+
|
|
458
|
+
def insert(self, table_name, data, primary_key="id"):
|
|
459
|
+
"""
|
|
460
|
+
Insert data based on table name and data provided - single or multiple records
|
|
461
|
+
:param str table_name: Name of table
|
|
462
|
+
:param None data: List or Dictionary containing the data to be inserted
|
|
463
|
+
:param str primary_key: The name of the primary key of the table
|
|
464
|
+
"""
|
|
465
|
+
if isinstance(data, dict):
|
|
466
|
+
data = [data]
|
|
467
|
+
|
|
468
|
+
if isinstance(data, list):
|
|
469
|
+
columns = ", ".join(data[0].keys())
|
|
470
|
+
placeholders = ", ".join(['?'] * len(data[0]))
|
|
471
|
+
|
|
472
|
+
sql = f"INSERT INTO {table_name} ({columns}) VALUES ({placeholders})"
|
|
473
|
+
|
|
474
|
+
if self.database_engine == FIREBIRD or self.database_engine == SQLITE or self.database_engine == POSTGRES:
|
|
475
|
+
sql += f" returning ({primary_key})"
|
|
476
|
+
|
|
477
|
+
records = DatabaseResult()
|
|
478
|
+
|
|
479
|
+
result = None
|
|
480
|
+
for record in data:
|
|
481
|
+
record = self.sanitize(record)
|
|
482
|
+
if self.database_engine == MSSQL:
|
|
483
|
+
self.execute(f"SET IDENTITY_INSERT {table_name} ON")
|
|
484
|
+
result = self.execute(sql, list(record.values()))
|
|
485
|
+
if self.database_engine == MSSQL:
|
|
486
|
+
self.execute(f"SET IDENTITY_INSERT {table_name} OFF")
|
|
487
|
+
records.records += result.records
|
|
488
|
+
if result.error is not None:
|
|
489
|
+
Debug("INSERT ERROR:", sql, result.error, Constant.TINA4_LOG_ERROR)
|
|
490
|
+
return False
|
|
491
|
+
|
|
492
|
+
records.columns = result.columns
|
|
493
|
+
records.count = len(records.records)
|
|
494
|
+
return records
|
|
495
|
+
|
|
496
|
+
def delete(self, table_name, filter=None):
|
|
497
|
+
"""
|
|
498
|
+
Delete data based on table name and filter provided - single or multiple filters
|
|
499
|
+
:param str table_name: Name of table
|
|
500
|
+
:param str filter: Expression for deleting records
|
|
501
|
+
"""
|
|
502
|
+
placeholder = "?"
|
|
503
|
+
|
|
504
|
+
if filter is not None:
|
|
505
|
+
# Updating a single record - record passed in is a dictionary
|
|
506
|
+
if isinstance(filter, dict):
|
|
507
|
+
filter = [filter]
|
|
508
|
+
|
|
509
|
+
# Updating multiple records - records passed in is a list
|
|
510
|
+
if isinstance(filter, list):
|
|
511
|
+
sql = ""
|
|
512
|
+
result = None
|
|
513
|
+
for record in filter:
|
|
514
|
+
pk_value = []
|
|
515
|
+
condition_records = []
|
|
516
|
+
|
|
517
|
+
for column, value in record.items():
|
|
518
|
+
condition_records.append(f"{column} = {placeholder}")
|
|
519
|
+
pk_value.append(value)
|
|
520
|
+
|
|
521
|
+
condition_records = " and ".join(condition_records)
|
|
522
|
+
|
|
523
|
+
sql = f"DELETE FROM {table_name} WHERE {condition_records}"
|
|
524
|
+
|
|
525
|
+
params = pk_value
|
|
526
|
+
|
|
527
|
+
result = self.execute(sql, params)
|
|
528
|
+
if result.error is not None:
|
|
529
|
+
break
|
|
530
|
+
|
|
531
|
+
if result.error is None:
|
|
532
|
+
return True
|
|
533
|
+
else:
|
|
534
|
+
Debug("DELETE ERROR:", sql, result.error, Constant.TINA4_LOG_ERROR)
|
|
535
|
+
return False
|
|
536
|
+
|
|
537
|
+
def update(self, table_name, data, primary_key="id"):
|
|
538
|
+
"""
|
|
539
|
+
Update data based on table name and record/primary key provided - single or multiple records
|
|
540
|
+
:param str table_name: Name of table
|
|
541
|
+
:param None data: List or Dictionary containing the data to be inserted
|
|
542
|
+
:param str primary_key: The name of the primary key of the table
|
|
543
|
+
"""
|
|
544
|
+
placeholder = "?"
|
|
545
|
+
|
|
546
|
+
if data is not None:
|
|
547
|
+
# Updating a single record - record passed in is a dictionary
|
|
548
|
+
if isinstance(data, dict):
|
|
549
|
+
data = [data]
|
|
550
|
+
|
|
551
|
+
# Updating multiple records - records passed in is a list
|
|
552
|
+
if isinstance(data, list):
|
|
553
|
+
sql = ""
|
|
554
|
+
result = None
|
|
555
|
+
for record in data:
|
|
556
|
+
pk_value = None
|
|
557
|
+
condition_records = ""
|
|
558
|
+
set_clause_list = []
|
|
559
|
+
set_values = []
|
|
560
|
+
|
|
561
|
+
for column, value in record.items():
|
|
562
|
+
if column == primary_key:
|
|
563
|
+
condition_records = f"{column} = {placeholder}"
|
|
564
|
+
pk_value = value
|
|
565
|
+
else:
|
|
566
|
+
set_clause_list.append(f"{column} = {placeholder}")
|
|
567
|
+
if isinstance(value, list) or isinstance(value, dict):
|
|
568
|
+
set_values.append(json.dumps(value))
|
|
569
|
+
else:
|
|
570
|
+
set_values.append(value)
|
|
571
|
+
|
|
572
|
+
set_clause = ", ".join(set_clause_list)
|
|
573
|
+
|
|
574
|
+
sql = f"UPDATE {table_name} SET {set_clause} WHERE {condition_records}"
|
|
575
|
+
|
|
576
|
+
params = set_values + [pk_value]
|
|
577
|
+
|
|
578
|
+
if self.database_engine == MSSQL:
|
|
579
|
+
self.execute(f"SET IDENTITY_UPDATE {table_name} ON")
|
|
580
|
+
result = self.execute(sql, params)
|
|
581
|
+
if self.database_engine == MSSQL:
|
|
582
|
+
self.execute(f"SET IDENTITY_UPDATE {table_name} OFF")
|
|
583
|
+
if result.error is not None:
|
|
584
|
+
break
|
|
585
|
+
|
|
586
|
+
if result.error is None:
|
|
587
|
+
return True
|
|
588
|
+
else:
|
|
589
|
+
Debug("UPDATE ERROR:", sql, result.error, Constant.TINA4_LOG_ERROR)
|
|
590
|
+
return False
|
|
591
|
+
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Tina4 - This is not a 4ramework.
|
|
3
|
+
# Copy-right 2007 - current Tina4
|
|
4
|
+
# License: MIT https://opensource.org/licenses/MIT
|
|
5
|
+
#
|
|
6
|
+
# flake8: noqa: E501
|
|
7
|
+
import base64
|
|
8
|
+
import json
|
|
9
|
+
import datetime
|
|
10
|
+
from decimal import Decimal
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DatabaseResult:
|
|
14
|
+
def __init__(self, _records=None, _columns=None, _error=None, count=None, limit=None, skip=None):
|
|
15
|
+
"""
|
|
16
|
+
DatabaseResult constructor
|
|
17
|
+
:param _records:
|
|
18
|
+
:param _columns:
|
|
19
|
+
:param _error:
|
|
20
|
+
:param count:
|
|
21
|
+
:param limit:
|
|
22
|
+
:param skip:
|
|
23
|
+
"""
|
|
24
|
+
if count is not None:
|
|
25
|
+
self.total_count = count
|
|
26
|
+
else:
|
|
27
|
+
self.total_count = 0
|
|
28
|
+
|
|
29
|
+
if limit is not None:
|
|
30
|
+
self.limit = limit
|
|
31
|
+
else:
|
|
32
|
+
self.limit = 0
|
|
33
|
+
|
|
34
|
+
if skip is not None:
|
|
35
|
+
self.skip = skip
|
|
36
|
+
else:
|
|
37
|
+
self.skip = 0
|
|
38
|
+
|
|
39
|
+
if _records is not None:
|
|
40
|
+
self.records = _records
|
|
41
|
+
else:
|
|
42
|
+
self.records = []
|
|
43
|
+
|
|
44
|
+
self.count = len(self.records)
|
|
45
|
+
|
|
46
|
+
if _columns is not None:
|
|
47
|
+
self.columns = _columns
|
|
48
|
+
else:
|
|
49
|
+
self.columns = []
|
|
50
|
+
|
|
51
|
+
self.error = _error
|
|
52
|
+
|
|
53
|
+
def to_paginate(self):
|
|
54
|
+
|
|
55
|
+
return {"recordsTotal": self.total_count, "recordsOffset": self.skip, "recordCount": self.count, "recordsFiltered": self.total_count, "fields": self.columns, "data": self.to_array(), "dataError": self.error}
|
|
56
|
+
|
|
57
|
+
def to_array(self, _filter=None):
|
|
58
|
+
"""
|
|
59
|
+
Creates an array or list of the items
|
|
60
|
+
:return:
|
|
61
|
+
"""
|
|
62
|
+
if self.error is not None:
|
|
63
|
+
return {"error": self.error}
|
|
64
|
+
elif len(self.records) > 0:
|
|
65
|
+
# check all the records - if we get bytes we base64encode them for the json to work
|
|
66
|
+
json_records = []
|
|
67
|
+
for record in self.records:
|
|
68
|
+
json_record = {}
|
|
69
|
+
for key in record:
|
|
70
|
+
if isinstance(record[key], Decimal):
|
|
71
|
+
json_record[key] = float(record[key])
|
|
72
|
+
elif isinstance(record[key], (datetime.date, datetime.datetime)):
|
|
73
|
+
json_record[key] = record[key].isoformat()
|
|
74
|
+
elif isinstance(record[key], memoryview):
|
|
75
|
+
json_record[key] = base64.b64encode(record[key].tobytes()).decode('utf-8')
|
|
76
|
+
elif isinstance(record[key], bytes):
|
|
77
|
+
json_record[key] = base64.b64encode(record[key]).decode('utf-8')
|
|
78
|
+
else:
|
|
79
|
+
json_record[key] = record[key]
|
|
80
|
+
|
|
81
|
+
if _filter is not None:
|
|
82
|
+
json_record = _filter(json_record)
|
|
83
|
+
|
|
84
|
+
json_records.append(json_record)
|
|
85
|
+
|
|
86
|
+
return json_records
|
|
87
|
+
else:
|
|
88
|
+
return []
|
|
89
|
+
|
|
90
|
+
def to_list(self, _filter=None):
|
|
91
|
+
return self.to_array(_filter)
|
|
92
|
+
|
|
93
|
+
def to_json(self, _filter=None):
|
|
94
|
+
return json.dumps(self.to_array(_filter))
|
|
95
|
+
|
|
96
|
+
def __iter__(self):
|
|
97
|
+
return iter(self.to_array())
|
|
98
|
+
|
|
99
|
+
def __getitem__(self, item):
|
|
100
|
+
if item < len(self.records):
|
|
101
|
+
return self.records[item]
|
|
102
|
+
else:
|
|
103
|
+
return {}
|
|
104
|
+
|
|
105
|
+
def __str__(self):
|
|
106
|
+
return self.to_json()
|
|
107
|
+
|