databased.databased

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

Sqli wrapper so queries don't need to be written except table definitions.

Supports saving and reading dates as datetime objects.

Supports using a context manager.

DataBased( dbpath: str | pathier.pathier.Pathier, logger_encoding: str = 'utf-8', logger_message_format: str = '{levelname}|-|{asctime}|-|{message}', connection_timeout: float = 10)
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.

def open(self):
 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.

def close(self):
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.

def vacuum(self):
190    def vacuum(self):
191        """Reduce disk size of the database with a `VACUUM` query."""
192        self.query("VACUUM;")

Reduce disk size of the database with a VACUUM query.

def query(self, query_) -> list[typing.Any]:
194    @_connect
195    def query(self, query_) -> list[Any]:
196        """Execute an arbitrary query and return the results."""
197        self.cursor.execute(query_)
198        return self.cursor.fetchall()

Execute an arbitrary query and return the results.

def create_tables(self, table_defs: list[str] = []):
200    @_connect
201    def create_tables(self, table_defs: list[str] = []):
202        """Create tables if they don't exist.
203
204        :param `table_defs`: Each definition should be in the form `table_name(column_definitions)`"""
205        if len(table_defs) > 0:
206            table_names = self.get_table_names()
207            for table in table_defs:
208                if table.split("(")[0].strip() not in table_names:
209                    self.cursor.execute(f"create table [{table}];")
210                    self.logger.info(f'{table.split("(")[0]} table created.')

Create tables if they don't exist.

Parameters
  • table_defs: Each definition should be in the form table_name(column_definitions)
def create_table(self, table: str, column_defs: list[str]):
212    @_connect
213    def create_table(self, table: str, column_defs: list[str]):
214        """Create a table if it doesn't exist.
215
216        #### :params:
217
218        `table`: Name of the table to create.
219
220        `column_defs`: List of column definitions in proper Sqlite3 sytax.
221        i.e. `"column_name text unique"` or `"column_name int primary key"` etc."""
222        if table not in self.get_table_names():
223            query = f"create table [{table}]({', '.join(column_defs)});"
224            self.cursor.execute(query)
225            self.logger.info(f"'{table}' table created.")

Create a table if it doesn't exist.

:params:

table: Name of the table to create.

column_defs: List of column definitions in proper Sqlite3 sytax. i.e. "column_name text unique" or "column_name int primary key" etc.

def get_table_names(self) -> list[str]:
227    @_connect
228    def get_table_names(self) -> list[str]:
229        """Returns a list of table names from the database."""
230        self.cursor.execute(
231            'select name from sqlite_Schema where type = "table" and name not like "sqlite_%";'
232        )
233        return [result[0] for result in self.cursor.fetchall()]

Returns a list of table names from the database.

def get_column_names(self, table: str) -> list[str]:
235    @_connect
236    def get_column_names(self, table: str) -> list[str]:
237        """Return a list of column names from a table."""
238        self.cursor.execute(f"select * from [{table}] where 1=0;")
239        return [description[0] for description in self.cursor.description]

Return a list of column names from a table.

def count( self, table: str, match_criteria: list[tuple] | dict | None = None, exact_match: bool = True) -> int:
241    @_connect
242    def count(
243        self,
244        table: str,
245        match_criteria: list[tuple] | dict | None = None,
246        exact_match: bool = True,
247    ) -> int:
248        """Return number of items in `table`.
249
250        #### :params:
251
252        `match_criteria`: Can be a list of 2-tuples where each
253        tuple is `(columnName, rowValue)` or a dictionary where
254        keys are column names and values are row values.
255        If `None`, all rows from the table will be counted.
256
257        `exact_match`: If `False`, the row value for a given column
258        in `match_criteria` will be matched as a substring.
259        Has no effect if `match_criteria` is `None`.
260        """
261        query = f"select count(_rowid_) from [{table}]"
262        try:
263            if match_criteria:
264                self.cursor.execute(
265                    f"{query} where {self._get_conditions(match_criteria, exact_match)};"
266                )
267            else:
268                self.cursor.execute(f"{query}")
269            return self.cursor.fetchone()[0]
270        except:
271            return 0

