databased.dbshell

  1import argshell
  2from griddle import griddy
  3from pathier import Pathier, Pathish
  4
  5from databased import Databased, __version__, dbparsers
  6from databased.create_shell import create_shell
  7
  8
  9class DBShell(argshell.ArgShell):
 10    _dbpath: Pathier = None  # type: ignore
 11    connection_timeout: float = 10
 12    detect_types: bool = True
 13    enforce_foreign_keys: bool = True
 14    intro = f"Starting dbshell v{__version__} (enter help or ? for arg info)...\n"
 15    prompt = f"based>"
 16
 17    @property
 18    def dbpath(self) -> Pathier:
 19        return self._dbpath
 20
 21    @dbpath.setter
 22    def dbpath(self, path: Pathish):
 23        self._dbpath = Pathier(path)
 24        self.prompt = f"{self._dbpath.name}>"
 25
 26    def _DB(self) -> Databased:
 27        return Databased(
 28            self.dbpath,
 29            self.connection_timeout,
 30            self.detect_types,
 31            self.enforce_foreign_keys,
 32        )
 33
 34    def default(self, line: str):
 35        line = line.strip("_")
 36        with self._DB() as db:
 37            self.display(db.query(line))
 38
 39    def display(self, data: list[dict]):
 40        """Print row data to terminal in a grid."""
 41        try:
 42            print(griddy(data, "keys"))
 43        except Exception as e:
 44            print("Could not fit data into grid :(")
 45            print(e)
 46
 47    # Seat
 48
 49    @argshell.with_parser(dbparsers.get_add_column_parser)
 50    def do_add_column(self, args: argshell.Namespace):
 51        """Add a new column to the specified tables."""
 52        with self._DB() as db:
 53            db.add_column(args.table, args.column_def)
 54
 55    @argshell.with_parser(dbparsers.get_add_table_parser)
 56    def do_add_table(self, args: argshell.Namespace):
 57        """Add a new table to the database."""
 58        with self._DB() as db:
 59            db.create_table(args.table, *args.columns)
 60
 61    @argshell.with_parser(dbparsers.get_backup_parser)
 62    def do_backup(self, args: argshell.Namespace):
 63        """Create a backup of the current db file."""
 64        print(f"Creating a back up for {self.dbpath}...")
 65        backup_path = self.dbpath.backup(args.timestamp)
 66        print("Creating backup is complete.")
 67        print(f"Backup path: {backup_path}")
 68
 69    def do_customize(self, name: str):
 70        """Generate a template file in the current working directory for creating a custom DBShell class.
 71        Expects one argument: the name of the custom dbshell.
 72        This will be used to name the generated file as well as several components in the file content.
 73        """
 74        try:
 75            create_shell(name)
 76        except Exception as e:
 77            print(f"{type(e).__name__}: {e}")
 78
 79    def do_dbpath(self, _: str):
 80        """Print the .db file in use."""
 81        print(self.dbpath)
 82
 83    @argshell.with_parser(dbparsers.get_delete_parser)
 84    def do_delete(self, args: argshell.Namespace):
 85        """Delete rows from the database.
 86
 87        Syntax:
 88        >>> delete {table} {where}
 89        >>> based>delete users "username LIKE '%chungus%"
 90
 91        ^will delete all rows in the 'users' table whose username contains 'chungus'^"""
 92        print("Deleting records...")
 93        with self._DB() as db:
 94            num_rows = db.delete(args.table, args.where)
 95            print(f"Deleted {num_rows} rows from {args.table} table.")
 96
 97    def do_describe(self, tables: str):
 98        """Describe each table in `tables`. If no table list is given, all tables will be described."""
 99        with self._DB() as db:
