databased.databased

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

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

Supports saving and reading dates as datetime objects.

Supports using a context manager.

DataBased( dbpath: str | pathier.pathier.Pathier, logger_encoding: str = 'utf-8', logger_message_format: str = '{levelname}|-|{asctime}|-|{message}')
34    def __init__(
35        self,
36        dbpath: str | Pathier,
37        logger_encoding: str = "utf-8",
38        logger_message_format: str = "{levelname}|-|{asctime}|-|{message}",
39    ):
40        """
41        #### :params:
42
43        `dbpath`: String or Path object to database file.
44        If a relative path is given, it will be relative to the
45        current working directory. The log file will be saved to the
46        same directory.
47
48        `logger_message_format`: '{' style format string for the logger object."""
49        self.dbpath = Pathier(dbpath)
50        self.dbname = Pathier(dbpath).name
51        self.dbpath.parent.mkdir(parents=True, exist_ok=True)
52        self._logger_init(
53            encoding=logger_encoding, message_format=logger_message_format
54        )
55        self.connection_open = False

:params:

dbpath: String or Path object to database file. If a relative path is given, it will be relative to the current working directory. The log file will be saved to the same directory.

logger_message_format: '{' style format string for the logger object.

def open(self):
64    def open(self):
65        """Open connection to db."""
66        self.connection = sqlite3.connect(
67            self.dbpath,
68            detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES,
69            timeout=10,
70        )
71        self.connection.execute("pragma foreign_keys = 1;")
72        self.cursor = self.connection.cursor()
73        self.connection_open = True

Open connection to db.

def close(self):
75    def close(self):
76        """Save and close connection to db.
77
78        Call this as soon as you are done using the database if you have
79        multiple threads or processes using the same database."""
80        if self.connection_open:
81            self.connection.commit()
82            self.connection.close()
83            self.connection_open = False

Save and close connection to db.

Call this as soon as you are done using the database if you have multiple threads or processes using the same database.

def query(self, query_) -> list[typing.Any]:
156    @_connect
157    def query(self, query_) -> list[Any]:
158        """Execute an arbitrary query and return the results."""
159        self.cursor.execute(query_)
160        return self.cursor.fetchall()

Execute an arbitrary query and return the results.

def create_tables(self, table_defs: list[str] = []):
162    @_connect
163    def create_tables(self, table_defs: list[str] = []):
164        """Create tables if they don't exist.
165
166        :param `table_defs`: Each definition should be in the form `table_name(column_definitions)`"""
167        if len(table_defs) > 0:
168            table_names = self.get_table_names()
169            for table in table_defs:
170                if table.split("(")[0].strip() not in table_names:
171                    self.cursor.execute(f"create table [{table}];")
172                    self.logger.info(f'{table.split("(")[0]} table created.')

Create tables if they don't exist.

Parameters
  • table_defs: Each definition should be in the form table_name(column_definitions)
def create_table(self, table: str, column_defs: list[str]):
174    @_connect
175    def create_table(self, table: str, column_defs: list[str]):
176        """Create a table if it doesn't exist.
177
178        #### :params:
179
180        `table`: Name of the table to create.
181
182        `column_defs`: List of column definitions in proper Sqlite3 sytax.
183        i.e. `"column_name text unique"` or `"column_name int primary key"` etc."""
184        if table not in self.get_table_names():
185            query = f"create table [{table}]({', '.join(column_defs)});"
186            self.cursor.execute(query)
187            self.logger.info(f"'{table}' table created.")

Create a table if it doesn't exist.

:params:

table: Name of the table to create.

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

def get_table_names(self) -> list[str]:
189    @_connect
190    def get_table_names(self) -> list[str]:
191        """Returns a list of table names from the database."""
192        self.cursor.execute(
193            'select name from sqlite_Schema where type = "table" and name not like "sqlite_%";'
194        )
195        return [result[0] for result in self.cursor.fetchall()]

Returns a list of table names from the database.

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

Return a list of column names from a table.

def count( self, table: str, match_criteria: list[tuple] | dict | None = None, exact_match: bool = True) -> int:
203    @_connect
204    def count(
205        self,
206        table: str,
207        match_criteria: list[tuple] | dict | None = None,
208        exact_match: bool = True,
209    ) -> int:
210        """Return number of items in `table`.
211
212        #### :params:
213
214        `match_criteria`: Can be a list of 2-tuples where each
215        tuple is `(columnName, rowValue)` or a dictionary where
216        keys are column names and values are row values.
217        If `None`, all rows from the table will be counted.
218
219        `exact_match`: If `False`, the row value for a given column
220        in `match_criteria` will be matched as a substring.
221        Has no effect if `match_criteria` is `None`.
222        """
223        query = f"select count(_rowid_) from [{table}]"
224        try:
225            if match_criteria:
226                self.cursor.execute(
227                    f"{query} where {self._get_conditions(match_criteria, exact_match)};"
228                )
229            else:
230                self.cursor.execute(f"{query}")
231            return self.cursor.fetchone()[0]
232        except:
233            return 0

