databased.dbshell

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

Subclass this to create custom ArgShells.

def default(*args, **kwargs) -> Any:
17        def wrapper(*args, **kwargs) -> Any:
18            timer = Timer(loops)
19            result = None
20            for _ in range(loops):
21                timer.start()
22                result = func(*args, **kwargs)
23                timer.stop()
24            print(
25                f"{func.__name__} {'average ' if loops > 1 else ''}execution time: {timer.average_elapsed_str}"
26            )
27            return result

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]):
44    def display(self, data: list[dict]):
45        """Print row data to terminal in a grid."""
46        try:
47            print(griddy(data, "keys"))
48        except Exception as e:
49            print("Could not fit data into grid :(")
50            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):
90    @argshell.with_parser(dbparsers.get_add_column_parser)
91    def do_add_column(self, args: argshell.Namespace):
92        """Add a new column to the specified tables."""
93        with self._DB() as db:
94            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):
 96    @argshell.with_parser(dbparsers.get_add_table_parser)
 97    def do_add_table(self, args: argshell.Namespace):
 98        """Add a new table to the database."""
 99        with self._DB() as db:
100            db.create_table(args.table, *args.columns)

Add a new table to the database.

def do_backup(*args, **kwargs) -> Any:
17        def wrapper(*args, **kwargs) -> Any:
18            timer = Timer(loops)
19            result = None
20            for _ in range(loops):
21                timer.start()
22                result = func(*args, **kwargs)
23                timer.stop()
24            print(
25                f"{func.__name__} {'average ' if loops > 1 else ''}execution time: {timer.average_elapsed_str}"
26            )
27            return result

Create a backup of the current db file.

def do_customize(self, name: str):
111    def do_customize(self, name: str):
112        """Generate a template file in the current working directory for creating a custom DBShell class.
113        Expects one argument: the name of the custom dbshell.
114        This will be used to name the generated file as well as several components in the file content.
115        """
116        try:
117            create_shell(name)
118        except Exception as e:
119            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):
121    def do_dbpath(self, _: str):
122        """Print the .db file in use."""
123        print(self.dbpath)

Print the .db file in use.

def do_delete(*args, **kwargs) -> Any:
17        def wrapper(*args, **kwargs) -> Any:
18            timer = Timer(loops)
19            result = None
20            for _ in range(loops):
21                timer.start()
22                result = func(*args, **kwargs)
23                timer.stop()
24            print(
25                f"{func.__name__} {'average ' if loops > 1 else ''}execution time: {timer.average_elapsed_str}"
26            )
27            return result

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):
140    def do_describe(self, tables: str):
141        """Describe each given table or view. If no list is given, all tables and views will be described."""
142        with self._DB() as db:
143            table_list = tables.split() or (db.tables + db.views)
144            for table in table_list:
145                print(f"<{table}>")
146                print(db.to_grid(db.describe(table)))

Describe each given table or view. If no list is given, all tables and views will be described.

@argshell.with_parser(dbparsers.get_drop_column_parser)
def do_drop_column(self, args: argshell.argshell.Namespace):
148    @argshell.with_parser(dbparsers.get_drop_column_parser)
149    def do_drop_column(self, args: argshell.Namespace):
150        """Drop the specified column from the specified table."""
151        with self._DB() as db:
152            db.drop_column(args.table, args.column)

Drop the specified column from the specified table.

def do_drop_table(self, table: str):
154    def do_drop_table(self, table: str):
155        """Drop the specified table."""
156        with self._DB() as db:
157            db.drop_table(table)

Drop the specified table.

def do_dump(*args, **kwargs) -> Any:
17        def wrapper(*args, **kwargs) -> Any:
18            timer = Timer(loops)
19            result = None
20            for _ in range(loops):
21                timer.start()
22                result = func(*args, **kwargs)
23                timer.stop()
24            print(
25                f"{func.__name__} {'average ' if loops > 1 else ''}execution time: {timer.average_elapsed_str}"
26            )
27            return result

Create .sql dump files for the current database.

def do_flush_log(self, _: str):
175    def do_flush_log(self, _: str):
176        """Clear the log file for this database."""
177        log_path = self.dbpath.with_name(self.dbpath.name.replace(".", "") + ".log")
178        if not log_path.exists():
179            print(f"No log file at path {log_path}")
180        else:
181            print(f"Flushing log...")
182            log_path.write_text("")