100            table_list = tables.split() or db.tables
101            for table in table_list:
102                print(f"<{table}>")
103                print(db.to_grid(db.describe(table)))
104
105    @argshell.with_parser(dbparsers.get_drop_column_parser)
106    def do_drop_column(self, args: argshell.Namespace):
107        """Drop the specified column from the specified table."""
108        with self._DB() as db:
109            db.drop_column(args.table, args.column)
110
111    def do_drop_table(self, table: str):
112        """Drop the specified table."""
113        with self._DB() as db:
114            db.drop_table(table)
115
116    def do_flush_log(self, _: str):
117        """Clear the log file for this database."""
118        log_path = self.dbpath.with_name(self.dbpath.name.replace(".", "") + ".log")
119        if not log_path.exists():
120            print(f"No log file at path {log_path}")
121        else:
122            print(f"Flushing log...")
123            log_path.write_text("")
124
125    def do_help(self, args: str):
126        """Display help messages."""
127        super().do_help(args)
128        if args == "":
129            print("Unrecognized commands will be executed as queries.")
130            print(
131                "Use the `query` command explicitly if you don't want to capitalize your key words."
132            )
133            print("All transactions initiated by commands are committed immediately.")
134        print()
135
136    def do_properties(self, _: str):
137        """See current database property settings."""
138        for property_ in ["connection_timeout", "detect_types", "enforce_foreign_keys"]:
139            print(f"{property_}: {getattr(self, property_)}")
140
141    def do_query(self, query: str):
142        """Execute a query against the current database."""
143        print(f"Executing {query}")
144        with self._DB() as db:
145            results = db.query(query)
146        self.display(results)
147        print(f"{db.cursor.rowcount} affected rows")
148
149    @argshell.with_parser(dbparsers.get_rename_column_parser)
150    def do_rename_column(self, args: argshell.Namespace):
151        """Rename a column."""
152        with self._DB() as db:
153            db.rename_column(args.table, args.column, args.new_name)
154
155    @argshell.with_parser(dbparsers.get_rename_table_parser)
156    def do_rename_table(self, args: argshell.Namespace):
157        """Rename a table."""
158        with self._DB() as db:
159            db.rename_table(args.table, args.new_name)
160
161    def do_restore(self, file: str):
162        """Replace the current db file with the given db backup file."""
163        backup = Pathier(file.strip('"'))
164        if not backup.exists():
165            print(f"{backup} does not exist.")
166        else:
167            print(f"Restoring from {file}...")
168            self.dbpath.write_bytes(backup.read_bytes())
169            print("Restore complete.")
170
171    @argshell.with_parser(dbparsers.get_scan_dbs_parser)
172    def do_scan(self, args: argshell.Namespace):
173        """Scan the current working directory for database files."""
174        dbs = self._scan(args.extensions, args.recursive)
175        for db in dbs:
176            print(db.separate(Pathier.cwd().stem))
177
178    @argshell.with_parser(dbparsers.get_schema_parser)
179    def do_schema(self, args: argshell.Namespace):
180        """Print out the names of the database tables, their columns, and, optionally, the number of rows."""
181        print("Getting database schema...")
182        with self._DB() as db:
183            tables = args.tables or db.tables
184            info = [
185                {
186                    "Table Name": table,
187                    "Columns": ", ".join(db.get_columns(table)),
188                    "Number of Rows": db.count(table) if args.rowcount else "n/a",
189                }
190                for table in tables
191            ]
192        self.display(info)
193
194    @argshell.with_parser(dbparsers.get_select_parser, [dbparsers.select_post_parser])
195    def do_select(self, args: argshell.Namespace):
196        """Execute a SELECT query with the given args."""
197        print(f"Querying {args.table}... ")
198        with self._DB() as db:
199            rows = db.select(
200                table=args.table,
201                columns=args.columns,
202                joins=args.joins,
203                where=args.where,
204                group_by=args.group_by,
205                having=args.Having,
206                order_by=args.order_by,
207                limit=args.limit,
208            )
209            print(f"Found {len(rows)} rows:")
210            self.display(rows)
211            print(f"{len(rows)} rows from {args.table}")
212
213    def do_set_connection_timeout(self, seconds: str):
214        """Set database connection timeout to this number of seconds."""
215        self.connection_timeout = float(seconds)
216
217    def do_set_detect_types(self, should_detect: str):
218        """Pass a `1` to turn on and a `0` to turn off."""
219        self.detect_types = bool(int(should_detect))
220
221    def do_set_enforce_foreign_keys(self, should_enforce: str):
222        """Pass a `1` to turn on and a `0` to turn off."""
223        self.enforce_foreign_keys = bool(int(should_enforce))
224
225    def do_size(self, _: str):
226        """Display the size of the the current db file."""
227        print(f"{self.dbpath.name} is {self.dbpath.formatted_size}.")
228
229    @argshell.with_parser(dbparsers.get_update_parser)
230    def do_update(self, args: argshell.Namespace):
231        """Update a column to a new value.
232
233        Syntax:
234        >>> update {table} {column} {value} {where}
235        >>> based>update users username big_chungus "username = lil_chungus"
236
237        ^will update the username in the users 'table' to 'big_chungus' where the username is currently 'lil_chungus'^
238        """
239        print("Updating rows...")
240        with self._DB() as db:
241            num_updates = db.update(args.table, args.column, args.new_value, args.where)
242            print(f"Updated {num_updates} rows in table {args.table}.")
243
244    def do_use(self, arg: str):
245        """Set which database file to use."""
246        dbpath = Pathier(arg)
247        if not dbpath.exists():
248            print(f"{dbpath} does not exist.")
249            print(f"Still using {self.dbpath}")
250        elif not dbpath.is_file():
251            print(f"{dbpath} is not a file.")
252            print(f"Still using {self.dbpath}")
253        else:
254            self.dbpath = dbpath
255            self.prompt = f"{self.dbpath.name}>"
256
257    def do_vacuum(self, _: str):
258        """Reduce database disk memory."""
259        print(f"Database size before vacuuming: {self.dbpath.formatted_size}")
260        print("Vacuuming database...")
261        with self._DB() as db:
262            freedspace = db.vacuum()
263        print(f"Database size after vacuuming: {self.dbpath.formatted_size}")
264        print(f"Freed up {Pathier.format_bytes(freedspace)} of disk space.")
265
266    # Seat
267
268    def _choose_db(self, options: list[Pathier]) -> Pathier:
269        """Prompt the user to select from a list of files."""
270        cwd = Pathier.cwd()
271        paths = [path.separate(cwd.stem) for path in options]
272        while True:
273            print(
274                f"DB options:\n{' '.join([f'({i}) {path}' for i, path in enumerate(paths, 1)])}"
275            )
276            choice = input("Enter the number of the option to use: ")
277            try:
278                index = int(choice)
279                if not 1 <= index <= len(options):
280                    print("Choice out of range.")
281                    continue
282                return options[index - 1]
283            except Exception as e:
284                print(f"{choice} is not a valid option.")
285
286    def _scan(
287        self, extensions: list[str] = [".sqlite3", ".db"], recursive: bool = False
288    ) -> list[Pathier]:
289        cwd = Pathier.cwd()
290        dbs = []
291        globber = cwd.glob
292        if recursive:
293            globber = cwd.rglob
294        for extension in extensions:
295            dbs.extend(list(globber(f"*{extension}")))
296        return dbs
297
298    def preloop(self):
299        """Scan the current directory for a .db file to use.
300        If not found, prompt the user for one or to try again recursively."""
301        if self.dbpath:
302            self.dbpath = Pathier(self.dbpath)
303            print(f"Defaulting to database {self.dbpath}")
304        else:
305            print("Searching for database...")
306            cwd = Pathier.cwd()
307            dbs = self._scan()
308            if len(dbs) == 1:
309                self.dbpath = dbs[0]
310                print(f"Using database {self.dbpath}.")
311            elif dbs:
312                self.dbpath = self._choose_db(dbs)
313            else:
314                print(f"Could not find a .db file in {cwd}.")
315                path = input(
316                    "Enter path to .db file to use or press enter to search again recursively: "
317                )
318                if path:
319                    self.dbpath = Pathier(path)
320                elif not path:
321                    print("Searching recursively...")
322                    dbs = self._scan(recursive=True)
323                    if len(dbs) == 1:
324                        self.dbpath = dbs[0]
325                        print(f"Using database {self.dbpath}.")
326                    elif dbs:
327                        self.dbpath = self._choose_db(dbs)
328                    else:
329                        print("Could not find a .db file.")
330                        self.dbpath = Pathier(input("Enter path to a .db file: "))
331        if not self.dbpath.exists():
332            raise FileNotFoundError(f"{self.dbpath} does not exist.")
333        if not self.dbpath.is_file():
334            raise ValueError(f"{self.dbpath} is not a file.")
335
336
337def main():
338    DBShell().cmdloop()
class DBShell(argshell.argshell.ArgShell):
 10class DBShell(argshell.ArgShell):
 11    _dbpath: Pathier = None  # type: ignore
 12    connection_timeout: float = 10
 13    detect_types: bool = True
 14    enforce_foreign_keys: bool = True
 15    intro = f"Starting dbshell v{__version__} (enter help or ? for arg info)...\n"
 16    prompt = f"based>"
 17
 18    @property
 19    def dbpath(self) -> Pathier:
 20        return self._dbpath
 21
 22    @dbpath.setter
 23    def dbpath(self, path: Pathish):
 24        self._dbpath = Pathier(path)
 25        self.prompt = f"{self._dbpath.name}>"
 26
 27    def _DB(self) -> Databased:
 28        return Databased(
 29            self.dbpath,
 30            self.connection_timeout,
 31            self.detect_types,
 32            self.enforce_foreign_keys,
 33        )
 34
 35    def default(self, line: str):
 36        line = line.strip("_")
 37        with self._DB() as db:
 38            self.display(db.query(line))
 39
 40    def display(self, data: list[dict]):
 41        """Print row data to terminal in a grid."""
 42        try:
 43            print(griddy(data, "keys"))
 44        except Exception as e:
 45            print("Could not fit data into grid :(")
 46            print(e)
 47
 48    # Seat
 49
 50    @argshell.with_parser(dbparsers.get_add_column_parser)
 51    def do_add_column(self, args: argshell.Namespace):
 52        """Add a new column to the specified tables."""
 53        with self._DB() as db:
 54            db.add_column(args.table, args.column_def)
 55
 56    @argshell.with_parser(dbparsers.get_add_table_parser)
 57    def do_add_table(self, args: argshell.Namespace):
 58        """Add a new table to the database."""
 59        with self._DB() as db:
 60            db.create_table(args.table, *args.columns)
 61
 62    @argshell.with_parser(dbparsers.get_backup_parser)
 63    def do_backup(self, args: argshell.Namespace):
 64        """Create a backup of the current db file."""
 65        print(f"Creating a back up for {self.dbpath}...")
 66        backup_path = self.dbpath.backup(args.timestamp)
 67        print("Creating backup is complete.")
 68        print(f"Backup path: {backup_path}")
 69
 70    def do_customize(self, name: str):
 71        """Generate a template file in the current working directory for creating a custom DBShell class.
 72        Expects one argument: the name of the custom dbshell.
 73        This will be used to name the generated file as well as several components in the file content.
 74        """
 75        try:
 76            create_shell(name)
 77        except Exception as e:
 78            print(f"{type(e).__name__}: {e}")
 79
 80    def do_dbpath(self, _: str):
 81        """Print the .db file in use."""
 82        print(self.dbpath)
 83
 84    @argshell.with_parser(dbparsers.get_delete_parser)
 85    def do_delete(self, args: argshell.Namespace):
 86        """Delete rows from the database.
 87
 88        Syntax:
 89        >>> delete {table} {where}
 90        >>> based>delete users "username LIKE '%chungus%"
 91
 92        ^will delete all rows in the 'users' table whose username contains 'chungus'^"""
 93        print("Deleting records...")
 94        with self._DB() as db:
 95            num_rows = db.delete(args.table, args.where)
 96            print(f"Deleted {num_rows} rows from {args.table} table.")
 97
 98    def do_describe(self, tables: str):
 99        """Describe each table in `tables`. If no table list is given, all tables will be described."""
