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) -> int: 190 """Reduce disk size of the database with a `VACUUM` query. 191 192 Returns space freed up in bytes.""" 193 size = self.dbpath.size 194 self.query("VACUUM;") 195 return size - self.dbpath.size 196 197 @_connect 198 def query(self, query_) -> list[Any]: 199 """Execute an arbitrary query and return the results.""" 200 self.cursor.execute(query_) 201 return self.cursor.fetchall() 202 203 @_connect 204 def create_tables(self, table_defs: list[str] = []): 205 """Create tables if they don't exist. 206 207 :param `table_defs`: Each definition should be in the form `table_name(column_definitions)`""" 208 if len(table_defs) > 0: 209 table_names = self.get_table_names() 210 for table in table_defs: 211 if table.split("(")[0].strip() not in table_names: 212 self.cursor.execute(f"create table [{table}];") 213 self.logger.info(f'{table.split("(")[0]} table created.') 214 215 @_connect 216 def create_table(self, table: str, column_defs: list[str]): 217 """Create a table if it doesn't exist. 218 219 #### :params: 220 221 `table`: Name of the table to create. 222 223 `column_defs`: List of column definitions in proper Sqlite3 sytax. 224 i.e. `"column_name text unique"` or `"column_name int primary key"` etc.""" 225 if table not in self.get_table_names(): 226 query = f"create table [{table}]({', '.join(column_defs)});" 227 self.cursor.execute(query) 228 self.logger.info(f"'{table}' table created.") 229 230 @_connect 231 def get_table_names(self) -> list[str]: 232 """Returns a list of table names from the database.""" 233 self.cursor.execute( 234 'select name from sqlite_Schema where type = "table" and name not like "sqlite_%";' 235 ) 236 return [result[0] for result in self.cursor.fetchall()] 237 238 @_connect 239 def get_column_names(self, table: str) -> list[str]: 240 """Return a list of column names from a table.""" 241 self.cursor.execute(f"select * from [{table}] where 1=0;") 242 return [description[0] for description in self.cursor.description] 243 244 @_connect 245 def count( 246 self, 247 table: str, 248 match_criteria: list[tuple] | dict | None = None, 249 exact_match: bool = True, 250 ) -> int: 251 """Return number of items in `table`. 252 253 #### :params: 254 255 `match_criteria`: Can be a list of 2-tuples where each 256 tuple is `(columnName, rowValue)` or a dictionary where 257 keys are column names and values are row values. 258 If `None`, all rows from the table will be counted. 259 260 `exact_match`: If `False`, the row value for a given column 261 in `match_criteria` will be matched as a substring. 262 Has no effect if `match_criteria` is `None`. 263 """ 264 query = f"select count(_rowid_) from [{table}]" 265 try: 266 if match_criteria: 267 self.cursor.execute( 268 f"{query} where {self._get_conditions(match_criteria, exact_match)};" 269 ) 270 else: 271 self.cursor.execute(f"{query}") 272 return self.cursor.fetchone()[0] 273 except: 274 return 0 275 276 @_connect 277 def add_row( 278 self, table: str, values: tuple[Any], columns: tuple[str] | None = None 279 ) -> bool: 280 """Add a row of values to a table. 281 282 Returns whether the addition was successful or not. 283 284 #### :params: 285 286 `table`: The table to insert values into. 287 288 `values`: A tuple of values to be inserted into the table. 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 parameterizer = ", ".join("?" for _ in values) 293 logger_values = ", ".join(str(value) for value in values) 294 try: 295 if columns: 296 columns_query = ", ".join(column for column in columns) 297 self.cursor.execute( 298 f"insert into [{table}] ({columns_query}) values({parameterizer});", 299 values, 300 ) 301 else: 302 self.cursor.execute( 303 f"insert into [{table}] values({parameterizer});", values 304 ) 305 self.logger.info(f'Added "{logger_values}" to {table} table.') 306 return True 307 except Exception as e: 308 if "constraint" not in str(e).lower(): 309 self.logger.exception( 310 f'Error adding "{logger_values}" to {table} table.' 311 ) 312 else: 313 self.logger.debug(str(e)) 314 return False 315 316 @_connect 317 def add_rows( 318 self, table: str, values: list[tuple[Any]], columns: tuple[str] | None = None 319 ) -> tuple[int, int]: 320 """Add multiple rows of values to a table. 321 322 Returns a tuple containing the number of successful additions and the number of failed additions. 323 324 #### :params: 325 326 `table`: The table to insert values into. 327 328 `values`: A list of tuples of values to be inserted into the table. 329 Each tuple constitutes a single row to be inserted 330 331 `columns`: If `None`, `values` is expected to supply a value for every column in the table. 332 If `columns` is provided, it should contain the same number of elements as `values`.""" 333 successes = 0 334 failures = 0 335 for row in values: 336 if self.add_row(table, row, columns): 337 successes += 1 338 else: 339 failures += 1 340 return (successes, failures) 341 342 @_connect 343 def get_rows( 344 self, 345 table: str, 346 match_criteria: list[tuple] | dict | None = None, 347 exact_match: bool = True, 348 sort_by_column: str | None = None, 349 columns_to_return: list[str] | None = None, 350 return_as_dataframe: bool = False, 351 values_only: bool = False, 352 order_by: str | None = None, 353 limit: str | int | None = None, 354 ) -> list[dict] | list[tuple] | pandas.DataFrame: 355 """Return matching rows from `table`. 356 357 By default, rows will be returned as a list of dictionaries of the form `[{"column_name": value, ...}, ...]` 358 359 360 #### :params: 361 362 `match_criteria`: Can be a list of 2-tuples where each 363 tuple is `(columnName, rowValue)` or a dictionary where 364 keys are column names and values are row values. 365 366 `exact_match`: If `False`, the row value for a given column will be matched as a substring. 367 368 `sort_by_column`: A column name to sort the results by. 369 This will sort results in Python after retrieving them from the db. 370 Use the 'order_by' param to use SQLite engine for ordering. 371 372 `columns_to_return`: Optional list of column names. 373 If provided, the elements returned by this function will only contain the provided columns. 374 Otherwise every column in the row is returned. 375 376 `return_as_dataframe`: Return the results as a `pandas.DataFrame` object. 377 378 `values_only`: Return the results as a list of tuples. 379 380 `order_by`: If given, a `order by {order_by}` clause will be added to the select query. 381 382 `limit`: If given, a `limit {limit}` clause will be added to the select query. 383 """ 384 385 if type(columns_to_return) is str: 386 columns_to_return = [columns_to_return] 387 query = f"select * from [{table}]" 388 matches = [] 389 if match_criteria: 390 query += f" where {self._get_conditions(match_criteria, exact_match)}" 391 if order_by: 392 query += f" order by {order_by}" 393 if limit: 394 query += f" limit {limit}" 395 query += ";" 396 self.cursor.execute(query) 397 matches = self.cursor.fetchall() 398 results = [self._get_dict(table, match, columns_to_return) for match in matches] 399 if sort_by_column: 400 results = sorted(results, key=lambda x: x[sort_by_column]) 401 if return_as_dataframe: 402 return pandas.DataFrame(results) 403 if values_only: 404 return [tuple(row.values()) for row in results] 405 else: 406 return results 407 408 @_connect 409 def find( 410 self, table: str, query_string: str, columns: list[str] | None = None 411 ) -> list[dict]: 412 """Search for rows that contain `query_string` as a substring of any column. 413 414 #### :params: 415 416 `table`: The table to search. 417 418 `query_string`: The substring to search for in all columns. 419 420 `columns`: A list of columns to search for query_string. 421 If None, all columns in the table will be searched. 422 """ 423 if type(columns) is str: 424 columns = [columns] 425 results = [] 426 if not columns: 427 columns = self.get_column_names(table) 428 for column in columns: 429 results.extend( 430 [ 431 row 432 for row in self.get_rows( 433 table, [(column, query_string)], exact_match=False 434 ) 435 if row not in results 436 ] 437 ) 438 return results 439 440 @_connect 441 def delete( 442 self, table: str, match_criteria: list[tuple] | dict, exact_match: bool = True 443 ) -> int: 444 """Delete records from `table`. 445 446 Returns the number of deleted records. 447 448 #### :params: 449 450 `match_criteria`: Can be a list of 2-tuples where each tuple is `(column_name, value)` 451 or a dictionary where keys are column names and values are corresponding values. 452 453 `exact_match`: If `False`, the value for a given column will be matched as a substring. 454 """ 455 conditions = self._get_conditions(match_criteria, exact_match) 456 try: 457 self.cursor.execute(f"delete from [{table}] where {conditions};") 458 num_deletions = self.cursor.rowcount 459 self.logger.info( 460 f'Deleted {num_deletions} rows from "{table}" where {conditions}".' 461 ) 462 return num_deletions 463 except Exception as e: 464 self.logger.debug( 465 f'Error deleting rows from "{table}" where {conditions}.\n{e}' 466 ) 467 return 0 468 469 @_connect 470 def update( 471 self, 472 table: str, 473 column_to_update: str, 474 new_value: Any, 475 match_criteria: list[tuple] | dict | None = None, 476 exact_match: bool = True, 477 ) -> int: 478 """Update the value in `column_to_update` to `new_value` for rows matched with `match_criteria`. 479 480 #### :params: 481 482 `table`: The table to update rows in. 483 484 `column_to_update`: The column to be updated in the matched rows. 485 486 `new_value`: The new value to insert. 487 488 `match_criteria`: Can be a list of 2-tuples where each tuple is `(columnName, rowValue)` 489 or a dictionary where keys are column names and values are corresponding values. 490 If `None`, every row in `table` will be updated. 491 492 `exact_match`: If `False`, `match_criteria` values will be treated as substrings. 493 494 Returns the number of updated rows.""" 495 query = f"update [{table}] set {column_to_update} = ?" 496 conditions = "" 497 if match_criteria: 498 conditions = self._get_conditions(match_criteria, exact_match) 499 query += f" where {conditions}" 500 else: 501 conditions = None 502 query += ";" 503 try: 504 self.cursor.execute( 505 query, 506 (new_value,), 507 ) 508 num_updates = self.cursor.rowcount 509 self.logger.info( 510 f'In {num_updates} rows, updated "{column_to_update}" in "{table}" table to "{new_value}" where {conditions}' 511 ) 512 return num_updates 513 except Exception as e: 514 self.logger.error( 515 f'Failed to update "{column_to_update}" in "{table}" table to "{new_value}" where {conditions}"\n{e}' 516 ) 517 return 0 518 519 @_connect 520 def drop_table(self, table: str) -> bool: 521 """Drop `table` from the database. 522 523 Returns `True` if successful, `False` if not.""" 524 try: 525 self.cursor.execute(f"drop Table [{table}];") 526 self.logger.info(f'Dropped table "{table}"') 527 return True 528 except Exception as e: 529 print(e) 530 self.logger.error(f'Failed to drop table "{table}"') 531 return False 532 533 @_connect 534 def add_column( 535 self, table: str, column: str, _type: str, default_value: str | None = None 536 ): 537 """Add a new column to `table`. 538 539 #### :params: 540 541 `column`: Name of the column to add. 542 543 `_type`: The data type of the new column. 544 545 `default_value`: Optional default value for the column.""" 546 try: 547 if default_value: 548 self.cursor.execute( 549 f"alter table [{table}] add column {column} {_type} default {default_value};" 550 ) 551 self.update(table, column, default_value) 552 else: 553 self.cursor.execute( 554 f"alter table [{table}] add column {column} {_type};" 555 ) 556 self.logger.info(f'Added column "{column}" to "{table}" table.') 557 except Exception as e: 558 self.logger.error(f'Failed to add column "{column}" to "{table}" table.') 559 560 @staticmethod 561 def data_to_string( 562 data: list[dict], sort_key: str | None = None, wrap_to_terminal: bool = True 563 ) -> str: 564 """Uses tabulate to produce pretty string output from a list of dictionaries. 565 566 #### :params: 567 568 `data`: The list of dictionaries to create a grid from. 569 Assumes all dictionaries in list have the same set of keys. 570 571 `sort_key`: Optional dictionary key to sort data with. 572 573 `wrap_to_terminal`: If `True`, the table width will be wrapped to fit within the current terminal window. 574 Pass as `False` if the output is going into something like a `.txt` file.""" 575 return data_to_string(data, sort_key, wrap_to_terminal) 576 577 578def data_to_string( 579 data: list[dict], sort_key: str | None = None, wrap_to_terminal: bool = True 580) -> str: 581 """Uses tabulate to produce pretty string output from a list of dictionaries. 582 583 #### :params: 584 585 `data`: The list of dictionaries to create a grid from. 586 Assumes all dictionaries in list have the same set of keys. 587 588 `sort_key`: Optional dictionary key to sort data with. 589 590 `wrap_to_terminal`: If `True`, the table width will be wrapped to fit within the current terminal window. 591 Pass as `False` if the output is going into something like a `.txt` file.""" 592 if len(data) == 0: 593 return "" 594 if sort_key: 595 data = sorted(data, key=lambda d: d[sort_key]) 596 for i, d in enumerate(data): 597 for k in d: 598 data[i][k] = str(data[i][k]) 599 600 try: 601 print("Resizing grid to fit within the terminal...\n") 602 return griddy(data, "keys", wrap_to_terminal) 603 except RuntimeError as e: 604 print(e) 605 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) -> int: 191 """Reduce disk size of the database with a `VACUUM` query. 192 193 Returns space freed up in bytes.""" 194 size = self.dbpath.size 195 self.query("VACUUM;") 196 return size - self.dbpath.size 197 198 @_connect 199 def query(self, query_) -> list[Any]: 200 """Execute an arbitrary query and return the results.""" 201 self.cursor.execute(query_) 202 return self.cursor.fetchall() 203 204 @_connect 205 def create_tables(self, table_defs: list[str] = []): 206 """Create tables if they don't exist. 207 208 :param `table_defs`: Each definition should be in the form `table_name(column_definitions)`""" 209 if len(table_defs) > 0: 210 table_names = self.get_table_names() 211 for table in table_defs: 212 if table.split("(")[0].strip() not in table_names: 213 self.cursor.execute(f"create table [{table}];") 214 self.logger.info(f'{table.split("(")[0]} table created.') 215 216 @_connect 217 def create_table(self, table: str, column_defs: list[str]): 218 """Create a table if it doesn't exist. 219 220 #### :params: 221 222 `table`: Name of the table to create. 223 224 `column_defs`: List of column definitions in proper Sqlite3 sytax. 225 i.e. `"column_name text unique"` or `"column_name int primary key"` etc.""" 226 if table not in self.get_table_names(): 227 query = f"create table [{table}]({', '.join(column_defs)});" 228 self.cursor.execute(query) 229 self.logger.info(f"'{table}' table created.") 230 231 @_connect 232 def get_table_names(self) -> list[str]: 233 """Returns a list of table names from the database.""" 234 self.cursor.execute( 235 'select name from sqlite_Schema where type = "table" and name not like "sqlite_%";' 236 ) 237 return [result[0] for result in self.cursor.fetchall()] 238 239 @_connect 240 def get_column_names(self, table: str) -> list[str]: 241 """Return a list of column names from a table.""" 242 self.cursor.execute(f"select * from [{table}] where 1=0;") 243 return [description[0] for description in self.cursor.description] 244 245 @_connect 246 def count( 247 self, 248 table: str, 249 match_criteria: list[tuple] | dict | None = None, 250 exact_match: bool = True, 251 ) -> int: 252 """Return number of items in `table`. 253 254 #### :params: 255 256 `match_criteria`: Can be a list of 2-tuples where each 257 tuple is `(columnName, rowValue)` or a dictionary where 258 keys are column names and values are row values. 259 If `None`, all rows from the table will be counted. 260 261 `exact_match`: If `False`, the row value for a given column 262 in `match_criteria` will be matched as a substring. 263 Has no effect if `match_criteria` is `None`. 264 """ 265 query = f"select count(_rowid_) from [{table}]" 266 try: 267 if match_criteria: 268 self.cursor.execute( 269 f"{query} where {self._get_conditions(match_criteria, exact_match)};" 270 ) 271 else: 272 self.cursor.execute(f"{query}") 273 return self.cursor.fetchone()[0] 274 except: 275 return 0 276 277 @_connect 278 def add_row( 279 self, table: str, values: tuple[Any], columns: tuple[str] | None = None 280 ) -> bool: 281 """Add a row of values to a table. 282 283 Returns whether the addition was successful or not. 284 285 #### :params: 286 287 `table`: The table to insert values into. 288 289 `values`: A tuple of values to be inserted into the table. 290 291 `columns`: If `None`, `values` is expected to supply a value for every column in the table. 292 If `columns` is provided, it should contain the same number of elements as `values`.""" 293 parameterizer = ", ".join("?" for _ in values) 294 logger_values = ", ".join(str(value) for value in values) 295 try: 296 if columns: 297 columns_query = ", ".join(column for column in columns) 298 self.cursor.execute( 299 f"insert into [{table}] ({columns_query}) values({parameterizer});", 300 values, 301 ) 302 else: 303 self.cursor.execute( 304 f"insert into [{table}] values({parameterizer});", values 305 ) 306 self.logger.info(f'Added "{logger_values}" to {table} table.') 307 return True 308 except Exception as e: 309 if "constraint" not in str(e).lower(): 310 self.logger.exception( 311 f'Error adding "{logger_values}" to {table} table.' 312 ) 313 else: 314 self.logger.debug(str(e)) 315 return False 316 317 @_connect 318 def add_rows( 319 self, table: str, values: list[tuple[Any]], columns: tuple[str] | None = None 320 ) -> tuple[int, int]: 321 """Add multiple rows of values to a table. 322 323 Returns a tuple containing the number of successful additions and the number of failed additions. 324 325 #### :params: 326 327 `table`: The table to insert values into. 328 329 `values`: A list of tuples of values to be inserted into the table. 330 Each tuple constitutes a single row to be inserted 331 332 `columns`: If `None`, `values` is expected to supply a value for every column in the table. 333 If `columns` is provided, it should contain the same number of elements as `values`.""" 334 successes = 0 335 failures = 0 336 for row in values: 337 if self.add_row(table, row, columns): 338 successes += 1 339 else: 340 failures += 1 341 return (successes, failures) 342 343 @_connect 344 def get_rows( 345 self, 346 table: str, 347 match_criteria: list[tuple] | dict | None = None, 348 exact_match: bool = True, 349 sort_by_column: str | None = None, 350 columns_to_return: list[str] | None = None, 351 return_as_dataframe: bool = False, 352 values_only: bool = False, 353 order_by: str | None = None, 354 limit: str | int | None = None, 355 ) -> list[dict] | list[tuple] | pandas.DataFrame: 356 """Return matching rows from `table`. 357 358 By default, rows will be returned as a list of dictionaries of the form `[{"column_name": value, ...}, ...]` 359 360 361 #### :params: 362 363 `match_criteria`: Can be a list of 2-tuples where each 364 tuple is `(columnName, rowValue)` or a dictionary where 365 keys are column names and values are row values. 366 367 `exact_match`: If `False`, the row value for a given column will be matched as a substring. 368 369 `sort_by_column`: A column name to sort the results by. 370 This will sort results in Python after retrieving them from the db. 371 Use the 'order_by' param to use SQLite engine for ordering. 372 373 `columns_to_return`: Optional list of column names. 374 If provided, the elements returned by this function will only contain the provided columns. 375 Otherwise every column in the row is returned. 376 377 `return_as_dataframe`: Return the results as a `pandas.DataFrame` object. 378 379 `values_only`: Return the results as a list of tuples. 380 381 `order_by`: If given, a `order by {order_by}` clause will be added to the select query. 382 383 `limit`: If given, a `limit {limit}` clause will be added to the select query. 384 """ 385 386 if type(columns_to_return) is str: 387 columns_to_return = [columns_to_return] 388 query = f"select * from [{table}]" 389 matches = [] 390 if match_criteria: 391 query += f" where {self._get_conditions(match_criteria, exact_match)}" 392 if order_by: 393 query += f" order by {order_by}" 394 if limit: 395 query += f" limit {limit}" 396 query += ";" 397 self.cursor.execute(query) 398 matches = self.cursor.fetchall() 399 results = [self._get_dict(table, match, columns_to_return) for match in matches] 400 if sort_by_column: 401 results = sorted(results, key=lambda x: x[sort_by_column]) 402 if return_as_dataframe: 403 return pandas.DataFrame(results) 404 if values_only: 405 return [tuple(row.values()) for row in results] 406 else: 407 return results 408 409 @_connect 410 def find( 411 self, table: str, query_string: str, columns: list[str] | None = None 412 ) -> list[dict]: 413 """Search for rows that contain `query_string` as a substring of any column. 414 415 #### :params: 416 417 `table`: The table to search. 418 419 `query_string`: The substring to search for in all columns. 420 421 `columns`: A list of columns to search for query_string. 422 If None, all columns in the table will be searched. 423 """ 424 if type(columns) is str: 425 columns = [columns] 426 results = [] 427 if not columns: 428 columns = self.get_column_names(table) 429 for column in columns: 430 results.extend( 431 [ 432 row 433 for row in self.get_rows( 434 table, [(column, query_string)], exact_match=False 435 ) 436 if row not in results 437 ] 438 ) 439 return results 440 441 @_connect 442 def delete( 443 self, table: str, match_criteria: list[tuple] | dict, exact_match: bool = True 444 ) -> int: 445 """Delete records from `table`. 446 447 Returns the number of deleted records. 448 449 #### :params: 450 451 `match_criteria`: Can be a list of 2-tuples where each tuple is `(column_name, value)` 452 or a dictionary where keys are column names and values are corresponding values. 453 454 `exact_match`: If `False`, the value for a given column will be matched as a substring. 455 """ 456 conditions = self._get_conditions(match_criteria, exact_match) 457 try: 458 self.cursor.execute(f"delete from [{table}] where {conditions};") 459 num_deletions = self.cursor.rowcount 460 self.logger.info( 461 f'Deleted {num_deletions} rows from "{table}" where {conditions}".' 462 ) 463 return num_deletions 464 except Exception as e: 465 self.logger.debug( 466 f'Error deleting rows from "{table}" where {conditions}.\n{e}' 467 ) 468 return 0 469 470 @_connect 471 def update( 472 self, 473 table: str, 474 column_to_update: str, 475 new_value: Any, 476 match_criteria: list[tuple] | dict | None = None, 477 exact_match: bool = True, 478 ) -> int: 479 """Update the value in `column_to_update` to `new_value` for rows matched with `match_criteria`. 480 481 #### :params: 482 483 `table`: The table to update rows in. 484 485 `column_to_update`: The column to be updated in the matched rows. 486 487 `new_value`: The new value to insert. 488 489 `match_criteria`: Can be a list of 2-tuples where each tuple is `(columnName, rowValue)` 490 or a dictionary where keys are column names and values are corresponding values. 491 If `None`, every row in `table` will be updated. 492 493 `exact_match`: If `False`, `match_criteria` values will be treated as substrings. 494 495 Returns the number of updated rows.""" 496 query = f"update [{table}] set {column_to_update} = ?" 497 conditions = "" 498 if match_criteria: 499 conditions = self._get_conditions(match_criteria, exact_match) 500 query += f" where {conditions}" 501 else: 502 conditions = None 503 query += ";" 504 try: 505 self.cursor.execute( 506 query, 507 (new_value,), 508 ) 509 num_updates = self.cursor.rowcount 510 self.logger.info( 511 f'In {num_updates} rows, updated "{column_to_update}" in "{table}" table to "{new_value}" where {conditions}' 512 ) 513 return num_updates 514 except Exception as e: 515 self.logger.error( 516 f'Failed to update "{column_to_update}" in "{table}" table to "{new_value}" where {conditions}"\n{e}' 517 ) 518 return 0 519 520 @_connect 521 def drop_table(self, table: str) -> bool: 522 """Drop `table` from the database. 523 524 Returns `True` if successful, `False` if not.""" 525 try: 526 self.cursor.execute(f"drop Table [{table}];") 527 self.logger.info(f'Dropped table "{table}"') 528 return True 529 except Exception as e: 530 print(e) 531 self.logger.error(f'Failed to drop table "{table}"') 532 return False 533 534 @_connect 535 def add_column( 536 self, table: str, column: str, _type: str, default_value: str | None = None 537 ): 538 """Add a new column to `table`. 539 540 #### :params: 541 542 `column`: Name of the column to add. 543 544 `_type`: The data type of the new column. 545 546 `default_value`: Optional default value for the column.""" 547 try: 548 if default_value: 549 self.cursor.execute( 550 f"alter table [{table}] add column {column} {_type} default {default_value};" 551 ) 552 self.update(table, column, default_value) 553 else: 554 self.cursor.execute( 555 f"alter table [{table}] add column {column} {_type};" 556 ) 557 self.logger.info(f'Added column "{column}" to "{table}" table.') 558 except Exception as e: 559 self.logger.error(f'Failed to add column "{column}" to "{table}" table.') 560 561 @staticmethod 562 def data_to_string( 563 data: list[dict], sort_key: str | None = None, wrap_to_terminal: bool = True 564 ) -> str: 565 """Uses tabulate to produce pretty string output from a list of dictionaries. 566 567 #### :params: 568 569 `data`: The list of dictionaries to create a grid from. 570 Assumes all dictionaries in list have the same set of keys. 571 572 `sort_key`: Optional dictionary key to sort data with. 573 574 `wrap_to_terminal`: If `True`, the table width will be wrapped to fit within the current terminal window. 575 Pass as `False` if the output is going into something like a `.txt` file.""" 576 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) -> int: 191 """Reduce disk size of the database with a `VACUUM` query. 192 193 Returns space freed up in bytes.""" 194 size = self.dbpath.size 195 self.query("VACUUM;") 196 return size - self.dbpath.size
Reduce disk size of the database with a VACUUM
query.
Returns space freed up in bytes.
198 @_connect 199 def query(self, query_) -> list[Any]: 200 """Execute an arbitrary query and return the results.""" 201 self.cursor.execute(query_) 202 return self.cursor.fetchall()
Execute an arbitrary query and return the results.
204 @_connect 205 def create_tables(self, table_defs: list[str] = []): 206 """Create tables if they don't exist. 207 208 :param `table_defs`: Each definition should be in the form `table_name(column_definitions)`""" 209 if len(table_defs) > 0: 210 table_names = self.get_table_names() 211 for table in table_defs: 212 if table.split("(")[0].strip() not in table_names: 213 self.cursor.execute(f"create table [{table}];") 214 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)
216 @_connect 217 def create_table(self, table: str, column_defs: list[str]): 218 """Create a table if it doesn't exist. 219 220 #### :params: 221 222 `table`: Name of the table to create. 223 224 `column_defs`: List of column definitions in proper Sqlite3 sytax. 225 i.e. `"column_name text unique"` or `"column_name int primary key"` etc.""" 226 if table not in self.get_table_names(): 227 query = f"create table [{table}]({', '.join(column_defs)});" 228 self.cursor.execute(query) 229 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.
231 @_connect 232 def get_table_names(self) -> list[str]: 233 """Returns a list of table names from the database.""" 234 self.cursor.execute( 235 'select name from sqlite_Schema where type = "table" and name not like "sqlite_%";' 236 ) 237 return [result[0] for result in self.cursor.fetchall()]
Returns a list of table names from the database.
239 @_connect 240 def get_column_names(self, table: str) -> list[str]: 241 """Return a list of column names from a table.""" 242 self.cursor.execute(f"select * from [{table}] where 1=0;") 243 return [description[0] for description in self.cursor.description]
Return a list of column names from a table.
245 @_connect 246 def count( 247 self, 248 table: str, 249 match_criteria: list[tuple] | dict | None = None, 250 exact_match: bool = True, 251 ) -> int: 252 """Return number of items in `table`. 253 254 #### :params: 255 256 `match_criteria`: Can be a list of 2-tuples where each 257 tuple is `(columnName, rowValue)` or a dictionary where 258 keys are column names and values are row values. 259 If `None`, all rows from the table will be counted. 260 261 `exact_match`: If `False`, the row value for a given column 262 in `match_criteria` will be matched as a substring. 263 Has no effect if `match_criteria` is `None`. 264 """ 265 query = f"select count(_rowid_) from [{table}]" 266 try: 267 if match_criteria: 268 self.cursor.execute( 269 f"{query} where {self._get_conditions(match_criteria, exact_match)};" 270 ) 271 else: 272 self.cursor.execute(f"{query}") 273 return self.cursor.fetchone()[0] 274 except: 275 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
.
277 @_connect 278 def add_row( 279 self, table: str, values: tuple[Any], columns: tuple[str] | None = None 280 ) -> bool: 281 """Add a row of values to a table. 282 283 Returns whether the addition was successful or not. 284 285 #### :params: 286 287 `table`: The table to insert values into. 288 289 `values`: A tuple of values to be inserted into the table. 290 291 `columns`: If `None`, `values` is expected to supply a value for every column in the table. 292 If `columns` is provided, it should contain the same number of elements as `values`.""" 293 parameterizer = ", ".join("?" for _ in values) 294 logger_values = ", ".join(str(value) for value in values) 295 try: 296 if columns: 297 columns_query = ", ".join(column for column in columns) 298 self.cursor.execute( 299 f"insert into [{table}] ({columns_query}) values({parameterizer});", 300 values, 301 ) 302 else: 303 self.cursor.execute( 304 f"insert into [{table}] values({parameterizer});", values 305 ) 306 self.logger.info(f'Added "{logger_values}" to {table} table.') 307 return True 308 except Exception as e: 309 if "constraint" not in str(e).lower(): 310 self.logger.exception( 311 f'Error adding "{logger_values}" to {table} table.' 312 ) 313 else: 314 self.logger.debug(str(e)) 315 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
.
317 @_connect 318 def add_rows( 319 self, table: str, values: list[tuple[Any]], columns: tuple[str] | None = None 320 ) -> tuple[int, int]: 321 """Add multiple rows of values to a table. 322 323 Returns a tuple containing the number of successful additions and the number of failed additions. 324 325 #### :params: 326 327 `table`: The table to insert values into. 328 329 `values`: A list of tuples of values to be inserted into the table. 330 Each tuple constitutes a single row to be inserted 331 332 `columns`: If `None`, `values` is expected to supply a value for every column in the table. 333 If `columns` is provided, it should contain the same number of elements as `values`.""" 334 successes = 0 335 failures = 0 336 for row in values: 337 if self.add_row(table, row, columns): 338 successes += 1 339 else: 340 failures += 1 341 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
.
343 @_connect 344 def get_rows( 345 self, 346 table: str, 347 match_criteria: list[tuple] | dict | None = None, 348 exact_match: bool = True, 349 sort_by_column: str | None = None, 350 columns_to_return: list[str] | None = None, 351 return_as_dataframe: bool = False, 352 values_only: bool = False, 353 order_by: str | None = None, 354 limit: str | int | None = None, 355 ) -> list[dict] | list[tuple] | pandas.DataFrame: 356 """Return matching rows from `table`. 357 358 By default, rows will be returned as a list of dictionaries of the form `[{"column_name": value, ...}, ...]` 359 360 361 #### :params: 362 363 `match_criteria`: Can be a list of 2-tuples where each 364 tuple is `(columnName, rowValue)` or a dictionary where 365 keys are column names and values are row values. 366 367 `exact_match`: If `False`, the row value for a given column will be matched as a substring. 368 369 `sort_by_column`: A column name to sort the results by. 370 This will sort results in Python after retrieving them from the db. 371 Use the 'order_by' param to use SQLite engine for ordering. 372 373 `columns_to_return`: Optional list of column names. 374 If provided, the elements returned by this function will only contain the provided columns. 375 Otherwise every column in the row is returned. 376 377 `return_as_dataframe`: Return the results as a `pandas.DataFrame` object. 378 379 `values_only`: Return the results as a list of tuples. 380 381 `order_by`: If given, a `order by {order_by}` clause will be added to the select query. 382 383 `limit`: If given, a `limit {limit}` clause will be added to the select query. 384 """ 385 386 if type(columns_to_return) is str: 387 columns_to_return = [columns_to_return] 388 query = f"select * from [{table}]" 389 matches = [] 390 if match_criteria: 391 query += f" where {self._get_conditions(match_criteria, exact_match)}" 392 if order_by: 393 query += f" order by {order_by}" 394 if limit: 395 query += f" limit {limit}" 396 query += ";" 397 self.cursor.execute(query) 398 matches = self.cursor.fetchall() 399 results = [self._get_dict(table, match, columns_to_return) for match in matches] 400 if sort_by_column: 401 results = sorted(results, key=lambda x: x[sort_by_column]) 402 if return_as_dataframe: 403 return pandas.DataFrame(results) 404 if values_only: 405 return [tuple(row.values()) for row in results] 406 else: 407 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.
409 @_connect 410 def find( 411 self, table: str, query_string: str, columns: list[str] | None = None 412 ) -> list[dict]: 413 """Search for rows that contain `query_string` as a substring of any column. 414 415 #### :params: 416 417 `table`: The table to search. 418 419 `query_string`: The substring to search for in all columns. 420 421 `columns`: A list of columns to search for query_string. 422 If None, all columns in the table will be searched. 423 """ 424 if type(columns) is str: 425 columns = [columns] 426 results = [] 427 if not columns: 428 columns = self.get_column_names(table) 429 for column in columns: 430 results.extend( 431 [ 432 row 433 for row in self.get_rows( 434 table, [(column, query_string)], exact_match=False 435 ) 436 if row not in results 437 ] 438 ) 439 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.
441 @_connect 442 def delete( 443 self, table: str, match_criteria: list[tuple] | dict, exact_match: bool = True 444 ) -> int: 445 """Delete records from `table`. 446 447 Returns the number of deleted records. 448 449 #### :params: 450 451 `match_criteria`: Can be a list of 2-tuples where each tuple is `(column_name, value)` 452 or a dictionary where keys are column names and values are corresponding values. 453 454 `exact_match`: If `False`, the value for a given column will be matched as a substring. 455 """ 456 conditions = self._get_conditions(match_criteria, exact_match) 457 try: 458 self.cursor.execute(f"delete from [{table}] where {conditions};") 459 num_deletions = self.cursor.rowcount 460 self.logger.info( 461 f'Deleted {num_deletions} rows from "{table}" where {conditions}".' 462 ) 463 return num_deletions 464 except Exception as e: 465 self.logger.debug( 466 f'Error deleting rows from "{table}" where {conditions}.\n{e}' 467 ) 468 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.
470 @_connect 471 def update( 472 self, 473 table: str, 474 column_to_update: str, 475 new_value: Any, 476 match_criteria: list[tuple] | dict | None = None, 477 exact_match: bool = True, 478 ) -> int: 479 """Update the value in `column_to_update` to `new_value` for rows matched with `match_criteria`. 480 481 #### :params: 482 483 `table`: The table to update rows in. 484 485 `column_to_update`: The column to be updated in the matched rows. 486 487 `new_value`: The new value to insert. 488 489 `match_criteria`: Can be a list of 2-tuples where each tuple is `(columnName, rowValue)` 490 or a dictionary where keys are column names and values are corresponding values. 491 If `None`, every row in `table` will be updated. 492 493 `exact_match`: If `False`, `match_criteria` values will be treated as substrings. 494 495 Returns the number of updated rows.""" 496 query = f"update [{table}] set {column_to_update} = ?" 497 conditions = "" 498 if match_criteria: 499 conditions = self._get_conditions(match_criteria, exact_match) 500 query += f" where {conditions}" 501 else: 502 conditions = None 503 query += ";" 504 try: 505 self.cursor.execute( 506 query, 507 (new_value,), 508 ) 509 num_updates = self.cursor.rowcount 510 self.logger.info( 511 f'In {num_updates} rows, updated "{column_to_update}" in "{table}" table to "{new_value}" where {conditions}' 512 ) 513 return num_updates 514 except Exception as e: 515 self.logger.error( 516 f'Failed to update "{column_to_update}" in "{table}" table to "{new_value}" where {conditions}"\n{e}' 517 ) 518 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.
520 @_connect 521 def drop_table(self, table: str) -> bool: 522 """Drop `table` from the database. 523 524 Returns `True` if successful, `False` if not.""" 525 try: 526 self.cursor.execute(f"drop Table [{table}];") 527 self.logger.info(f'Dropped table "{table}"') 528 return True 529 except Exception as e: 530 print(e) 531 self.logger.error(f'Failed to drop table "{table}"') 532 return False
Drop table
from the database.
Returns True
if successful, False
if not.
534 @_connect 535 def add_column( 536 self, table: str, column: str, _type: str, default_value: str | None = None 537 ): 538 """Add a new column to `table`. 539 540 #### :params: 541 542 `column`: Name of the column to add. 543 544 `_type`: The data type of the new column. 545 546 `default_value`: Optional default value for the column.""" 547 try: 548 if default_value: 549 self.cursor.execute( 550 f"alter table [{table}] add column {column} {_type} default {default_value};" 551 ) 552 self.update(table, column, default_value) 553 else: 554 self.cursor.execute( 555 f"alter table [{table}] add column {column} {_type};" 556 ) 557 self.logger.info(f'Added column "{column}" to "{table}" table.') 558 except Exception as e: 559 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.
561 @staticmethod 562 def data_to_string( 563 data: list[dict], sort_key: str | None = None, wrap_to_terminal: bool = True 564 ) -> str: 565 """Uses tabulate to produce pretty string output from a list of dictionaries. 566 567 #### :params: 568 569 `data`: The list of dictionaries to create a grid from. 570 Assumes all dictionaries in list have the same set of keys. 571 572 `sort_key`: Optional dictionary key to sort data with. 573 574 `wrap_to_terminal`: If `True`, the table width will be wrapped to fit within the current terminal window. 575 Pass as `False` if the output is going into something like a `.txt` file.""" 576 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.
579def data_to_string( 580 data: list[dict], sort_key: str | None = None, wrap_to_terminal: bool = True 581) -> str: 582 """Uses tabulate to produce pretty string output from a list of dictionaries. 583 584 #### :params: 585 586 `data`: The list of dictionaries to create a grid from. 587 Assumes all dictionaries in list have the same set of keys. 588 589 `sort_key`: Optional dictionary key to sort data with. 590 591 `wrap_to_terminal`: If `True`, the table width will be wrapped to fit within the current terminal window. 592 Pass as `False` if the output is going into something like a `.txt` file.""" 593 if len(data) == 0: 594 return "" 595 if sort_key: 596 data = sorted(data, key=lambda d: d[sort_key]) 597 for i, d in enumerate(data): 598 for k in d: 599 data[i][k] = str(data[i][k]) 600 601 try: 602 print("Resizing grid to fit within the terminal...\n") 603 return griddy(data, "keys", wrap_to_terminal) 604 except RuntimeError as e: 605 print(e) 606 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.