Clear the log file for this database.

def do_help(self, args: str):
184    def do_help(self, args: str):
185        """Display help messages."""
186        super().do_help(args)
187        if args == "":
188            print("Unrecognized commands will be executed as queries.")
189            print(
190                "Use the `query` command explicitly if you don't want to capitalize your key words."
191            )
192            print("All transactions initiated by commands are committed immediately.")
193        print()

Display help messages.

def do_new_db(self, dbname: str):
195    def do_new_db(self, dbname: str):
196        """Create a new, empty database with the given name."""
197        dbpath = Pathier(dbname)
198        self.dbpath = dbpath
199        self.prompt = f"{self.dbpath.name}>"

Create a new, empty database with the given name.

def do_properties(self, _: str):
201    def do_properties(self, _: str):
202        """See current database property settings."""
203        for property_ in ["connection_timeout", "detect_types", "enforce_foreign_keys"]:
204            print(f"{property_}: {getattr(self, property_)}")

See current database property settings.

def do_query(*args, **kwargs) -> Any:
17        def wrapper(*args, **kwargs) -> Any:
18            timer = Timer(loops)
19            result = None
20            for _ in range(loops):
21                timer.start()
22                result = func(*args, **kwargs)
23                timer.stop()
24            print(
25                f"{func.__name__} {'average ' if loops > 1 else ''}execution time: {timer.average_elapsed_str}"
26            )
27            return result

Execute a query against the current database.

@argshell.with_parser(dbparsers.get_rename_column_parser)
def do_rename_column(self, args: argshell.argshell.Namespace):
215    @argshell.with_parser(dbparsers.get_rename_column_parser)
216    def do_rename_column(self, args: argshell.Namespace):
217        """Rename a column."""
218        with self._DB() as db:
219            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):
221    @argshell.with_parser(dbparsers.get_rename_table_parser)
222    def do_rename_table(self, args: argshell.Namespace):
223        """Rename a table."""
224        with self._DB() as db:
225            db.rename_table(args.table, args.new_name)

Rename a table.

def do_restore(self, file: str):
227    def do_restore(self, file: str):
228        """Replace the current db file with the given db backup file."""
229        backup = Pathier(file.strip('"'))
230        if not backup.exists():
231            print(f"{backup} does not exist.")
232        else:
233            print(f"Restoring from {file}...")
234            self.dbpath.write_bytes(backup.read_bytes())
235            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):
237    @argshell.with_parser(dbparsers.get_scan_dbs_parser)
238    def do_scan(self, args: argshell.Namespace):
239        """Scan the current working directory for database files."""
240        dbs = self._scan(args.extensions, args.recursive)
241        for db in dbs:
242            print(db.separate(Pathier.cwd().stem))

Scan the current working directory for database files.

def do_schema(*args, **kwargs) -> Any:
17        def wrapper(*args, **kwargs) -> Any:
18            timer = Timer(loops)
19            result = None
20            for _ in range(loops):
21                timer.start()
22                result = func(*args, **kwargs)
23                timer.stop()
24            print(
25                f"{func.__name__} {'average ' if loops > 1 else ''}execution time: {timer.average_elapsed_str}"
26            )
27            return result

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

def do_script(*args, **kwargs) -> Any:
17        def wrapper(*args, **kwargs) -> Any:
18            timer = Timer(loops)
19            result = None
20            for _ in range(loops):
21                timer.start()
22                result = func(*args, **kwargs)
23                timer.stop()
24            print(
25                f"{func.__name__} {'average ' if loops > 1 else ''}execution time: {timer.average_elapsed_str}"
26            )
27            return result

Execute the given SQL script.

def do_select(*args, **kwargs) -> Any:
17        def wrapper(*args, **kwargs) -> Any:
18            timer = Timer(loops)
19            result = None
20            for _ in range(loops):
21                timer.start()
22                result = func(*args, **kwargs)
23                timer.stop()
24            print(
25                f"{func.__name__} {'average ' if loops > 1 else ''}execution time: {timer.average_elapsed_str}"
26            )
27            return result

Execute a SELECT query with the given args.

def do_set_connection_timeout(self, seconds: str):
277    def do_set_connection_timeout(self, seconds: str):
278        """Set database connection timeout to this number of seconds."""
279        self.connection_timeout = float(seconds)