Return number of items in table.

:params:

match_criteria: Can be a list of 2-tuples where each tuple is (columnName, rowValue) or a dictionary where keys are column names and values are row values. If None, all rows from the table will be counted.

exact_match: If False, the row value for a given column in match_criteria will be matched as a substring. Has no effect if match_criteria is None.

def add_row( self, table: str, values: tuple[typing.Any], columns: tuple[str] | None = None) -> bool:
273    @_connect
274    def add_row(
275        self, table: str, values: tuple[Any], columns: tuple[str] | None = None
276    ) -> bool:
277        """Add a row of values to a table.
278
279        Returns whether the addition was successful or not.
280
281        #### :params:
282
283        `table`: The table to insert values into.
284
285        `values`: A tuple of values to be inserted into the table.
286
287        `columns`: If `None`, `values` is expected to supply a value for every column in the table.
288        If `columns` is provided, it should contain the same number of elements as `values`."""
289        parameterizer = ", ".join("?" for _ in values)
290        logger_values = ", ".join(str(value) for value in values)
291        try:
292            if columns:
293                columns_query = ", ".join(column for column in columns)
294                self.cursor.execute(
295                    f"insert into [{table}] ({columns_query}) values({parameterizer});",
296                    values,
297                )
298            else:
299                self.cursor.execute(
300                    f"insert into [{table}] values({parameterizer});", values
301                )
302            self.logger.info(f'Added "{logger_values}" to {table} table.')
303            return True
304        except Exception as e:
305            if "constraint" not in str(e).lower():
306                self.logger.exception(
307                    f'Error adding "{logger_values}" to {table} table.'
308                )
309            else:
310                self.logger.debug(str(e))
311            return False

Add a row of values to a table.

Returns whether the addition was successful or not.

:params:

table: The table to insert values into.

values: A tuple of values to be inserted into the table.

columns: If None, values is expected to supply a value for every column in the table. If columns is provided, it should contain the same number of elements as values.

def add_rows( self, table: str, values: list[tuple[typing.Any]], columns: tuple[str] | None = None) -> tuple[int, int]:
313    @_connect
314    def add_rows(
315        self, table: str, values: list[tuple[Any]], columns: tuple[str] | None = None
316    ) -> tuple[int, int]:
317        """Add multiple rows of values to a table.
318
319        Returns a tuple containing the number of successful additions and the number of failed additions.
320
321        #### :params:
322
323        `table`: The table to insert values into.
324
325        `values`: A list of tuples of values to be inserted into the table.
326        Each tuple constitutes a single row to be inserted
327
328        `columns`: If `None`, `values` is expected to supply a value for every column in the table.
329        If `columns` is provided, it should contain the same number of elements as `values`."""
330        successes = 0
331        failures = 0
332        for row in values:
333            if self.add_row(table, row, columns):
334                successes += 1
335            else:
336                failures += 1
337        return (successes, failures)

Add multiple rows of values to a table.

Returns a tuple containing the number of successful additions and the number of failed additions.

:params:

table: The table to insert values into.

values: A list of tuples of values to be inserted into the table. Each tuple constitutes a single row to be inserted

columns: If None, values is expected to supply a value for every column in the table. If columns is provided, it should contain the same number of elements as values.