100        with self._DB() as db:
101            table_list = tables.split() or db.tables
102            for table in table_list:
103                print(f"<{table}>")
104                print(db.to_grid(db.describe(table)))
105
106    @argshell.with_parser(dbparsers.get_drop_column_parser)
107    def do_drop_column(self, args: argshell.Namespace):
108        """Drop the specified column from the specified table."""
109        with self._DB() as db:
110            db.drop_column(args.table, args.column)
111
112    def do_drop_table(self, table: str):
113        """Drop the specified table."""
114        with self._DB() as db:
115            db.drop_table(table)
116
117    def do_flush_log(self, _: str):
118        """Clear the log file for this database."""
119        log_path = self.dbpath.with_name(self.dbpath.name.replace(".", "") + ".log")
120        if not log_path.exists():
121            print(f"No log file at path {log_path}")
122        else:
123            print(f"Flushing log...")
124            log_path.write_text("")
125
126    def do_help(self, args: str):
127        """Display help messages."""
128        super().do_help(args)
129        if args == "":
130            print("Unrecognized commands will be executed as queries.")
131            print(
132                "Use the `query` command explicitly if you don't want to capitalize your key words."
133            )
134            print("All transactions initiated by commands are committed immediately.")
135        print()
136
137    def do_properties(self, _: str):
138        """See current database property settings."""
139        for property_ in ["connection_timeout", "detect_types", "enforce_foreign_keys"]:
140            print(f"{property_}: {getattr(self, property_)}")
141
142    def do_query(self, query: str):
143        """Execute a query against the current database."""
144        print(f"Executing {query}")
145        with self._DB() as db:
146            results = db.query(query)
147        self.display(results)
148        print(f"{db.cursor.rowcount} affected rows")
149
150    @argshell.with_parser(dbparsers.get_rename_column_parser)
151    def do_rename_column(self, args: argshell.Namespace):
152        """Rename a column."""
153        with self._DB() as db:
154            db.rename_column(args.table, args.column, args.new_name)
155
156    @argshell.with_parser(dbparsers.get_rename_table_parser)
157    def do_rename_table(self, args: argshell.Namespace):
158        """Rename a table."""
159        with self._DB() as db:
160            db.rename_table(args.table, args.new_name)
161
162    def do_restore(self, file: str):
163        """Replace the current db file with the given db backup file."""
164        backup = Pathier(file.strip('"'))
165        if not backup.exists():
166            print(f"{backup} does not exist.")
167        else:
168            print(f"Restoring from {file}...")
169            self.dbpath.write_bytes(backup.read_bytes())
170            print("Restore complete.")
171
172    @argshell.with_parser(dbparsers.get_scan_dbs_parser)
173    def do_scan(self, args: argshell.Namespace):
174        """Scan the current working directory for database files."""
175        dbs = self._scan(args.extensions, args.recursive)
176        for db in dbs:
177            print(db.separate(Pathier.cwd().stem))
178
179    @argshell.with_parser(dbparsers.get_schema_parser)
180    def do_schema(self, args: argshell.Namespace):
181        """Print out the names of the database tables, their columns, and, optionally, the number of rows."""
182        print("Getting database schema...")
183        with self._DB() as db:
184            tables = args.tables or db.tables
185            info = [
186                {
187                    "Table Name": table,
188                    "Columns": ", ".join(db.get_columns(table)),
189                    "Number of Rows": db.count(table) if args.rowcount else "n/a",
190                }
191                for table in tables
192            ]
193        self.display(info)
194
195    @argshell.with_parser(dbparsers.get_select_parser, [dbparsers.select_post_parser])
196    def do_select(self, args: argshell.Namespace):
197        """Execute a SELECT query with the given args."""
198        print(f"Querying {args.table}... ")
199        with self._DB() as db:
200            rows = db.select(
201                table=args.table,
202                columns=args.columns,
203                joins=args.joins,
204                where=args.where,
205                group_by=args.group_by,
206                having=args.Having,
207                order_by=args.order_by,
208                limit=args.limit,
209            )
210            print(f"Found {len(rows)} rows:")
211            self.display(rows)
212            print(f"{len(rows)} rows from {args.table}")
213
214    def do_set_connection_timeout(self, seconds: str):
215        """Set database connection timeout to this number of seconds."""
216        self.connection_timeout = float(seconds)
217
218    def do_set_detect_types(self, should_detect: str):
219        """Pass a `1` to turn on and a `0` to turn off."""
220        self.detect_types = bool(int(should_detect))
221
222    def do_set_enforce_foreign_keys(self, should_enforce: str):
223        """Pass a `1` to turn on and a `0` to turn off."""
224        self.enforce_foreign_keys = bool(int(should_enforce))
225
226    def do_size(self, _: str):
227        """Display the size of the the current db file."""
228        print(f"{self.dbpath.name} is {self.dbpath.formatted_size}.")
229
230    @argshell.with_parser(dbparsers.get_update_parser)
231    def do_update(self, args: argshell.Namespace):
232        """Update a column to a new value.
233
234        Syntax:
235        >>> update {table} {column} {value} {where}
236        >>> based>update users username big_chungus "username = lil_chungus"
237
238        ^will update the username in the users 'table' to 'big_chungus' where the username is currently 'lil_chungus'^
239        """
240        print("Updating rows...")
241        with self._DB() as db:
242            num_updates = db.update(args.table, args.column, args.new_value, args.where)
243            print(f"Updated {num_updates} rows in table {args.table}.")
244
245    def do_use(self, arg: str):
246        """Set which database file to use."""
247        dbpath = Pathier(arg)
248        if not dbpath.exists():
249            print(f"{dbpath} does not exist.")
250            print(f"Still using {self.dbpath}")
251        elif not dbpath.is_file():
252            print(f"{dbpath} is not a file.")
253            print(f"Still using {self.dbpath}")
254        else:
255            self.dbpath = dbpath
256            self.prompt = f"{self.dbpath.name}>"
257
258    def do_vacuum(self, _: str):
259        """Reduce database disk memory."""
260        print(f"Database size before vacuuming: {self.dbpath.formatted_size}")
261        print("Vacuuming database...")
262        with self._DB() as db:
263            freedspace = db.vacuum()
264        print(f"Database size after vacuuming: {self.dbpath.formatted_size}")
265        print(f"Freed up {Pathier.format_bytes(freedspace)} of disk space.")
266
267    # Seat
268
269    def _choose_db(self, options: list[Pathier]) -> Pathier:
270        """Prompt the user to select from a list of files."""
271        cwd = Pathier.cwd()
272        paths = [path.separate(cwd.stem) for path in options]
273        while True:
274            print(
275                f"DB options:\n{' '.join([f'({i}) {path}' for i, path in enumerate(paths, 1)])}"
276            )
277            choice = input("Enter the number of the option to use: ")
278            try:
279                index = int(choice)
280                if not 1 <= index <= len(options):
281                    print("Choice out of range.")
282                    continue
283                return options[index - 1]
284            except Exception as e:
285                print(f"{choice} is not a valid option.")
286
287    def _scan(
288        self, extensions: list[str] = [".sqlite3", ".db"], recursive: bool = False
289    ) -> list[Pathier]:
290        cwd = Pathier.cwd()
291        dbs = []
292        globber = cwd.glob
293        if recursive:
294            globber = cwd.rglob
295        for extension in extensions:
296            dbs.extend(list(globber(f"*{extension}")))
297        return dbs
298
299    def preloop(self):
300        """Scan the current directory for a .db file to use.
301        If not found, prompt the user for one or to try again recursively."""
302        if self.dbpath:
303            self.dbpath = Pathier(self.dbpath)
304            print(f"Defaulting to database {self.dbpath}")
305        else:
306            print("Searching for database...")
307            cwd = Pathier.cwd()
308            dbs = self._scan()
309            if len(dbs) == 1:
310                self.dbpath = dbs[0]
311                print(f"Using database {self.dbpath}.")
312            elif dbs:
313                self.dbpath = self._choose_db(dbs)
314            else:
315                print(f"Could not find a .db file in {cwd}.")
316                path = input(
317                    "Enter path to .db file to use or press enter to search again recursively: "
318                )
319                if path:
320                    self.dbpath = Pathier(path)
321                elif not path:
322                    print("Searching recursively...")
323                    dbs = self._scan(recursive=True)
324                    if len(dbs) == 1:
325                        self.dbpath = dbs[0]
326                        print(f"Using database {self.dbpath}.")
327                    elif dbs:
328                        self.dbpath = self._choose_db(dbs)
329                    else:
330                        print("Could not find a .db file.")
331                        self.dbpath = Pathier(input("Enter path to a .db file: "))
332        if not self.dbpath.exists():
333            raise FileNotFoundError(f"{self.dbpath} does not exist.")
334        if not self.dbpath.is_file():
335            raise ValueError(f"{self.dbpath} is not a file.")