Return number of items in table.

:params:

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

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

def add_row( self, table: str, values: tuple[typing.Any], columns: tuple[str] | None = None) -> bool:
235    @_connect
236    def add_row(
237        self, table: str, values: tuple[Any], columns: tuple[str] | None = None
238    ) -> bool:
239        """Add a row of values to a table.
240
241        Returns whether the addition was successful or not.
242
243        #### :params:
244
245        `table`: The table to insert values into.
246
247        `values`: A tuple of values to be inserted into the table.
248
249        `columns`: If `None`, `values` is expected to supply a value for every column in the table.
250        If `columns` is provided, it should contain the same number of elements as `values`."""
251        parameterizer = ", ".join("?" for _ in values)
252        logger_values = ", ".join(str(value) for value in values)
253        try:
254            if columns:
255                columns_query = ", ".join(column for column in columns)
256                self.cursor.execute(
257                    f"insert into [{table}] ({columns_query}) values({parameterizer});",
258                    values,
259                )
260            else:
261                self.cursor.execute(
262                    f"insert into [{table}] values({parameterizer});", values
263                )
264            self.logger.info(f'Added "{logger_values}" to {table} table.')
265            return True
266        except Exception as e:
267            if "constraint" not in str(e).lower():
268                self.logger.exception(
269                    f'Error adding "{logger_values}" to {table} table.'
270                )
271            else:
272                self.logger.debug(str(e))
273            return False

Add a row of values to a table.

Returns whether the addition was successful or not.

:params:

table: The table to insert values into.

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

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

def add_rows( self, table: str, values: list[tuple[typing.Any]], columns: tuple[str] | None = None) -> tuple[int, int]:
275    @_connect
276    def add_rows(
277        self, table: str, values: list[tuple[Any]], columns: tuple[str] | None = None
278    ) -> tuple[int, int]:
279        """Add multiple rows of values to a table.
280
281        Returns a tuple containing the number of successful additions and the number of failed additions.
282
283        #### :params:
284
285        `table`: The table to insert values into.
286
287        `values`: A list of tuples of values to be inserted into the table.
288        Each tuple constitutes a single row to be inserted
289
290        `columns`: If `None`, `values` is expected to supply a value for every column in the table.
291        If `columns` is provided, it should contain the same number of elements as `values`."""
292        successes = 0
293        failures = 0
294        for row in values:
295            if self.add_row(table, row, columns):
296                successes += 1
297            else:
298                failures += 1
299        return (successes, failures)

Add multiple rows of values to a table.

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

:params:

table: The table to insert values into.

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

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

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:
301    @_connect
302    def get_rows(
303        self,
304        table: str,
305        match_criteria: list[tuple] | dict | None = None,
306        exact_match: bool = True,
307        sort_by_column: str | None = None,
308        columns_to_return: list[str] | None = None,
309        return_as_dataframe: bool = False,
310        values_only: bool = False,
311        order_by: str | None = None,
312        limit: str | int | None = None,
313    ) -> list[dict] | list[tuple] | pandas.DataFrame:
314        """Return matching rows from `table`.
315
316        By default, rows will be returned as a list of dictionaries of the form `[{"column_name": value, ...}, ...]`
317
318
319        #### :params:
320
321        `match_criteria`: Can be a list of 2-tuples where each
322        tuple is `(columnName, rowValue)` or a dictionary where
323        keys are column names and values are row values.
324
325        `exact_match`: If `False`, the row value for a given column will be matched as a substring.
326
327        `sort_by_column`: A column name to sort the results by.
328        This will sort results in Python after retrieving them from the db.
329        Use the 'order_by' param to use SQLite engine for ordering.
330
331        `columns_to_return`: Optional list of column names.
332        If provided, the elements returned by this function will only contain the provided columns.
333        Otherwise every column in the row is returned.
334
335        `return_as_dataframe`: Return the results as a `pandas.DataFrame` object.
336
337        `values_only`: Return the results as a list of tuples.
338
339        `order_by`: If given, a `order by {order_by}` clause will be added to the select query.
340
341        `limit`: If given, a `limit {limit}` clause will be added to the select query.
342        """
343
344        if type(columns_to_return) is str:
345            columns_to_return = [columns_to_return]
346        query = f"select * from [{table}]"
347        matches = []
348        if match_criteria:
349            query += f" where {self._get_conditions(match_criteria, exact_match)}"
350        if order_by:
351            query += f" order by {order_by}"
352        if limit:
353            query += f" limit {limit}"
354        query += ";"
355        self.cursor.execute(query)
356        matches = self.cursor.fetchall()
357        results = [self._get_dict(table, match, columns_to_return) for match in matches]
358        if sort_by_column:
359            results = sorted(results, key=lambda x: x[sort_by_column])
360        if return_as_dataframe:
361            return pandas.DataFrame(results)
362        if values_only:
363            return [tuple(row.values()) for row in results]
364        else:
365            return results