def get_rows( self, table: str, match_criteria: list[tuple] | dict | None = None, exact_match: bool = True, sort_by_column: str | None = None, columns_to_return: list[str] | None = None, return_as_dataframe: bool = False, values_only: bool = False, order_by: str | None = None, limit: str | int | None = None) -> list[dict] | list[tuple] | pandas.core.frame.DataFrame:
339    @_connect
340    def get_rows(
341        self,
342        table: str,
343        match_criteria: list[tuple] | dict | None = None,
344        exact_match: bool = True,
345        sort_by_column: str | None = None,
346        columns_to_return: list[str] | None = None,
347        return_as_dataframe: bool = False,
348        values_only: bool = False,
349        order_by: str | None = None,
350        limit: str | int | None = None,
351    ) -> list[dict] | list[tuple] | pandas.DataFrame:
352        """Return matching rows from `table`.
353
354        By default, rows will be returned as a list of dictionaries of the form `[{"column_name": value, ...}, ...]`
355
356
357        #### :params:
358
359        `match_criteria`: Can be a list of 2-tuples where each
360        tuple is `(columnName, rowValue)` or a dictionary where
361        keys are column names and values are row values.
362
363        `exact_match`: If `False`, the row value for a given column will be matched as a substring.
364
365        `sort_by_column`: A column name to sort the results by.
366        This will sort results in Python after retrieving them from the db.
367        Use the 'order_by' param to use SQLite engine for ordering.
368
369        `columns_to_return`: Optional list of column names.
370        If provided, the elements returned by this function will only contain the provided columns.
371        Otherwise every column in the row is returned.
372
373        `return_as_dataframe`: Return the results as a `pandas.DataFrame` object.
374
375        `values_only`: Return the results as a list of tuples.
376
377        `order_by`: If given, a `order by {order_by}` clause will be added to the select query.
378
379        `limit`: If given, a `limit {limit}` clause will be added to the select query.
380        """
381
382        if type(columns_to_return) is str:
383            columns_to_return = [columns_to_return]
384        query = f"select * from [{table}]"
385        matches = []
386        if match_criteria:
387            query += f" where {self._get_conditions(match_criteria, exact_match)}"
388        if order_by:
389            query += f" order by {order_by}"
390        if limit:
391            query += f" limit {limit}"
392        query += ";"
393        self.cursor.execute(query)
394        matches = self.cursor.fetchall()
395        results = [self._get_dict(table, match, columns_to_return) for match in matches]
396        if sort_by_column:
397            results = sorted(results, key=lambda x: x[sort_by_column])
398        if return_as_dataframe:
399            return pandas.DataFrame(results)
400        if values_only:
401            return [tuple(row.values()) for row in results]
402        else:
403            return results

Return matching rows from table.

By default, rows will be returned as a list of dictionaries of the form [{"column_name": value, ...}, ...]

:params:

match_criteria: Can be a list of 2-tuples where each tuple is (columnName, rowValue) or a dictionary where keys are column names and values are row values.

exact_match: If False, the row value for a given column will be matched as a substring.

sort_by_column: A column name to sort the results by. This will sort results in Python after retrieving them from the db. Use the 'order_by' param to use SQLite engine for ordering.

columns_to_return: Optional list of column names. If provided, the elements returned by this function will only contain the provided columns. Otherwise every column in the row is returned.

return_as_dataframe: Return the results as a pandas.DataFrame object.

values_only: Return the results as a list of tuples.

order_by: If given, a order by {order_by} clause will be added to the select query.

limit: If given, a limit {limit} clause will be added to the select query.

def find( self, table: str, query_string: str, columns: list[str] | None = None) -> list[dict]:
405    @_connect
406    def find(
407        self, table: str, query_string: str, columns: list[str] | None = None
408    ) -> list[dict]:
409        """Search for rows that contain `query_string` as a substring of any column.
410
411        #### :params:
412
413        `table`: The table to search.
414
415        `query_string`: The substring to search for in all columns.
416
417        `columns`: A list of columns to search for query_string.
418        If None, all columns in the table will be searched.
419        """
420        if type(columns) is str:
421            columns = [columns]
422        results = []
423        if not columns:
424            columns = self.get_column_names(table)
425        for column in columns:
426            results.extend(
427                [
428                    row
429                    for row in self.get_rows(
430                        table, [(column, query_string)], exact_match=False
431                    )
432                    if row not in results
433                ]
434            )
435        return results

Search for rows that contain query_string as a substring of any column.