Subclass this to create custom ArgShells.

def default(self, line: str):
35    def default(self, line: str):
36        line = line.strip("_")
37        with self._DB() as db:
38            self.display(db.query(line))

Called on an input line when the command prefix is not recognized.

If this method is not overridden, it prints an error message and returns.

def display(self, data: list[dict]):
40    def display(self, data: list[dict]):
41        """Print row data to terminal in a grid."""
42        try:
43            print(griddy(data, "keys"))
44        except Exception as e:
45            print("Could not fit data into grid :(")
46            print(e)

Print row data to terminal in a grid.

@argshell.with_parser(dbparsers.get_add_column_parser)
def do_add_column(self, args: argshell.argshell.Namespace):
50    @argshell.with_parser(dbparsers.get_add_column_parser)
51    def do_add_column(self, args: argshell.Namespace):
52        """Add a new column to the specified tables."""
53        with self._DB() as db:
54            db.add_column(args.table, args.column_def)

Add a new column to the specified tables.

@argshell.with_parser(dbparsers.get_add_table_parser)
def do_add_table(self, args: argshell.argshell.Namespace):
56    @argshell.with_parser(dbparsers.get_add_table_parser)
57    def do_add_table(self, args: argshell.Namespace):
58        """Add a new table to the database."""
59        with self._DB() as db:
60            db.create_table(args.table, *args.columns)

