Package sqlite_integrated
Expand source code
import pandas as pd
import sqlite3
import os
from dataclasses import dataclass
@dataclass
class ForeignKey:
"""Class representing an sql foreign key"""
table: str
"""The table the foreign key points to"""
to_col: str
"""Column the foreign key points to"""
from_col: str = None
"""Column in current table, containing the key value"""
id: int = None
"""The foreign key id"""
seq: int = None
"""The foreign key sequence attribute"""
on_update: str = None
"""The action the column will do if the data the key is pointing to changes. (Provide sql action)."""
on_delete: str = None
"""The action the column will do if the data the key is pointing to changes. (Provide sql action)."""
match: str = None
def to_sql(self):
rep = f"FOREIGN KEY ({self.from_col}) REFERENCES {self.table} ({self.to_col})"
if self.on_update:
rep += f" ON UPDATE {self.on_update}"
if self.on_delete:
rep += f" ON DELETE {self.on_delete}"
return(rep)
# TODO check that datatype is integer if col is a primary key
@dataclass
class Column:
"""Class representing en sql column."""
def __init__(self, name: str, type: str, not_null: bool = None, default_value: any = None, primary_key: bool = False, col_id: int = None, foreign_key: ForeignKey = None) -> None:
self.name = name
"""Name of the column."""
self.type = type
"""Type of the data in the column."""
self.not_null = not_null
"""Sql NOT NULL constraint."""
self.default_value = default_value
"""Sql DEFAULT. Default value for the column."""
self.primary_key = primary_key
"""Sql PRIMARY KEY. Automatic column that ensures that every entry has a unique."""
self.col_id = col_id
"""Id if the column in the table."""
if foreign_key:
foreign_key.from_col = name
self.foreign_key = foreign_key
"""ForeignKey object, that representing an sql foreign key."""
def __repr__(self) -> str:
attrs = []
if self.col_id:
attrs.append(str(self.col_id))
attrs.append(self.name)
attrs.append(self.type)
if self.not_null:
attrs.append("NOT NULL")
if self.default_value:
attrs.append(f"DEFAULT: {self.default_value}")
if self.primary_key:
attrs.append("PRIMARY KEY")
if self.foreign_key:
attrs.append(self.foreign_key.to_sql())
return(f"Column({', '.join(attrs)})")
class DatabaseEntry(dict):
"""
A python dictionary that keeps track of the table it belongs to. This class is not meant to be created manually.
Parameters
----------
entry_dict : dict
A dictionary containing all the information. This information can be accesed just like any other python dict with `my_entry[my_key]`.
table : str
The name of the table the entry is a part of
"""
def __init__(self, entry_dict: dict, table: str):
self.table = table
self.update(entry_dict)
@classmethod
def from_raw_entry(cls, raw_entry: tuple, table_fields: list, table_name: str):
"""
Alternative constructor for converting a raw entry to a DatabaseEntry.
Parameters
----------
raw_entry : tuple
A tuple with the data for the entry. Ex: `(2, "Tom", "Builder", 33)`
table_fields : list
A list of column names for the data. Ex: `["id", "FirstName", "LastName", "Age"]`
table_name : str
The name of the table (in the database) that the data belongs to. Ex: "people"
"""
entry_dict = {}
if isinstance(table_fields, str):
table_fields = string_to_list(table_fields)
elif not isinstance(table_fields, list):
raise ValueError(f"table_fields must be either `list` or `str`. Got: {table_fields}")
if len(raw_entry) != len(table_fields):
raise DatabaseError(f"There must be as many names for table fields as there are fields in the entry: len({raw_entry}) != len({table_fields}) => {len(raw_entry)} != {len(table_fields)}")
for n, field in enumerate(table_fields):
entry_dict[field] = raw_entry[n]
entry = DatabaseEntry(entry_dict, table_name)
return(entry)
def __repr__(self) -> str:
"""Represent a Database entry"""
return f"DatabaseEntry(table: {self.table}, data: {super().__repr__()})"
def raw_table_to_table(raw_table: list, fields: list, table_name: str) -> list[DatabaseEntry]:
"""
Convert a raw table (list of tuples) to a table (list of dictionaries).
Parameters
----------
raw_table : list
A list of tuples with the data for the entries.
fields : list
A list of column names for the data. Ex: `["id", "FirstName", "LastName", "Age"]`
table_name: str
The name of the table (in the database) that the data belongs to. Ex: "people".
"""
table = []
if len(raw_table) == 0:
return([])
if len(raw_table[0]) != len(fields):
raise DatabaseError(f"There must be one raw column per field. {raw_table[0] = }, {fields = }")
for raw_entry in raw_table:
entry = {}
for n, field in enumerate(fields):
entry[field] = raw_entry[n]
table.append(DatabaseEntry(entry, table_name))
return(table)
def string_to_list(string: str) -> list:
"""Takes a string with comma seperated values, returns a list of the values. (spaces are ignored)"""
return(string.replace(" ", "").split(","))
def value_to_sql_value(value) -> str:
"""Converts python values to sql values. Basically just puts quotes around strings and not ints or floats. Also converts None to null"""
if isinstance(value, str):
return(value.__repr__())
elif isinstance(value, int):
return(str(value))
elif isinstance(value, float):
return(str(value))
elif value == None:
return("null")
elif isinstance(value, list):
try:
return(",".join(value))
except TypeError:
raise TypeError("Cannot convert list on non-string objects to sql")
else:
raise TypeError(f"Cannot convert value of type {type(value)} to sql")
def dict_to_sql(data: dict) -> str:
"""Converts a dict into sql key value pairs. Ex: \"key1 = value1, key2 = value2...\""""
set_list = []
for field in data:
set_list.append(f"{field} = {value_to_sql_value(data[field])}")
return(", ".join(set_list))
class DatabaseError(Exception):
"""Raised when the database fails to execute command"""
class QueryError(Exception):
"""Raised when trying to create an invalid or unsupperted query"""
# TODO implement JOIN and LEFTJOIN (RIGHTJOIN?): https://www.w3schools.com/sql/sql_join.asp
class Query:
"""
A class for writing sql queries. Queries can be run on the attached database or a seperate one with the `run` method.
Parameters
----------
db : Database, optional
The attached Database. This is the default database to run queries on.
silent : bool, optional
If true: disables prints.
"""
def __init__(self, db=None, silent=False) -> None:
self._db: Database = db
"""The attached Database"""
self.sql = ""
"""The current raw sql command"""
self.history = []
"""The history of commandmethods run on this object"""
self.fields = None
"""The selected fields"""
self.table = None
"""The table the sql query is interacting with"""
self.silent = silent
"""If true: disables prints"""
def valid_prefixes(self, prefixes: list) -> None:
"""Check if a statement is valid given its prefix"""
prefix = None
if len(self.history) > 0:
prefix = self.history[-1]
if prefix in prefixes:
return(True)
raise QueryError(f"Query syntax incorrect or not supported. Prefix: \"{prefix}\" is not a part of the valid prefixes: {prefixes}")
def SELECT(self, selection="*"):
"""
Sql `SELECT` statement. Must be followed by `FROM` statement.
Parameters
----------
selection : str/list, optional
Either a python list or sql list of table names. Selects all columns if not set.
"""
self.valid_prefixes([None])
self.history.append("SELECT")
if isinstance(selection, str):
if selection == "*":
self.fields = "*"
else:
self.fields = string_to_list(selection)
self.sql += f"SELECT {selection} "
elif isinstance(selection, list):
self.fields = selection
self.sql += f"SELECT {', '.join(selection)} "
else:
raise QueryError("SELECT statement selection must be either `str` or `list`")
return(self)
def FROM(self, table_name):
"""
Sql `FROM` statement. Has to be preceded by a SELECT statement. Can be followed by `WHERE` statement.
Parameters
----------
table_name : str
Name of the table you are selecting from.
"""
self.valid_prefixes(["SELECT"])
self.table = table_name
if self._db:
table_fields = set(self._db.get_column_names(table_name)) # check if selected fields are in table
if self.fields != "*" and self._db and not set(self.fields).issubset(table_fields):
raise QueryError(f"Some selected field(s): {set(self.fields) - table_fields} are not fields/columns in the table: {table_name!r}. The table has the following fields: {table_fields}")
self.history.append("FROM")
self.sql += f"FROM {table_name} "
return(self)
def WHERE(self, col_name:str, value = ""):
"""
Sql `WHERE` statement. Can be followed by `LIKE` statement.
Parameters
----------
col_name : str
The name of the column. You can also just pass it a statement like: `"id" = 4` instead of providing a value.
value : optional
The value of the column.
"""
self.valid_prefixes(["FROM", "SET", "DELETE_FROM"])
self.history.append("WHERE")
if value != "":
if value == None:
self.sql += f"WHERE {col_name} is null"
else:
self.sql += f"WHERE {col_name} = {value_to_sql_value(value)}"
else:
self.sql += f"WHERE {col_name} "
if col_name.find("=") == -1: # expects LIKE statement
self.col = col_name.replace(" ", "")
return(self)
def LIKE(self, pattern: str):
"""
Sql LIKE statement. Has to be preceded by a WHERE statement.
Parameters
----------
pattern : str
A typical sql LIKE pattern with % and _.
"""
self.valid_prefixes(["WHERE"])
self.history.append("LIKE")
self.sql += f"LIKE {value_to_sql_value(pattern)} "
return(self)
def UPDATE(self, table_name: str):
"""
Sql UPDATE statement. Must be followed by `SET` statement.
Parameters
----------
table_name : str
Name of the table you are updating.
"""
self.valid_prefixes([None])
self.history.append("UPDATE")
if self._db:
if not self._db.is_table(table_name):
raise QueryError(f"Database has no table called {table_name!r}")
self.fields = self._db.get_column_names(table_name)
self.table = table_name
self.sql += f"UPDATE {table_name} "
return(self)
def SET(self, data: dict):
"""
Sql SET statement. Must be preceded by an UPDATE statement. Must be followed by `WHERE` statement.
Parameters
----------
data : dict
A dictionaty with key and value pairs.
"""
self.valid_prefixes(["UPDATE"])
self.history.append("SET")
data = dict(data)
if not set(data).issubset(self.fields):
raise DatabaseError(f"Data keys: {set(data)} are not a subset of table fields/columns. Table fields/columns: {set(self.fields)}")
self.sql += f"SET {dict_to_sql(data)} "
return(self)
def INSERT_INTO(self, table_name: str):
"""
Sql `INSERT INTO` statement. Must be followed by `VALUES` statement.
Parameters
----------
table_name : str
Name of the table you want to insert into.
"""
self.valid_prefixes([None])
self.history.append("INSERT_INTO")
self.table = table_name
if self._db:
self.fields = self._db.get_column_names(table_name)
self.sql += f"INSERT INTO {table_name} "
return(self)
def VALUES(self, data: dict):
"""
Sql `VALUES` statement. Must be preceded by INSERT_INTO statement.
Parameters
----------
data : dict
Dictionary with key value pairs.
"""
self.valid_prefixes(["INSERT_INTO"])
self.history.append("VALUES")
if not set(data).issubset(self.fields):
raise DatabaseError(f"Data keys: {set(data)} are not a subset of table fields/columns. Unknown keys: {set(data) - set(self.fields)}. Table fields/columns: {set(self.fields)}")
self.sql += f"({', '.join([str(v) for v in list(data)])}) VALUES ({', '.join([str(value_to_sql_value(v)) for v in data.values()])}) "
return(self)
def DELETE_FROM(self, table_name: str):
"""
Sql `DELETE FROM` statement. Must be followed by `WHERE` statement.
Parameters
----------
data : dict
Dictionary with key value pairs.
"""
self.valid_prefixes([None])
self.history.append("DELETE_FROM")
if self._db and not table_name in self._db.get_table_names():
raise QueryError(f"Can not perform DELETE FROM on a non-existing table: {table_name!r}")
self.table = table_name
self.sql = f"DELETE FROM {table_name} "
return(self)
def run(self, db=None, raw = False, silent=False) -> list[DatabaseEntry]:
"""
Execute the query in the attached database or in a seperate one. Returns the results in a table (list of DatabaseEntry) or `None` if no results.
Parameters
----------
db : Database, optional
The database to execute to query on.
raw : bool, optional
If True: returns the raw table (list of tuples) instead of the normal table.
silent : bool, optional
If True: disables all prints.
"""
if not db:
db = self._db
if not db:
raise DatabaseError("Query does not have a database to execute")
try:
db.cursor.execute(self.sql)
except sqlite3.OperationalError as e:
raise QueryError(f"\n\n{e}\n\nError while running following sql: {self.sql}")
if not db.silent and not self.silent and not silent:
print(f"Executed sql: {self.sql}")
results = db.cursor.fetchall()
if len(results) == 0:
return(None)
if raw:
return(results)
if self.fields == "*":
self.fields = db.get_column_names(self.table)
return(raw_table_to_table(results, self.fields, self.table))
def __repr__(self) -> str:
return(f"> {self.sql.strip()} <")
# TODO Create open bool for the Database
class Database:
"""
Main database class for manipulating sqlite3 databases.
Parameters
----------
path : str
Path to the database file.
new : bool, optional
A new blank database will be created where the `self.path` is pointing.
silent : bool, optional
Disables all feedback in the form of prints .
"""
def __init__(self, path: str, new = False, silent=True):
if not new and not os.path.isfile(path):
raise(DatabaseError(f"No database file at \"{path}\". If you want to create one, pass \"new=True\""))
self.path = path
"""Path to the database file."""
self.conn = sqlite3.connect(path)
"""The sqlite3 connection."""
self.cursor = self.conn.cursor()
"""The sqlite3 cursor. Use `cursor.execute(cmd)` to execute raw sql."""
# TODO
self.connected: bool = True
"""Is true if the Database is connected to a database."""
self.silent=silent
"""Disables all feedback in the form of prints."""
self.conn.execute("PRAGMA foreign_keys = ON")
# TODO respect: on_update, on_delete, match
def create_table(self, name: str, cols: list[Column]):
"""
Creates a table in the Database.
Parameters
----------
name : str
Name of the new table.
cols : list[Column]
List of columns in the new table.
"""
sql = f"CREATE TABLE {name} (\n"
foreign_keys: list[ForeignKey] = []
for col in cols:
sql += f"{col.name!r} {col.type}"
if col.primary_key:
sql += " PRIMARY KEY"
if col.not_null:
sql += " NOT NULL"
if col.default_value:
sql += f" DEFAULT {col.default_value!r}"
if col.foreign_key:
foreign_keys.append(col.foreign_key)
sql += ",\n"
for key in foreign_keys:
sql += f"FOREIGN KEY({key.from_col}) REFERENCES {key.table}({key.to_col}),\n"
if key.on_update:
sql = sql[:-2] + f"\nON UPDATE {key.on_update},\n"
if key.on_delete:
sql = sql[:-2] + f"\nON DELETE {key.on_delete},\n"
sql = sql[:-2] + "\n)" # remove last ",\n"
self.cursor.execute(sql)
def rename_table(self, current_name: str, new_name: str):
"""
Renames a table in the database.
Parameters
----------
current_name : str
Current name of a table.
new_name : str
New name of the table.
"""
self.cursor.execute(f"ALTER TABLE {current_name} RENAME TO {new_name}")
def delete_table(self, table_name: str) -> None:
"""
Deletes a table in the database.
Parameters
----------
table_name : Name of the table.
"""
self.cursor.execute(f"DROP TABLE {table_name}")
def add_column(self, table_name: str, col: Column):
"""
Add column to a table in the database.
Parameters
----------
table_name : str
Name of the table.
col : Column
The column to add to table.
"""
# Check that the table exists
if not self.is_table(table_name):
raise DatabaseError(f"Database contains no table with the name {table_name!r}")
sql = f"ALTER TABLE {table_name} ADD COLUMN {col.name} {col.type}"
if col.primary_key:
sql += " PRIMARY KEY"
if col.not_null:
sql += " NOT NULL"
if col.default_value:
sql += f" DEFAULT {col.default_value}"
if col.foreign_key:
raise DatabaseError(f"Sqlite3 and therefore sqlite-integrated, does not support adding columns with foreign key constraings to existing tables. They have to be declared with the creation of the table.")
self.cursor.execute(sql)
def rename_column(self, table_name: str, current_column_name: str, new_column_name: str):
"""
Renames a column in the database.
Parameters
----------
table_name : str
Name of the table.
current_column_name : str
Current name of a column.
new_column_name : str
New name of the column.
"""
# Check that the table exists
if not self.is_table(table_name):
raise DatabaseError(f"Database contains no table with the name {table_name!r}")
self.cursor.execute(f"ALTER TABLE {table_name} RENAME COLUMN {current_column_name} TO {new_column_name}")
def delete_column(self, table_name: str, col):
"""
Deletes a column in a table.
Parameters
----------
table_name : str
Name of the table the column is in.
col : str/Column
Column, or column name, of the column that should be deleted.
"""
# Check that the table exists
if not self.is_table(table_name):
raise DatabaseError(f"Database contains no table with the name {table_name!r}")
if col is Column:
col = col.name
self.cursor.execute(f"ALTER TABLE {table_name} DROP COLUMN {col}")
def get_table_names(self) -> list:
"""Returns the names of all tables in the database."""
res = self.conn.execute("SELECT name FROM sqlite_master WHERE type='table';")
names = []
for name in res:
names.append(name[0])
return(names)
def is_table(self, table_name: str) -> bool:
"""
Check if database has a table with a certain name.
Parameters
----------
table_name : str
Name to check.
"""
if table_name in self.get_table_names():
return True
return False
def get_table_raw(self, name: str, get_only = None) -> list:
"""
Returns all entries in a table as a list of tuples.
Parameters
----------
name : str
Name of the table.
get_only : list, optional
Can be set to a list of column/field names, to only retrieve those columns/fields.
"""
selected = "*"
if get_only:
if isinstance(get_only, list):
fields = self.get_column_names(name)
for field in get_only:
if not field in fields:
raise DatabaseError(f"Table \"{name}\" contains no field/column with the name: \"{field}\". Available fields are: {fields}")
selected = ','.join(get_only)
else:
raise ValueError(f"get_only can either be `None` or `list`. Got: {get_only}")
self.cursor.execute(f"SELECT {selected} FROM {name}")
return(self.cursor.fetchall())
def get_table(self, name: str, get_only=None) -> list:
"""
Returns all entries in a table as a table (list of DatabaseEntry). This function loops over all entries in the table, so it is not the best in very big databases.
Parameters
----------
name : str
Name of the table.
get_only : list/None, optional
Can be set to a list of column/field names, to only retrieve those columns/fields.
"""
raw_table = self.get_table_raw(name, get_only)
return(raw_table_to_table(raw_table, self.get_column_names(name), name))
def get_table_cols(self, name: str) -> list[Column]:
"""
Returns a list of Column objects, that contain information about the table columns.
Parameters
----------
name : str
Name of the table.
"""
self.cursor.execute(f"PRAGMA table_info({name});")
cols_raw_info = self.cursor.fetchall()
cols = []
for col_raw_info in cols_raw_info:
is_primary = False
if col_raw_info[5] == 1:
is_primary = True
not_null = False
if col_raw_info[3] == 1:
not_null = True
cols.append(Column(col_raw_info[1], col_raw_info[2], not_null, col_raw_info[4], is_primary, col_id=col_raw_info[0]))
# Add foreign keys to cols
self.cursor.execute(f"PRAGMA foreign_key_list({name});")
foreign_key_list = self.cursor.fetchall()
if len(foreign_key_list) > 0:
for raw_foreign_key in foreign_key_list:
foreign_key = ForeignKey(
raw_foreign_key[2],
raw_foreign_key[4],
id=raw_foreign_key[0],
seq=raw_foreign_key[1],
from_col=raw_foreign_key[3],
on_update=raw_foreign_key[5],
on_delete=raw_foreign_key[6],
match=raw_foreign_key[7]
)
for n, col in enumerate(cols):
if col.name == foreign_key.from_col:
cols[n].foreign_key = foreign_key
break
return(cols)
def get_table_id_field(self, table: str, do_error=False) -> str:
"""
Takes a table and returns the name of the field/column marked as a `PRIMARY KEY`. (This function assumes that there is only ONE field marked as a `PRIMARY KEY`).
Parameters
----------
table : str
Name of the table.
do_error : bool, optional
If True: Raises error if the table does not contain a field marked as `PRIMARY KEY`.
"""
cols = self.get_table_cols(table)
for col in cols:
if col.primary_key == True: # col_info[5] is 1 if field is a primary key. Otherwise it is 0.
return col.name # col_info[1] is the name of the column
if do_error:
raise DatabaseError(f"The table `{table}` has no id_field (column defined as a `PRIMARY KEY`)")
return(None)
def table_overview(self, name: str, max_len:int = 40, get_only = None) -> None:
"""
Prints a pretty table (with a name).
Parameters
----------
name : str
Name of the table.
max_len : int, optional
The max number of rows shown.
get_only : list, optional
If given a list of column/field names: only shows those.
"""
text = "" # the output text
raw_table = self.get_table_raw(name, get_only=get_only)
if get_only:
fields = get_only
else:
fields = self.get_column_names(name)
cols = len(fields)
longest_words = [0] * cols
words_table = raw_table + [fields]
for col in range(cols):
for entry in words_table:
if len(str(entry[col])) > longest_words[col]:
longest_words[col] = len(str(entry[col]))
seperator = " ║ "
def formatRow(row, longest_words):
formatted_list = []
for i, string in enumerate(row):
string = str(string)
formatted_list.append(string + " " * (longest_words[i] - len(string)))
return(seperator.join(formatted_list))
text += formatRow(fields, longest_words) + "\n"
underline = "═" * (sum(longest_words) + len(seperator))
# This block is for placing the intersections
offset = 0
for n in longest_words[:-1]: # we dont create the an intersection after the last column
offset += n
underline = underline[:offset +1] + "╬" + underline[offset:]
offset += len(seperator)
text += underline + "\n"
if len(raw_table) >= max_len:
for row in raw_table[:max_len-5]:
text += formatRow(row, longest_words) + "\n"
text += " .\n .\n .\n"
for row in raw_table[-5:]:
text += formatRow(row, longest_words) + "\n"
else:
for row in raw_table:
text += formatRow(row, longest_words) + "\n"
print(text)
def overview(self, more=False) -> None:
"""
Prints an overview of all the tables in the database with their fields.
Parameters
----------
more : optional
If true: Prints more information on the columns in each table.
"""
table_names = self.get_table_names()
# if there are no tables in database
if len(table_names) == 0:
print(f"There are no tables in sqlite database at \"{self.path}\".")
return(None)
text = "Tables\n"
for table_name in table_names:
text += "\t" + table_name + "\n"
for col in self.get_table_cols(table_name):
text += f"\t\t{col.name}"
if more:
text += f"\t\t[{col}]"
text += "\n"
print(text)
def get_column_names(self, table_name: str) -> list[str]:
"""
Returns the field/column names for a given table.
Parameters
----------
table_name : str
Name of the table.
"""
if not self.is_table(table_name):
raise DatabaseError(f"Can not get column names of non-existing table {table_name!r}.")
names = []
for col in self.get_table_cols(table_name):
names.append(col.name)
return(names)
def is_column(self, table_name: str, col_name: str) -> bool:
"""
Returns True if the given column name exists in the given table. Else returns False.
Parameters
----------
table_name : str
Name of a table.
col_name : str
Name of a column that may be in the table.
"""
if col_name in self.get_column_names(table_name):
return(True)
return(False)
def fill_null(self, entry: DatabaseEntry) -> DatabaseEntry:
"""
Fills out any unpopulated fields in a DatabaseEntry (fields that exist in the database table but not in the entry) and returns it.
Parameters
----------
entry : DatabaseEntry
The DatabaseEntry.
"""
t_fields = self.get_column_names(entry.table)
e_fields = list(entry)
for f in e_fields:
t_fields.remove(f)
for null_field in t_fields:
entry[null_field] = None
return(entry)
def get_entry_by_id(self, table, ID) -> DatabaseEntry:
"""
Get table entry by id.
Parameters
----------
table : str
Name of the table.
ID :
The entry id.
"""
id_field = self.get_table_id_field(table, do_error=True)
if not self.is_table(table):
raise DatabaseError(f"Database contains no table with the name: \"{table}\". These are the available tables: {self.get_table_names()}")
sql = f"SELECT * FROM {table} WHERE {id_field} = {ID}"
self.cursor.execute(sql)
answer = self.cursor.fetchall()
# some checks
if len(answer) != 1:
if len(answer) > 1:
raise DatabaseError(f"There are more than one entry in table \"{table}\" with an id field \"{id_field}\" with the value \"{id}\": {answer}")
elif len(answer) == 0:
raise DatabaseError(f"There is no entry in table \"{table}\" with an id_field \"{id_field}\" with a value of {ID}")
else:
raise DatabaseError("Something went very wrong, please contact the package author") # this will never be run... i think
return(DatabaseEntry.from_raw_entry(answer[0], self.get_column_names(table), table))
def add_entry(self, entry, table = None, fill_null=False, silent=False) -> None:
"""
Add an entry to the database by passing a DatabaseEntry, or with a dictionary and specifying a table name. The entry must have values for all fields in the table. You can pass `fill_null=True` to fill remaining fields with None/null. Use `silent=True` to suppress warnings and messages.
Parameters
----------
entry : DatabaseEntry/dict
The entry.
table : str, optional
Name of the table the entry belongs to. **Needed if adding an entry with a dictionary**.
fill_null : bool, optional
Fill in unpopulated fields with null values.
silent : bool, optional
If True: disables prints.
"""
if type(entry) == dict:
if not table:
raise DatabaseError(f"Please provide the table that the data should be inserted in.")
entry = DatabaseEntry(entry, table)
if not self.is_table(entry.table):
raise DatabaseError(f"Database has no table with the name \"{self.table}\". Possible tablenames are: {self.get_table_names()}")
table_fields = self.get_column_names(entry.table)
id_field = self.get_table_id_field(entry.table)
if id_field:
entry[id_field] = None
if fill_null:
entry = self.fill_null(entry)
if set(entry) != set(table_fields):
raise DatabaseError(f"entry fields are not the same as the table fields: {set(entry)} != {set(table_fields)}")
self.INSERT_INTO(entry.table).VALUES(entry).run(silent=True)
if not silent and not self.silent:
print(f"added entry to table \"{entry.table}\": {entry}")
def update_entry(self, entry: dict, table=None, part=False, fill_null=False, silent=False) -> None:
"""
Update entry in database with a DatabaseEntry, or with a dictionary + the name of the table you want to update.
Parameters
----------
entry : DatabaseEntry/dict
DatabaseEntry or dictionary, if dictionary you also need to provide table and id_field.
table : str, optional
The table name. **Needed if updating an entry with a dictionary**.
part : bool, optional
If True: Only updates the provided fields.
fill_null : bool, optional
Fill in unpopulated fields with null values.
silent : bool, optional
If True: disables prints.
"""
if not isinstance(entry, DatabaseEntry): # the input is a dict
if not table:
raise DatabaseError(f"Please provide a table when updating an entry with a python dictionary")
entry = DatabaseEntry(entry, table)
id_field = self.get_table_id_field(entry.table)
if not self.is_table(entry.table):
raise DatabaseError(f"Database has no table with the name \"{entry.table}\". Possible tablenames are: {self.get_table_names()}")
if fill_null:
entry = self.fill_null(entry)
# check that entry fields and table fields match
table_fields = self.get_column_names(entry.table)
if set(table_fields) != set(entry):
if not (part and set(entry).issubset(set(table_fields))):
raise DatabaseError(f"Table fields do not match entry fields: {table_fields} != {list(entry)}. Pass `part = True` or `fill_null = True` if entry are a subset of the table fields")
self.UPDATE(entry.table).SET(entry).WHERE(id_field, entry[id_field]).run()
if not silent and not self.silent:
print(f"updated entry in table \"{entry.table}\": {entry}")
def delete_entry(self, entry: DatabaseEntry):
"""
Delete an entry from the database.
Parameters
----------
entry : DatabaseEntry
The entry that is to be deleted.
"""
id_field = self.get_table_id_field(entry.table)
self.DELETE_FROM(entry.table).WHERE(id_field, entry[id_field]).run()
def delete_entry_by_id(self, table: str, id: int):
"""
Deletes an entry with a certain id. (Note: the table must have a primary key column, as that is what is meant by id. It is assumed that there is only one primary key column in the table.}
Parameters
----------
table : str
The table to delete the entry from.
id : int
"""
id_field = self.get_table_id_field(table)
self.DELETE_FROM(table).WHERE(id_field, id).run()
def save(self) -> None:
"""Writes any changes to the database file"""
self.conn.commit()
def close(self) -> None:
"""Saves and closes the database. If you want to explicitly close without saving use: `self.conn.close()`"""
self.conn.commit()
self.conn.close()
def reconnect(self) -> None:
"""Reopen database after closing it"""
self.conn = sqlite3.connect(self.path)
self.cursor = self.conn.cursor()
def delete_table(self, table_name) -> None:
"""
Takes a table name and deletes the table from the database.
Parameters
----------
table_name : str
Name of the table.
"""
self.cursor.execute(f"DROP TABLE {table_name};")
def table_to_dataframe(self, table) -> pd.DataFrame:
"""
Converts a table to a pandas.Dataframe.
Parameters
----------
table : str
Name of the table.
"""
cols = {}
fields = self.get_column_names(table)
for f in fields:
cols[f] = []
for raw_entry in self.get_table_raw(table):
for n, field in enumerate(fields):
cols[field].append(raw_entry[n])
return(pd.DataFrame(cols))
def export_to_csv(self, out_dir: str, tables: list = None, sep: str = "\t") -> None:
"""
Export all or some tables in the database to csv files
Parameters
----------
out_dir : str
Path to the output directory.
tables : list[str]/None, optional
Can be set to only export certain tables.
sep : str, optional
Seperator to use when writing csv-file.
"""
if not os.path.isdir(out_dir):
raise NotADirectoryError(f"{out_dir!r} is not a directory")
if not tables:
tables = self.get_table_names()
for table_name in tables:
df = self.table_to_dataframe(table_name)
df.to_csv(f"{out_dir}/{table_name}.csv", index=False, sep=sep)
def SELECT(self, pattern="*") -> Query:
"""
Start sql SELECT query from the database. Returns a Query to build from.
Parameters
----------
pattern : str, optional
Either a python list or sql list of table names.
"""
return(Query(db=self, silent=True).SELECT(pattern))
def UPDATE(self, table_name) -> Query:
"""
Start sql UPDATE query from the database. Returns a Query to build from.
Parameters
----------
table_name : str
Name of the table.
"""
return(Query(db=self, silent=True).UPDATE(table_name))
def INSERT_INTO(self, table_name) -> Query:
"""
Start sql INSERT INTO query from the database. Returns a Query to build from.
Parameters
----------
table_name : str
Name of the table to insert into.
"""
return(Query(db=self, silent=True).INSERT_INTO(table_name))
def DELETE_FROM(self, table_name: str) -> Query:
"""
Start sql DELETE FROM query from the database. Returns a Query to build from.
Parameters
----------
table_name : str
Name of the table to delete from.
"""
return(Query(db=self, silent=True).DELETE_FROM(table_name))
def __eq__(self, other: object) -> bool:
tables = self.get_table_names()
if tables != other.get_table_names():
return(False)
for table in tables:
if self.get_table_raw(table) != other.get_table_raw(table):
return(False)
elif self.get_table_cols(table) != other.get_table_cols(table):
return(False)
return(True)
Functions
def dict_to_sql(data: dict) ‑> str
-
Converts a dict into sql key value pairs. Ex: "key1 = value1, key2 = value2…"
Expand source code
def dict_to_sql(data: dict) -> str: """Converts a dict into sql key value pairs. Ex: \"key1 = value1, key2 = value2...\"""" set_list = [] for field in data: set_list.append(f"{field} = {value_to_sql_value(data[field])}") return(", ".join(set_list))
def raw_table_to_table(raw_table: list, fields: list, table_name: str) ‑> list[DatabaseEntry]
-
Convert a raw table (list of tuples) to a table (list of dictionaries).
Parameters
raw_table
:list
- A list of tuples with the data for the entries.
fields
:list
- A list of column names for the data. Ex:
["id", "FirstName", "LastName", "Age"]
table_name
:str
- The name of the table (in the database) that the data belongs to. Ex: "people".
Expand source code
def raw_table_to_table(raw_table: list, fields: list, table_name: str) -> list[DatabaseEntry]: """ Convert a raw table (list of tuples) to a table (list of dictionaries). Parameters ---------- raw_table : list A list of tuples with the data for the entries. fields : list A list of column names for the data. Ex: `["id", "FirstName", "LastName", "Age"]` table_name: str The name of the table (in the database) that the data belongs to. Ex: "people". """ table = [] if len(raw_table) == 0: return([]) if len(raw_table[0]) != len(fields): raise DatabaseError(f"There must be one raw column per field. {raw_table[0] = }, {fields = }") for raw_entry in raw_table: entry = {} for n, field in enumerate(fields): entry[field] = raw_entry[n] table.append(DatabaseEntry(entry, table_name)) return(table)
def string_to_list(string: str) ‑> list
-
Takes a string with comma seperated values, returns a list of the values. (spaces are ignored)
Expand source code
def string_to_list(string: str) -> list: """Takes a string with comma seperated values, returns a list of the values. (spaces are ignored)""" return(string.replace(" ", "").split(","))
def value_to_sql_value(value) ‑> str
-
Converts python values to sql values. Basically just puts quotes around strings and not ints or floats. Also converts None to null
Expand source code
def value_to_sql_value(value) -> str: """Converts python values to sql values. Basically just puts quotes around strings and not ints or floats. Also converts None to null""" if isinstance(value, str): return(value.__repr__()) elif isinstance(value, int): return(str(value)) elif isinstance(value, float): return(str(value)) elif value == None: return("null") elif isinstance(value, list): try: return(",".join(value)) except TypeError: raise TypeError("Cannot convert list on non-string objects to sql") else: raise TypeError(f"Cannot convert value of type {type(value)} to sql")
Classes
class Column (name: str, type: str, not_null: bool = None, default_value:
= None, primary_key: bool = False, col_id: int = None, foreign_key: ForeignKey = None) -
Class representing en sql column.
Expand source code
@dataclass class Column: """Class representing en sql column.""" def __init__(self, name: str, type: str, not_null: bool = None, default_value: any = None, primary_key: bool = False, col_id: int = None, foreign_key: ForeignKey = None) -> None: self.name = name """Name of the column.""" self.type = type """Type of the data in the column.""" self.not_null = not_null """Sql NOT NULL constraint.""" self.default_value = default_value """Sql DEFAULT. Default value for the column.""" self.primary_key = primary_key """Sql PRIMARY KEY. Automatic column that ensures that every entry has a unique.""" self.col_id = col_id """Id if the column in the table.""" if foreign_key: foreign_key.from_col = name self.foreign_key = foreign_key """ForeignKey object, that representing an sql foreign key.""" def __repr__(self) -> str: attrs = [] if self.col_id: attrs.append(str(self.col_id)) attrs.append(self.name) attrs.append(self.type) if self.not_null: attrs.append("NOT NULL") if self.default_value: attrs.append(f"DEFAULT: {self.default_value}") if self.primary_key: attrs.append("PRIMARY KEY") if self.foreign_key: attrs.append(self.foreign_key.to_sql()) return(f"Column({', '.join(attrs)})")
Instance variables
var col_id
-
Id if the column in the table.
var default_value
-
Sql DEFAULT. Default value for the column.
var foreign_key
-
ForeignKey object, that representing an sql foreign key.
var name
-
Name of the column.
var not_null
-
Sql NOT NULL constraint.
var primary_key
-
Sql PRIMARY KEY. Automatic column that ensures that every entry has a unique.
var type
-
Type of the data in the column.
class Database (path: str, new=False, silent=True)
-
Main database class for manipulating sqlite3 databases.
Parameters
path
:str
- Path to the database file.
new
:bool
, optional- A new blank database will be created where the
self.path
is pointing. silent
:bool
, optional- Disables all feedback in the form of prints .
Expand source code
class Database: """ Main database class for manipulating sqlite3 databases. Parameters ---------- path : str Path to the database file. new : bool, optional A new blank database will be created where the `self.path` is pointing. silent : bool, optional Disables all feedback in the form of prints . """ def __init__(self, path: str, new = False, silent=True): if not new and not os.path.isfile(path): raise(DatabaseError(f"No database file at \"{path}\". If you want to create one, pass \"new=True\"")) self.path = path """Path to the database file.""" self.conn = sqlite3.connect(path) """The sqlite3 connection.""" self.cursor = self.conn.cursor() """The sqlite3 cursor. Use `cursor.execute(cmd)` to execute raw sql.""" # TODO self.connected: bool = True """Is true if the Database is connected to a database.""" self.silent=silent """Disables all feedback in the form of prints.""" self.conn.execute("PRAGMA foreign_keys = ON") # TODO respect: on_update, on_delete, match def create_table(self, name: str, cols: list[Column]): """ Creates a table in the Database. Parameters ---------- name : str Name of the new table. cols : list[Column] List of columns in the new table. """ sql = f"CREATE TABLE {name} (\n" foreign_keys: list[ForeignKey] = [] for col in cols: sql += f"{col.name!r} {col.type}" if col.primary_key: sql += " PRIMARY KEY" if col.not_null: sql += " NOT NULL" if col.default_value: sql += f" DEFAULT {col.default_value!r}" if col.foreign_key: foreign_keys.append(col.foreign_key) sql += ",\n" for key in foreign_keys: sql += f"FOREIGN KEY({key.from_col}) REFERENCES {key.table}({key.to_col}),\n" if key.on_update: sql = sql[:-2] + f"\nON UPDATE {key.on_update},\n" if key.on_delete: sql = sql[:-2] + f"\nON DELETE {key.on_delete},\n" sql = sql[:-2] + "\n)" # remove last ",\n" self.cursor.execute(sql) def rename_table(self, current_name: str, new_name: str): """ Renames a table in the database. Parameters ---------- current_name : str Current name of a table. new_name : str New name of the table. """ self.cursor.execute(f"ALTER TABLE {current_name} RENAME TO {new_name}") def delete_table(self, table_name: str) -> None: """ Deletes a table in the database. Parameters ---------- table_name : Name of the table. """ self.cursor.execute(f"DROP TABLE {table_name}") def add_column(self, table_name: str, col: Column): """ Add column to a table in the database. Parameters ---------- table_name : str Name of the table. col : Column The column to add to table. """ # Check that the table exists if not self.is_table(table_name): raise DatabaseError(f"Database contains no table with the name {table_name!r}") sql = f"ALTER TABLE {table_name} ADD COLUMN {col.name} {col.type}" if col.primary_key: sql += " PRIMARY KEY" if col.not_null: sql += " NOT NULL" if col.default_value: sql += f" DEFAULT {col.default_value}" if col.foreign_key: raise DatabaseError(f"Sqlite3 and therefore sqlite-integrated, does not support adding columns with foreign key constraings to existing tables. They have to be declared with the creation of the table.") self.cursor.execute(sql) def rename_column(self, table_name: str, current_column_name: str, new_column_name: str): """ Renames a column in the database. Parameters ---------- table_name : str Name of the table. current_column_name : str Current name of a column. new_column_name : str New name of the column. """ # Check that the table exists if not self.is_table(table_name): raise DatabaseError(f"Database contains no table with the name {table_name!r}") self.cursor.execute(f"ALTER TABLE {table_name} RENAME COLUMN {current_column_name} TO {new_column_name}") def delete_column(self, table_name: str, col): """ Deletes a column in a table. Parameters ---------- table_name : str Name of the table the column is in. col : str/Column Column, or column name, of the column that should be deleted. """ # Check that the table exists if not self.is_table(table_name): raise DatabaseError(f"Database contains no table with the name {table_name!r}") if col is Column: col = col.name self.cursor.execute(f"ALTER TABLE {table_name} DROP COLUMN {col}") def get_table_names(self) -> list: """Returns the names of all tables in the database.""" res = self.conn.execute("SELECT name FROM sqlite_master WHERE type='table';") names = [] for name in res: names.append(name[0]) return(names) def is_table(self, table_name: str) -> bool: """ Check if database has a table with a certain name. Parameters ---------- table_name : str Name to check. """ if table_name in self.get_table_names(): return True return False def get_table_raw(self, name: str, get_only = None) -> list: """ Returns all entries in a table as a list of tuples. Parameters ---------- name : str Name of the table. get_only : list, optional Can be set to a list of column/field names, to only retrieve those columns/fields. """ selected = "*" if get_only: if isinstance(get_only, list): fields = self.get_column_names(name) for field in get_only: if not field in fields: raise DatabaseError(f"Table \"{name}\" contains no field/column with the name: \"{field}\". Available fields are: {fields}") selected = ','.join(get_only) else: raise ValueError(f"get_only can either be `None` or `list`. Got: {get_only}") self.cursor.execute(f"SELECT {selected} FROM {name}") return(self.cursor.fetchall()) def get_table(self, name: str, get_only=None) -> list: """ Returns all entries in a table as a table (list of DatabaseEntry). This function loops over all entries in the table, so it is not the best in very big databases. Parameters ---------- name : str Name of the table. get_only : list/None, optional Can be set to a list of column/field names, to only retrieve those columns/fields. """ raw_table = self.get_table_raw(name, get_only) return(raw_table_to_table(raw_table, self.get_column_names(name), name)) def get_table_cols(self, name: str) -> list[Column]: """ Returns a list of Column objects, that contain information about the table columns. Parameters ---------- name : str Name of the table. """ self.cursor.execute(f"PRAGMA table_info({name});") cols_raw_info = self.cursor.fetchall() cols = [] for col_raw_info in cols_raw_info: is_primary = False if col_raw_info[5] == 1: is_primary = True not_null = False if col_raw_info[3] == 1: not_null = True cols.append(Column(col_raw_info[1], col_raw_info[2], not_null, col_raw_info[4], is_primary, col_id=col_raw_info[0])) # Add foreign keys to cols self.cursor.execute(f"PRAGMA foreign_key_list({name});") foreign_key_list = self.cursor.fetchall() if len(foreign_key_list) > 0: for raw_foreign_key in foreign_key_list: foreign_key = ForeignKey( raw_foreign_key[2], raw_foreign_key[4], id=raw_foreign_key[0], seq=raw_foreign_key[1], from_col=raw_foreign_key[3], on_update=raw_foreign_key[5], on_delete=raw_foreign_key[6], match=raw_foreign_key[7] ) for n, col in enumerate(cols): if col.name == foreign_key.from_col: cols[n].foreign_key = foreign_key break return(cols) def get_table_id_field(self, table: str, do_error=False) -> str: """ Takes a table and returns the name of the field/column marked as a `PRIMARY KEY`. (This function assumes that there is only ONE field marked as a `PRIMARY KEY`). Parameters ---------- table : str Name of the table. do_error : bool, optional If True: Raises error if the table does not contain a field marked as `PRIMARY KEY`. """ cols = self.get_table_cols(table) for col in cols: if col.primary_key == True: # col_info[5] is 1 if field is a primary key. Otherwise it is 0. return col.name # col_info[1] is the name of the column if do_error: raise DatabaseError(f"The table `{table}` has no id_field (column defined as a `PRIMARY KEY`)") return(None) def table_overview(self, name: str, max_len:int = 40, get_only = None) -> None: """ Prints a pretty table (with a name). Parameters ---------- name : str Name of the table. max_len : int, optional The max number of rows shown. get_only : list, optional If given a list of column/field names: only shows those. """ text = "" # the output text raw_table = self.get_table_raw(name, get_only=get_only) if get_only: fields = get_only else: fields = self.get_column_names(name) cols = len(fields) longest_words = [0] * cols words_table = raw_table + [fields] for col in range(cols): for entry in words_table: if len(str(entry[col])) > longest_words[col]: longest_words[col] = len(str(entry[col])) seperator = " ║ " def formatRow(row, longest_words): formatted_list = [] for i, string in enumerate(row): string = str(string) formatted_list.append(string + " " * (longest_words[i] - len(string))) return(seperator.join(formatted_list)) text += formatRow(fields, longest_words) + "\n" underline = "═" * (sum(longest_words) + len(seperator)) # This block is for placing the intersections offset = 0 for n in longest_words[:-1]: # we dont create the an intersection after the last column offset += n underline = underline[:offset +1] + "╬" + underline[offset:] offset += len(seperator) text += underline + "\n" if len(raw_table) >= max_len: for row in raw_table[:max_len-5]: text += formatRow(row, longest_words) + "\n" text += " .\n .\n .\n" for row in raw_table[-5:]: text += formatRow(row, longest_words) + "\n" else: for row in raw_table: text += formatRow(row, longest_words) + "\n" print(text) def overview(self, more=False) -> None: """ Prints an overview of all the tables in the database with their fields. Parameters ---------- more : optional If true: Prints more information on the columns in each table. """ table_names = self.get_table_names() # if there are no tables in database if len(table_names) == 0: print(f"There are no tables in sqlite database at \"{self.path}\".") return(None) text = "Tables\n" for table_name in table_names: text += "\t" + table_name + "\n" for col in self.get_table_cols(table_name): text += f"\t\t{col.name}" if more: text += f"\t\t[{col}]" text += "\n" print(text) def get_column_names(self, table_name: str) -> list[str]: """ Returns the field/column names for a given table. Parameters ---------- table_name : str Name of the table. """ if not self.is_table(table_name): raise DatabaseError(f"Can not get column names of non-existing table {table_name!r}.") names = [] for col in self.get_table_cols(table_name): names.append(col.name) return(names) def is_column(self, table_name: str, col_name: str) -> bool: """ Returns True if the given column name exists in the given table. Else returns False. Parameters ---------- table_name : str Name of a table. col_name : str Name of a column that may be in the table. """ if col_name in self.get_column_names(table_name): return(True) return(False) def fill_null(self, entry: DatabaseEntry) -> DatabaseEntry: """ Fills out any unpopulated fields in a DatabaseEntry (fields that exist in the database table but not in the entry) and returns it. Parameters ---------- entry : DatabaseEntry The DatabaseEntry. """ t_fields = self.get_column_names(entry.table) e_fields = list(entry) for f in e_fields: t_fields.remove(f) for null_field in t_fields: entry[null_field] = None return(entry) def get_entry_by_id(self, table, ID) -> DatabaseEntry: """ Get table entry by id. Parameters ---------- table : str Name of the table. ID : The entry id. """ id_field = self.get_table_id_field(table, do_error=True) if not self.is_table(table): raise DatabaseError(f"Database contains no table with the name: \"{table}\". These are the available tables: {self.get_table_names()}") sql = f"SELECT * FROM {table} WHERE {id_field} = {ID}" self.cursor.execute(sql) answer = self.cursor.fetchall() # some checks if len(answer) != 1: if len(answer) > 1: raise DatabaseError(f"There are more than one entry in table \"{table}\" with an id field \"{id_field}\" with the value \"{id}\": {answer}") elif len(answer) == 0: raise DatabaseError(f"There is no entry in table \"{table}\" with an id_field \"{id_field}\" with a value of {ID}") else: raise DatabaseError("Something went very wrong, please contact the package author") # this will never be run... i think return(DatabaseEntry.from_raw_entry(answer[0], self.get_column_names(table), table)) def add_entry(self, entry, table = None, fill_null=False, silent=False) -> None: """ Add an entry to the database by passing a DatabaseEntry, or with a dictionary and specifying a table name. The entry must have values for all fields in the table. You can pass `fill_null=True` to fill remaining fields with None/null. Use `silent=True` to suppress warnings and messages. Parameters ---------- entry : DatabaseEntry/dict The entry. table : str, optional Name of the table the entry belongs to. **Needed if adding an entry with a dictionary**. fill_null : bool, optional Fill in unpopulated fields with null values. silent : bool, optional If True: disables prints. """ if type(entry) == dict: if not table: raise DatabaseError(f"Please provide the table that the data should be inserted in.") entry = DatabaseEntry(entry, table) if not self.is_table(entry.table): raise DatabaseError(f"Database has no table with the name \"{self.table}\". Possible tablenames are: {self.get_table_names()}") table_fields = self.get_column_names(entry.table) id_field = self.get_table_id_field(entry.table) if id_field: entry[id_field] = None if fill_null: entry = self.fill_null(entry) if set(entry) != set(table_fields): raise DatabaseError(f"entry fields are not the same as the table fields: {set(entry)} != {set(table_fields)}") self.INSERT_INTO(entry.table).VALUES(entry).run(silent=True) if not silent and not self.silent: print(f"added entry to table \"{entry.table}\": {entry}") def update_entry(self, entry: dict, table=None, part=False, fill_null=False, silent=False) -> None: """ Update entry in database with a DatabaseEntry, or with a dictionary + the name of the table you want to update. Parameters ---------- entry : DatabaseEntry/dict DatabaseEntry or dictionary, if dictionary you also need to provide table and id_field. table : str, optional The table name. **Needed if updating an entry with a dictionary**. part : bool, optional If True: Only updates the provided fields. fill_null : bool, optional Fill in unpopulated fields with null values. silent : bool, optional If True: disables prints. """ if not isinstance(entry, DatabaseEntry): # the input is a dict if not table: raise DatabaseError(f"Please provide a table when updating an entry with a python dictionary") entry = DatabaseEntry(entry, table) id_field = self.get_table_id_field(entry.table) if not self.is_table(entry.table): raise DatabaseError(f"Database has no table with the name \"{entry.table}\". Possible tablenames are: {self.get_table_names()}") if fill_null: entry = self.fill_null(entry) # check that entry fields and table fields match table_fields = self.get_column_names(entry.table) if set(table_fields) != set(entry): if not (part and set(entry).issubset(set(table_fields))): raise DatabaseError(f"Table fields do not match entry fields: {table_fields} != {list(entry)}. Pass `part = True` or `fill_null = True` if entry are a subset of the table fields") self.UPDATE(entry.table).SET(entry).WHERE(id_field, entry[id_field]).run() if not silent and not self.silent: print(f"updated entry in table \"{entry.table}\": {entry}") def delete_entry(self, entry: DatabaseEntry): """ Delete an entry from the database. Parameters ---------- entry : DatabaseEntry The entry that is to be deleted. """ id_field = self.get_table_id_field(entry.table) self.DELETE_FROM(entry.table).WHERE(id_field, entry[id_field]).run() def delete_entry_by_id(self, table: str, id: int): """ Deletes an entry with a certain id. (Note: the table must have a primary key column, as that is what is meant by id. It is assumed that there is only one primary key column in the table.} Parameters ---------- table : str The table to delete the entry from. id : int """ id_field = self.get_table_id_field(table) self.DELETE_FROM(table).WHERE(id_field, id).run() def save(self) -> None: """Writes any changes to the database file""" self.conn.commit() def close(self) -> None: """Saves and closes the database. If you want to explicitly close without saving use: `self.conn.close()`""" self.conn.commit() self.conn.close() def reconnect(self) -> None: """Reopen database after closing it""" self.conn = sqlite3.connect(self.path) self.cursor = self.conn.cursor() def delete_table(self, table_name) -> None: """ Takes a table name and deletes the table from the database. Parameters ---------- table_name : str Name of the table. """ self.cursor.execute(f"DROP TABLE {table_name};") def table_to_dataframe(self, table) -> pd.DataFrame: """ Converts a table to a pandas.Dataframe. Parameters ---------- table : str Name of the table. """ cols = {} fields = self.get_column_names(table) for f in fields: cols[f] = [] for raw_entry in self.get_table_raw(table): for n, field in enumerate(fields): cols[field].append(raw_entry[n]) return(pd.DataFrame(cols)) def export_to_csv(self, out_dir: str, tables: list = None, sep: str = "\t") -> None: """ Export all or some tables in the database to csv files Parameters ---------- out_dir : str Path to the output directory. tables : list[str]/None, optional Can be set to only export certain tables. sep : str, optional Seperator to use when writing csv-file. """ if not os.path.isdir(out_dir): raise NotADirectoryError(f"{out_dir!r} is not a directory") if not tables: tables = self.get_table_names() for table_name in tables: df = self.table_to_dataframe(table_name) df.to_csv(f"{out_dir}/{table_name}.csv", index=False, sep=sep) def SELECT(self, pattern="*") -> Query: """ Start sql SELECT query from the database. Returns a Query to build from. Parameters ---------- pattern : str, optional Either a python list or sql list of table names. """ return(Query(db=self, silent=True).SELECT(pattern)) def UPDATE(self, table_name) -> Query: """ Start sql UPDATE query from the database. Returns a Query to build from. Parameters ---------- table_name : str Name of the table. """ return(Query(db=self, silent=True).UPDATE(table_name)) def INSERT_INTO(self, table_name) -> Query: """ Start sql INSERT INTO query from the database. Returns a Query to build from. Parameters ---------- table_name : str Name of the table to insert into. """ return(Query(db=self, silent=True).INSERT_INTO(table_name)) def DELETE_FROM(self, table_name: str) -> Query: """ Start sql DELETE FROM query from the database. Returns a Query to build from. Parameters ---------- table_name : str Name of the table to delete from. """ return(Query(db=self, silent=True).DELETE_FROM(table_name)) def __eq__(self, other: object) -> bool: tables = self.get_table_names() if tables != other.get_table_names(): return(False) for table in tables: if self.get_table_raw(table) != other.get_table_raw(table): return(False) elif self.get_table_cols(table) != other.get_table_cols(table): return(False) return(True)
Instance variables
var conn
-
The sqlite3 connection.
var connected
-
Is true if the Database is connected to a database.
var cursor
-
The sqlite3 cursor. Use
cursor.execute(cmd)
to execute raw sql. var path
-
Path to the database file.
var silent
-
Disables all feedback in the form of prints.
Methods
def DELETE_FROM(self, table_name: str) ‑> Query
-
Start sql DELETE FROM query from the database. Returns a Query to build from.
Parameters
table_name
:str
- Name of the table to delete from.
Expand source code
def DELETE_FROM(self, table_name: str) -> Query: """ Start sql DELETE FROM query from the database. Returns a Query to build from. Parameters ---------- table_name : str Name of the table to delete from. """ return(Query(db=self, silent=True).DELETE_FROM(table_name))
def INSERT_INTO(self, table_name) ‑> Query
-
Start sql INSERT INTO query from the database. Returns a Query to build from.
Parameters
table_name
:str
- Name of the table to insert into.
Expand source code
def INSERT_INTO(self, table_name) -> Query: """ Start sql INSERT INTO query from the database. Returns a Query to build from. Parameters ---------- table_name : str Name of the table to insert into. """ return(Query(db=self, silent=True).INSERT_INTO(table_name))
def SELECT(self, pattern='*') ‑> Query
-
Start sql SELECT query from the database. Returns a Query to build from.
Parameters
pattern
:str
, optional- Either a python list or sql list of table names.
Expand source code
def SELECT(self, pattern="*") -> Query: """ Start sql SELECT query from the database. Returns a Query to build from. Parameters ---------- pattern : str, optional Either a python list or sql list of table names. """ return(Query(db=self, silent=True).SELECT(pattern))
def UPDATE(self, table_name) ‑> Query
-
Start sql UPDATE query from the database. Returns a Query to build from.
Parameters
table_name
:str
- Name of the table.
Expand source code
def UPDATE(self, table_name) -> Query: """ Start sql UPDATE query from the database. Returns a Query to build from. Parameters ---------- table_name : str Name of the table. """ return(Query(db=self, silent=True).UPDATE(table_name))
def add_column(self, table_name: str, col: Column)
-
Add column to a table in the database.
Parameters
table_name
:str
- Name of the table.
col
:Column
- The column to add to table.
Expand source code
def add_column(self, table_name: str, col: Column): """ Add column to a table in the database. Parameters ---------- table_name : str Name of the table. col : Column The column to add to table. """ # Check that the table exists if not self.is_table(table_name): raise DatabaseError(f"Database contains no table with the name {table_name!r}") sql = f"ALTER TABLE {table_name} ADD COLUMN {col.name} {col.type}" if col.primary_key: sql += " PRIMARY KEY" if col.not_null: sql += " NOT NULL" if col.default_value: sql += f" DEFAULT {col.default_value}" if col.foreign_key: raise DatabaseError(f"Sqlite3 and therefore sqlite-integrated, does not support adding columns with foreign key constraings to existing tables. They have to be declared with the creation of the table.") self.cursor.execute(sql)
def add_entry(self, entry, table=None, fill_null=False, silent=False) ‑> None
-
Add an entry to the database by passing a DatabaseEntry, or with a dictionary and specifying a table name. The entry must have values for all fields in the table. You can pass
fill_null=True
to fill remaining fields with None/null. Usesilent=True
to suppress warnings and messages.Parameters
entry
:DatabaseEntry/dict
- The entry.
table
:str
, optional- Name of the table the entry belongs to. Needed if adding an entry with a dictionary.
fill_null
:bool
, optional- Fill in unpopulated fields with null values.
silent
:bool
, optional- If True: disables prints.
Expand source code
def add_entry(self, entry, table = None, fill_null=False, silent=False) -> None: """ Add an entry to the database by passing a DatabaseEntry, or with a dictionary and specifying a table name. The entry must have values for all fields in the table. You can pass `fill_null=True` to fill remaining fields with None/null. Use `silent=True` to suppress warnings and messages. Parameters ---------- entry : DatabaseEntry/dict The entry. table : str, optional Name of the table the entry belongs to. **Needed if adding an entry with a dictionary**. fill_null : bool, optional Fill in unpopulated fields with null values. silent : bool, optional If True: disables prints. """ if type(entry) == dict: if not table: raise DatabaseError(f"Please provide the table that the data should be inserted in.") entry = DatabaseEntry(entry, table) if not self.is_table(entry.table): raise DatabaseError(f"Database has no table with the name \"{self.table}\". Possible tablenames are: {self.get_table_names()}") table_fields = self.get_column_names(entry.table) id_field = self.get_table_id_field(entry.table) if id_field: entry[id_field] = None if fill_null: entry = self.fill_null(entry) if set(entry) != set(table_fields): raise DatabaseError(f"entry fields are not the same as the table fields: {set(entry)} != {set(table_fields)}") self.INSERT_INTO(entry.table).VALUES(entry).run(silent=True) if not silent and not self.silent: print(f"added entry to table \"{entry.table}\": {entry}")
def close(self) ‑> None
-
Saves and closes the database. If you want to explicitly close without saving use:
self.conn.close()
Expand source code
def close(self) -> None: """Saves and closes the database. If you want to explicitly close without saving use: `self.conn.close()`""" self.conn.commit() self.conn.close()
def create_table(self, name: str, cols: list[Column])
-
Creates a table in the Database.
Parameters
name
:str
- Name of the new table.
cols
:list[Column]
- List of columns in the new table.
Expand source code
def create_table(self, name: str, cols: list[Column]): """ Creates a table in the Database. Parameters ---------- name : str Name of the new table. cols : list[Column] List of columns in the new table. """ sql = f"CREATE TABLE {name} (\n" foreign_keys: list[ForeignKey] = [] for col in cols: sql += f"{col.name!r} {col.type}" if col.primary_key: sql += " PRIMARY KEY" if col.not_null: sql += " NOT NULL" if col.default_value: sql += f" DEFAULT {col.default_value!r}" if col.foreign_key: foreign_keys.append(col.foreign_key) sql += ",\n" for key in foreign_keys: sql += f"FOREIGN KEY({key.from_col}) REFERENCES {key.table}({key.to_col}),\n" if key.on_update: sql = sql[:-2] + f"\nON UPDATE {key.on_update},\n" if key.on_delete: sql = sql[:-2] + f"\nON DELETE {key.on_delete},\n" sql = sql[:-2] + "\n)" # remove last ",\n" self.cursor.execute(sql)
def delete_column(self, table_name: str, col)
-
Deletes a column in a table.
Parameters
table_name
:str
- Name of the table the column is in.
col
:str/Column
- Column, or column name, of the column that should be deleted.
Expand source code
def delete_column(self, table_name: str, col): """ Deletes a column in a table. Parameters ---------- table_name : str Name of the table the column is in. col : str/Column Column, or column name, of the column that should be deleted. """ # Check that the table exists if not self.is_table(table_name): raise DatabaseError(f"Database contains no table with the name {table_name!r}") if col is Column: col = col.name self.cursor.execute(f"ALTER TABLE {table_name} DROP COLUMN {col}")
def delete_entry(self, entry: DatabaseEntry)
-
Delete an entry from the database.
Parameters
entry
:DatabaseEntry
- The entry that is to be deleted.
Expand source code
def delete_entry(self, entry: DatabaseEntry): """ Delete an entry from the database. Parameters ---------- entry : DatabaseEntry The entry that is to be deleted. """ id_field = self.get_table_id_field(entry.table) self.DELETE_FROM(entry.table).WHERE(id_field, entry[id_field]).run()
def delete_entry_by_id(self, table: str, id: int)
-
Deletes an entry with a certain id. (Note: the table must have a primary key column, as that is what is meant by id. It is assumed that there is only one primary key column in the table.}
Parameters
table
:str
- The table to delete the entry from.
id
:int
Expand source code
def delete_entry_by_id(self, table: str, id: int): """ Deletes an entry with a certain id. (Note: the table must have a primary key column, as that is what is meant by id. It is assumed that there is only one primary key column in the table.} Parameters ---------- table : str The table to delete the entry from. id : int """ id_field = self.get_table_id_field(table) self.DELETE_FROM(table).WHERE(id_field, id).run()
def delete_table(self, table_name) ‑> None
-
Takes a table name and deletes the table from the database.
Parameters
table_name
:str
- Name of the table.
Expand source code
def delete_table(self, table_name) -> None: """ Takes a table name and deletes the table from the database. Parameters ---------- table_name : str Name of the table. """ self.cursor.execute(f"DROP TABLE {table_name};")
def export_to_csv(self, out_dir: str, tables: list = None, sep: str = '\t') ‑> None
-
Export all or some tables in the database to csv files
Parameters
out_dir
:str
- Path to the output directory.
tables
:list[str]/None
, optional- Can be set to only export certain tables.
sep
:str
, optional- Seperator to use when writing csv-file.
Expand source code
def export_to_csv(self, out_dir: str, tables: list = None, sep: str = "\t") -> None: """ Export all or some tables in the database to csv files Parameters ---------- out_dir : str Path to the output directory. tables : list[str]/None, optional Can be set to only export certain tables. sep : str, optional Seperator to use when writing csv-file. """ if not os.path.isdir(out_dir): raise NotADirectoryError(f"{out_dir!r} is not a directory") if not tables: tables = self.get_table_names() for table_name in tables: df = self.table_to_dataframe(table_name) df.to_csv(f"{out_dir}/{table_name}.csv", index=False, sep=sep)
def fill_null(self, entry: DatabaseEntry) ‑> DatabaseEntry
-
Fills out any unpopulated fields in a DatabaseEntry (fields that exist in the database table but not in the entry) and returns it.
Parameters
entry
:DatabaseEntry
- The DatabaseEntry.
Expand source code
def fill_null(self, entry: DatabaseEntry) -> DatabaseEntry: """ Fills out any unpopulated fields in a DatabaseEntry (fields that exist in the database table but not in the entry) and returns it. Parameters ---------- entry : DatabaseEntry The DatabaseEntry. """ t_fields = self.get_column_names(entry.table) e_fields = list(entry) for f in e_fields: t_fields.remove(f) for null_field in t_fields: entry[null_field] = None return(entry)
def get_column_names(self, table_name: str) ‑> list[str]
-
Returns the field/column names for a given table.
Parameters
table_name
:str
- Name of the table.
Expand source code
def get_column_names(self, table_name: str) -> list[str]: """ Returns the field/column names for a given table. Parameters ---------- table_name : str Name of the table. """ if not self.is_table(table_name): raise DatabaseError(f"Can not get column names of non-existing table {table_name!r}.") names = [] for col in self.get_table_cols(table_name): names.append(col.name) return(names)
def get_entry_by_id(self, table, ID) ‑> DatabaseEntry
-
Get table entry by id.
Parameters
table
:str
- Name of the table.
ID
:- The entry id.
Expand source code
def get_entry_by_id(self, table, ID) -> DatabaseEntry: """ Get table entry by id. Parameters ---------- table : str Name of the table. ID : The entry id. """ id_field = self.get_table_id_field(table, do_error=True) if not self.is_table(table): raise DatabaseError(f"Database contains no table with the name: \"{table}\". These are the available tables: {self.get_table_names()}") sql = f"SELECT * FROM {table} WHERE {id_field} = {ID}" self.cursor.execute(sql) answer = self.cursor.fetchall() # some checks if len(answer) != 1: if len(answer) > 1: raise DatabaseError(f"There are more than one entry in table \"{table}\" with an id field \"{id_field}\" with the value \"{id}\": {answer}") elif len(answer) == 0: raise DatabaseError(f"There is no entry in table \"{table}\" with an id_field \"{id_field}\" with a value of {ID}") else: raise DatabaseError("Something went very wrong, please contact the package author") # this will never be run... i think return(DatabaseEntry.from_raw_entry(answer[0], self.get_column_names(table), table))
def get_table(self, name: str, get_only=None) ‑> list
-
Returns all entries in a table as a table (list of DatabaseEntry). This function loops over all entries in the table, so it is not the best in very big databases.
Parameters
name
:str
- Name of the table.
get_only
:list/None
, optional- Can be set to a list of column/field names, to only retrieve those columns/fields.
Expand source code
def get_table(self, name: str, get_only=None) -> list: """ Returns all entries in a table as a table (list of DatabaseEntry). This function loops over all entries in the table, so it is not the best in very big databases. Parameters ---------- name : str Name of the table. get_only : list/None, optional Can be set to a list of column/field names, to only retrieve those columns/fields. """ raw_table = self.get_table_raw(name, get_only) return(raw_table_to_table(raw_table, self.get_column_names(name), name))
def get_table_cols(self, name: str) ‑> list[Column]
-
Returns a list of Column objects, that contain information about the table columns.
Parameters
name : str Name of the table.
Expand source code
def get_table_cols(self, name: str) -> list[Column]: """ Returns a list of Column objects, that contain information about the table columns. Parameters ---------- name : str Name of the table. """ self.cursor.execute(f"PRAGMA table_info({name});") cols_raw_info = self.cursor.fetchall() cols = [] for col_raw_info in cols_raw_info: is_primary = False if col_raw_info[5] == 1: is_primary = True not_null = False if col_raw_info[3] == 1: not_null = True cols.append(Column(col_raw_info[1], col_raw_info[2], not_null, col_raw_info[4], is_primary, col_id=col_raw_info[0])) # Add foreign keys to cols self.cursor.execute(f"PRAGMA foreign_key_list({name});") foreign_key_list = self.cursor.fetchall() if len(foreign_key_list) > 0: for raw_foreign_key in foreign_key_list: foreign_key = ForeignKey( raw_foreign_key[2], raw_foreign_key[4], id=raw_foreign_key[0], seq=raw_foreign_key[1], from_col=raw_foreign_key[3], on_update=raw_foreign_key[5], on_delete=raw_foreign_key[6], match=raw_foreign_key[7] ) for n, col in enumerate(cols): if col.name == foreign_key.from_col: cols[n].foreign_key = foreign_key break return(cols)
def get_table_id_field(self, table: str, do_error=False) ‑> str
-
Takes a table and returns the name of the field/column marked as a
PRIMARY KEY
. (This function assumes that there is only ONE field marked as aPRIMARY KEY
).Parameters
table
:str
- Name of the table.
do_error
:bool
, optional- If True: Raises error if the table does not contain a field marked as
PRIMARY KEY
.
Expand source code
def get_table_id_field(self, table: str, do_error=False) -> str: """ Takes a table and returns the name of the field/column marked as a `PRIMARY KEY`. (This function assumes that there is only ONE field marked as a `PRIMARY KEY`). Parameters ---------- table : str Name of the table. do_error : bool, optional If True: Raises error if the table does not contain a field marked as `PRIMARY KEY`. """ cols = self.get_table_cols(table) for col in cols: if col.primary_key == True: # col_info[5] is 1 if field is a primary key. Otherwise it is 0. return col.name # col_info[1] is the name of the column if do_error: raise DatabaseError(f"The table `{table}` has no id_field (column defined as a `PRIMARY KEY`)") return(None)
def get_table_names(self) ‑> list
-
Returns the names of all tables in the database.
Expand source code
def get_table_names(self) -> list: """Returns the names of all tables in the database.""" res = self.conn.execute("SELECT name FROM sqlite_master WHERE type='table';") names = [] for name in res: names.append(name[0]) return(names)
def get_table_raw(self, name: str, get_only=None) ‑> list
-
Returns all entries in a table as a list of tuples.
Parameters
name
:str
- Name of the table.
get_only
:list
, optional- Can be set to a list of column/field names, to only retrieve those columns/fields.
Expand source code
def get_table_raw(self, name: str, get_only = None) -> list: """ Returns all entries in a table as a list of tuples. Parameters ---------- name : str Name of the table. get_only : list, optional Can be set to a list of column/field names, to only retrieve those columns/fields. """ selected = "*" if get_only: if isinstance(get_only, list): fields = self.get_column_names(name) for field in get_only: if not field in fields: raise DatabaseError(f"Table \"{name}\" contains no field/column with the name: \"{field}\". Available fields are: {fields}") selected = ','.join(get_only) else: raise ValueError(f"get_only can either be `None` or `list`. Got: {get_only}") self.cursor.execute(f"SELECT {selected} FROM {name}") return(self.cursor.fetchall())
def is_column(self, table_name: str, col_name: str) ‑> bool
-
Returns True if the given column name exists in the given table. Else returns False.
Parameters
table_name
:str
- Name of a table.
col_name
:str
- Name of a column that may be in the table.
Expand source code
def is_column(self, table_name: str, col_name: str) -> bool: """ Returns True if the given column name exists in the given table. Else returns False. Parameters ---------- table_name : str Name of a table. col_name : str Name of a column that may be in the table. """ if col_name in self.get_column_names(table_name): return(True) return(False)
def is_table(self, table_name: str) ‑> bool
-
Check if database has a table with a certain name.
Parameters
table_name
:str
- Name to check.
Expand source code
def is_table(self, table_name: str) -> bool: """ Check if database has a table with a certain name. Parameters ---------- table_name : str Name to check. """ if table_name in self.get_table_names(): return True return False
def overview(self, more=False) ‑> None
-
Prints an overview of all the tables in the database with their fields.
Parameters
more
:optional
- If true: Prints more information on the columns in each table.
Expand source code
def overview(self, more=False) -> None: """ Prints an overview of all the tables in the database with their fields. Parameters ---------- more : optional If true: Prints more information on the columns in each table. """ table_names = self.get_table_names() # if there are no tables in database if len(table_names) == 0: print(f"There are no tables in sqlite database at \"{self.path}\".") return(None) text = "Tables\n" for table_name in table_names: text += "\t" + table_name + "\n" for col in self.get_table_cols(table_name): text += f"\t\t{col.name}" if more: text += f"\t\t[{col}]" text += "\n" print(text)
def reconnect(self) ‑> None
-
Reopen database after closing it
Expand source code
def reconnect(self) -> None: """Reopen database after closing it""" self.conn = sqlite3.connect(self.path) self.cursor = self.conn.cursor()
def rename_column(self, table_name: str, current_column_name: str, new_column_name: str)
-
Renames a column in the database.
Parameters
table_name
:str
- Name of the table.
current_column_name
:str
- Current name of a column.
new_column_name
:str
- New name of the column.
Expand source code
def rename_column(self, table_name: str, current_column_name: str, new_column_name: str): """ Renames a column in the database. Parameters ---------- table_name : str Name of the table. current_column_name : str Current name of a column. new_column_name : str New name of the column. """ # Check that the table exists if not self.is_table(table_name): raise DatabaseError(f"Database contains no table with the name {table_name!r}") self.cursor.execute(f"ALTER TABLE {table_name} RENAME COLUMN {current_column_name} TO {new_column_name}")
def rename_table(self, current_name: str, new_name: str)
-
Renames a table in the database.
Parameters
current_name
:str
- Current name of a table.
new_name
:str
- New name of the table.
Expand source code
def rename_table(self, current_name: str, new_name: str): """ Renames a table in the database. Parameters ---------- current_name : str Current name of a table. new_name : str New name of the table. """ self.cursor.execute(f"ALTER TABLE {current_name} RENAME TO {new_name}")
def save(self) ‑> None
-
Writes any changes to the database file
Expand source code
def save(self) -> None: """Writes any changes to the database file""" self.conn.commit()
def table_overview(self, name: str, max_len: int = 40, get_only=None) ‑> None
-
Prints a pretty table (with a name).
Parameters
name
:str
- Name of the table.
max_len
:int
, optional- The max number of rows shown.
get_only
:list
, optional- If given a list of column/field names: only shows those.
Expand source code
def table_overview(self, name: str, max_len:int = 40, get_only = None) -> None: """ Prints a pretty table (with a name). Parameters ---------- name : str Name of the table. max_len : int, optional The max number of rows shown. get_only : list, optional If given a list of column/field names: only shows those. """ text = "" # the output text raw_table = self.get_table_raw(name, get_only=get_only) if get_only: fields = get_only else: fields = self.get_column_names(name) cols = len(fields) longest_words = [0] * cols words_table = raw_table + [fields] for col in range(cols): for entry in words_table: if len(str(entry[col])) > longest_words[col]: longest_words[col] = len(str(entry[col])) seperator = " ║ " def formatRow(row, longest_words): formatted_list = [] for i, string in enumerate(row): string = str(string) formatted_list.append(string + " " * (longest_words[i] - len(string))) return(seperator.join(formatted_list)) text += formatRow(fields, longest_words) + "\n" underline = "═" * (sum(longest_words) + len(seperator)) # This block is for placing the intersections offset = 0 for n in longest_words[:-1]: # we dont create the an intersection after the last column offset += n underline = underline[:offset +1] + "╬" + underline[offset:] offset += len(seperator) text += underline + "\n" if len(raw_table) >= max_len: for row in raw_table[:max_len-5]: text += formatRow(row, longest_words) + "\n" text += " .\n .\n .\n" for row in raw_table[-5:]: text += formatRow(row, longest_words) + "\n" else: for row in raw_table: text += formatRow(row, longest_words) + "\n" print(text)
def table_to_dataframe(self, table) ‑> pandas.core.frame.DataFrame
-
Converts a table to a pandas.Dataframe.
Parameters
table
:str
- Name of the table.
Expand source code
def table_to_dataframe(self, table) -> pd.DataFrame: """ Converts a table to a pandas.Dataframe. Parameters ---------- table : str Name of the table. """ cols = {} fields = self.get_column_names(table) for f in fields: cols[f] = [] for raw_entry in self.get_table_raw(table): for n, field in enumerate(fields): cols[field].append(raw_entry[n]) return(pd.DataFrame(cols))
def update_entry(self, entry: dict, table=None, part=False, fill_null=False, silent=False) ‑> None
-
Update entry in database with a DatabaseEntry, or with a dictionary + the name of the table you want to update.
Parameters
entry
:DatabaseEntry/dict
- DatabaseEntry or dictionary, if dictionary you also need to provide table and id_field.
table
:str
, optional- The table name. Needed if updating an entry with a dictionary.
part
:bool
, optional- If True: Only updates the provided fields.
fill_null
:bool
, optional- Fill in unpopulated fields with null values.
silent
:bool
, optional- If True: disables prints.
Expand source code
def update_entry(self, entry: dict, table=None, part=False, fill_null=False, silent=False) -> None: """ Update entry in database with a DatabaseEntry, or with a dictionary + the name of the table you want to update. Parameters ---------- entry : DatabaseEntry/dict DatabaseEntry or dictionary, if dictionary you also need to provide table and id_field. table : str, optional The table name. **Needed if updating an entry with a dictionary**. part : bool, optional If True: Only updates the provided fields. fill_null : bool, optional Fill in unpopulated fields with null values. silent : bool, optional If True: disables prints. """ if not isinstance(entry, DatabaseEntry): # the input is a dict if not table: raise DatabaseError(f"Please provide a table when updating an entry with a python dictionary") entry = DatabaseEntry(entry, table) id_field = self.get_table_id_field(entry.table) if not self.is_table(entry.table): raise DatabaseError(f"Database has no table with the name \"{entry.table}\". Possible tablenames are: {self.get_table_names()}") if fill_null: entry = self.fill_null(entry) # check that entry fields and table fields match table_fields = self.get_column_names(entry.table) if set(table_fields) != set(entry): if not (part and set(entry).issubset(set(table_fields))): raise DatabaseError(f"Table fields do not match entry fields: {table_fields} != {list(entry)}. Pass `part = True` or `fill_null = True` if entry are a subset of the table fields") self.UPDATE(entry.table).SET(entry).WHERE(id_field, entry[id_field]).run() if not silent and not self.silent: print(f"updated entry in table \"{entry.table}\": {entry}")
class DatabaseEntry (entry_dict: dict, table: str)
-
A python dictionary that keeps track of the table it belongs to. This class is not meant to be created manually.
Parameters
entry_dict
:dict
- A dictionary containing all the information. This information can be accesed just like any other python dict with
my_entry[my_key]
. table
:str
- The name of the table the entry is a part of
Expand source code
class DatabaseEntry(dict): """ A python dictionary that keeps track of the table it belongs to. This class is not meant to be created manually. Parameters ---------- entry_dict : dict A dictionary containing all the information. This information can be accesed just like any other python dict with `my_entry[my_key]`. table : str The name of the table the entry is a part of """ def __init__(self, entry_dict: dict, table: str): self.table = table self.update(entry_dict) @classmethod def from_raw_entry(cls, raw_entry: tuple, table_fields: list, table_name: str): """ Alternative constructor for converting a raw entry to a DatabaseEntry. Parameters ---------- raw_entry : tuple A tuple with the data for the entry. Ex: `(2, "Tom", "Builder", 33)` table_fields : list A list of column names for the data. Ex: `["id", "FirstName", "LastName", "Age"]` table_name : str The name of the table (in the database) that the data belongs to. Ex: "people" """ entry_dict = {} if isinstance(table_fields, str): table_fields = string_to_list(table_fields) elif not isinstance(table_fields, list): raise ValueError(f"table_fields must be either `list` or `str`. Got: {table_fields}") if len(raw_entry) != len(table_fields): raise DatabaseError(f"There must be as many names for table fields as there are fields in the entry: len({raw_entry}) != len({table_fields}) => {len(raw_entry)} != {len(table_fields)}") for n, field in enumerate(table_fields): entry_dict[field] = raw_entry[n] entry = DatabaseEntry(entry_dict, table_name) return(entry) def __repr__(self) -> str: """Represent a Database entry""" return f"DatabaseEntry(table: {self.table}, data: {super().__repr__()})"
Ancestors
- builtins.dict
Static methods
def from_raw_entry(raw_entry: tuple, table_fields: list, table_name: str)
-
Alternative constructor for converting a raw entry to a DatabaseEntry.
Parameters
raw_entry
:tuple
- A tuple with the data for the entry. Ex:
(2, "Tom", "Builder", 33)
table_fields
:list
- A list of column names for the data. Ex:
["id", "FirstName", "LastName", "Age"]
table_name
:str
- The name of the table (in the database) that the data belongs to. Ex: "people"
Expand source code
@classmethod def from_raw_entry(cls, raw_entry: tuple, table_fields: list, table_name: str): """ Alternative constructor for converting a raw entry to a DatabaseEntry. Parameters ---------- raw_entry : tuple A tuple with the data for the entry. Ex: `(2, "Tom", "Builder", 33)` table_fields : list A list of column names for the data. Ex: `["id", "FirstName", "LastName", "Age"]` table_name : str The name of the table (in the database) that the data belongs to. Ex: "people" """ entry_dict = {} if isinstance(table_fields, str): table_fields = string_to_list(table_fields) elif not isinstance(table_fields, list): raise ValueError(f"table_fields must be either `list` or `str`. Got: {table_fields}") if len(raw_entry) != len(table_fields): raise DatabaseError(f"There must be as many names for table fields as there are fields in the entry: len({raw_entry}) != len({table_fields}) => {len(raw_entry)} != {len(table_fields)}") for n, field in enumerate(table_fields): entry_dict[field] = raw_entry[n] entry = DatabaseEntry(entry_dict, table_name) return(entry)
class DatabaseError (*args, **kwargs)
-
Raised when the database fails to execute command
Expand source code
class DatabaseError(Exception): """Raised when the database fails to execute command"""
Ancestors
- builtins.Exception
- builtins.BaseException
class ForeignKey (table: str, to_col: str, from_col: str = None, id: int = None, seq: int = None, on_update: str = None, on_delete: str = None, match: str = None)
-
Class representing an sql foreign key
Expand source code
@dataclass class ForeignKey: """Class representing an sql foreign key""" table: str """The table the foreign key points to""" to_col: str """Column the foreign key points to""" from_col: str = None """Column in current table, containing the key value""" id: int = None """The foreign key id""" seq: int = None """The foreign key sequence attribute""" on_update: str = None """The action the column will do if the data the key is pointing to changes. (Provide sql action).""" on_delete: str = None """The action the column will do if the data the key is pointing to changes. (Provide sql action).""" match: str = None def to_sql(self): rep = f"FOREIGN KEY ({self.from_col}) REFERENCES {self.table} ({self.to_col})" if self.on_update: rep += f" ON UPDATE {self.on_update}" if self.on_delete: rep += f" ON DELETE {self.on_delete}" return(rep)
Class variables
var from_col : str
-
Column in current table, containing the key value
var id : int
-
The foreign key id
var match : str
var on_delete : str
-
The action the column will do if the data the key is pointing to changes. (Provide sql action).
var on_update : str
-
The action the column will do if the data the key is pointing to changes. (Provide sql action).
var seq : int
-
The foreign key sequence attribute
var table : str
-
The table the foreign key points to
var to_col : str
-
Column the foreign key points to
Methods
def to_sql(self)
-
Expand source code
def to_sql(self): rep = f"FOREIGN KEY ({self.from_col}) REFERENCES {self.table} ({self.to_col})" if self.on_update: rep += f" ON UPDATE {self.on_update}" if self.on_delete: rep += f" ON DELETE {self.on_delete}" return(rep)
class Query (db=None, silent=False)
-
A class for writing sql queries. Queries can be run on the attached database or a seperate one with the
run
method.Parameters
db
:Database
, optional- The attached Database. This is the default database to run queries on.
silent
:bool
, optional- If true: disables prints.
Expand source code
class Query: """ A class for writing sql queries. Queries can be run on the attached database or a seperate one with the `run` method. Parameters ---------- db : Database, optional The attached Database. This is the default database to run queries on. silent : bool, optional If true: disables prints. """ def __init__(self, db=None, silent=False) -> None: self._db: Database = db """The attached Database""" self.sql = "" """The current raw sql command""" self.history = [] """The history of commandmethods run on this object""" self.fields = None """The selected fields""" self.table = None """The table the sql query is interacting with""" self.silent = silent """If true: disables prints""" def valid_prefixes(self, prefixes: list) -> None: """Check if a statement is valid given its prefix""" prefix = None if len(self.history) > 0: prefix = self.history[-1] if prefix in prefixes: return(True) raise QueryError(f"Query syntax incorrect or not supported. Prefix: \"{prefix}\" is not a part of the valid prefixes: {prefixes}") def SELECT(self, selection="*"): """ Sql `SELECT` statement. Must be followed by `FROM` statement. Parameters ---------- selection : str/list, optional Either a python list or sql list of table names. Selects all columns if not set. """ self.valid_prefixes([None]) self.history.append("SELECT") if isinstance(selection, str): if selection == "*": self.fields = "*" else: self.fields = string_to_list(selection) self.sql += f"SELECT {selection} " elif isinstance(selection, list): self.fields = selection self.sql += f"SELECT {', '.join(selection)} " else: raise QueryError("SELECT statement selection must be either `str` or `list`") return(self) def FROM(self, table_name): """ Sql `FROM` statement. Has to be preceded by a SELECT statement. Can be followed by `WHERE` statement. Parameters ---------- table_name : str Name of the table you are selecting from. """ self.valid_prefixes(["SELECT"]) self.table = table_name if self._db: table_fields = set(self._db.get_column_names(table_name)) # check if selected fields are in table if self.fields != "*" and self._db and not set(self.fields).issubset(table_fields): raise QueryError(f"Some selected field(s): {set(self.fields) - table_fields} are not fields/columns in the table: {table_name!r}. The table has the following fields: {table_fields}") self.history.append("FROM") self.sql += f"FROM {table_name} " return(self) def WHERE(self, col_name:str, value = ""): """ Sql `WHERE` statement. Can be followed by `LIKE` statement. Parameters ---------- col_name : str The name of the column. You can also just pass it a statement like: `"id" = 4` instead of providing a value. value : optional The value of the column. """ self.valid_prefixes(["FROM", "SET", "DELETE_FROM"]) self.history.append("WHERE") if value != "": if value == None: self.sql += f"WHERE {col_name} is null" else: self.sql += f"WHERE {col_name} = {value_to_sql_value(value)}" else: self.sql += f"WHERE {col_name} " if col_name.find("=") == -1: # expects LIKE statement self.col = col_name.replace(" ", "") return(self) def LIKE(self, pattern: str): """ Sql LIKE statement. Has to be preceded by a WHERE statement. Parameters ---------- pattern : str A typical sql LIKE pattern with % and _. """ self.valid_prefixes(["WHERE"]) self.history.append("LIKE") self.sql += f"LIKE {value_to_sql_value(pattern)} " return(self) def UPDATE(self, table_name: str): """ Sql UPDATE statement. Must be followed by `SET` statement. Parameters ---------- table_name : str Name of the table you are updating. """ self.valid_prefixes([None]) self.history.append("UPDATE") if self._db: if not self._db.is_table(table_name): raise QueryError(f"Database has no table called {table_name!r}") self.fields = self._db.get_column_names(table_name) self.table = table_name self.sql += f"UPDATE {table_name} " return(self) def SET(self, data: dict): """ Sql SET statement. Must be preceded by an UPDATE statement. Must be followed by `WHERE` statement. Parameters ---------- data : dict A dictionaty with key and value pairs. """ self.valid_prefixes(["UPDATE"]) self.history.append("SET") data = dict(data) if not set(data).issubset(self.fields): raise DatabaseError(f"Data keys: {set(data)} are not a subset of table fields/columns. Table fields/columns: {set(self.fields)}") self.sql += f"SET {dict_to_sql(data)} " return(self) def INSERT_INTO(self, table_name: str): """ Sql `INSERT INTO` statement. Must be followed by `VALUES` statement. Parameters ---------- table_name : str Name of the table you want to insert into. """ self.valid_prefixes([None]) self.history.append("INSERT_INTO") self.table = table_name if self._db: self.fields = self._db.get_column_names(table_name) self.sql += f"INSERT INTO {table_name} " return(self) def VALUES(self, data: dict): """ Sql `VALUES` statement. Must be preceded by INSERT_INTO statement. Parameters ---------- data : dict Dictionary with key value pairs. """ self.valid_prefixes(["INSERT_INTO"]) self.history.append("VALUES") if not set(data).issubset(self.fields): raise DatabaseError(f"Data keys: {set(data)} are not a subset of table fields/columns. Unknown keys: {set(data) - set(self.fields)}. Table fields/columns: {set(self.fields)}") self.sql += f"({', '.join([str(v) for v in list(data)])}) VALUES ({', '.join([str(value_to_sql_value(v)) for v in data.values()])}) " return(self) def DELETE_FROM(self, table_name: str): """ Sql `DELETE FROM` statement. Must be followed by `WHERE` statement. Parameters ---------- data : dict Dictionary with key value pairs. """ self.valid_prefixes([None]) self.history.append("DELETE_FROM") if self._db and not table_name in self._db.get_table_names(): raise QueryError(f"Can not perform DELETE FROM on a non-existing table: {table_name!r}") self.table = table_name self.sql = f"DELETE FROM {table_name} " return(self) def run(self, db=None, raw = False, silent=False) -> list[DatabaseEntry]: """ Execute the query in the attached database or in a seperate one. Returns the results in a table (list of DatabaseEntry) or `None` if no results. Parameters ---------- db : Database, optional The database to execute to query on. raw : bool, optional If True: returns the raw table (list of tuples) instead of the normal table. silent : bool, optional If True: disables all prints. """ if not db: db = self._db if not db: raise DatabaseError("Query does not have a database to execute") try: db.cursor.execute(self.sql) except sqlite3.OperationalError as e: raise QueryError(f"\n\n{e}\n\nError while running following sql: {self.sql}") if not db.silent and not self.silent and not silent: print(f"Executed sql: {self.sql}") results = db.cursor.fetchall() if len(results) == 0: return(None) if raw: return(results) if self.fields == "*": self.fields = db.get_column_names(self.table) return(raw_table_to_table(results, self.fields, self.table)) def __repr__(self) -> str: return(f"> {self.sql.strip()} <")
Instance variables
var fields
-
The selected fields
var history
-
The history of commandmethods run on this object
var silent
-
If true: disables prints
var sql
-
The current raw sql command
var table
-
The table the sql query is interacting with
Methods
def DELETE_FROM(self, table_name: str)
-
Sql
DELETE FROM
statement. Must be followed byWHERE
statement.Parameters
data
:dict
- Dictionary with key value pairs.
Expand source code
def DELETE_FROM(self, table_name: str): """ Sql `DELETE FROM` statement. Must be followed by `WHERE` statement. Parameters ---------- data : dict Dictionary with key value pairs. """ self.valid_prefixes([None]) self.history.append("DELETE_FROM") if self._db and not table_name in self._db.get_table_names(): raise QueryError(f"Can not perform DELETE FROM on a non-existing table: {table_name!r}") self.table = table_name self.sql = f"DELETE FROM {table_name} " return(self)
def FROM(self, table_name)
-
Sql
FROM
statement. Has to be preceded by a SELECT statement. Can be followed byWHERE
statement.Parameters
table_name
:str
- Name of the table you are selecting from.
Expand source code
def FROM(self, table_name): """ Sql `FROM` statement. Has to be preceded by a SELECT statement. Can be followed by `WHERE` statement. Parameters ---------- table_name : str Name of the table you are selecting from. """ self.valid_prefixes(["SELECT"]) self.table = table_name if self._db: table_fields = set(self._db.get_column_names(table_name)) # check if selected fields are in table if self.fields != "*" and self._db and not set(self.fields).issubset(table_fields): raise QueryError(f"Some selected field(s): {set(self.fields) - table_fields} are not fields/columns in the table: {table_name!r}. The table has the following fields: {table_fields}") self.history.append("FROM") self.sql += f"FROM {table_name} " return(self)
def INSERT_INTO(self, table_name: str)
-
Sql
INSERT INTO
statement. Must be followed byVALUES
statement.Parameters
table_name
:str
- Name of the table you want to insert into.
Expand source code
def INSERT_INTO(self, table_name: str): """ Sql `INSERT INTO` statement. Must be followed by `VALUES` statement. Parameters ---------- table_name : str Name of the table you want to insert into. """ self.valid_prefixes([None]) self.history.append("INSERT_INTO") self.table = table_name if self._db: self.fields = self._db.get_column_names(table_name) self.sql += f"INSERT INTO {table_name} " return(self)
def LIKE(self, pattern: str)
-
Sql LIKE statement. Has to be preceded by a WHERE statement.
Parameters
pattern
:str
- A typical sql LIKE pattern with % and _.
Expand source code
def LIKE(self, pattern: str): """ Sql LIKE statement. Has to be preceded by a WHERE statement. Parameters ---------- pattern : str A typical sql LIKE pattern with % and _. """ self.valid_prefixes(["WHERE"]) self.history.append("LIKE") self.sql += f"LIKE {value_to_sql_value(pattern)} " return(self)
def SELECT(self, selection='*')
-
Sql
SELECT
statement. Must be followed byFROM
statement.Parameters
selection
:str/list
, optional- Either a python list or sql list of table names. Selects all columns if not set.
Expand source code
def SELECT(self, selection="*"): """ Sql `SELECT` statement. Must be followed by `FROM` statement. Parameters ---------- selection : str/list, optional Either a python list or sql list of table names. Selects all columns if not set. """ self.valid_prefixes([None]) self.history.append("SELECT") if isinstance(selection, str): if selection == "*": self.fields = "*" else: self.fields = string_to_list(selection) self.sql += f"SELECT {selection} " elif isinstance(selection, list): self.fields = selection self.sql += f"SELECT {', '.join(selection)} " else: raise QueryError("SELECT statement selection must be either `str` or `list`") return(self)
def SET(self, data: dict)
-
Sql SET statement. Must be preceded by an UPDATE statement. Must be followed by
WHERE
statement.Parameters
data
:dict
- A dictionaty with key and value pairs.
Expand source code
def SET(self, data: dict): """ Sql SET statement. Must be preceded by an UPDATE statement. Must be followed by `WHERE` statement. Parameters ---------- data : dict A dictionaty with key and value pairs. """ self.valid_prefixes(["UPDATE"]) self.history.append("SET") data = dict(data) if not set(data).issubset(self.fields): raise DatabaseError(f"Data keys: {set(data)} are not a subset of table fields/columns. Table fields/columns: {set(self.fields)}") self.sql += f"SET {dict_to_sql(data)} " return(self)
def UPDATE(self, table_name: str)
-
Sql UPDATE statement. Must be followed by
SET
statement.Parameters
table_name
:str
- Name of the table you are updating.
Expand source code
def UPDATE(self, table_name: str): """ Sql UPDATE statement. Must be followed by `SET` statement. Parameters ---------- table_name : str Name of the table you are updating. """ self.valid_prefixes([None]) self.history.append("UPDATE") if self._db: if not self._db.is_table(table_name): raise QueryError(f"Database has no table called {table_name!r}") self.fields = self._db.get_column_names(table_name) self.table = table_name self.sql += f"UPDATE {table_name} " return(self)
def VALUES(self, data: dict)
-
Sql
VALUES
statement. Must be preceded by INSERT_INTO statement.Parameters
data
:dict
- Dictionary with key value pairs.
Expand source code
def VALUES(self, data: dict): """ Sql `VALUES` statement. Must be preceded by INSERT_INTO statement. Parameters ---------- data : dict Dictionary with key value pairs. """ self.valid_prefixes(["INSERT_INTO"]) self.history.append("VALUES") if not set(data).issubset(self.fields): raise DatabaseError(f"Data keys: {set(data)} are not a subset of table fields/columns. Unknown keys: {set(data) - set(self.fields)}. Table fields/columns: {set(self.fields)}") self.sql += f"({', '.join([str(v) for v in list(data)])}) VALUES ({', '.join([str(value_to_sql_value(v)) for v in data.values()])}) " return(self)
def WHERE(self, col_name: str, value='')
-
Sql
WHERE
statement. Can be followed byLIKE
statement.Parameters
col_name
:str
- The name of the column. You can also just pass it a statement like:
"id" = 4
instead of providing a value. value
:optional
- The value of the column.
Expand source code
def WHERE(self, col_name:str, value = ""): """ Sql `WHERE` statement. Can be followed by `LIKE` statement. Parameters ---------- col_name : str The name of the column. You can also just pass it a statement like: `"id" = 4` instead of providing a value. value : optional The value of the column. """ self.valid_prefixes(["FROM", "SET", "DELETE_FROM"]) self.history.append("WHERE") if value != "": if value == None: self.sql += f"WHERE {col_name} is null" else: self.sql += f"WHERE {col_name} = {value_to_sql_value(value)}" else: self.sql += f"WHERE {col_name} " if col_name.find("=") == -1: # expects LIKE statement self.col = col_name.replace(" ", "") return(self)
def run(self, db=None, raw=False, silent=False) ‑> list[DatabaseEntry]
-
Execute the query in the attached database or in a seperate one. Returns the results in a table (list of DatabaseEntry) or
None
if no results.Parameters
db
:Database
, optional- The database to execute to query on.
raw
:bool
, optional- If True: returns the raw table (list of tuples) instead of the normal table.
silent
:bool
, optional- If True: disables all prints.
Expand source code
def run(self, db=None, raw = False, silent=False) -> list[DatabaseEntry]: """ Execute the query in the attached database or in a seperate one. Returns the results in a table (list of DatabaseEntry) or `None` if no results. Parameters ---------- db : Database, optional The database to execute to query on. raw : bool, optional If True: returns the raw table (list of tuples) instead of the normal table. silent : bool, optional If True: disables all prints. """ if not db: db = self._db if not db: raise DatabaseError("Query does not have a database to execute") try: db.cursor.execute(self.sql) except sqlite3.OperationalError as e: raise QueryError(f"\n\n{e}\n\nError while running following sql: {self.sql}") if not db.silent and not self.silent and not silent: print(f"Executed sql: {self.sql}") results = db.cursor.fetchall() if len(results) == 0: return(None) if raw: return(results) if self.fields == "*": self.fields = db.get_column_names(self.table) return(raw_table_to_table(results, self.fields, self.table))
def valid_prefixes(self, prefixes: list) ‑> None
-
Check if a statement is valid given its prefix
Expand source code
def valid_prefixes(self, prefixes: list) -> None: """Check if a statement is valid given its prefix""" prefix = None if len(self.history) > 0: prefix = self.history[-1] if prefix in prefixes: return(True) raise QueryError(f"Query syntax incorrect or not supported. Prefix: \"{prefix}\" is not a part of the valid prefixes: {prefixes}")
class QueryError (*args, **kwargs)
-
Raised when trying to create an invalid or unsupperted query
Expand source code
class QueryError(Exception): """Raised when trying to create an invalid or unsupperted query"""
Ancestors
- builtins.Exception
- builtins.BaseException