:params:

table: The table to search.

query_string: The substring to search for in all columns.

columns: A list of columns to search for query_string. If None, all columns in the table will be searched.

def delete( self, table: str, match_criteria: list[tuple] | dict, exact_match: bool = True) -> int:
437    @_connect
438    def delete(
439        self, table: str, match_criteria: list[tuple] | dict, exact_match: bool = True
440    ) -> int:
441        """Delete records from `table`.
442
443        Returns the number of deleted records.
444
445        #### :params:
446
447        `match_criteria`: Can be a list of 2-tuples where each tuple is `(column_name, value)`
448        or a dictionary where keys are column names and values are corresponding values.
449
450        `exact_match`: If `False`, the value for a given column will be matched as a substring.
451        """
452        conditions = self._get_conditions(match_criteria, exact_match)
453        try:
454            self.cursor.execute(f"delete from [{table}] where {conditions};")
455            num_deletions = self.cursor.rowcount
456            self.logger.info(
457                f'Deleted {num_deletions} rows from "{table}" where {conditions}".'
458            )
459            return num_deletions
460        except Exception as e:
461            self.logger.debug(
462                f'Error deleting rows from "{table}" where {conditions}.\n{e}'
463            )
464            return 0

Delete records from table.

Returns the number of deleted records.

:params:

match_criteria: Can be a list of 2-tuples where each tuple is (column_name, value) or a dictionary where keys are column names and values are corresponding values.

exact_match: If False, the value for a given column will be matched as a substring.

def update( self, table: str, column_to_update: str, new_value: Any, match_criteria: list[tuple] | dict | None = None, exact_match: bool = True) -> int:
466    @_connect
467    def update(
468        self,
469        table: str,
470        column_to_update: str,
471        new_value: Any,
472        match_criteria: list[tuple] | dict | None = None,
473        exact_match: bool = True,
474    ) -> int:
475        """Update the value in `column_to_update` to `new_value` for rows matched with `match_criteria`.
476
477        #### :params:
478
479        `table`: The table to update rows in.
480
481        `column_to_update`: The column to be updated in the matched rows.
482
483        `new_value`: The new value to insert.
484
485        `match_criteria`: Can be a list of 2-tuples where each tuple is `(columnName, rowValue)`
486        or a dictionary where keys are column names and values are corresponding values.
487        If `None`, every row in `table` will be updated.
488
489        `exact_match`: If `False`, `match_criteria` values will be treated as substrings.
490
491        Returns the number of updated rows."""
492        query = f"update [{table}] set {column_to_update} = ?"
493        conditions = ""
494        if match_criteria:
495            conditions = self._get_conditions(match_criteria, exact_match)
496            query += f" where {conditions}"
497        else:
498            conditions = None
499        query += ";"
500        try:
501            self.cursor.execute(
502                query,
503                (new_value,),
504            )
505            num_updates = self.cursor.rowcount
506            self.logger.info(
507                f'In {num_updates} rows, updated "{column_to_update}" in "{table}" table to "{new_value}" where {conditions}'
508            )
509            return num_updates
510        except Exception as e:
511            self.logger.error(
512                f'Failed to update "{column_to_update}" in "{table}" table to "{new_value}" where {conditions}"\n{e}'
513            )
514            return 0

Update the value in column_to_update to new_value for rows matched with match_criteria.

:params:

table: The table to update rows in.

column_to_update: The column to be updated in the matched rows.

new_value: The new value to insert.

match_criteria: Can be a list of 2-tuples where each tuple is (columnName, rowValue) or a dictionary where keys are column names and values are corresponding values. If None, every row in table will be updated.

exact_match: If False, match_criteria values will be treated as substrings.

Returns the number of updated rows.

def drop_table(self, table: str) -> bool:
516    @_connect
517    def drop_table(self, table: str) -> bool:
518        """Drop `table` from the database.
519
520        Returns `True` if successful, `False` if not."""
521        try:
522            self.cursor.execute(f"drop Table [{table}];")
523            self.logger.info(f'Dropped table "{table}"')
524            return True
525        except Exception as e:
526            print(e)
527            self.logger.error(f'Failed to drop table "{table}"')
528            return False

