databased.databased

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

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

Supports saving and reading dates as datetime objects.

Supports using a context manager.

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) -> int:
190    def vacuum(self) -> int:
191        """Reduce disk size of the database with a `VACUUM` query.
192
193        Returns space freed up in bytes."""
194        size = self.dbpath.size
195        self.query("VACUUM;")
196        return size - self.dbpath.size

Reduce disk size of the database with a VACUUM query.

Returns space freed up in bytes.

def query(self, query_) -> list[typing.Any]:
198    @_connect
199    def query(self, query_) -> list[Any]:
200        """Execute an arbitrary query and return the results."""
201        self.cursor.execute(query_)
202        return self.cursor.fetchall()

Execute an arbitrary query and return the results.

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

Create tables if they don't exist.

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

Create a table if it doesn't exist.

:params:

table: Name of the table to create.

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

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

Returns a list of table names from the database.

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

Return a list of column names from a table.

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

Return number of items in table.

:params:

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

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

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

Add a row of values to a table.

Returns whether the addition was successful or not.

:params:

table: The table to insert values into.

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

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

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

Add multiple rows of values to a table.

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

:params:

table: The table to insert values into.

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

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

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

Return matching rows from table.

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

:params:

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

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

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

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

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

values_only: Return the results as a list of tuples.

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

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

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

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

:params:

table: The table to search.

query_string: The substring to search for in all columns.

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

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

Delete records from table.

Returns the number of deleted records.

:params:

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

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

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

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

:params:

table: The table to update rows in.

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

new_value: The new value to insert.

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

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

Returns the number of updated rows.

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

Drop table from the database.

Returns True if successful, False if not.

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

Add a new column to table.

:params:

column: Name of the column to add.

_type: The data type of the new column.

default_value: Optional default value for the column.

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

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

:params:

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

sort_key: Optional dictionary key to sort data with.

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

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

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

:params:

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

sort_key: Optional dictionary key to sort data with.

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