Return matching rows from table.

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

:params:

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

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

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

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

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

values_only: Return the results as a list of tuples.

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

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

def find( self, table: str, query_string: str, columns: list[str] | None = None) -> list[dict]:
367    @_connect
368    def find(
369        self, table: str, query_string: str, columns: list[str] | None = None
370    ) -> list[dict]:
371        """Search for rows that contain `query_string` as a substring of any column.
372
373        #### :params:
374
375        `table`: The table to search.
376
377        `query_string`: The substring to search for in all columns.
378
379        `columns`: A list of columns to search for query_string.
380        If None, all columns in the table will be searched.
381        """
382        if type(columns) is str:
383            columns = [columns]
384        results = []
385        if not columns:
386            columns = self.get_column_names(table)
387        for column in columns:
388            results.extend(
389                [
390                    row
391                    for row in self.get_rows(
392                        table, [(column, query_string)], exact_match=False
393                    )
394                    if row not in results
395                ]
396            )
397        return results

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

:params:

table: The table to search.

query_string: The substring to search for in all columns.

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

def delete( self, table: str, match_criteria: list[tuple] | dict, exact_match: bool = True) -> int:
399    @_connect
400    def delete(
401        self, table: str, match_criteria: list[tuple] | dict, exact_match: bool = True
402    ) -> int:
403        """Delete records from `table`.
404
405        Returns the number of deleted records.
406
407        #### :params:
408
409        `match_criteria`: Can be a list of 2-tuples where each tuple is `(column_name, value)`
410        or a dictionary where keys are column names and values are corresponding values.
411
412        `exact_match`: If `False`, the value for a given column will be matched as a substring.
413        """
414        conditions = self._get_conditions(match_criteria, exact_match)
415        try:
416            self.cursor.execute(f"delete from [{table}] where {conditions};")
417            num_deletions = self.cursor.rowcount
418            self.logger.info(
419                f'Deleted {num_deletions} rows from "{table}" where {conditions}".'
420            )
421            return num_deletions
422        except Exception as e:
423            self.logger.debug(
424                f'Error deleting rows from "{table}" where {conditions}.\n{e}'
425            )
426            return 0

Delete records from table.

Returns the number of deleted records.

:params:

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

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

def update( self, table: str, column_to_update: str, new_value: Any, match_criteria: list[tuple] | dict | None = None, exact_match: bool = True) -> int:
428    @_connect
429    def update(
430        self,
431        table: str,
432        column_to_update: str,
433        new_value: Any,
434        match_criteria: list[tuple] | dict | None = None,
435        exact_match: bool = True,
436    ) -> int:
437        """Update the value in `column_to_update` to `new_value` for rows matched with `match_criteria`.
438
439        #### :params:
440
441        `table`: The table to update rows in.
442
443        `column_to_update`: The column to be updated in the matched rows.
444
445        `new_value`: The new value to insert.
446
447        `match_criteria`: Can be a list of 2-tuples where each tuple is `(columnName, rowValue)`
448        or a dictionary where keys are column names and values are corresponding values.
449        If `None`, every row in `table` will be updated.
450
451        `exact_match`: If `False`, `match_criteria` values will be treated as substrings.
452
453        Returns the number of updated rows."""
454        query = f"update [{table}] set {column_to_update} = ?"
455        conditions = ""
456        if match_criteria:
457            conditions = self._get_conditions(match_criteria, exact_match)
458            query += f" where {conditions}"
459        else:
460            conditions = None
461        query += ";"
462        try:
463            self.cursor.execute(
464                query,
465                (new_value,),
466            )
467            num_updates = self.cursor.rowcount
468            self.logger.info(
469                f'In {num_updates} rows, updated "{column_to_update}" in "{table}" table to "{new_value}" where {conditions}'
470            )
471            return num_updates
472        except Exception as e:
473            self.logger.error(
474                f'Failed to update "{column_to_update}" in "{table}" table to "{new_value}" where {conditions}"\n{e}'
475            )
476            return 0

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

:params:

table: The table to update rows in.

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

new_value: The new value to insert.

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

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

Returns the number of updated rows.

