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

Create a backup of the current db file.

def do_customize(self, name: str):
64    def do_customize(self, name: str):
65        """Generate a template file in the current working directory for creating a custom DBShell class.
66        Expects one argument: the name of the custom dbshell.
67        This will be used to name the generated file as well as several components in the file content.
68        """
69        try:
70            create_shell(name)
71        except Exception as e:
72            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):
74    def do_dbpath(self, _: str):
75        """Print the .db file in use."""
76        print(self.dbpath)

Print the .db file in use.

@argshell.with_parser(dbparsers.get_delete_parser)
def do_delete(self, args: argshell.argshell.Namespace):
78    @argshell.with_parser(dbparsers.get_delete_parser)
79    def do_delete(self, args: argshell.Namespace):
80        """Delete rows from the database.
81
82        Syntax:
83        >>> delete {table} {where}
84        >>> based>delete users "username LIKE '%chungus%"
85
86        ^will delete all rows in the 'users' table whose username contains 'chungus'^"""
87        print("Deleting records...")
88        with self._DB() as db:
89            num_rows = db.delete(args.table, args.where)
90            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):
92    def do_describe(self, tables: str):
93        """Describe each table in `tables`. If no table list is given, all tables will be described."""
94        with self._DB() as db:
95            table_list = tables.split() or db.tables
96            for table in table_list:
97                print(f"<{table}>")
98                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):
100    @argshell.with_parser(dbparsers.get_drop_column_parser)
101    def do_drop_column(self, args: argshell.Namespace):
102        """Drop the specified column from the specified table."""
103        with self._DB() as db:
104            db.drop_column(args.table, args.column)

Drop the specified column from the specified table.

def do_drop_table(self, table: str):
106    def do_drop_table(self, table: str):
107        """Drop the specified table."""
108        with self._DB() as db:
109            db.drop_table(table)

Drop the specified table.

def do_flush_log(self, _: str):
111    def do_flush_log(self, _: str):
112        """Clear the log file for this database."""
113        log_path = self.dbpath.with_name(self.dbpath.name.replace(".", "") + ".log")
114        if not log_path.exists():
115            print(f"No log file at path {log_path}")
116        else:
117            print(f"Flushing log...")
118            log_path.write_text("")

Clear the log file for this database.

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

Display help messages.

def do_properties(self, _: str):
131    def do_properties(self, _: str):
132        """See current database property settings."""
133        for property_ in ["connection_timeout", "detect_types", "enforce_foreign_keys"]:
134            print(f"{property_}: {getattr(self, property_)}")

See current database property settings.

def do_query(self, query: str):
136    def do_query(self, query: str):
137        """Execute a query against the current database."""
138        print(f"Executing {query}")
139        with self._DB() as db:
140            results = db.query(query)
141        self.display(results)
142        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):
144    @argshell.with_parser(dbparsers.get_rename_column_parser)
145    def do_rename_column(self, args: argshell.Namespace):
146        """Rename a column."""
147        with self._DB() as db:
148            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):
150    @argshell.with_parser(dbparsers.get_rename_table_parser)
151    def do_rename_table(self, args: argshell.Namespace):
152        """Rename a table."""
153        with self._DB() as db:
154            db.rename_table(args.table, args.new_name)

Rename a table.

def do_restore(self, file: str):
156    def do_restore(self, file: str):
157        """Replace the current db file with the given db backup file."""
158        backup = Pathier(file.strip('"'))
159        if not backup.exists():
160            print(f"{backup} does not exist.")
161        else:
162            print(f"Restoring from {file}...")
163            self.dbpath.write_bytes(backup.read_bytes())
164            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):
166    @argshell.with_parser(dbparsers.get_scan_dbs_parser)
167    def do_scan(self, args: argshell.Namespace):
168        """Scan the current working directory for database files."""
169        dbs = self._scan(args.extensions, args.recursive)
170        for db in dbs:
171            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):
173    @argshell.with_parser(dbparsers.get_schema_parser)
174    def do_schema(self, args: argshell.Namespace):
175        """Print out the names of the database tables, their columns, and, optionally, the number of rows."""
176        print("Getting database schema...")
177        with self._DB() as db:
178            tables = args.tables or db.tables
179            info = [
180                {
181                    "Table Name": table,
182                    "Columns": ", ".join(db.get_columns(table)),
183                    "Number of Rows": db.count(table) if args.rowcount else "n/a",
184                }
185                for table in tables
186            ]
187        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):
189    @argshell.with_parser(dbparsers.get_select_parser, [dbparsers.select_post_parser])
190    def do_select(self, args: argshell.Namespace):
191        """Execute a SELECT query with the given args."""
192        print(f"Querying {args.table}... ")
193        with self._DB() as db:
194            rows = db.select(
195                table=args.table,
196                columns=args.columns,
197                joins=args.joins,
198                where=args.where,
199                group_by=args.group_by,
200                having=args.Having,
201                order_by=args.order_by,
202                limit=args.limit,
203            )
204            print(f"Found {len(rows)} rows:")
205            self.display(rows)
206            print(f"{len(rows)} rows from {args.table}")

Execute a SELECT query with the given args.

def do_set_connection_timeout(self, seconds: str):
208    def do_set_connection_timeout(self, seconds: str):
209        """Set database connection timeout to this number of seconds."""
210        self.connection_timeout = float(seconds)

Set database connection timeout to this number of seconds.

def do_set_detect_types(self, should_detect: str):
212    def do_set_detect_types(self, should_detect: str):
213        """Pass a `1` to turn on and a `0` to turn off."""
214        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):
216    def do_set_enforce_foreign_keys(self, should_enforce: str):
217        """Pass a `1` to turn on and a `0` to turn off."""
218        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):
220    def do_size(self, _: str):
221        """Display the size of the the current db file."""
222        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):
224    @argshell.with_parser(dbparsers.get_update_parser)
225    def do_update(self, args: argshell.Namespace):
226        """Update a column to a new value.
227
228        Syntax:
229        >>> update {table} {column} {value} {where}
230        >>> based>update users username big_chungus "username = lil_chungus"
231
232        ^will update the username in the users 'table' to 'big_chungus' where the username is currently 'lil_chungus'^
233        """
234        print("Updating rows...")
235        with self._DB() as db:
236            num_updates = db.update(args.table, args.column, args.new_value, args.where)
237            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):
239    def do_use(self, arg: str):
240        """Set which database file to use."""
241        dbpath = Pathier(arg)
242        if not dbpath.exists():
243            print(f"{dbpath} does not exist.")
244            print(f"Still using {self.dbpath}")
245        elif not dbpath.is_file():
246            print(f"{dbpath} is not a file.")
247            print(f"Still using {self.dbpath}")
248        else:
249            self.dbpath = dbpath
250            self.prompt = f"{self.dbpath.name}>"

Set which database file to use.

def do_vacuum(self, _: str):
252    def do_vacuum(self, _: str):
253        """Reduce database disk memory."""
254        print(f"Database size before vacuuming: {self.dbpath.formatted_size}")
255        print("Vacuuming database...")
256        with self._DB() as db:
257            freedspace = db.vacuum()
258        print(f"Database size after vacuuming: {self.dbpath.formatted_size}")
259        print(f"Freed up {Pathier.format_bytes(freedspace)} of disk space.")

Reduce database disk memory.

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