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