Add a new table to the database.

@argshell.with_parser(dbparsers.get_backup_parser)
def do_backup(self, args: argshell.argshell.Namespace):
62    @argshell.with_parser(dbparsers.get_backup_parser)
63    def do_backup(self, args: argshell.Namespace):
64        """Create a backup of the current db file."""
65        print(f"Creating a back up for {self.dbpath}...")
66        backup_path = self.dbpath.backup(args.timestamp)
67        print("Creating backup is complete.")
68        print(f"Backup path: {backup_path}")

Create a backup of the current db file.

def do_customize(self, name: str):
70    def do_customize(self, name: str):
71        """Generate a template file in the current working directory for creating a custom DBShell class.
72        Expects one argument: the name of the custom dbshell.
73        This will be used to name the generated file as well as several components in the file content.
74        """
75        try:
76            create_shell(name)
77        except Exception as e:
78            print(f"{type(e).__name__}: {e}")

Generate a template file in the current working directory for creating a custom DBShell class. Expects one argument: the name of the custom dbshell. This will be used to name the generated file as well as several components in the file content.

def do_dbpath(self, _: str):
80    def do_dbpath(self, _: str):
81        """Print the .db file in use."""
82        print(self.dbpath)

Print the .db file in use.

@argshell.with_parser(dbparsers.get_delete_parser)
def do_delete(self, args: argshell.argshell.Namespace):
84    @argshell.with_parser(dbparsers.get_delete_parser)
85    def do_delete(self, args: argshell.Namespace):
86        """Delete rows from the database.
87
88        Syntax:
89        >>> delete {table} {where}
90        >>> based>delete users "username LIKE '%chungus%"
91
92        ^will delete all rows in the 'users' table whose username contains 'chungus'^"""
93        print("Deleting records...")
94        with self._DB() as db:
95            num_rows = db.delete(args.table, args.where)
96            print(f"Deleted {num_rows} rows from {args.table} table.")

