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    @argshell.with_parser(dbparsers.get_schema_parser)
131    def do_schema(self, args: argshell.Namespace):
132        """Print out the names of the database tables, their columns, and, optionally, the number of rows."""
133        print("Getting database schema...")
134        with self._DB() as db:
135            tables = args.tables or db.tables
136            info = [
137                {
138                    "Table Name": table,
139                    "Columns": ", ".join(db.get_columns(table)),
140                    "Number of Rows": db.count(table) if args.rowcount else "n/a",
141                }
142                for table in tables
143            ]
144        self.display(info)
145
146    def do_properties(self, _: str):
147        """See current database property settings."""
148        for property_ in ["connection_timeout", "detect_types", "enforce_foreign_keys"]:
149            print(f"{property_}: {getattr(self, property_)}")
150
151    def do_query(self, query: str):
152        """Execute a query against the current database."""
153        print(f"Executing {query}")
154        with self._DB() as db:
155            results = db.query(query)
156        self.display(results)
157        print(f"{db.cursor.rowcount} affected rows")
158
159    def do_restore(self, file: str):
160        """Replace the current db file with the given db backup file."""
161        backup = Pathier(file.strip('"'))
162        if not backup.exists():
163            print(f"{backup} does not exist.")
164        else:
165            print(f"Restoring from {file}...")
166            self.dbpath.write_bytes(backup.read_bytes())
167            print("Restore complete.")
168
169    @argshell.with_parser(dbparsers.get_scan_dbs_parser)
170    def do_scan(self, args: argshell.Namespace):
171        """Scan the current working directory for database files."""
172        dbs = self._scan(args.extensions, args.recursive)
173        for db in dbs:
174            print(db.separate(Pathier.cwd().stem))
175
176    @argshell.with_parser(dbparsers.get_select_parser, [dbparsers.select_post_parser])
177    def do_select(self, args: argshell.Namespace):
178        """Execute a SELECT query with the given args."""
179        print(f"Querying {args.table}... ")
180        with self._DB() as db:
181            rows = db.select(
182                table=args.table,
183                columns=args.columns,
184                joins=args.joins,
185                where=args.where,
186                group_by=args.group_by,
187                having=args.Having,
188                order_by=args.order_by,
189                limit=args.limit,
190            )
191            print(f"Found {len(rows)} rows:")
192            self.display(rows)
193            print(f"{len(rows)} rows from {args.table}")
194
195    def do_set_connection_timeout(self, seconds: str):
196        """Set database connection timeout to this number of seconds."""
197        self.connection_timeout = float(seconds)
198
199    def do_set_detect_types(self, should_detect: str):
200        """Pass a `1` to turn on and a `0` to turn off."""
201        self.detect_types = bool(int(should_detect))
202
203    def do_set_enforce_foreign_keys(self, should_enforce: str):
204        """Pass a `1` to turn on and a `0` to turn off."""
205        self.enforce_foreign_keys = bool(int(should_enforce))
206
207    def do_size(self, _: str):
208        """Display the size of the the current db file."""
209        print(f"{self.dbpath.name} is {self.dbpath.formatted_size}.")
210
211    @argshell.with_parser(dbparsers.get_update_parser)
212    def do_update(self, args: argshell.Namespace):
213        """Update a column to a new value.
214
215        Syntax:
216        >>> update {table} {column} {value} {where}
217        >>> based>update users username big_chungus "username = lil_chungus"
218
219        ^will update the username in the users 'table' to 'big_chungus' where the username is currently 'lil_chungus'^
220        """
221        print("Updating rows...")
222        with self._DB() as db:
223            num_updates = db.update(args.table, args.column, args.new_value, args.where)
224            print(f"Updated {num_updates} rows in table {args.table}.")
225
226    def do_use(self, arg: str):
227        """Set which database file to use."""
228        dbpath = Pathier(arg)
229        if not dbpath.exists():
230            print(f"{dbpath} does not exist.")
231            print(f"Still using {self.dbpath}")
232        elif not dbpath.is_file():
233            print(f"{dbpath} is not a file.")
234            print(f"Still using {self.dbpath}")
235        else:
236            self.dbpath = dbpath
237            self.prompt = f"{self.dbpath.name}>"
238
239    def do_vacuum(self, _: str):
240        """Reduce database disk memory."""
241        print(f"Database size before vacuuming: {self.dbpath.formatted_size}")
242        print("Vacuuming database...")
243        with self._DB() as db:
244            freedspace = db.vacuum()
245        print(f"Database size after vacuuming: {self.dbpath.formatted_size}")
246        print(f"Freed up {Pathier.format_bytes(freedspace)} of disk space.")
247
248    # Seat
249
250    def _choose_db(self, options: list[Pathier]) -> Pathier:
251        """Prompt the user to select from a list of files."""
252        cwd = Pathier.cwd()
253        paths = [path.separate(cwd.stem) for path in options]
254        while True:
255            print(
256                f"DB options:\n{' '.join([f'({i}) {path}' for i, path in enumerate(paths, 1)])}"
257            )
258            choice = input("Enter the number of the option to use: ")
259            try:
260                index = int(choice)
261                if not 1 <= index <= len(options):
262                    print("Choice out of range.")
263                    continue
264                return options[index - 1]
265            except Exception as e:
266                print(f"{choice} is not a valid option.")
267
268    def _scan(
269        self, extensions: list[str] = [".sqlite3", ".db"], recursive: bool = False
270    ) -> list[Pathier]:
271        cwd = Pathier.cwd()
272        dbs = []
273        globber = cwd.glob
274        if recursive:
275            globber = cwd.rglob
276        for extension in extensions:
277            dbs.extend(list(globber(f"*{extension}")))
278        return dbs
279
280    def preloop(self):
281        """Scan the current directory for a .db file to use.
282        If not found, prompt the user for one or to try again recursively."""
283        if self.dbpath:
284            self.dbpath = Pathier(self.dbpath)
285            print(f"Defaulting to database {self.dbpath}")
286        else:
287            print("Searching for database...")
288            cwd = Pathier.cwd()
289            dbs = self._scan()
290            if len(dbs) == 1:
291                self.dbpath = dbs[0]
292                print(f"Using database {self.dbpath}.")
293            elif dbs:
294                self.dbpath = self._choose_db(dbs)
295            else:
296                print(f"Could not find a .db file in {cwd}.")
297                path = input(
298                    "Enter path to .db file to use or press enter to search again recursively: "
299                )
300                if path:
301                    self.dbpath = Pathier(path)
302                elif not path:
303                    print("Searching recursively...")
304                    dbs = self._scan(recursive=True)
305                    if len(dbs) == 1:
306                        self.dbpath = dbs[0]
307                        print(f"Using database {self.dbpath}.")
308                    elif dbs:
309                        self.dbpath = self._choose_db(dbs)
310                    else:
311                        print("Could not find a .db file.")
312                        self.dbpath = Pathier(input("Enter path to a .db file: "))
313        if not self.dbpath.exists():
314            raise FileNotFoundError(f"{self.dbpath} does not exist.")
315        if not self.dbpath.is_file():
316            raise ValueError(f"{self.dbpath} is not a file.")
317
318
319def main():
320    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    @argshell.with_parser(dbparsers.get_schema_parser)
132    def do_schema(self, args: argshell.Namespace):
133        """Print out the names of the database tables, their columns, and, optionally, the number of rows."""
134        print("Getting database schema...")
135        with self._DB() as db:
136            tables = args.tables or db.tables
137            info = [
138                {
139                    "Table Name": table,
140                    "Columns": ", ".join(db.get_columns(table)),
141                    "Number of Rows": db.count(table) if args.rowcount else "n/a",
142                }
143                for table in tables
144            ]
145        self.display(info)
146
147    def do_properties(self, _: str):
148        """See current database property settings."""
149        for property_ in ["connection_timeout", "detect_types", "enforce_foreign_keys"]:
150            print(f"{property_}: {getattr(self, property_)}")
151
152    def do_query(self, query: str):
153        """Execute a query against the current database."""
154        print(f"Executing {query}")
155        with self._DB() as db:
156            results = db.query(query)
157        self.display(results)
158        print(f"{db.cursor.rowcount} affected rows")
159
160    def do_restore(self, file: str):
161        """Replace the current db file with the given db backup file."""
162        backup = Pathier(file.strip('"'))
163        if not backup.exists():
164            print(f"{backup} does not exist.")
165        else:
166            print(f"Restoring from {file}...")
167            self.dbpath.write_bytes(backup.read_bytes())
168            print("Restore complete.")
169
170    @argshell.with_parser(dbparsers.get_scan_dbs_parser)
171    def do_scan(self, args: argshell.Namespace):
172        """Scan the current working directory for database files."""
173        dbs = self._scan(args.extensions, args.recursive)
174        for db in dbs:
175            print(db.separate(Pathier.cwd().stem))
176
177    @argshell.with_parser(dbparsers.get_select_parser, [dbparsers.select_post_parser])
178    def do_select(self, args: argshell.Namespace):
179        """Execute a SELECT query with the given args."""
180        print(f"Querying {args.table}... ")
181        with self._DB() as db:
182            rows = db.select(
183                table=args.table,
184                columns=args.columns,
185                joins=args.joins,
186                where=args.where,
187                group_by=args.group_by,
188                having=args.Having,
189                order_by=args.order_by,
190                limit=args.limit,
191            )
192            print(f"Found {len(rows)} rows:")
193            self.display(rows)
194            print(f"{len(rows)} rows from {args.table}")
195
196    def do_set_connection_timeout(self, seconds: str):
197        """Set database connection timeout to this number of seconds."""
198        self.connection_timeout = float(seconds)
199
200    def do_set_detect_types(self, should_detect: str):
201        """Pass a `1` to turn on and a `0` to turn off."""
202        self.detect_types = bool(int(should_detect))
203
204    def do_set_enforce_foreign_keys(self, should_enforce: str):
205        """Pass a `1` to turn on and a `0` to turn off."""
206        self.enforce_foreign_keys = bool(int(should_enforce))
207
208    def do_size(self, _: str):
209        """Display the size of the the current db file."""
210        print(f"{self.dbpath.name} is {self.dbpath.formatted_size}.")
211
212    @argshell.with_parser(dbparsers.get_update_parser)
213    def do_update(self, args: argshell.Namespace):
214        """Update a column to a new value.
215
216        Syntax:
217        >>> update {table} {column} {value} {where}
218        >>> based>update users username big_chungus "username = lil_chungus"
219
220        ^will update the username in the users 'table' to 'big_chungus' where the username is currently 'lil_chungus'^
221        """
222        print("Updating rows...")
223        with self._DB() as db:
224            num_updates = db.update(args.table, args.column, args.new_value, args.where)
225            print(f"Updated {num_updates} rows in table {args.table}.")
226
227    def do_use(self, arg: str):
228        """Set which database file to use."""
229        dbpath = Pathier(arg)
230        if not dbpath.exists():
231            print(f"{dbpath} does not exist.")
232            print(f"Still using {self.dbpath}")
233        elif not dbpath.is_file():
234            print(f"{dbpath} is not a file.")
235            print(f"Still using {self.dbpath}")
236        else:
237            self.dbpath = dbpath
238            self.prompt = f"{self.dbpath.name}>"
239
240    def do_vacuum(self, _: str):
241        """Reduce database disk memory."""
242        print(f"Database size before vacuuming: {self.dbpath.formatted_size}")
243        print("Vacuuming database...")
244        with self._DB() as db:
245            freedspace = db.vacuum()
246        print(f"Database size after vacuuming: {self.dbpath.formatted_size}")
247        print(f"Freed up {Pathier.format_bytes(freedspace)} of disk space.")
248
249    # Seat
250
251    def _choose_db(self, options: list[Pathier]) -> Pathier:
252        """Prompt the user to select from a list of files."""
253        cwd = Pathier.cwd()
254        paths = [path.separate(cwd.stem) for path in options]
255        while True:
256            print(
257                f"DB options:\n{' '.join([f'({i}) {path}' for i, path in enumerate(paths, 1)])}"
258            )
259            choice = input("Enter the number of the option to use: ")
260            try:
261                index = int(choice)
262                if not 1 <= index <= len(options):
263                    print("Choice out of range.")
264                    continue
265                return options[index - 1]
266            except Exception as e:
267                print(f"{choice} is not a valid option.")
268
269    def _scan(
270        self, extensions: list[str] = [".sqlite3", ".db"], recursive: bool = False
271    ) -> list[Pathier]:
272        cwd = Pathier.cwd()
273        dbs = []
274        globber = cwd.glob
275        if recursive:
276            globber = cwd.rglob
277        for extension in extensions:
278            dbs.extend(list(globber(f"*{extension}")))
279        return dbs
280
281    def preloop(self):
282        """Scan the current directory for a .db file to use.
283        If not found, prompt the user for one or to try again recursively."""
284        if self.dbpath:
285            self.dbpath = Pathier(self.dbpath)
286            print(f"Defaulting to database {self.dbpath}")
287        else:
288            print("Searching for database...")
289            cwd = Pathier.cwd()
290            dbs = self._scan()
291            if len(dbs) == 1:
292                self.dbpath = dbs[0]
293                print(f"Using database {self.dbpath}.")
294            elif dbs:
295                self.dbpath = self._choose_db(dbs)
296            else:
297                print(f"Could not find a .db file in {cwd}.")
298                path = input(
299                    "Enter path to .db file to use or press enter to search again recursively: "
300                )
301                if path:
302                    self.dbpath = Pathier(path)
303                elif not path:
304                    print("Searching recursively...")
305                    dbs = self._scan(recursive=True)
306                    if len(dbs) == 1:
307                        self.dbpath = dbs[0]
308                        print(f"Using database {self.dbpath}.")
309                    elif dbs:
310                        self.dbpath = self._choose_db(dbs)
311                    else:
312                        print("Could not find a .db file.")
313                        self.dbpath = Pathier(input("Enter path to a .db file: "))
314        if not self.dbpath.exists():
315            raise FileNotFoundError(f"{self.dbpath} does not exist.")
316        if not self.dbpath.is_file():
317            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.