Set database connection timeout to this number of seconds.

def do_set_detect_types(self, should_detect: str):
281    def do_set_detect_types(self, should_detect: str):
282        """Pass a `1` to turn on and a `0` to turn off."""
283        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):
285    def do_set_enforce_foreign_keys(self, should_enforce: str):
286        """Pass a `1` to turn on and a `0` to turn off."""
287        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):
289    def do_size(self, _: str):
290        """Display the size of the the current db file."""
291        print(f"{self.dbpath.name} is {self.dbpath.formatted_size}.")

Display the size of the the current db file.

def do_tables(*args, **kwargs) -> Any:
17        def wrapper(*args, **kwargs) -> Any:
18            timer = Timer(loops)
19            result = None
20            for _ in range(loops):
21                timer.start()
22                result = func(*args, **kwargs)
23                timer.stop()
24            print(
25                f"{func.__name__} {'average ' if loops > 1 else ''}execution time: {timer.average_elapsed_str}"
26            )
27            return result

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

def do_update(*args, **kwargs) -> Any:
17        def wrapper(*args, **kwargs) -> Any:
18            timer = Timer(loops)
19            result = None
20            for _ in range(loops):
21                timer.start()
22                result = func(*args, **kwargs)
23                timer.stop()
24            print(
25                f"{func.__name__} {'average ' if loops > 1 else ''}execution time: {timer.average_elapsed_str}"
26            )
27            return result

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, dbname: str):
315    def do_use(self, dbname: str):
316        """Set which database file to use."""
317        dbpath = Pathier(dbname)
318        if not dbpath.exists():
319            print(f"{dbpath} does not exist.")
320            print(f"Still using {self.dbpath}")
321        elif not dbpath.is_file():
322            print(f"{dbpath} is not a file.")
323            print(f"Still using {self.dbpath}")
324        else:
325            self.dbpath = dbpath
326            self.prompt = f"{self.dbpath.name}>"

Set which database file to use.

def do_vacuum(*args, **kwargs) -> Any:
17        def wrapper(*args, **kwargs) -> Any:
18            timer = Timer(loops)
19            result = None
20            for _ in range(loops):
21                timer.start()
22                result = func(*args, **kwargs)
23                timer.stop()
24            print(
25                f"{func.__name__} {'average ' if loops > 1 else ''}execution time: {timer.average_elapsed_str}"
26            )
27            return result

Reduce database disk memory.

def do_views(*args, **kwargs) -> Any:
17        def wrapper(*args, **kwargs) -> Any:
18            timer = Timer(loops)
19            result = None
20            for _ in range(loops):
21                timer.start()
22                result = func(*args, **kwargs)
23                timer.stop()
24            print(
25                f"{func.__name__} {'average ' if loops > 1 else ''}execution time: {timer.average_elapsed_str}"
26            )
27            return result

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

def preloop(self):
376    def preloop(self):
377        """Scan the current directory for a .db file to use.
378        If not found, prompt the user for one or to try again recursively."""
379        if self.dbpath:
380            self.dbpath = Pathier(self.dbpath)
381            print(f"Defaulting to database {self.dbpath}")
382        else:
383            print("Searching for database...")
384            cwd = Pathier.cwd()
385            dbs = self._scan()
386            if len(dbs) == 1:
387                self.dbpath = dbs[0]
388                print(f"Using database {self.dbpath}.")
389            elif dbs:
390                self.dbpath = self._choose_db(dbs)
391            else:
392                print(f"Could not find a .db file in {cwd}.")
393                path = input(
394                    "Enter path to .db file to use or press enter to search again recursively: "
395                )
396                if path:
397                    self.dbpath = Pathier(path)
398                elif not path:
399                    print("Searching recursively...")
400                    dbs = self._scan(recursive=True)
401                    if len(dbs) == 1:
402                        self.dbpath = dbs[0]
403                        print(f"Using database {self.dbpath}.")
404                    elif dbs:
405                        self.dbpath = self._choose_db(dbs)
406                    else:
407                        print("Could not find a .db file.")
408                        self.dbpath = Pathier(input("Enter path to a .db file: "))
409        if not self.dbpath.exists():
410            raise FileNotFoundError(f"{self.dbpath} does not exist.")
411        if not self.dbpath.is_file():
412            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():
415def main():
416    DBShell().cmdloop()