def drop_table(self, table: str) -> bool:
478    @_connect
479    def drop_table(self, table: str) -> bool:
480        """Drop `table` from the database.
481
482        Returns `True` if successful, `False` if not."""
483        try:
484            self.cursor.execute(f"drop Table [{table}];")
485            self.logger.info(f'Dropped table "{table}"')
486            return True
487        except Exception as e:
488            print(e)
489            self.logger.error(f'Failed to drop table "{table}"')
490            return False

Drop table from the database.

Returns True if successful, False if not.

def add_column( self, table: str, column: str, _type: str, default_value: str | None = None):
492    @_connect
493    def add_column(
494        self, table: str, column: str, _type: str, default_value: str | None = None
495    ):
496        """Add a new column to `table`.
497
498        #### :params:
499
500        `column`: Name of the column to add.
501
502        `_type`: The data type of the new column.
503
504        `default_value`: Optional default value for the column."""
505        try:
506            if default_value:
507                self.cursor.execute(
508                    f"alter table [{table}] add column {column} {_type} default {default_value};"
509                )
510                self.update(table, column, default_value)
511            else:
512                self.cursor.execute(
513                    f"alter table [{table}] add column {column} {_type};"
514                )
515            self.logger.info(f'Added column "{column}" to "{table}" table.')
516        except Exception as e:
517            self.logger.error(f'Failed to add column "{column}" to "{table}" table.')

Add a new column to table.

:params:

column: Name of the column to add.

_type: The data type of the new column.

default_value: Optional default value for the column.

@staticmethod
def data_to_string( data: list[dict], sort_key: str | None = None, wrap_to_terminal: bool = True) -> str:
519    @staticmethod
520    def data_to_string(
521        data: list[dict], sort_key: str | None = None, wrap_to_terminal: bool = True
522    ) -> str:
523        """Uses tabulate to produce pretty string output from a list of dictionaries.
524
525        #### :params:
526
527        `data`: The list of dictionaries to create a grid from.
528        Assumes all dictionaries in list have the same set of keys.
529
530        `sort_key`: Optional dictionary key to sort data with.
531
532        `wrap_to_terminal`: If `True`, the table width will be wrapped to fit within the current terminal window.
533        Pass as `False` if the output is going into something like a `.txt` file."""
534        return data_to_string(data, sort_key, wrap_to_terminal)

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

:params:

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

sort_key: Optional dictionary key to sort data with.

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

def data_to_string( data: list[dict], sort_key: str | None = None, wrap_to_terminal: bool = True) -> str:
537def data_to_string(
538    data: list[dict], sort_key: str | None = None, wrap_to_terminal: bool = True
539) -> str:
540    """Uses tabulate to produce pretty string output from a list of dictionaries.
541
542    #### :params:
543
544    `data`: The list of dictionaries to create a grid from.
545    Assumes all dictionaries in list have the same set of keys.
546
547    `sort_key`: Optional dictionary key to sort data with.
548
549    `wrap_to_terminal`: If `True`, the table width will be wrapped to fit within the current terminal window.
550    Pass as `False` if the output is going into something like a `.txt` file."""
551    if len(data) == 0:
552        return ""
553    if sort_key:
554        data = sorted(data, key=lambda d: d[sort_key])
555    for i, d in enumerate(data):
556        for k in d:
557            data[i][k] = str(data[i][k])
558
559    too_wide = True
560    terminal_width = os.get_terminal_size().columns
561    max_col_widths = terminal_width
562    # Make an output with effectively unrestricted column widths
563    # to see if shrinking is necessary
564    output = tabulate(
565        data,
566        headers="keys",
567        disable_numparse=True,
568        tablefmt="grid",
569        maxcolwidths=max_col_widths,
570    )
571    current_width = output.index("\n")
572    if current_width < terminal_width:
573        too_wide = False
574    if wrap_to_terminal and too_wide:
575        print("Resizing grid to fit within the terminal...\n")
576        previous_col_widths = max_col_widths
577        acceptable_width = terminal_width - 10
578        while too_wide and max_col_widths > 1:
579            if current_width >= terminal_width:
580                previous_col_widths = max_col_widths
581                max_col_widths = int(max_col_widths * 0.5)
582            elif current_width < terminal_width:
583                # Without lowering acceptable_width, this condition will cause infinite loop
584                if max_col_widths == previous_col_widths - 1:
585                    acceptable_width -= 10
586                max_col_widths = int(
587                    max_col_widths + (0.5 * (previous_col_widths - max_col_widths))
588                )
589            output = tabulate(
590                data,
591                headers="keys",
592                disable_numparse=True,
593                tablefmt="grid",
594                maxcolwidths=max_col_widths,
595            )
596            current_width = output.index("\n")
597            if acceptable_width < current_width < terminal_width:
598                too_wide = False
599        if too_wide:
600            print("Couldn't resize grid to fit within the terminal.")
601            return str(data)
602    return output

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

:params:

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

sort_key: Optional dictionary key to sort data with.

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