Delete rows from the database.

Syntax:

>>> delete {table} {where}
>>> based>delete users "username LIKE '%chungus%"

^will delete all rows in the 'users' table whose username contains 'chungus'^

def do_describe(self, tables: str):
 98    def do_describe(self, tables: str):
 99        """Describe each table in `tables`. If no table list is given, all tables will be described."""
100        with self._DB() as db:
101            table_list = tables.split() or db.tables
102            for table in table_list:
103                print(f"<{table}>")
104                print(db.to_grid(db.describe(table)))

Describe each table in tables. If no table list is given, all tables will be described.

@argshell.with_parser(dbparsers.get_drop_column_parser)
def do_drop_column(self, args: argshell.argshell.Namespace):
106    @argshell.with_parser(dbparsers.get_drop_column_parser)
107    def do_drop_column(self, args: argshell.Namespace):
108        """Drop the specified column from the specified table."""
109        with self._DB() as db:
110            db.drop_column(args.table, args.column)

Drop the specified column from the specified table.

def do_drop_table(self, table: str):
112    def do_drop_table(self, table: str):
113        """Drop the specified table."""
114        with self._DB() as db:
115            db.drop_table(table)

Drop the specified table.

def do_flush_log(self, _: str):
117    def do_flush_log(self, _: str):
118        """Clear the log file for this database."""
119        log_path = self.dbpath.with_name(self.dbpath.name.replace(".", "") + ".log")
120        if not log_path.exists():
121            print(f"No log file at path {log_path}")
122        else:
123            print(f"Flushing log...")
124            log_path.write_text("")

