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

Create a backup of the current db file.

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

Print the .db file in use.

@argshell.with_parser(dbparsers.get_delete_parser)
def do_delete(self, args: argshell.argshell.Namespace):
120    @argshell.with_parser(dbparsers.get_delete_parser)
121    def do_delete(self, args: argshell.Namespace):
122        """Delete rows from the database.
123
124        Syntax:
125        >>> delete {table} {where}
126        >>> based>delete users "username LIKE '%chungus%"
127
128        ^will delete all rows in the 'users' table whose username contains 'chungus'^"""
129        print("Deleting records...")
130        with self._DB() as db:
131            num_rows = db.delete(args.table, args.where)
132            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):
134    def do_describe(self, tables: str):
135        """Describe each table in `tables`. If no table list is given, all tables will be described."""
136        with self._DB() as db:
137            table_list = tables.split() or db.tables
138            for table in table_list:
139                print(f"<{table}>")
140                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):
142    @argshell.with_parser(dbparsers.get_drop_column_parser)
143    def do_drop_column(self, args: argshell.Namespace):
144        """Drop the specified column from the specified table."""
145        with self._DB() as db:
146            db.drop_column(args.table, args.column)

Drop the specified column from the specified table.

def do_drop_table(self, table: str):
148    def do_drop_table(self, table: str):
149        """Drop the specified table."""
150        with self._DB() as db:
151            db.drop_table(table)

Drop the specified table.

def do_flush_log(self, _: str):
153    def do_flush_log(self, _: str):
154        """Clear the log file for this database."""
155        log_path = self.dbpath.with_name(self.dbpath.name.replace(".", "") + ".log")
156        if not log_path.exists():
157            print(f"No log file at path {log_path}")
158        else:
159            print(f"Flushing log...")
160            log_path.write_text("")

Clear the log file for this database.

def do_help(self, args: str):
162    def do_help(self, args: str):
163        """Display help messages."""
164        super().do_help(args)
165        if args == "":
166            print("Unrecognized commands will be executed as queries.")
167            print(
168                "Use the `query` command explicitly if you don't want to capitalize your key words."
169            )
170            print("All transactions initiated by commands are committed immediately.")
171        print()

Display help messages.

def do_properties(self, _: str):
173    def do_properties(self, _: str):
174        """See current database property settings."""
175        for property_ in ["connection_timeout", "detect_types", "enforce_foreign_keys"]:
176            print(f"{property_}: {getattr(self, property_)}")

See current database property settings.

def do_query(self, query: str):
178    def do_query(self, query: str):
179        """Execute a query against the current database."""
180        print(f"Executing {query}")
181        with self._DB() as db:
182            results = db.query(query)
183        self.display(results)
184        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):
186    @argshell.with_parser(dbparsers.get_rename_column_parser)
187    def do_rename_column(self, args: argshell.Namespace):
188        """Rename a column."""
189        with self._DB() as db:
190            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):
192    @argshell.with_parser(dbparsers.get_rename_table_parser)
193    def do_rename_table(self, args: argshell.Namespace):
194        """Rename a table."""
195        with self._DB() as db:
196            db.rename_table(args.table, args.new_name)

Rename a table.

def do_restore(self, file: str):
198    def do_restore(self, file: str):
199        """Replace the current db file with the given db backup file."""
200        backup = Pathier(file.strip('"'))
201        if not backup.exists():
202            print(f"{backup} does not exist.")
203        else:
204            print(f"Restoring from {file}...")
205            self.dbpath.write_bytes(backup.read_bytes())
206            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):
208    @argshell.with_parser(dbparsers.get_scan_dbs_parser)
209    def do_scan(self, args: argshell.Namespace):
210        """Scan the current working directory for database files."""
211        dbs = self._scan(args.extensions, args.recursive)
212        for db in dbs:
213            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):
215    @argshell.with_parser(dbparsers.get_schema_parser)
216    def do_schema(self, args: argshell.Namespace):
217        """Print out the names of the database tables and views, their columns, and, optionally, the number of rows."""
218        self._show_tables(args)
219        self._show_views(args)

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

def do_script(self, path: str):
221    def do_script(self, path: str):
222        """Execute the given SQL script."""
223        with self._DB() as db:
224            self.display(db.execute_script(path))

Execute the given SQL script.

@argshell.with_parser(dbparsers.get_select_parser, [dbparsers.select_post_parser])
def do_select(self, args: argshell.argshell.Namespace):
226    @argshell.with_parser(dbparsers.get_select_parser, [dbparsers.select_post_parser])
227    def do_select(self, args: argshell.Namespace):
228        """Execute a SELECT query with the given args."""
229        print(f"Querying {args.table}... ")
230        with self._DB() as db:
231            rows = db.select(
232                table=args.table,
233                columns=args.columns,
234                joins=args.joins,
235                where=args.where,
236                group_by=args.group_by,
237                having=args.Having,
238                order_by=args.order_by,
239                limit=args.limit,
240            )
241            print(f"Found {len(rows)} rows:")
242            self.display(rows)
243            print(f"{len(rows)} rows from {args.table}")