Drop table from the database.

Returns True if successful, False if not.

def add_column( self, table: str, column: str, _type: str, default_value: str | None = None):
530    @_connect
531    def add_column(
532        self, table: str, column: str, _type: str, default_value: str | None = None
533    ):
534        """Add a new column to `table`.
535
536        #### :params:
537
538        `column`: Name of the column to add.
539
540        `_type`: The data type of the new column.
541
542        `default_value`: Optional default value for the column."""
543        try:
544            if default_value:
545                self.cursor.execute(
546                    f"alter table [{table}] add column {column} {_type} default {default_value};"
547                )
548                self.update(table, column, default_value)
549            else:
550                self.cursor.execute(
551                    f"alter table [{table}] add column {column} {_type};"
552                )
553            self.logger.info(f'Added column "{column}" to "{table}" table.')
554        except Exception as e:
555            self.logger.error(f'Failed to add column "{column}" to "{table}" table.')

Add a new column to table.

:params:

column: Name of the column to add.

_type: The data type of the new column.

default_value: Optional default value for the column.

@staticmethod
def data_to_string( data: list[dict], sort_key: str | None = None, wrap_to_terminal: bool = True) -> str:
557    @staticmethod
558    def data_to_string(
559        data: list[dict], sort_key: str | None = None, wrap_to_terminal: bool = True
560    ) -> str:
561        """Uses tabulate to produce pretty string output from a list of dictionaries.
562
563        #### :params:
564
565        `data`: The list of dictionaries to create a grid from.
566        Assumes all dictionaries in list have the same set of keys.
567
568        `sort_key`: Optional dictionary key to sort data with.
569
570        `wrap_to_terminal`: If `True`, the table width will be wrapped to fit within the current terminal window.
571        Pass as `False` if the output is going into something like a `.txt` file."""
572        return data_to_string(data, sort_key, wrap_to_terminal)

Uses tabulate to produce pretty string output from a list of dictionaries.

:params:

data: The list of dictionaries to create a grid from. Assumes all dictionaries in list have the same set of keys.

sort_key: Optional dictionary key to sort data with.

wrap_to_terminal: If True, the table width will be wrapped to fit within the current terminal window. Pass as False if the output is going into something like a .txt file.

def data_to_string( data: list[dict], sort_key: str | None = None, wrap_to_terminal: bool = True) -> str:
575def data_to_string(
576    data: list[dict], sort_key: str | None = None, wrap_to_terminal: bool = True
577) -> str:
578    """Uses tabulate to produce pretty string output from a list of dictionaries.
579
580    #### :params:
581
582    `data`: The list of dictionaries to create a grid from.
583    Assumes all dictionaries in list have the same set of keys.
584
585    `sort_key`: Optional dictionary key to sort data with.
586
587    `wrap_to_terminal`: If `True`, the table width will be wrapped to fit within the current terminal window.
588    Pass as `False` if the output is going into something like a `.txt` file."""
589    if len(data) == 0:
590        return ""
591    if sort_key:
592        data = sorted(data, key=lambda d: d[sort_key])
593    for i, d in enumerate(data):
594        for k in d:
595            data[i][k] = str(data[i][k])
596
597    try:
598        print("Resizing grid to fit within the terminal...\n")
599        return griddy(data, "keys", wrap_to_terminal)
600    except RuntimeError as e:
601        print(e)
602        return str(data)

Uses tabulate to produce pretty string output from a list of dictionaries.

:params:

data: The list of dictionaries to create a grid from. Assumes all dictionaries in list have the same set of keys.

sort_key: Optional dictionary key to sort data with.

wrap_to_terminal: If True, the table width will be wrapped to fit within the current terminal window. Pass as False if the output is going into something like a .txt file.