Clear the log file for this database.

def do_help(self, args: str):
126    def do_help(self, args: str):
127        """Display help messages."""
128        super().do_help(args)
129        if args == "":
130            print("Unrecognized commands will be executed as queries.")
131            print(
132                "Use the `query` command explicitly if you don't want to capitalize your key words."
133            )
134            print("All transactions initiated by commands are committed immediately.")
135        print()

Display help messages.

def do_properties(self, _: str):
137    def do_properties(self, _: str):
138        """See current database property settings."""
139        for property_ in ["connection_timeout", "detect_types", "enforce_foreign_keys"]:
140            print(f"{property_}: {getattr(self, property_)}")

See current database property settings.

def do_query(self, query: str):
142    def do_query(self, query: str):
143        """Execute a query against the current database."""
144        print(f"Executing {query}")
145        with self._DB() as db:
146            results = db.query(query)
147        self.display(results)
148        print(f"{db.cursor.rowcount} affected rows")

Execute a query against the current database.

@argshell.with_parser(dbparsers.get_rename_column_parser)
def do_rename_column(self, args: argshell.argshell.Namespace):
150    @argshell.with_parser(dbparsers.get_rename_column_parser)
151    def do_rename_column(self, args: argshell.Namespace):
152        """Rename a column."""
153        with self._DB() as db:
154            db.rename_column(args.table, args.column, args.new_name)

Rename a column.

@argshell.with_parser(dbparsers.get_rename_table_parser)
def do_rename_table(self, args: argshell.argshell.Namespace):
156    @argshell.with_parser(dbparsers.get_rename_table_parser)
157    def do_rename_table(self, args: argshell.Namespace):
158        """Rename a table."""
159        with self._DB() as db:
160            db.rename_table(args.table, args.new_name)

Rename a table.

def do_restore(self, file: str):
162    def do_restore(self, file: str):
163        """Replace the current db file with the given db backup file."""
164        backup = Pathier(file.strip('"'))
165        if not backup.exists():
166            print(f"{backup} does not exist.")
167        else:
168            print(f"Restoring from {file}...")
169            self.dbpath.write_bytes(backup.read_bytes())
170            print("Restore complete.")

Replace the current db file with the given db backup file.

@argshell.with_parser(dbparsers.get_scan_dbs_parser)
def do_scan(self, args: argshell.argshell.Namespace):
172    @argshell.with_parser(dbparsers.get_scan_dbs_parser)
173    def do_scan(self, args: argshell.Namespace):
174        """Scan the current working directory for database files."""
175        dbs = self._scan(args.extensions, args.recursive)
176        for db in dbs:
177            print(db.separate(Pathier.cwd().stem))

Scan the current working directory for database files.

@argshell.with_parser(dbparsers.get_schema_parser)
def do_schema(self, args: argshell.argshell.Namespace):
179    @argshell.with_parser(dbparsers.get_schema_parser)
180    def do_schema(self, args: argshell.Namespace):
181        """Print out the names of the database tables, their columns, and, optionally, the number of rows."""
182        print("Getting database schema...")
183        with self._DB() as db:
184            tables = args.tables or db.tables
185            info = [
186                {
187                    "Table Name": table,
188                    "Columns": ", ".join(db.get_columns(table)),
189                    "Number of Rows": db.count(table) if args.rowcount else "n/a",
190                }
191                for table in tables
192            ]
193        self.display(info)

Print out the names of the database tables, their columns, and, optionally, the number of rows.

@argshell.with_parser(dbparsers.get_select_parser, [dbparsers.select_post_parser])
def do_select(self, args: argshell.argshell.Namespace):
195    @argshell.with_parser(dbparsers.get_select_parser, [dbparsers.select_post_parser])
196    def do_select(self, args: argshell.Namespace):
197        """Execute a SELECT query with the given args."""
198        print(f"Querying {args.table}... ")
199        with self._DB() as db:
200            rows = db.select(
201                table=args.table,
202                columns=args.columns,
203                joins=args.joins,
204                where=args.where,
205                group_by=args.group_by,
206                having=args.Having,
207                order_by=args.order_by,
208                limit=args.limit,
209            )
210            print(f"Found {len(rows)} rows:")
211            self.display(rows)
212            print(f"{len(rows)} rows from {args.table}")

Execute a SELECT query with the given args.

