databased.dbshell

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

Subclass this to create custom ArgShells.

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

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

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

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

Print row data to terminal in a grid.

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

Add a new column to the specified tables.

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

Add a new table to the database.

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

Create a backup of the current db file.

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

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

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

Print the .db file in use.

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

Delete rows from the database.

Syntax:

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

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

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

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

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

Drop the specified column from the specified table.

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

Drop the specified table.

def do_script(self, path: str):
117    def do_script(self, path: str):
118        """Execute the given SQL script."""
119        with self._DB() as db:
120            self.display(db.execute_script(path))

Execute the given SQL script.

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

Clear the log file for this database.

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

Display help messages.

def do_properties(self, _: str):
142    def do_properties(self, _: str):
143        """See current database property settings."""
144        for property_ in ["connection_timeout", "detect_types", "enforce_foreign_keys"]:
145            print(f"{property_}: {getattr(self, property_)}")

See current database property settings.

def do_query(self, query: str):
147    def do_query(self, query: str):
148        """Execute a query against the current database."""
149        print(f"Executing {query}")
150        with self._DB() as db:
151            results = db.query(query)
152        self.display(results)
153        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):
155    @argshell.with_parser(dbparsers.get_rename_column_parser)
156    def do_rename_column(self, args: argshell.Namespace):
157        """Rename a column."""
158        with self._DB() as db:
159            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):
161    @argshell.with_parser(dbparsers.get_rename_table_parser)
162    def do_rename_table(self, args: argshell.Namespace):
163        """Rename a table."""
164        with self._DB() as db:
165            db.rename_table(args.table, args.new_name)

Rename a table.

def do_restore(self, file: str):
167    def do_restore(self, file: str):
168        """Replace the current db file with the given db backup file."""
169        backup = Pathier(file.strip('"'))
170        if not backup.exists():
171            print(f"{backup} does not exist.")
172        else:
173            print(f"Restoring from {file}...")
174            self.dbpath.write_bytes(backup.read_bytes())
175            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):
177    @argshell.with_parser(dbparsers.get_scan_dbs_parser)
178    def do_scan(self, args: argshell.Namespace):
179        """Scan the current working directory for database files."""
180        dbs = self._scan(args.extensions, args.recursive)
181        for db in dbs:
182            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):
184    @argshell.with_parser(dbparsers.get_schema_parser)
185    def do_schema(self, args: argshell.Namespace):
186        """Print out the names of the database tables, their columns, and, optionally, the number of rows."""
187        print("Getting database schema...")
188        with self._DB() as db:
189            tables = args.tables or db.tables
190            info = [
191                {
192                    "Table Name": table,
193                    "Columns": ", ".join(db.get_columns(table)),
194                    "Number of Rows": db.count(table) if args.rowcount else "n/a",
195                }
196                for table in tables
197            ]
198        self.display(info)

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

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

Execute a SELECT query with the given args.

def do_set_connection_timeout(self, seconds: str):
219    def do_set_connection_timeout(self, seconds: str):
220        """Set database connection timeout to this number of seconds."""
221        self.connection_timeout = float(seconds)

Set database connection timeout to this number of seconds.

def do_set_detect_types(self, should_detect: str):
223    def do_set_detect_types(self, should_detect: str):
224        """Pass a `1` to turn on and a `0` to turn off."""
225        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):
227    def do_set_enforce_foreign_keys(self, should_enforce: str):
228        """Pass a `1` to turn on and a `0` to turn off."""
229        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):
231    def do_size(self, _: str):
232        """Display the size of the the current db file."""
233        print(f"{self.dbpath.name} is {self.dbpath.formatted_size}.")

Display the size of the the current db file.

@argshell.with_parser(dbparsers.get_update_parser)
def do_update(self, args: argshell.argshell.Namespace):
235    @argshell.with_parser(dbparsers.get_update_parser)
236    def do_update(self, args: argshell.Namespace):
237        """Update a column to a new value.
238
239        Syntax:
240        >>> update {table} {column} {value} {where}
241        >>> based>update users username big_chungus "username = lil_chungus"
242
243        ^will update the username in the users 'table' to 'big_chungus' where the username is currently 'lil_chungus'^
244        """
245        print("Updating rows...")
246        with self._DB() as db:
247            num_updates = db.update(args.table, args.column, args.new_value, args.where)
248            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):
250    def do_use(self, arg: str):
251        """Set which database file to use."""
252        dbpath = Pathier(arg)
253        if not dbpath.exists():
254            print(f"{dbpath} does not exist.")
255            print(f"Still using {self.dbpath}")
256        elif not dbpath.is_file():
257            print(f"{dbpath} is not a file.")
258            print(f"Still using {self.dbpath}")
259        else:
260            self.dbpath = dbpath
261            self.prompt = f"{self.dbpath.name}>"

Set which database file to use.

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

Reduce database disk memory.

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