@argshell.with_parser(dbparsers.get_schema_parser)
def do_schema(self, args: argshell.argshell.Namespace):
131    @argshell.with_parser(dbparsers.get_schema_parser)
132    def do_schema(self, args: argshell.Namespace):
133        """Print out the names of the database tables, their columns, and, optionally, the number of rows."""
134        print("Getting database schema...")
135        with self._DB() as db:
136            tables = args.tables or db.tables
137            info = [
138                {
139                    "Table Name": table,
140                    "Columns": ", ".join(db.get_columns(table)),
141                    "Number of Rows": db.count(table) if args.rowcount else "n/a",
142                }
143                for table in tables
144            ]
145        self.display(info)

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

def do_properties(self, _: str):
147    def do_properties(self, _: str):
148        """See current database property settings."""
149        for property_ in ["connection_timeout", "detect_types", "enforce_foreign_keys"]:
150            print(f"{property_}: {getattr(self, property_)}")

See current database property settings.

def do_query(self, query: str):
152    def do_query(self, query: str):
153        """Execute a query against the current database."""
154        print(f"Executing {query}")
155        with self._DB() as db:
156            results = db.query(query)
157        self.display(results)
158        print(f"{db.cursor.rowcount} affected rows")

Execute a query against the current database.

def do_restore(self, file: str):
160    def do_restore(self, file: str):
161        """Replace the current db file with the given db backup file."""
162        backup = Pathier(file.strip('"'))
163        if not backup.exists():
164            print(f"{backup} does not exist.")
165        else:
166            print(f"Restoring from {file}...")
167            self.dbpath.write_bytes(backup.read_bytes())
168            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):
170    @argshell.with_parser(dbparsers.get_scan_dbs_parser)
171    def do_scan(self, args: argshell.Namespace):
172        """Scan the current working directory for database files."""
173        dbs = self._scan(args.extensions, args.recursive)
174        for db in dbs:
175            print(db.separate(Pathier.cwd().stem))