Execute a SELECT query with the given args.

def do_set_connection_timeout(self, seconds: str):
245    def do_set_connection_timeout(self, seconds: str):
246        """Set database connection timeout to this number of seconds."""
247        self.connection_timeout = float(seconds)

Set database connection timeout to this number of seconds.

def do_set_detect_types(self, should_detect: str):
249    def do_set_detect_types(self, should_detect: str):
250        """Pass a `1` to turn on and a `0` to turn off."""
251        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):
253    def do_set_enforce_foreign_keys(self, should_enforce: str):
254        """Pass a `1` to turn on and a `0` to turn off."""
255        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):
257    def do_size(self, _: str):
258        """Display the size of the the current db file."""
259        print(f"{self.dbpath.name} is {self.dbpath.formatted_size}.")

Display the size of the the current db file.

@argshell.with_parser(dbparsers.get_schema_parser)
def do_tables(self, args: argshell.argshell.Namespace):
261    @argshell.with_parser(dbparsers.get_schema_parser)
262    def do_tables(self, args: argshell.Namespace):
263        """Print out the names of the database tables, their columns, and, optionally, the number of rows."""
264        self._show_tables(args)

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

@argshell.with_parser(dbparsers.get_update_parser)
def do_update(self, args: argshell.argshell.Namespace):
266    @argshell.with_parser(dbparsers.get_update_parser)
267    def do_update(self, args: argshell.Namespace):
268        """Update a column to a new value.
269
270        Syntax:
271        >>> update {table} {column} {value} {where}
272        >>> based>update users username big_chungus "username = lil_chungus"
273
274        ^will update the username in the users 'table' to 'big_chungus' where the username is currently 'lil_chungus'^
275        """
276        print("Updating rows...")
277        with self._DB() as db:
278            num_updates = db.update(args.table, args.column, args.new_value, args.where)
279            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):
281    def do_use(self, arg: str):
282        """Set which database file to use."""
283        dbpath = Pathier(arg)
284        if not dbpath.exists():
285            print(f"{dbpath} does not exist.")
286            print(f"Still using {self.dbpath}")
287        elif not dbpath.is_file():
288            print(f"{dbpath} is not a file.")
289            print(f"Still using {self.dbpath}")
290        else:
291            self.dbpath = dbpath
292            self.prompt = f"{self.dbpath.name}>"

Set which database file to use.

def do_vacuum(self, _: str):
294    def do_vacuum(self, _: str):
295        """Reduce database disk memory."""
296        print(f"Database size before vacuuming: {self.dbpath.formatted_size}")
297        print("Vacuuming database...")
298        with self._DB() as db:
299            freedspace = db.vacuum()
300        print(f"Database size after vacuuming: {self.dbpath.formatted_size}")
301        print(f"Freed up {Pathier.format_bytes(freedspace)} of disk space.")

Reduce database disk memory.

@argshell.with_parser(dbparsers.get_schema_parser)
def do_views(self, args: argshell.argshell.Namespace):
303    @argshell.with_parser(dbparsers.get_schema_parser)
304    def do_views(self, args: argshell.Namespace):
305        """Print out the names of the database views, their columns, and, optionally, the number of rows."""
306        self._show_views(args)

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

def preloop(self):
340    def preloop(self):
341        """Scan the current directory for a .db file to use.
342        If not found, prompt the user for one or to try again recursively."""
343        if self.dbpath:
344            self.dbpath = Pathier(self.dbpath)
345            print(f"Defaulting to database {self.dbpath}")
346        else:
347            print("Searching for database...")
348            cwd = Pathier.cwd()
349            dbs = self._scan()
350            if len(dbs) == 1:
351                self.dbpath = dbs[0]
352                print(f"Using database {self.dbpath}.")
353            elif dbs:
354                self.dbpath = self._choose_db(dbs)
355            else:
356                print(f"Could not find a .db file in {cwd}.")
357                path = input(
358                    "Enter path to .db file to use or press enter to search again recursively: "
359                )
360                if path:
361                    self.dbpath = Pathier(path)
362                elif not path:
363                    print("Searching recursively...")
364                    dbs = self._scan(recursive=True)
365                    if len(dbs) == 1:
366                        self.dbpath = dbs[0]
367                        print(f"Using database {self.dbpath}.")
368                    elif dbs:
369                        self.dbpath = self._choose_db(dbs)
370                    else:
371                        print("Could not find a .db file.")
372                        self.dbpath = Pathier(input("Enter path to a .db file: "))
373        if not self.dbpath.exists():
374            raise FileNotFoundError(f"{self.dbpath} does not exist.")
375        if not self.dbpath.is_file():
376            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():
379def main():
380    DBShell().cmdloop()