def do_set_connection_timeout(self, seconds: str):
214    def do_set_connection_timeout(self, seconds: str):
215        """Set database connection timeout to this number of seconds."""
216        self.connection_timeout = float(seconds)

Set database connection timeout to this number of seconds.

def do_set_detect_types(self, should_detect: str):
218    def do_set_detect_types(self, should_detect: str):
219        """Pass a `1` to turn on and a `0` to turn off."""
220        self.detect_types = bool(int(should_detect))

Pass a 1 to turn on and a 0 to turn off.

def do_set_enforce_foreign_keys(self, should_enforce: str):
222    def do_set_enforce_foreign_keys(self, should_enforce: str):
223        """Pass a `1` to turn on and a `0` to turn off."""
224        self.enforce_foreign_keys = bool(int(should_enforce))

Pass a 1 to turn on and a 0 to turn off.

def do_size(self, _: str):
226    def do_size(self, _: str):
227        """Display the size of the the current db file."""
228        print(f"{self.dbpath.name} is {self.dbpath.formatted_size}.")

Display the size of the the current db file.

@argshell.with_parser(dbparsers.get_update_parser)
def do_update(self, args: argshell.argshell.Namespace):
230    @argshell.with_parser(dbparsers.get_update_parser)
231    def do_update(self, args: argshell.Namespace):
232        """Update a column to a new value.
233
234        Syntax:
235        >>> update {table} {column} {value} {where}
236        >>> based>update users username big_chungus "username = lil_chungus"
237
238        ^will update the username in the users 'table' to 'big_chungus' where the username is currently 'lil_chungus'^
239        """
240        print("Updating rows...")
241        with self._DB() as db:
242            num_updates = db.update(args.table, args.column, args.new_value, args.where)
243            print(f"Updated {num_updates} rows in table {args.table}.")

Update a column to a new value.

Syntax:

>>> update {table} {column} {value} {where}
>>> based>update users username big_chungus "username = lil_chungus"

^will update the username in the users 'table' to 'big_chungus' where the username is currently 'lil_chungus'^

def do_use(self, arg: str):
245    def do_use(self, arg: str):
246        """Set which database file to use."""
247        dbpath = Pathier(arg)
248        if not dbpath.exists():
249            print(f"{dbpath} does not exist.")
250            print(f"Still using {self.dbpath}")
251        elif not dbpath.is_file():
252            print(f"{dbpath} is not a file.")
253            print(f"Still using {self.dbpath}")
254        else:
255            self.dbpath = dbpath
256            self.prompt = f"{self.dbpath.name}>"

Set which database file to use.

def do_vacuum(self, _: str):
258    def do_vacuum(self, _: str):
259        """Reduce database disk memory."""
260        print(f"Database size before vacuuming: {self.dbpath.formatted_size}")
261        print("Vacuuming database...")
262        with self._DB() as db:
263            freedspace = db.vacuum()
264        print(f"Database size after vacuuming: {self.dbpath.formatted_size}")
265        print(f"Freed up {Pathier.format_bytes(freedspace)} of disk space.")

Reduce database disk memory.

def preloop(self):
299    def preloop(self):
300        """Scan the current directory for a .db file to use.
301        If not found, prompt the user for one or to try again recursively."""
302        if self.dbpath:
303            self.dbpath = Pathier(self.dbpath)
304            print(f"Defaulting to database {self.dbpath}")
305        else:
306            print("Searching for database...")
307            cwd = Pathier.cwd()
308            dbs = self._scan()
309            if len(dbs) == 1:
310                self.dbpath = dbs[0]
311                print(f"Using database {self.dbpath}.")
312            elif dbs:
313                self.dbpath = self._choose_db(dbs)
314            else:
315                print(f"Could not find a .db file in {cwd}.")
316                path = input(
317                    "Enter path to .db file to use or press enter to search again recursively: "
318                )
319                if path:
320                    self.dbpath = Pathier(path)
321                elif not path:
322                    print("Searching recursively...")
323                    dbs = self._scan(recursive=True)
324                    if len(dbs) == 1:
325                        self.dbpath = dbs[0]
326                        print(f"Using database {self.dbpath}.")
327                    elif dbs:
328                        self.dbpath = self._choose_db(dbs)
329                    else:
330                        print("Could not find a .db file.")
331                        self.dbpath = Pathier(input("Enter path to a .db file: "))
332        if not self.dbpath.exists():
333            raise FileNotFoundError(f"{self.dbpath} does not exist.")
334        if not self.dbpath.is_file():
335            raise ValueError(f"{self.dbpath} is not a file.")

Scan the current directory for a .db file to use. If not found, prompt the user for one or to try again recursively.

Inherited Members
cmd.Cmd
Cmd
precmd
postcmd
postloop
parseline
onecmd
completedefault
completenames
complete
get_names
complete_help
print_topics
columnize
argshell.argshell.ArgShell
do_quit
do_sys
cmdloop
emptyline
def main():
338def main():
339    DBShell().cmdloop()