Scan the current working directory for database files.

@argshell.with_parser(dbparsers.get_select_parser, [dbparsers.select_post_parser])
def do_select(self, args: argshell.argshell.Namespace):
177    @argshell.with_parser(dbparsers.get_select_parser, [dbparsers.select_post_parser])
178    def do_select(self, args: argshell.Namespace):
179        """Execute a SELECT query with the given args."""
180        print(f"Querying {args.table}... ")
181        with self._DB() as db:
182            rows = db.select(
183                table=args.table,
184                columns=args.columns,
185                joins=args.joins,
186                where=args.where,
187                group_by=args.group_by,
188                having=args.Having,
189                order_by=args.order_by,
190                limit=args.limit,
191            )
192            print(f"Found {len(rows)} rows:")
193            self.display(rows)
194            print(f"{len(rows)} rows from {args.table}")

Execute a SELECT query with the given args.

def do_set_connection_timeout(self, seconds: str):
196    def do_set_connection_timeout(self, seconds: str):
197        """Set database connection timeout to this number of seconds."""
198        self.connection_timeout = float(seconds)

Set database connection timeout to this number of seconds.

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

Set which database file to use.

def do_vacuum(self, _: str):
240    def do_vacuum(self, _: str):
241        """Reduce database disk memory."""
242        print(f"Database size before vacuuming: {self.dbpath.formatted_size}")
243        print("Vacuuming database...")
244        with self._DB() as db:
245            freedspace = db.vacuum()
246        print(f"Database size after vacuuming: {self.dbpath.formatted_size}")
247        print(f"Freed up {Pathier.format_bytes(freedspace)} of disk space.")

Reduce database disk memory.

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