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.
Files changed (47) hide show
  1. tina4_python/Auth.py +222 -0
  2. tina4_python/Constant.py +43 -0
  3. tina4_python/Database.py +591 -0
  4. tina4_python/DatabaseResult.py +107 -0
  5. tina4_python/DatabaseTypes.py +15 -0
  6. tina4_python/Debug.py +126 -0
  7. tina4_python/Env.py +37 -0
  8. tina4_python/Localization.py +42 -0
  9. tina4_python/Messages.py +30 -0
  10. tina4_python/MiddleWare.py +90 -0
  11. tina4_python/Migration.py +107 -0
  12. tina4_python/ORM.py +639 -0
  13. tina4_python/Queue.py +615 -0
  14. tina4_python/Request.py +19 -0
  15. tina4_python/Response.py +121 -0
  16. tina4_python/Router.py +423 -0
  17. tina4_python/Session.py +342 -0
  18. tina4_python/ShellColors.py +20 -0
  19. tina4_python/Swagger.py +228 -0
  20. tina4_python/Template.py +107 -0
  21. tina4_python/Webserver.py +429 -0
  22. tina4_python/Websocket.py +49 -0
  23. tina4_python/__init__.py +392 -0
  24. tina4_python/messages.pot +83 -0
  25. tina4_python/public/css/readme.md +0 -0
  26. tina4_python/public/favicon.ico +0 -0
  27. tina4_python/public/images/403.png +0 -0
  28. tina4_python/public/images/404.png +0 -0
  29. tina4_python/public/images/500.png +0 -0
  30. tina4_python/public/images/logo.png +0 -0
  31. tina4_python/public/images/readme.md +0 -0
  32. tina4_python/public/js/readme.md +0 -0
  33. tina4_python/public/js/reconnecting-websocket.js +365 -0
  34. tina4_python/public/js/tina4helper.js +397 -0
  35. tina4_python/public/swagger/index.html +90 -0
  36. tina4_python/public/swagger/oauth2-redirect.html +63 -0
  37. tina4_python/templates/errors/403.twig +10 -0
  38. tina4_python/templates/errors/404.twig +10 -0
  39. tina4_python/templates/errors/500.twig +11 -0
  40. tina4_python/templates/readme.md +1 -0
  41. tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  42. tina4_python/translations/en/LC_MESSAGES/messages.po +80 -0
  43. tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  44. tina4_python/translations/fr/LC_MESSAGES/messages.po +84 -0
  45. tina4_python-0.2.122.dist-info/METADATA +465 -0
  46. tina4_python-0.2.122.dist-info/RECORD +47 -0
  47. tina4_python-0.2.122.dist-info/WHEEL +4 -0
@@ -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
+