databased.databased
1import logging 2import os 3import sqlite3 4from datetime import datetime 5from functools import wraps 6from typing import Any 7 8import pandas 9from griddle import griddy 10from pathier import Pathier 11from tabulate import tabulate 12 13 14def _connect(func): 15 """Decorator to open db connection if it isn't already open.""" 16 17 @wraps(func) 18 def inner(self, *args, **kwargs): 19 if not self.connection_open: 20 self.open() 21 results = func(self, *args, **kwargs) 22 return results 23 24 return inner 25 26 27def _disconnect(func): 28 """Decorator to commit and close db connection. 29 30 Primarily intended for when `DataBased` is subclassed and the inhereting class 31 has functions that call parent class functions that are decorated with `_connect`. 32 Decorating the child class function with `_disconnect` avoids having to manually close 33 the connection or use a context manager in an application.""" 34 35 @wraps(func) 36 def inner(self, *args, **kwargs): 37 result = func(self, *args, **kwargs) 38 if self.connection_open: 39 self.close() 40 return result 41 42 return inner 43 44 45class DataBased: 46 """Sqli wrapper so queries don't need to be written except table definitions. 47 48 Supports saving and reading dates as datetime objects. 49 50 Supports using a context manager.""" 51 52 def __init__( 53 self, 54 dbpath: str | Pathier, 55 logger_encoding: str = "utf-8", 56 logger_message_format: str = "{levelname}|-|{asctime}|-|{message}", 57 connection_timeout: float = 10, 58 ): 59 """ 60 #### :params: 61 62 * `dbpath`: String or Path object to database file. 63 If a relative path is given, it will be relative to the 64 current working directory. The log file will be saved to the 65 same directory. 66 67 * `logger_message_format`: `{` style format string for the logger object. 68 69 * `connection_timeout`: The number of seconds to wait when trying to connect to the database before throwing an error.""" 70 self.dbpath = Pathier(dbpath) 71 self.dbname = Pathier(dbpath).name 72 self.dbpath.parent.mkdir(parents=True, exist_ok=True) 73 self._logger_init( 74 encoding=logger_encoding, message_format=logger_message_format 75 ) 76 self.connection_open = False 77 self.connection_timeout = connection_timeout 78 79 def __enter__(self): 80 self.open() 81 return self 82 83 def __exit__(self, exception_type, exception_value, exception_traceback): 84 self.close() 85 86 @property 87 def connection_timeout(self) -> float: 88 return self._connection_timeout 89 90 @connection_timeout.setter 91 def connection_timeout(self, num_seconds: float): 92 self._connection_timeout = num_seconds 93 if self.connection_open: 94 self.close() 95 self.open() 96 97 def open(self): 98 """Open connection to db.""" 99 self.connection = sqlite3.connect( 100 self.dbpath, 101 detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES, 102 timeout=self.connection_timeout, 103 ) 104 self.connection.execute("pragma foreign_keys = 1;") 105 self.cursor = self.connection.cursor() 106 self.connection_open = True 107 108 def close(self): 109 """Save and close connection to db. 110 111 Call this as soon as you are done using the database if you have 112 multiple threads or processes using the same database.""" 113 if self.connection_open: 114 self.connection.commit() 115 self.connection.close() 116 self.connection_open = False 117 118 def _logger_init( 119 self, 120 message_format: str = "{levelname}|-|{asctime}|-|{message}", 121 encoding: str = "utf-8", 122 ): 123 """:param `message_format`: '{' style format string""" 124 self.logger = logging.getLogger(self.dbname) 125 if not self.logger.hasHandlers(): 126 handler = logging.FileHandler( 127 str(self.dbpath).replace(".", "") + ".log", encoding=encoding 128 ) 129 handler.setFormatter( 130 logging.Formatter( 131 message_format, style="{", datefmt="%m/%d/%Y %I:%M:%S %p" 132 ) 133 ) 134 self.logger.addHandler(handler) 135 self.logger.setLevel(logging.INFO) 136 137 def _get_dict( 138 self, table: str, values: list, columns_to_return: list[str] | None = None 139 ) -> dict: 140 """Converts the values of a row into a dictionary with column names as keys. 141 142 #### :params: 143 144 `table`: The table that values were pulled from. 145 146 `values`: List of values expected to be the same quantity 147 and in the same order as the column names of table. 148 149 `columns_to_return`: An optional list of column names. 150 If given, only these columns will be included in the returned dictionary. 151 Otherwise all columns and values are returned.""" 152 return { 153 column: value 154 for column, value in zip(self.get_column_names(table), values) 155 if not columns_to_return or column in columns_to_return 156 } 157 158 def _get_conditions( 159 self, match_criteria: list[tuple] | dict, exact_match: bool = True 160 ) -> str: 161 """Builds and returns the conditional portion of a query. 162 163 #### :params: 164 165 `match_criteria`: Can be a list of 2-tuples where each 166 tuple is `(columnName, rowValue)` or a dictionary where 167 keys are column names and values are row values. 168 169 `exact_match`: If `False`, the row value for a given column 170 will be matched as a substring. 171 172 Usage e.g.: 173 174 >>> self.cursor.execute(f'select * from {table} where {conditions};')""" 175 if type(match_criteria) == dict: 176 match_criteria = [(k, v) for k, v in match_criteria.items()] 177 if exact_match: 178 conditions = " and ".join( 179 f'"{column_row[0]}" = "{column_row[1]}"' 180 for column_row in match_criteria 181 ) 182 else: 183 conditions = " and ".join( 184 f'"{column_row[0]}" like "%{column_row[1]}%"' 185 for column_row in match_criteria 186 ) 187 return f"({conditions})" 188 189 def vacuum(self): 190 """Reduce disk size of the database with a `VACUUM` query.""" 191 self.query("VACUUM;") 192 193 @_connect 194 def query(self, query_) -> list[Any]: 195 """Execute an arbitrary query and return the results.""" 196 self.cursor.execute(query_) 197 return self.cursor.fetchall() 198 199 @_connect 200 def create_tables(self, table_defs: list[str] = []): 201 """Create tables if they don't exist. 202 203 :param `table_defs`: Each definition should be in the form `table_name(column_definitions)`""" 204 if len(table_defs) > 0: 205 table_names = self.get_table_names() 206 for table in table_defs: 207 if table.split("(")[0].strip() not in table_names: 208 self.cursor.execute(f"create table [{table}];") 209 self.logger.info(f'{table.split("(")[0]} table created.') 210 211 @_connect 212 def create_table(self, table: str, column_defs: list[str]): 213 """Create a table if it doesn't exist. 214 215 #### :params: 216 217 `table`: Name of the table to create. 218 219 `column_defs`: List of column definitions in proper Sqlite3 sytax. 220 i.e. `"column_name text unique"` or `"column_name int primary key"` etc.""" 221 if table not in self.get_table_names(): 222 query = f"create table [{table}]({', '.join(column_defs)});" 223 self.cursor.execute(query) 224 self.logger.info(f"'{table}' table created.") 225 226 @_connect 227 def get_table_names(self) -> list[str]: 228 """Returns a list of table names from the database.""" 229 self.cursor.execute( 230 'select name from sqlite_Schema where type = "table" and name not like "sqlite_%";' 231 ) 232 return [result[0] for result in self.cursor.fetchall()] 233 234 @_connect 235 def get_column_names(self, table: str) -> list[str]: 236 """Return a list of column names from a table.""" 237 self.cursor.execute(f"select * from [{table}] where 1=0;") 238 return [description[0] for description in self.cursor.description] 239 240 @_connect 241 def count( 242 self, 243 table: str, 244 match_criteria: list[tuple] | dict | None = None, 245 exact_match: bool = True, 246 ) -> int: 247 """Return number of items in `table`. 248 249 #### :params: 250 251 `match_criteria`: Can be a list of 2-tuples where each 252 tuple is `(columnName, rowValue)` or a dictionary where 253 keys are column names and values are row values. 254 If `None`, all rows from the table will be counted. 255 256 `exact_match`: If `False`, the row value for a given column 257 in `match_criteria` will be matched as a substring. 258 Has no effect if `match_criteria` is `None`. 259 """ 260 query = f"select count(_rowid_) from [{table}]" 261 try: 262 if match_criteria: 263 self.cursor.execute( 264 f"{query} where {self._get_conditions(match_criteria, exact_match)};" 265 ) 266 else: 267 self.cursor.execute(f"{query}") 268 return self.cursor.fetchone()[0] 269 except: 270 return 0 271 272 @_connect 273 def add_row( 274 self, table: str, values: tuple[Any], columns: tuple[str] | None = None 275 ) -> bool: 276 """Add a row of values to a table. 277 278 Returns whether the addition was successful or not. 279 280 #### :params: 281 282 `table`: The table to insert values into. 283 284 `values`: A tuple of values to be inserted into the table. 285 286 `columns`: If `None`, `values` is expected to supply a value for every column in the table. 287 If `columns` is provided, it should contain the same number of elements as `values`.""" 288 parameterizer = ", ".join("?" for _ in values) 289 logger_values = ", ".join(str(value) for value in values) 290 try: 291 if columns: 292 columns_query = ", ".join(column for column in columns) 293 self.cursor.execute( 294 f"insert into [{table}] ({columns_query}) values({parameterizer});", 295 values, 296 ) 297 else: 298 self.cursor.execute( 299 f"insert into [{table}] values({parameterizer});", values 300 ) 301 self.logger.info(f'Added "{logger_values}" to {table} table.') 302 return True 303 except Exception as e: 304 if "constraint" not in str(e).lower(): 305 self.logger.exception( 306 f'Error adding "{logger_values}" to {table} table.' 307 ) 308 else: 309 self.logger.debug(str(e)) 310 return False 311 312 @_connect 313 def add_rows( 314 self, table: str, values: list[tuple[Any]], columns: tuple[str] | None = None 315 ) -> tuple[int, int]: 316 """Add multiple rows of values to a table. 317 318 Returns a tuple containing the number of successful additions and the number of failed additions. 319 320 #### :params: 321 322 `table`: The table to insert values into. 323 324 `values`: A list of tuples of values to be inserted into the table. 325 Each tuple constitutes a single row to be inserted 326 327 `columns`: If `None`, `values` is expected to supply a value for every column in the table. 328 If `columns` is provided, it should contain the same number of elements as `values`.""" 329 successes = 0 330 failures = 0 331 for row in values: 332 if self.add_row(table, row, columns): 333 successes += 1 334 else: 335 failures += 1 336 return (successes, failures) 337 338 @_connect 339 def get_rows( 340 self, 341 table: str, 342 match_criteria: list[tuple] | dict | None = None, 343 exact_match: bool = True, 344 sort_by_column: str | None = None, 345 columns_to_return: list[str] | None = None, 346 return_as_dataframe: bool = False, 347 values_only: bool = False, 348 order_by: str | None = None, 349 limit: str | int | None = None, 350 ) -> list[dict] | list[tuple] | pandas.DataFrame: 351 """Return matching rows from `table`. 352 353 By default, rows will be returned as a list of dictionaries of the form `[{"column_name": value, ...}, ...]` 354 355 356 #### :params: 357 358 `match_criteria`: Can be a list of 2-tuples where each 359 tuple is `(columnName, rowValue)` or a dictionary where 360 keys are column names and values are row values. 361 362 `exact_match`: If `False`, the row value for a given column will be matched as a substring. 363 364 `sort_by_column`: A column name to sort the results by. 365 This will sort results in Python after retrieving them from the db. 366 Use the 'order_by' param to use SQLite engine for ordering. 367 368 `columns_to_return`: Optional list of column names. 369 If provided, the elements returned by this function will only contain the provided columns. 370 Otherwise every column in the row is returned. 371 372 `return_as_dataframe`: Return the results as a `pandas.DataFrame` object. 373 374 `values_only`: Return the results as a list of tuples. 375 376 `order_by`: If given, a `order by {order_by}` clause will be added to the select query. 377 378 `limit`: If given, a `limit {limit}` clause will be added to the select query. 379 """ 380 381 if type(columns_to_return) is str: 382 columns_to_return = [columns_to_return] 383 query = f"select * from [{table}]" 384 matches = [] 385 if match_criteria: 386 query += f" where {self._get_conditions(match_criteria, exact_match)}" 387 if order_by: 388 query += f" order by {order_by}" 389 if limit: 390 query += f" limit {limit}" 391 query += ";" 392 self.cursor.execute(query) 393 matches = self.cursor.fetchall() 394 results = [self._get_dict(table, match, columns_to_return) for match in matches] 395 if sort_by_column: 396 results = sorted(results, key=lambda x: x[sort_by_column]) 397 if return_as_dataframe: 398 return pandas.DataFrame(results) 399 if values_only: 400 return [tuple(row.values()) for row in results] 401 else: 402 return results 403 404 @_connect 405 def find( 406 self, table: str, query_string: str, columns: list[str] | None = None 407 ) -> list[dict]: 408 """Search for rows that contain `query_string` as a substring of any column. 409 410 #### :params: 411 412 `table`: The table to search. 413 414 `query_string`: The substring to search for in all columns. 415 416 `columns`: A list of columns to search for query_string. 417 If None, all columns in the table will be searched. 418 """ 419 if type(columns) is str: 420 columns = [columns] 421 results = [] 422 if not columns: 423 columns = self.get_column_names(table) 424 for column in columns: 425 results.extend( 426 [ 427 row 428 for row in self.get_rows( 429 table, [(column, query_string)], exact_match=False 430 ) 431 if row not in results 432 ] 433 ) 434 return results 435 436 @_connect 437 def delete( 438 self, table: str, match_criteria: list[tuple] | dict, exact_match: bool = True 439 ) -> int: 440 """Delete records from `table`. 441 442 Returns the number of deleted records. 443 444 #### :params: 445 446 `match_criteria`: Can be a list of 2-tuples where each tuple is `(column_name, value)` 447 or a dictionary where keys are column names and values are corresponding values. 448 449 `exact_match`: If `False`, the value for a given column will be matched as a substring. 450 """ 451 conditions = self._get_conditions(match_criteria, exact_match) 452 try: 453 self.cursor.execute(f"delete from [{table}] where {conditions};") 454 num_deletions = self.cursor.rowcount 455 self.logger.info( 456 f'Deleted {num_deletions} rows from "{table}" where {conditions}".' 457 ) 458 return num_deletions 459 except Exception as e: 460 self.logger.debug( 461 f'Error deleting rows from "{table}" where {conditions}.\n{e}' 462 ) 463 return 0 464 465 @_connect 466 def update( 467 self, 468 table: str, 469 column_to_update: str, 470 new_value: Any, 471 match_criteria: list[tuple] | dict | None = None, 472 exact_match: bool = True, 473 ) -> int: 474 """Update the value in `column_to_update` to `new_value` for rows matched with `match_criteria`. 475 476 #### :params: 477 478 `table`: The table to update rows in. 479 480 `column_to_update`: The column to be updated in the matched rows. 481 482 `new_value`: The new value to insert. 483 484 `match_criteria`: Can be a list of 2-tuples where each tuple is `(columnName, rowValue)` 485 or a dictionary where keys are column names and values are corresponding values. 486 If `None`, every row in `table` will be updated. 487 488 `exact_match`: If `False`, `match_criteria` values will be treated as substrings. 489 490 Returns the number of updated rows.""" 491 query = f"update [{table}] set {column_to_update} = ?" 492 conditions = "" 493 if match_criteria: 494 conditions = self._get_conditions(match_criteria, exact_match) 495 query += f" where {conditions}" 496 else: 497 conditions = None 498 query += ";" 499 try: 500 self.cursor.execute( 501 query, 502 (new_value,), 503 ) 504 num_updates = self.cursor.rowcount 505 self.logger.info( 506 f'In {num_updates} rows, updated "{column_to_update}" in "{table}" table to "{new_value}" where {conditions}' 507 ) 508 return num_updates 509 except Exception as e: 510 self.logger.error( 511 f'Failed to update "{column_to_update}" in "{table}" table to "{new_value}" where {conditions}"\n{e}' 512 ) 513 return 0 514 515 @_connect 516 def drop_table(self, table: str) -> bool: 517 """Drop `table` from the database. 518 519 Returns `True` if successful, `False` if not.""" 520 try: 521 self.cursor.execute(f"drop Table [{table}];") 522 self.logger.info(f'Dropped table "{table}"') 523 return True 524 except Exception as e: 525 print(e) 526 self.logger.error(f'Failed to drop table "{table}"') 527 return False 528 529 @_connect 530 def add_column( 531 self, table: str, column: str, _type: str, default_value: str | None = None 532 ): 533 """Add a new column to `table`. 534 535 #### :params: 536 537 `column`: Name of the column to add. 538 539 `_type`: The data type of the new column. 540 541 `default_value`: Optional default value for the column.""" 542 try: 543 if default_value: 544 self.cursor.execute( 545 f"alter table [{table}] add column {column} {_type} default {default_value};" 546 ) 547 self.update(table, column, default_value) 548 else: 549 self.cursor.execute( 550 f"alter table [{table}] add column {column} {_type};" 551 ) 552 self.logger.info(f'Added column "{column}" to "{table}" table.') 553 except Exception as e: 554 self.logger.error(f'Failed to add column "{column}" to "{table}" table.') 555 556 @staticmethod 557 def data_to_string( 558 data: list[dict], sort_key: str | None = None, wrap_to_terminal: bool = True 559 ) -> str: 560 """Uses tabulate to produce pretty string output from a list of dictionaries. 561 562 #### :params: 563 564 `data`: The list of dictionaries to create a grid from. 565 Assumes all dictionaries in list have the same set of keys. 566 567 `sort_key`: Optional dictionary key to sort data with. 568 569 `wrap_to_terminal`: If `True`, the table width will be wrapped to fit within the current terminal window. 570 Pass as `False` if the output is going into something like a `.txt` file.""" 571 return data_to_string(data, sort_key, wrap_to_terminal) 572 573 574def data_to_string( 575 data: list[dict], sort_key: str | None = None, wrap_to_terminal: bool = True 576) -> str: 577 """Uses tabulate to produce pretty string output from a list of dictionaries. 578 579 #### :params: 580 581 `data`: The list of dictionaries to create a grid from. 582 Assumes all dictionaries in list have the same set of keys. 583 584 `sort_key`: Optional dictionary key to sort data with. 585 586 `wrap_to_terminal`: If `True`, the table width will be wrapped to fit within the current terminal window. 587 Pass as `False` if the output is going into something like a `.txt` file.""" 588 if len(data) == 0: 589 return "" 590 if sort_key: 591 data = sorted(data, key=lambda d: d[sort_key]) 592 for i, d in enumerate(data): 593 for k in d: 594 data[i][k] = str(data[i][k]) 595 596 try: 597 print("Resizing grid to fit within the terminal...\n") 598 return griddy(data, "keys", wrap_to_terminal) 599 except RuntimeError as e: 600 print(e) 601 return str(data)
46class DataBased: 47 """Sqli wrapper so queries don't need to be written except table definitions. 48 49 Supports saving and reading dates as datetime objects. 50 51 Supports using a context manager.""" 52 53 def __init__( 54 self, 55 dbpath: str | Pathier, 56 logger_encoding: str = "utf-8", 57 logger_message_format: str = "{levelname}|-|{asctime}|-|{message}", 58 connection_timeout: float = 10, 59 ): 60 """ 61 #### :params: 62 63 * `dbpath`: String or Path object to database file. 64 If a relative path is given, it will be relative to the 65 current working directory. The log file will be saved to the 66 same directory. 67 68 * `logger_message_format`: `{` style format string for the logger object. 69 70 * `connection_timeout`: The number of seconds to wait when trying to connect to the database before throwing an error.""" 71 self.dbpath = Pathier(dbpath) 72 self.dbname = Pathier(dbpath).name 73 self.dbpath.parent.mkdir(parents=True, exist_ok=True) 74 self._logger_init( 75 encoding=logger_encoding, message_format=logger_message_format 76 ) 77 self.connection_open = False 78 self.connection_timeout = connection_timeout 79 80 def __enter__(self): 81 self.open() 82 return self 83 84 def __exit__(self, exception_type, exception_value, exception_traceback): 85 self.close() 86 87 @property 88 def connection_timeout(self) -> float: 89 return self._connection_timeout 90 91 @connection_timeout.setter 92 def connection_timeout(self, num_seconds: float): 93 self._connection_timeout = num_seconds 94 if self.connection_open: 95 self.close() 96 self.open() 97 98 def open(self): 99 """Open connection to db.""" 100 self.connection = sqlite3.connect( 101 self.dbpath, 102 detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES, 103 timeout=self.connection_timeout, 104 ) 105 self.connection.execute("pragma foreign_keys = 1;") 106 self.cursor = self.connection.cursor() 107 self.connection_open = True 108 109 def close(self): 110 """Save and close connection to db. 111 112 Call this as soon as you are done using the database if you have 113 multiple threads or processes using the same database.""" 114 if self.connection_open: 115 self.connection.commit() 116 self.connection.close() 117 self.connection_open = False 118 119 def _logger_init( 120 self, 121 message_format: str = "{levelname}|-|{asctime}|-|{message}", 122 encoding: str = "utf-8", 123 ): 124 """:param `message_format`: '{' style format string""" 125 self.logger = logging.getLogger(self.dbname) 126 if not self.logger.hasHandlers(): 127 handler = logging.FileHandler( 128 str(self.dbpath).replace(".", "") + ".log", encoding=encoding 129 ) 130 handler.setFormatter( 131 logging.Formatter( 132 message_format, style="{", datefmt="%m/%d/%Y %I:%M:%S %p" 133 ) 134 ) 135 self.logger.addHandler(handler) 136 self.logger.setLevel(logging.INFO) 137 138 def _get_dict( 139 self, table: str, values: list, columns_to_return: list[str] | None = None 140 ) -> dict: 141 """Converts the values of a row into a dictionary with column names as keys. 142 143 #### :params: 144 145 `table`: The table that values were pulled from. 146 147 `values`: List of values expected to be the same quantity 148 and in the same order as the column names of table. 149 150 `columns_to_return`: An optional list of column names. 151 If given, only these columns will be included in the returned dictionary. 152 Otherwise all columns and values are returned.""" 153 return { 154 column: value 155 for column, value in zip(self.get_column_names(table), values) 156 if not columns_to_return or column in columns_to_return 157 } 158 159 def _get_conditions( 160 self, match_criteria: list[tuple] | dict, exact_match: bool = True 161 ) -> str: 162 """Builds and returns the conditional portion of a query. 163 164 #### :params: 165 166 `match_criteria`: Can be a list of 2-tuples where each 167 tuple is `(columnName, rowValue)` or a dictionary where 168 keys are column names and values are row values. 169 170 `exact_match`: If `False`, the row value for a given column 171 will be matched as a substring. 172 173 Usage e.g.: 174 175 >>> self.cursor.execute(f'select * from {table} where {conditions};')""" 176 if type(match_criteria) == dict: 177 match_criteria = [(k, v) for k, v in match_criteria.items()] 178 if exact_match: 179 conditions = " and ".join( 180 f'"{column_row[0]}" = "{column_row[1]}"' 181 for column_row in match_criteria 182 ) 183 else: 184 conditions = " and ".join( 185 f'"{column_row[0]}" like "%{column_row[1]}%"' 186 for column_row in match_criteria 187 ) 188 return f"({conditions})" 189 190 def vacuum(self): 191 """Reduce disk size of the database with a `VACUUM` query.""" 192 self.query("VACUUM;") 193 194 @_connect 195 def query(self, query_) -> list[Any]: 196 """Execute an arbitrary query and return the results.""" 197 self.cursor.execute(query_) 198 return self.cursor.fetchall() 199 200 @_connect 201 def create_tables(self, table_defs: list[str] = []): 202 """Create tables if they don't exist. 203 204 :param `table_defs`: Each definition should be in the form `table_name(column_definitions)`""" 205 if len(table_defs) > 0: 206 table_names = self.get_table_names() 207 for table in table_defs: 208 if table.split("(")[0].strip() not in table_names: 209 self.cursor.execute(f"create table [{table}];") 210 self.logger.info(f'{table.split("(")[0]} table created.') 211 212 @_connect 213 def create_table(self, table: str, column_defs: list[str]): 214 """Create a table if it doesn't exist. 215 216 #### :params: 217 218 `table`: Name of the table to create. 219 220 `column_defs`: List of column definitions in proper Sqlite3 sytax. 221 i.e. `"column_name text unique"` or `"column_name int primary key"` etc.""" 222 if table not in self.get_table_names(): 223 query = f"create table [{table}]({', '.join(column_defs)});" 224 self.cursor.execute(query) 225 self.logger.info(f"'{table}' table created.") 226 227 @_connect 228 def get_table_names(self) -> list[str]: 229 """Returns a list of table names from the database.""" 230 self.cursor.execute( 231 'select name from sqlite_Schema where type = "table" and name not like "sqlite_%";' 232 ) 233 return [result[0] for result in self.cursor.fetchall()] 234 235 @_connect 236 def get_column_names(self, table: str) -> list[str]: 237 """Return a list of column names from a table.""" 238 self.cursor.execute(f"select * from [{table}] where 1=0;") 239 return [description[0] for description in self.cursor.description] 240 241 @_connect 242 def count( 243 self, 244 table: str, 245 match_criteria: list[tuple] | dict | None = None, 246 exact_match: bool = True, 247 ) -> int: 248 """Return number of items in `table`. 249 250 #### :params: 251 252 `match_criteria`: Can be a list of 2-tuples where each 253 tuple is `(columnName, rowValue)` or a dictionary where 254 keys are column names and values are row values. 255 If `None`, all rows from the table will be counted. 256 257 `exact_match`: If `False`, the row value for a given column 258 in `match_criteria` will be matched as a substring. 259 Has no effect if `match_criteria` is `None`. 260 """ 261 query = f"select count(_rowid_) from [{table}]" 262 try: 263 if match_criteria: 264 self.cursor.execute( 265 f"{query} where {self._get_conditions(match_criteria, exact_match)};" 266 ) 267 else: 268 self.cursor.execute(f"{query}") 269 return self.cursor.fetchone()[0] 270 except: 271 return 0 272 273 @_connect 274 def add_row( 275 self, table: str, values: tuple[Any], columns: tuple[str] | None = None 276 ) -> bool: 277 """Add a row of values to a table. 278 279 Returns whether the addition was successful or not. 280 281 #### :params: 282 283 `table`: The table to insert values into. 284 285 `values`: A tuple of values to be inserted into the table. 286 287 `columns`: If `None`, `values` is expected to supply a value for every column in the table. 288 If `columns` is provided, it should contain the same number of elements as `values`.""" 289 parameterizer = ", ".join("?" for _ in values) 290 logger_values = ", ".join(str(value) for value in values) 291 try: 292 if columns: 293 columns_query = ", ".join(column for column in columns) 294 self.cursor.execute( 295 f"insert into [{table}] ({columns_query}) values({parameterizer});", 296 values, 297 ) 298 else: 299 self.cursor.execute( 300 f"insert into [{table}] values({parameterizer});", values 301 ) 302 self.logger.info(f'Added "{logger_values}" to {table} table.') 303 return True 304 except Exception as e: 305 if "constraint" not in str(e).lower(): 306 self.logger.exception( 307 f'Error adding "{logger_values}" to {table} table.' 308 ) 309 else: 310 self.logger.debug(str(e)) 311 return False 312 313 @_connect 314 def add_rows( 315 self, table: str, values: list[tuple[Any]], columns: tuple[str] | None = None 316 ) -> tuple[int, int]: 317 """Add multiple rows of values to a table. 318 319 Returns a tuple containing the number of successful additions and the number of failed additions. 320 321 #### :params: 322 323 `table`: The table to insert values into. 324 325 `values`: A list of tuples of values to be inserted into the table. 326 Each tuple constitutes a single row to be inserted 327 328 `columns`: If `None`, `values` is expected to supply a value for every column in the table. 329 If `columns` is provided, it should contain the same number of elements as `values`.""" 330 successes = 0 331 failures = 0 332 for row in values: 333 if self.add_row(table, row, columns): 334 successes += 1 335 else: 336 failures += 1 337 return (successes, failures) 338 339 @_connect 340 def get_rows( 341 self, 342 table: str, 343 match_criteria: list[tuple] | dict | None = None, 344 exact_match: bool = True, 345 sort_by_column: str | None = None, 346 columns_to_return: list[str] | None = None, 347 return_as_dataframe: bool = False, 348 values_only: bool = False, 349 order_by: str | None = None, 350 limit: str | int | None = None, 351 ) -> list[dict] | list[tuple] | pandas.DataFrame: 352 """Return matching rows from `table`. 353 354 By default, rows will be returned as a list of dictionaries of the form `[{"column_name": value, ...}, ...]` 355 356 357 #### :params: 358 359 `match_criteria`: Can be a list of 2-tuples where each 360 tuple is `(columnName, rowValue)` or a dictionary where 361 keys are column names and values are row values. 362 363 `exact_match`: If `False`, the row value for a given column will be matched as a substring. 364 365 `sort_by_column`: A column name to sort the results by. 366 This will sort results in Python after retrieving them from the db. 367 Use the 'order_by' param to use SQLite engine for ordering. 368 369 `columns_to_return`: Optional list of column names. 370 If provided, the elements returned by this function will only contain the provided columns. 371 Otherwise every column in the row is returned. 372 373 `return_as_dataframe`: Return the results as a `pandas.DataFrame` object. 374 375 `values_only`: Return the results as a list of tuples. 376 377 `order_by`: If given, a `order by {order_by}` clause will be added to the select query. 378 379 `limit`: If given, a `limit {limit}` clause will be added to the select query. 380 """ 381 382 if type(columns_to_return) is str: 383 columns_to_return = [columns_to_return] 384 query = f"select * from [{table}]" 385 matches = [] 386 if match_criteria: 387 query += f" where {self._get_conditions(match_criteria, exact_match)}" 388 if order_by: 389 query += f" order by {order_by}" 390 if limit: 391 query += f" limit {limit}" 392 query += ";" 393 self.cursor.execute(query) 394 matches = self.cursor.fetchall() 395 results = [self._get_dict(table, match, columns_to_return) for match in matches] 396 if sort_by_column: 397 results = sorted(results, key=lambda x: x[sort_by_column]) 398 if return_as_dataframe: 399 return pandas.DataFrame(results) 400 if values_only: 401 return [tuple(row.values()) for row in results] 402 else: 403 return results 404 405 @_connect 406 def find( 407 self, table: str, query_string: str, columns: list[str] | None = None 408 ) -> list[dict]: 409 """Search for rows that contain `query_string` as a substring of any column. 410 411 #### :params: 412 413 `table`: The table to search. 414 415 `query_string`: The substring to search for in all columns. 416 417 `columns`: A list of columns to search for query_string. 418 If None, all columns in the table will be searched. 419 """ 420 if type(columns) is str: 421 columns = [columns] 422 results = [] 423 if not columns: 424 columns = self.get_column_names(table) 425 for column in columns: 426 results.extend( 427 [ 428 row 429 for row in self.get_rows( 430 table, [(column, query_string)], exact_match=False 431 ) 432 if row not in results 433 ] 434 ) 435 return results 436 437 @_connect 438 def delete( 439 self, table: str, match_criteria: list[tuple] | dict, exact_match: bool = True 440 ) -> int: 441 """Delete records from `table`. 442 443 Returns the number of deleted records. 444 445 #### :params: 446 447 `match_criteria`: Can be a list of 2-tuples where each tuple is `(column_name, value)` 448 or a dictionary where keys are column names and values are corresponding values. 449 450 `exact_match`: If `False`, the value for a given column will be matched as a substring. 451 """ 452 conditions = self._get_conditions(match_criteria, exact_match) 453 try: 454 self.cursor.execute(f"delete from [{table}] where {conditions};") 455 num_deletions = self.cursor.rowcount 456 self.logger.info( 457 f'Deleted {num_deletions} rows from "{table}" where {conditions}".' 458 ) 459 return num_deletions 460 except Exception as e: 461 self.logger.debug( 462 f'Error deleting rows from "{table}" where {conditions}.\n{e}' 463 ) 464 return 0 465 466 @_connect 467 def update( 468 self, 469 table: str, 470 column_to_update: str, 471 new_value: Any, 472 match_criteria: list[tuple] | dict | None = None, 473 exact_match: bool = True, 474 ) -> int: 475 """Update the value in `column_to_update` to `new_value` for rows matched with `match_criteria`. 476 477 #### :params: 478 479 `table`: The table to update rows in. 480 481 `column_to_update`: The column to be updated in the matched rows. 482 483 `new_value`: The new value to insert. 484 485 `match_criteria`: Can be a list of 2-tuples where each tuple is `(columnName, rowValue)` 486 or a dictionary where keys are column names and values are corresponding values. 487 If `None`, every row in `table` will be updated. 488 489 `exact_match`: If `False`, `match_criteria` values will be treated as substrings. 490 491 Returns the number of updated rows.""" 492 query = f"update [{table}] set {column_to_update} = ?" 493 conditions = "" 494 if match_criteria: 495 conditions = self._get_conditions(match_criteria, exact_match) 496 query += f" where {conditions}" 497 else: 498 conditions = None 499 query += ";" 500 try: 501 self.cursor.execute( 502 query, 503 (new_value,), 504 ) 505 num_updates = self.cursor.rowcount 506 self.logger.info( 507 f'In {num_updates} rows, updated "{column_to_update}" in "{table}" table to "{new_value}" where {conditions}' 508 ) 509 return num_updates 510 except Exception as e: 511 self.logger.error( 512 f'Failed to update "{column_to_update}" in "{table}" table to "{new_value}" where {conditions}"\n{e}' 513 ) 514 return 0 515 516 @_connect 517 def drop_table(self, table: str) -> bool: 518 """Drop `table` from the database. 519 520 Returns `True` if successful, `False` if not.""" 521 try: 522 self.cursor.execute(f"drop Table [{table}];") 523 self.logger.info(f'Dropped table "{table}"') 524 return True 525 except Exception as e: 526 print(e) 527 self.logger.error(f'Failed to drop table "{table}"') 528 return False 529 530 @_connect 531 def add_column( 532 self, table: str, column: str, _type: str, default_value: str | None = None 533 ): 534 """Add a new column to `table`. 535 536 #### :params: 537 538 `column`: Name of the column to add. 539 540 `_type`: The data type of the new column. 541 542 `default_value`: Optional default value for the column.""" 543 try: 544 if default_value: 545 self.cursor.execute( 546 f"alter table [{table}] add column {column} {_type} default {default_value};" 547 ) 548 self.update(table, column, default_value) 549 else: 550 self.cursor.execute( 551 f"alter table [{table}] add column {column} {_type};" 552 ) 553 self.logger.info(f'Added column "{column}" to "{table}" table.') 554 except Exception as e: 555 self.logger.error(f'Failed to add column "{column}" to "{table}" table.') 556 557 @staticmethod 558 def data_to_string( 559 data: list[dict], sort_key: str | None = None, wrap_to_terminal: bool = True 560 ) -> str: 561 """Uses tabulate to produce pretty string output from a list of dictionaries. 562 563 #### :params: 564 565 `data`: The list of dictionaries to create a grid from. 566 Assumes all dictionaries in list have the same set of keys. 567 568 `sort_key`: Optional dictionary key to sort data with. 569 570 `wrap_to_terminal`: If `True`, the table width will be wrapped to fit within the current terminal window. 571 Pass as `False` if the output is going into something like a `.txt` file.""" 572 return data_to_string(data, sort_key, wrap_to_terminal)
Sqli wrapper so queries don't need to be written except table definitions.
Supports saving and reading dates as datetime objects.
Supports using a context manager.
53 def __init__( 54 self, 55 dbpath: str | Pathier, 56 logger_encoding: str = "utf-8", 57 logger_message_format: str = "{levelname}|-|{asctime}|-|{message}", 58 connection_timeout: float = 10, 59 ): 60 """ 61 #### :params: 62 63 * `dbpath`: String or Path object to database file. 64 If a relative path is given, it will be relative to the 65 current working directory. The log file will be saved to the 66 same directory. 67 68 * `logger_message_format`: `{` style format string for the logger object. 69 70 * `connection_timeout`: The number of seconds to wait when trying to connect to the database before throwing an error.""" 71 self.dbpath = Pathier(dbpath) 72 self.dbname = Pathier(dbpath).name 73 self.dbpath.parent.mkdir(parents=True, exist_ok=True) 74 self._logger_init( 75 encoding=logger_encoding, message_format=logger_message_format 76 ) 77 self.connection_open = False 78 self.connection_timeout = connection_timeout
:params:
dbpath
: String or Path object to database file. If a relative path is given, it will be relative to the current working directory. The log file will be saved to the same directory.logger_message_format
:{
style format string for the logger object.connection_timeout
: The number of seconds to wait when trying to connect to the database before throwing an error.
98 def open(self): 99 """Open connection to db.""" 100 self.connection = sqlite3.connect( 101 self.dbpath, 102 detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES, 103 timeout=self.connection_timeout, 104 ) 105 self.connection.execute("pragma foreign_keys = 1;") 106 self.cursor = self.connection.cursor() 107 self.connection_open = True
Open connection to db.
109 def close(self): 110 """Save and close connection to db. 111 112 Call this as soon as you are done using the database if you have 113 multiple threads or processes using the same database.""" 114 if self.connection_open: 115 self.connection.commit() 116 self.connection.close() 117 self.connection_open = False
Save and close connection to db.
Call this as soon as you are done using the database if you have multiple threads or processes using the same database.
190 def vacuum(self): 191 """Reduce disk size of the database with a `VACUUM` query.""" 192 self.query("VACUUM;")
Reduce disk size of the database with a VACUUM
query.
194 @_connect 195 def query(self, query_) -> list[Any]: 196 """Execute an arbitrary query and return the results.""" 197 self.cursor.execute(query_) 198 return self.cursor.fetchall()
Execute an arbitrary query and return the results.
200 @_connect 201 def create_tables(self, table_defs: list[str] = []): 202 """Create tables if they don't exist. 203 204 :param `table_defs`: Each definition should be in the form `table_name(column_definitions)`""" 205 if len(table_defs) > 0: 206 table_names = self.get_table_names() 207 for table in table_defs: 208 if table.split("(")[0].strip() not in table_names: 209 self.cursor.execute(f"create table [{table}];") 210 self.logger.info(f'{table.split("(")[0]} table created.')
Create tables if they don't exist.
Parameters
table_defs
: Each definition should be in the formtable_name(column_definitions)
212 @_connect 213 def create_table(self, table: str, column_defs: list[str]): 214 """Create a table if it doesn't exist. 215 216 #### :params: 217 218 `table`: Name of the table to create. 219 220 `column_defs`: List of column definitions in proper Sqlite3 sytax. 221 i.e. `"column_name text unique"` or `"column_name int primary key"` etc.""" 222 if table not in self.get_table_names(): 223 query = f"create table [{table}]({', '.join(column_defs)});" 224 self.cursor.execute(query) 225 self.logger.info(f"'{table}' table created.")
Create a table if it doesn't exist.
:params:
table
: Name of the table to create.
column_defs
: List of column definitions in proper Sqlite3 sytax.
i.e. "column_name text unique"
or "column_name int primary key"
etc.
227 @_connect 228 def get_table_names(self) -> list[str]: 229 """Returns a list of table names from the database.""" 230 self.cursor.execute( 231 'select name from sqlite_Schema where type = "table" and name not like "sqlite_%";' 232 ) 233 return [result[0] for result in self.cursor.fetchall()]
Returns a list of table names from the database.
235 @_connect 236 def get_column_names(self, table: str) -> list[str]: 237 """Return a list of column names from a table.""" 238 self.cursor.execute(f"select * from [{table}] where 1=0;") 239 return [description[0] for description in self.cursor.description]
Return a list of column names from a table.
241 @_connect 242 def count( 243 self, 244 table: str, 245 match_criteria: list[tuple] | dict | None = None, 246 exact_match: bool = True, 247 ) -> int: 248 """Return number of items in `table`. 249 250 #### :params: 251 252 `match_criteria`: Can be a list of 2-tuples where each 253 tuple is `(columnName, rowValue)` or a dictionary where 254 keys are column names and values are row values. 255 If `None`, all rows from the table will be counted. 256 257 `exact_match`: If `False`, the row value for a given column 258 in `match_criteria` will be matched as a substring. 259 Has no effect if `match_criteria` is `None`. 260 """ 261 query = f"select count(_rowid_) from [{table}]" 262 try: 263 if match_criteria: 264 self.cursor.execute( 265 f"{query} where {self._get_conditions(match_criteria, exact_match)};" 266 ) 267 else: 268 self.cursor.execute(f"{query}") 269 return self.cursor.fetchone()[0] 270 except: 271 return 0
Return number of items in table
.
:params:
match_criteria
: Can be a list of 2-tuples where each
tuple is (columnName, rowValue)
or a dictionary where
keys are column names and values are row values.
If None
, all rows from the table will be counted.
exact_match
: If False
, the row value for a given column
in match_criteria
will be matched as a substring.
Has no effect if match_criteria
is None
.
273 @_connect 274 def add_row( 275 self, table: str, values: tuple[Any], columns: tuple[str] | None = None 276 ) -> bool: 277 """Add a row of values to a table. 278 279 Returns whether the addition was successful or not. 280 281 #### :params: 282 283 `table`: The table to insert values into. 284 285 `values`: A tuple of values to be inserted into the table. 286 287 `columns`: If `None`, `values` is expected to supply a value for every column in the table. 288 If `columns` is provided, it should contain the same number of elements as `values`.""" 289 parameterizer = ", ".join("?" for _ in values) 290 logger_values = ", ".join(str(value) for value in values) 291 try: 292 if columns: 293 columns_query = ", ".join(column for column in columns) 294 self.cursor.execute( 295 f"insert into [{table}] ({columns_query}) values({parameterizer});", 296 values, 297 ) 298 else: 299 self.cursor.execute( 300 f"insert into [{table}] values({parameterizer});", values 301 ) 302 self.logger.info(f'Added "{logger_values}" to {table} table.') 303 return True 304 except Exception as e: 305 if "constraint" not in str(e).lower(): 306 self.logger.exception( 307 f'Error adding "{logger_values}" to {table} table.' 308 ) 309 else: 310 self.logger.debug(str(e)) 311 return False
Add a row of values to a table.
Returns whether the addition was successful or not.
:params:
table
: The table to insert values into.
values
: A tuple of values to be inserted into the table.
columns
: If None
, values
is expected to supply a value for every column in the table.
If columns
is provided, it should contain the same number of elements as values
.
313 @_connect 314 def add_rows( 315 self, table: str, values: list[tuple[Any]], columns: tuple[str] | None = None 316 ) -> tuple[int, int]: 317 """Add multiple rows of values to a table. 318 319 Returns a tuple containing the number of successful additions and the number of failed additions. 320 321 #### :params: 322 323 `table`: The table to insert values into. 324 325 `values`: A list of tuples of values to be inserted into the table. 326 Each tuple constitutes a single row to be inserted 327 328 `columns`: If `None`, `values` is expected to supply a value for every column in the table. 329 If `columns` is provided, it should contain the same number of elements as `values`.""" 330 successes = 0 331 failures = 0 332 for row in values: 333 if self.add_row(table, row, columns): 334 successes += 1 335 else: 336 failures += 1 337 return (successes, failures)
Add multiple rows of values to a table.
Returns a tuple containing the number of successful additions and the number of failed additions.
:params:
table
: The table to insert values into.
values
: A list of tuples of values to be inserted into the table.
Each tuple constitutes a single row to be inserted
columns
: If None
, values
is expected to supply a value for every column in the table.
If columns
is provided, it should contain the same number of elements as values
.
339 @_connect 340 def get_rows( 341 self, 342 table: str, 343 match_criteria: list[tuple] | dict | None = None, 344 exact_match: bool = True, 345 sort_by_column: str | None = None, 346 columns_to_return: list[str] | None = None, 347 return_as_dataframe: bool = False, 348 values_only: bool = False, 349 order_by: str | None = None, 350 limit: str | int | None = None, 351 ) -> list[dict] | list[tuple] | pandas.DataFrame: 352 """Return matching rows from `table`. 353 354 By default, rows will be returned as a list of dictionaries of the form `[{"column_name": value, ...}, ...]` 355 356 357 #### :params: 358 359 `match_criteria`: Can be a list of 2-tuples where each 360 tuple is `(columnName, rowValue)` or a dictionary where 361 keys are column names and values are row values. 362 363 `exact_match`: If `False`, the row value for a given column will be matched as a substring. 364 365 `sort_by_column`: A column name to sort the results by. 366 This will sort results in Python after retrieving them from the db. 367 Use the 'order_by' param to use SQLite engine for ordering. 368 369 `columns_to_return`: Optional list of column names. 370 If provided, the elements returned by this function will only contain the provided columns. 371 Otherwise every column in the row is returned. 372 373 `return_as_dataframe`: Return the results as a `pandas.DataFrame` object. 374 375 `values_only`: Return the results as a list of tuples. 376 377 `order_by`: If given, a `order by {order_by}` clause will be added to the select query. 378 379 `limit`: If given, a `limit {limit}` clause will be added to the select query. 380 """ 381 382 if type(columns_to_return) is str: 383 columns_to_return = [columns_to_return] 384 query = f"select * from [{table}]" 385 matches = [] 386 if match_criteria: 387 query += f" where {self._get_conditions(match_criteria, exact_match)}" 388 if order_by: 389 query += f" order by {order_by}" 390 if limit: 391 query += f" limit {limit}" 392 query += ";" 393 self.cursor.execute(query) 394 matches = self.cursor.fetchall() 395 results = [self._get_dict(table, match, columns_to_return) for match in matches] 396 if sort_by_column: 397 results = sorted(results, key=lambda x: x[sort_by_column]) 398 if return_as_dataframe: 399 return pandas.DataFrame(results) 400 if values_only: 401 return [tuple(row.values()) for row in results] 402 else: 403 return results
Return matching rows from table
.
By default, rows will be returned as a list of dictionaries of the form [{"column_name": value, ...}, ...]
:params:
match_criteria
: Can be a list of 2-tuples where each
tuple is (columnName, rowValue)
or a dictionary where
keys are column names and values are row values.
exact_match
: If False
, the row value for a given column will be matched as a substring.
sort_by_column
: A column name to sort the results by.
This will sort results in Python after retrieving them from the db.
Use the 'order_by' param to use SQLite engine for ordering.
columns_to_return
: Optional list of column names.
If provided, the elements returned by this function will only contain the provided columns.
Otherwise every column in the row is returned.
return_as_dataframe
: Return the results as a pandas.DataFrame
object.
values_only
: Return the results as a list of tuples.
order_by
: If given, a order by {order_by}
clause will be added to the select query.
limit
: If given, a limit {limit}
clause will be added to the select query.
405 @_connect 406 def find( 407 self, table: str, query_string: str, columns: list[str] | None = None 408 ) -> list[dict]: 409 """Search for rows that contain `query_string` as a substring of any column. 410 411 #### :params: 412 413 `table`: The table to search. 414 415 `query_string`: The substring to search for in all columns. 416 417 `columns`: A list of columns to search for query_string. 418 If None, all columns in the table will be searched. 419 """ 420 if type(columns) is str: 421 columns = [columns] 422 results = [] 423 if not columns: 424 columns = self.get_column_names(table) 425 for column in columns: 426 results.extend( 427 [ 428 row 429 for row in self.get_rows( 430 table, [(column, query_string)], exact_match=False 431 ) 432 if row not in results 433 ] 434 ) 435 return results
Search for rows that contain query_string
as a substring of any column.
:params:
table
: The table to search.
query_string
: The substring to search for in all columns.
columns
: A list of columns to search for query_string.
If None, all columns in the table will be searched.
437 @_connect 438 def delete( 439 self, table: str, match_criteria: list[tuple] | dict, exact_match: bool = True 440 ) -> int: 441 """Delete records from `table`. 442 443 Returns the number of deleted records. 444 445 #### :params: 446 447 `match_criteria`: Can be a list of 2-tuples where each tuple is `(column_name, value)` 448 or a dictionary where keys are column names and values are corresponding values. 449 450 `exact_match`: If `False`, the value for a given column will be matched as a substring. 451 """ 452 conditions = self._get_conditions(match_criteria, exact_match) 453 try: 454 self.cursor.execute(f"delete from [{table}] where {conditions};") 455 num_deletions = self.cursor.rowcount 456 self.logger.info( 457 f'Deleted {num_deletions} rows from "{table}" where {conditions}".' 458 ) 459 return num_deletions 460 except Exception as e: 461 self.logger.debug( 462 f'Error deleting rows from "{table}" where {conditions}.\n{e}' 463 ) 464 return 0
Delete records from table
.
Returns the number of deleted records.
:params:
match_criteria
: Can be a list of 2-tuples where each tuple is (column_name, value)
or a dictionary where keys are column names and values are corresponding values.
exact_match
: If False
, the value for a given column will be matched as a substring.
466 @_connect 467 def update( 468 self, 469 table: str, 470 column_to_update: str, 471 new_value: Any, 472 match_criteria: list[tuple] | dict | None = None, 473 exact_match: bool = True, 474 ) -> int: 475 """Update the value in `column_to_update` to `new_value` for rows matched with `match_criteria`. 476 477 #### :params: 478 479 `table`: The table to update rows in. 480 481 `column_to_update`: The column to be updated in the matched rows. 482 483 `new_value`: The new value to insert. 484 485 `match_criteria`: Can be a list of 2-tuples where each tuple is `(columnName, rowValue)` 486 or a dictionary where keys are column names and values are corresponding values. 487 If `None`, every row in `table` will be updated. 488 489 `exact_match`: If `False`, `match_criteria` values will be treated as substrings. 490 491 Returns the number of updated rows.""" 492 query = f"update [{table}] set {column_to_update} = ?" 493 conditions = "" 494 if match_criteria: 495 conditions = self._get_conditions(match_criteria, exact_match) 496 query += f" where {conditions}" 497 else: 498 conditions = None 499 query += ";" 500 try: 501 self.cursor.execute( 502 query, 503 (new_value,), 504 ) 505 num_updates = self.cursor.rowcount 506 self.logger.info( 507 f'In {num_updates} rows, updated "{column_to_update}" in "{table}" table to "{new_value}" where {conditions}' 508 ) 509 return num_updates 510 except Exception as e: 511 self.logger.error( 512 f'Failed to update "{column_to_update}" in "{table}" table to "{new_value}" where {conditions}"\n{e}' 513 ) 514 return 0
Update the value in column_to_update
to new_value
for rows matched with match_criteria
.
:params:
table
: The table to update rows in.
column_to_update
: The column to be updated in the matched rows.
new_value
: The new value to insert.
match_criteria
: Can be a list of 2-tuples where each tuple is (columnName, rowValue)
or a dictionary where keys are column names and values are corresponding values.
If None
, every row in table
will be updated.
exact_match
: If False
, match_criteria
values will be treated as substrings.
Returns the number of updated rows.
516 @_connect 517 def drop_table(self, table: str) -> bool: 518 """Drop `table` from the database. 519 520 Returns `True` if successful, `False` if not.""" 521 try: 522 self.cursor.execute(f"drop Table [{table}];") 523 self.logger.info(f'Dropped table "{table}"') 524 return True 525 except Exception as e: 526 print(e) 527 self.logger.error(f'Failed to drop table "{table}"') 528 return False
Drop table
from the database.
Returns True
if successful, False
if not.
530 @_connect 531 def add_column( 532 self, table: str, column: str, _type: str, default_value: str | None = None 533 ): 534 """Add a new column to `table`. 535 536 #### :params: 537 538 `column`: Name of the column to add. 539 540 `_type`: The data type of the new column. 541 542 `default_value`: Optional default value for the column.""" 543 try: 544 if default_value: 545 self.cursor.execute( 546 f"alter table [{table}] add column {column} {_type} default {default_value};" 547 ) 548 self.update(table, column, default_value) 549 else: 550 self.cursor.execute( 551 f"alter table [{table}] add column {column} {_type};" 552 ) 553 self.logger.info(f'Added column "{column}" to "{table}" table.') 554 except Exception as e: 555 self.logger.error(f'Failed to add column "{column}" to "{table}" table.')
Add a new column to table
.
:params:
column
: Name of the column to add.
_type
: The data type of the new column.
default_value
: Optional default value for the column.
557 @staticmethod 558 def data_to_string( 559 data: list[dict], sort_key: str | None = None, wrap_to_terminal: bool = True 560 ) -> str: 561 """Uses tabulate to produce pretty string output from a list of dictionaries. 562 563 #### :params: 564 565 `data`: The list of dictionaries to create a grid from. 566 Assumes all dictionaries in list have the same set of keys. 567 568 `sort_key`: Optional dictionary key to sort data with. 569 570 `wrap_to_terminal`: If `True`, the table width will be wrapped to fit within the current terminal window. 571 Pass as `False` if the output is going into something like a `.txt` file.""" 572 return data_to_string(data, sort_key, wrap_to_terminal)
Uses tabulate to produce pretty string output from a list of dictionaries.
:params:
data
: The list of dictionaries to create a grid from.
Assumes all dictionaries in list have the same set of keys.
sort_key
: Optional dictionary key to sort data with.
wrap_to_terminal
: If True
, the table width will be wrapped to fit within the current terminal window.
Pass as False
if the output is going into something like a .txt
file.
575def data_to_string( 576 data: list[dict], sort_key: str | None = None, wrap_to_terminal: bool = True 577) -> str: 578 """Uses tabulate to produce pretty string output from a list of dictionaries. 579 580 #### :params: 581 582 `data`: The list of dictionaries to create a grid from. 583 Assumes all dictionaries in list have the same set of keys. 584 585 `sort_key`: Optional dictionary key to sort data with. 586 587 `wrap_to_terminal`: If `True`, the table width will be wrapped to fit within the current terminal window. 588 Pass as `False` if the output is going into something like a `.txt` file.""" 589 if len(data) == 0: 590 return "" 591 if sort_key: 592 data = sorted(data, key=lambda d: d[sort_key]) 593 for i, d in enumerate(data): 594 for k in d: 595 data[i][k] = str(data[i][k]) 596 597 try: 598 print("Resizing grid to fit within the terminal...\n") 599 return griddy(data, "keys", wrap_to_terminal) 600 except RuntimeError as e: 601 print(e) 602 return str(data)
Uses tabulate to produce pretty string output from a list of dictionaries.
:params:
data
: The list of dictionaries to create a grid from.
Assumes all dictionaries in list have the same set of keys.
sort_key
: Optional dictionary key to sort data with.
wrap_to_terminal
: If True
, the table width will be wrapped to fit within the current terminal window.
Pass as False
if the output is going into something like a .txt
file.