databased.dbshell

  1import argshell
  2from griddle import griddy
  3from pathier import Pathier
  4
  5from databased import DataBased, create_shell, dbparsers
  6
  7
  8class DBShell(argshell.ArgShell):
  9    intro = "Starting dbshell (enter help or ? for arg info)..."
 10    prompt = "based>"
 11    dbpath: Pathier = None  # type: ignore
 12
 13    def do_use_db(self, arg: str):
 14        """Set which database file to use."""
 15        dbpath = Pathier(arg)
 16        if not dbpath.exists():
 17            print(f"{dbpath} does not exist.")
 18            print(f"Still using {self.dbpath}")
 19        elif not dbpath.is_file():
 20            print(f"{dbpath} is not a file.")
 21            print(f"Still using {self.dbpath}")
 22        else:
 23            self.dbpath = dbpath
 24
 25    def do_dbpath(self, arg: str):
 26        """Print the .db file in use."""
 27        print(self.dbpath)
 28
 29    @argshell.with_parser(dbparsers.get_backup_parser)
 30    def do_backup(self, args: argshell.Namespace):
 31        """Create a backup of the current db file."""
 32        print(f"Creating a back up for {self.dbpath}...")
 33        backup_path = self.dbpath.backup(args.timestamp)
 34        print("Creating backup is complete.")
 35        print(f"Backup path: {backup_path}")
 36
 37    def do_size(self, arg: str):
 38        """Display the size of the the current db file."""
 39        print(f"{self.dbpath.name} is {self.dbpath.formatted_size}.")
 40
 41    @argshell.with_parser(dbparsers.get_create_table_parser)
 42    def do_add_table(self, args: argshell.Namespace):
 43        """Add a new table to the database."""
 44        with DataBased(self.dbpath) as db:
 45            db.create_table(args.table_name, args.columns)
 46
 47    def do_drop_table(self, arg: str):
 48        """Drop the specified table."""
 49        with DataBased(self.dbpath) as db:
 50            db.drop_table(arg)
 51
 52    @argshell.with_parser(
 53        dbparsers.get_add_row_parser, [dbparsers.verify_matching_length]
 54    )
 55    def do_add_row(self, args: argshell.Namespace):
 56        """Add a row to a table."""
 57        with DataBased(self.dbpath) as db:
 58            if db.add_row(args.table_name, args.values, args.columns or None):
 59                print(f"Added row to {args.table_name} table successfully.")
 60            else:
 61                print(f"Failed to add row to {args.table_name} table.")
 62
 63    @argshell.with_parser(dbparsers.get_info_parser)
 64    def do_info(self, args: argshell.Namespace):
 65        """Print out the names of the database tables, their columns, and, optionally, the number of rows."""
 66        print("Getting database info...")
 67        with DataBased(self.dbpath) as db:
 68            tables = args.tables or db.get_table_names()
 69            info = [
 70                {
 71                    "Table Name": table,
 72                    "Columns": ", ".join(db.get_column_names(table)),
 73                    "Number of Rows": db.count(table) if args.rowcount else "n/a",
 74                }
 75                for table in tables
 76            ]
 77        print(DataBased.data_to_string(info))
 78
 79    @argshell.with_parser(dbparsers.get_lookup_parser, [dbparsers.convert_match_pairs])
 80    def do_show(self, args: argshell.Namespace):
 81        """Find and print rows from the database.
 82        Use the -t/--tables, -m/--match_pairs, and -l/--limit flags to limit the search.
 83        Use the -c/--columns flag to limit what columns are printed.
 84        Use the -o/--order_by flag to order the results.
 85        Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs
 86        Pass -h/--help flag for parser help."""
 87        print("Finding records... ")
 88        if len(args.columns) == 0:
 89            args.columns = None
 90        with DataBased(self.dbpath) as db:
 91            tables = args.tables or db.get_table_names()
 92            for table in tables:
 93                results = db.get_rows(
 94                    table,
 95                    args.match_pairs,
 96                    columns_to_return=args.columns,
 97                    order_by=args.order_by,
 98                    limit=args.limit,
 99                    exact_match=not args.partial_matching,
100                )
101                db.close()
102                print(f"{len(results)} matching rows in {table} table.")
103                try:
104                    print(DataBased.data_to_string(results))  # type: ignore
105                except Exception as e:
106                    print("Couldn't fit data into a grid.")
107                    print(*results, sep="\n")
108                if results:
109                    print(f"{len(results)} matching rows in {table} table.")
110                print()
111
112    @argshell.with_parser(dbparsers.get_search_parser)
113    def do_search(self, args: argshell.Namespace):
114        """Search and return any rows containg the searched substring in any of its columns.
115        Use the -t/--tables flag to limit the search to a specific table(s).
116        Use the -c/--columns flag to limit the search to a specific column(s)."""
117        print(f"Searching for {args.search_string}...")
118        with DataBased(self.dbpath) as db:
119            tables = args.tables or db.get_table_names()
120            for table in tables:
121                columns = args.columns or db.get_column_names(table)
122                matcher = " OR ".join(
123                    f'{column} LIKE "%{args.search_string}%"' for column in columns
124                )
125                query = f"SELECT * FROM {table} WHERE {matcher};"
126                results = db.query(query)
127                results = [db._get_dict(table, result) for result in results]
128                print(f"Found {len(results)} results in {table} table.")
129                print(DataBased.data_to_string(results))
130
131    @argshell.with_parser(dbparsers.get_lookup_parser, [dbparsers.convert_match_pairs])
132    def do_count(self, args: argshell.Namespace):
133        """Print the number of rows in the database.
134        Use the -t/--tables flag to limit results to a specific table(s).
135        Use the -m/--match_pairs flag to limit the results to rows matching these criteria.
136        Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs.
137        Pass -h/--help flag for parser help."""
138        print("Counting rows...")
139        with DataBased(self.dbpath) as db:
140            tables = args.tables or db.get_table_names()
141            for table in tables:
142                num_rows = db.count(table, args.match_pairs, not args.partial_matching)
143                print(f"{num_rows} matching rows in {table} table.")
144
145    def do_query(self, arg: str):
146        """Execute a query against the current database."""
147        print(f"Executing {arg}")
148        with DataBased(self.dbpath) as db:
149            results = db.query(arg)
150        try:
151            try:
152                print(griddy(results))
153            except Exception as e:
154                for result in results:
155                    print(*result, sep="|-|")
156        except Exception as e:
157            print(f"{type(e).__name__}: {e}")
158        print(f"{db.cursor.rowcount} affected rows")
159
160    @argshell.with_parser(dbparsers.get_update_parser, [dbparsers.convert_match_pairs])
161    def do_update(self, args: argshell.Namespace):
162        """Update a column to a new value.
163        Two required args: the column (-c/--column) to update and the value (-v/--value) to update to.
164        Use the -t/--tables flag to limit what tables are updated.
165        Use the -m/--match_pairs flag to specify which rows are updated.
166        Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs.
167        >>> based>update -c username -v big_chungus -t users -m username lil_chungus
168
169        ^will update the username in the users 'table' to 'big_chungus' where the username is currently 'lil_chungus'^"""
170        print("Updating rows...")
171        with DataBased(self.dbpath) as db:
172            tables = args.tables or db.get_table_names()
173            for table in tables:
174                num_updates = db.update(
175                    table,
176                    args.column,
177                    args.new_value,
178                    args.match_pairs,
179                    not args.partial_matching,
180                )
181                print(f"Updated {num_updates} rows in {table} table.")
182
183    @argshell.with_parser(dbparsers.get_lookup_parser, [dbparsers.convert_match_pairs])
184    def do_delete(self, args: argshell.Namespace):
185        """Delete rows from the database.
186        Use the -t/--tables flag to limit what tables rows are deleted from.
187        Use the -m/--match_pairs flag to specify which rows are deleted.
188        Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs.
189        >>> based>delete -t users -m username chungus -p
190
191        ^will delete all rows in the 'users' table whose username contains 'chungus'^"""
192        print("Deleting records...")
193        with DataBased(self.dbpath) as db:
194            tables = args.tables or db.get_table_names()
195            for table in tables:
196                num_rows = db.delete(table, args.match_pairs, not args.partial_matching)
197                print(f"Deleted {num_rows} rows from {table} table.")
198
199    @argshell.with_parser(dbparsers.get_add_column_parser)
200    def do_add_column(self, args: argshell.Namespace):
201        """Add a new column to the specified tables."""
202        with DataBased(self.dbpath) as db:
203            tables = args.tables or db.get_table_names()
204            for table in tables:
205                db.add_column(table, args.column_name, args.type, args.default_value)
206
207    def do_flush_log(self, arg: str):
208        """Clear the log file for this database."""
209        log_path = self.dbpath.with_name(self.dbpath.stem + "db.log")
210        if not log_path.exists():
211            print(f"No log file at path {log_path}")
212        else:
213            print(f"Flushing log...")
214            log_path.write_text("")
215
216    def do_scan_dbs(self, arg: str):
217        """Scan the current working directory for `*.db` files and display them.
218
219        If the command is entered as `based>scan_dbs r`, the scan will be performed recursively."""
220        cwd = Pathier.cwd()
221        if arg.strip() == "r":
222            dbs = cwd.rglob("*.db")
223        else:
224            dbs = cwd.glob("*.db")
225        for db in dbs:
226            print(db.separate(cwd.stem))
227
228    def do_customize(self, arg: str):
229        """Generate a template file in the current working directory for creating a custom DBShell class.
230        Expects one argument: the name of the custom dbshell.
231        This will be used to name the generated file as well as several components in the file content."""
232        try:
233            create_shell(arg)
234        except Exception as e:
235            print(f"{type(e).__name__}: {e}")
236
237    def do_vacuum(self, arg: str):
238        """Reduce database disk memory."""
239        starting_size = self.dbpath.size
240        print(f"Database size before vacuuming: {self.dbpath.formatted_size}")
241        print("Vacuuming database...")
242        with DataBased(self.dbpath) as db:
243            freedspace = db.vacuum()
244        print(f"Database size after vacuuming: {self.dbpath.formatted_size}")
245        print(f"Freed up {Pathier.format_bytes(freedspace)} of disk space.")
246
247    def _choose_db(self, options: list[Pathier]) -> Pathier:
248        """Prompt the user to select from a list of files."""
249        cwd = Pathier.cwd()
250        paths = [path.separate(cwd.stem) for path in options]
251        while True:
252            print(
253                f"DB options:\n{' '.join([f'({i}) {path}' for i,path in enumerate(paths,1)])}"
254            )
255            choice = input("Enter the number of the option to use: ")
256            try:
257                index = int(choice)
258                if not 1 <= index <= len(options):
259                    print("Choice out of range.")
260                    continue
261                return options[index - 1]
262            except Exception as e:
263                print(f"{choice} is not a valid option.")
264
265    def preloop(self):
266        """Scan the current directory for a .db file to use.
267        If not found, prompt the user for one or to try again recursively."""
268        if self.dbpath:
269            self.dbpath = Pathier(self.dbpath)
270            print(f"Defaulting to database {self.dbpath}")
271        else:
272            print("Searching for database...")
273            cwd = Pathier.cwd()
274            dbs = list(cwd.glob("*.db"))
275            if len(dbs) == 1:
276                self.dbpath = dbs[0]
277                print(f"Using database {self.dbpath}.")
278            elif dbs:
279                self.dbpath = self._choose_db(dbs)
280            else:
281                print(f"Could not find a .db file in {cwd}.")
282                path = input(
283                    "Enter path to .db file to use or press enter to search again recursively: "
284                )
285                if path:
286                    self.dbpath = Pathier(path)
287                elif not path:
288                    print("Searching recursively...")
289                    dbs = list(cwd.rglob("*.db"))
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("Could not find a .db file.")
297                        self.dbpath = Pathier(input("Enter path to a .db file: "))
298        if not self.dbpath.exists():
299            raise FileNotFoundError(f"{self.dbpath} does not exist.")
300        if not self.dbpath.is_file():
301            raise ValueError(f"{self.dbpath} is not a file.")
302
303
304def main():
305    DBShell().cmdloop()
class DBShell(argshell.argshell.ArgShell):
  9class DBShell(argshell.ArgShell):
 10    intro = "Starting dbshell (enter help or ? for arg info)..."
 11    prompt = "based>"
 12    dbpath: Pathier = None  # type: ignore
 13
 14    def do_use_db(self, arg: str):
 15        """Set which database file to use."""
 16        dbpath = Pathier(arg)
 17        if not dbpath.exists():
 18            print(f"{dbpath} does not exist.")
 19            print(f"Still using {self.dbpath}")
 20        elif not dbpath.is_file():
 21            print(f"{dbpath} is not a file.")
 22            print(f"Still using {self.dbpath}")
 23        else:
 24            self.dbpath = dbpath
 25
 26    def do_dbpath(self, arg: str):
 27        """Print the .db file in use."""
 28        print(self.dbpath)
 29
 30    @argshell.with_parser(dbparsers.get_backup_parser)
 31    def do_backup(self, args: argshell.Namespace):
 32        """Create a backup of the current db file."""
 33        print(f"Creating a back up for {self.dbpath}...")
 34        backup_path = self.dbpath.backup(args.timestamp)
 35        print("Creating backup is complete.")
 36        print(f"Backup path: {backup_path}")
 37
 38    def do_size(self, arg: str):
 39        """Display the size of the the current db file."""
 40        print(f"{self.dbpath.name} is {self.dbpath.formatted_size}.")
 41
 42    @argshell.with_parser(dbparsers.get_create_table_parser)
 43    def do_add_table(self, args: argshell.Namespace):
 44        """Add a new table to the database."""
 45        with DataBased(self.dbpath) as db:
 46            db.create_table(args.table_name, args.columns)
 47
 48    def do_drop_table(self, arg: str):
 49        """Drop the specified table."""
 50        with DataBased(self.dbpath) as db:
 51            db.drop_table(arg)
 52
 53    @argshell.with_parser(
 54        dbparsers.get_add_row_parser, [dbparsers.verify_matching_length]
 55    )
 56    def do_add_row(self, args: argshell.Namespace):
 57        """Add a row to a table."""
 58        with DataBased(self.dbpath) as db:
 59            if db.add_row(args.table_name, args.values, args.columns or None):
 60                print(f"Added row to {args.table_name} table successfully.")
 61            else:
 62                print(f"Failed to add row to {args.table_name} table.")
 63
 64    @argshell.with_parser(dbparsers.get_info_parser)
 65    def do_info(self, args: argshell.Namespace):
 66        """Print out the names of the database tables, their columns, and, optionally, the number of rows."""
 67        print("Getting database info...")
 68        with DataBased(self.dbpath) as db:
 69            tables = args.tables or db.get_table_names()
 70            info = [
 71                {
 72                    "Table Name": table,
 73                    "Columns": ", ".join(db.get_column_names(table)),
 74                    "Number of Rows": db.count(table) if args.rowcount else "n/a",
 75                }
 76                for table in tables
 77            ]
 78        print(DataBased.data_to_string(info))
 79
 80    @argshell.with_parser(dbparsers.get_lookup_parser, [dbparsers.convert_match_pairs])
 81    def do_show(self, args: argshell.Namespace):
 82        """Find and print rows from the database.
 83        Use the -t/--tables, -m/--match_pairs, and -l/--limit flags to limit the search.
 84        Use the -c/--columns flag to limit what columns are printed.
 85        Use the -o/--order_by flag to order the results.
 86        Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs
 87        Pass -h/--help flag for parser help."""
 88        print("Finding records... ")
 89        if len(args.columns) == 0:
 90            args.columns = None
 91        with DataBased(self.dbpath) as db:
 92            tables = args.tables or db.get_table_names()
 93            for table in tables:
 94                results = db.get_rows(
 95                    table,
 96                    args.match_pairs,
 97                    columns_to_return=args.columns,
 98                    order_by=args.order_by,
 99                    limit=args.limit,
100                    exact_match=not args.partial_matching,
101                )
102                db.close()
103                print(f"{len(results)} matching rows in {table} table.")
104                try:
105                    print(DataBased.data_to_string(results))  # type: ignore
106                except Exception as e:
107                    print("Couldn't fit data into a grid.")
108                    print(*results, sep="\n")
109                if results:
110                    print(f"{len(results)} matching rows in {table} table.")
111                print()
112
113    @argshell.with_parser(dbparsers.get_search_parser)
114    def do_search(self, args: argshell.Namespace):
115        """Search and return any rows containg the searched substring in any of its columns.
116        Use the -t/--tables flag to limit the search to a specific table(s).
117        Use the -c/--columns flag to limit the search to a specific column(s)."""
118        print(f"Searching for {args.search_string}...")
119        with DataBased(self.dbpath) as db:
120            tables = args.tables or db.get_table_names()
121            for table in tables:
122                columns = args.columns or db.get_column_names(table)
123                matcher = " OR ".join(
124                    f'{column} LIKE "%{args.search_string}%"' for column in columns
125                )
126                query = f"SELECT * FROM {table} WHERE {matcher};"
127                results = db.query(query)
128                results = [db._get_dict(table, result) for result in results]
129                print(f"Found {len(results)} results in {table} table.")
130                print(DataBased.data_to_string(results))
131
132    @argshell.with_parser(dbparsers.get_lookup_parser, [dbparsers.convert_match_pairs])
133    def do_count(self, args: argshell.Namespace):
134        """Print the number of rows in the database.
135        Use the -t/--tables flag to limit results to a specific table(s).
136        Use the -m/--match_pairs flag to limit the results to rows matching these criteria.
137        Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs.
138        Pass -h/--help flag for parser help."""
139        print("Counting rows...")
140        with DataBased(self.dbpath) as db:
141            tables = args.tables or db.get_table_names()
142            for table in tables:
143                num_rows = db.count(table, args.match_pairs, not args.partial_matching)
144                print(f"{num_rows} matching rows in {table} table.")
145
146    def do_query(self, arg: str):
147        """Execute a query against the current database."""
148        print(f"Executing {arg}")
149        with DataBased(self.dbpath) as db:
150            results = db.query(arg)
151        try:
152            try:
153                print(griddy(results))
154            except Exception as e:
155                for result in results:
156                    print(*result, sep="|-|")
157        except Exception as e:
158            print(f"{type(e).__name__}: {e}")
159        print(f"{db.cursor.rowcount} affected rows")
160
161    @argshell.with_parser(dbparsers.get_update_parser, [dbparsers.convert_match_pairs])
162    def do_update(self, args: argshell.Namespace):
163        """Update a column to a new value.
164        Two required args: the column (-c/--column) to update and the value (-v/--value) to update to.
165        Use the -t/--tables flag to limit what tables are updated.
166        Use the -m/--match_pairs flag to specify which rows are updated.
167        Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs.
168        >>> based>update -c username -v big_chungus -t users -m username lil_chungus
169
170        ^will update the username in the users 'table' to 'big_chungus' where the username is currently 'lil_chungus'^"""
171        print("Updating rows...")
172        with DataBased(self.dbpath) as db:
173            tables = args.tables or db.get_table_names()
174            for table in tables:
175                num_updates = db.update(
176                    table,
177                    args.column,
178                    args.new_value,
179                    args.match_pairs,
180                    not args.partial_matching,
181                )
182                print(f"Updated {num_updates} rows in {table} table.")
183
184    @argshell.with_parser(dbparsers.get_lookup_parser, [dbparsers.convert_match_pairs])
185    def do_delete(self, args: argshell.Namespace):
186        """Delete rows from the database.
187        Use the -t/--tables flag to limit what tables rows are deleted from.
188        Use the -m/--match_pairs flag to specify which rows are deleted.
189        Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs.
190        >>> based>delete -t users -m username chungus -p
191
192        ^will delete all rows in the 'users' table whose username contains 'chungus'^"""
193        print("Deleting records...")
194        with DataBased(self.dbpath) as db:
195            tables = args.tables or db.get_table_names()
196            for table in tables:
197                num_rows = db.delete(table, args.match_pairs, not args.partial_matching)
198                print(f"Deleted {num_rows} rows from {table} table.")
199
200    @argshell.with_parser(dbparsers.get_add_column_parser)
201    def do_add_column(self, args: argshell.Namespace):
202        """Add a new column to the specified tables."""
203        with DataBased(self.dbpath) as db:
204            tables = args.tables or db.get_table_names()
205            for table in tables:
206                db.add_column(table, args.column_name, args.type, args.default_value)
207
208    def do_flush_log(self, arg: str):
209        """Clear the log file for this database."""
210        log_path = self.dbpath.with_name(self.dbpath.stem + "db.log")
211        if not log_path.exists():
212            print(f"No log file at path {log_path}")
213        else:
214            print(f"Flushing log...")
215            log_path.write_text("")
216
217    def do_scan_dbs(self, arg: str):
218        """Scan the current working directory for `*.db` files and display them.
219
220        If the command is entered as `based>scan_dbs r`, the scan will be performed recursively."""
221        cwd = Pathier.cwd()
222        if arg.strip() == "r":
223            dbs = cwd.rglob("*.db")
224        else:
225            dbs = cwd.glob("*.db")
226        for db in dbs:
227            print(db.separate(cwd.stem))
228
229    def do_customize(self, arg: str):
230        """Generate a template file in the current working directory for creating a custom DBShell class.
231        Expects one argument: the name of the custom dbshell.
232        This will be used to name the generated file as well as several components in the file content."""
233        try:
234            create_shell(arg)
235        except Exception as e:
236            print(f"{type(e).__name__}: {e}")
237
238    def do_vacuum(self, arg: str):
239        """Reduce database disk memory."""
240        starting_size = self.dbpath.size
241        print(f"Database size before vacuuming: {self.dbpath.formatted_size}")
242        print("Vacuuming database...")
243        with DataBased(self.dbpath) 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    def _choose_db(self, options: list[Pathier]) -> Pathier:
249        """Prompt the user to select from a list of files."""
250        cwd = Pathier.cwd()
251        paths = [path.separate(cwd.stem) for path in options]
252        while True:
253            print(
254                f"DB options:\n{' '.join([f'({i}) {path}' for i,path in enumerate(paths,1)])}"
255            )
256            choice = input("Enter the number of the option to use: ")
257            try:
258                index = int(choice)
259                if not 1 <= index <= len(options):
260                    print("Choice out of range.")
261                    continue
262                return options[index - 1]
263            except Exception as e:
264                print(f"{choice} is not a valid option.")
265
266    def preloop(self):
267        """Scan the current directory for a .db file to use.
268        If not found, prompt the user for one or to try again recursively."""
269        if self.dbpath:
270            self.dbpath = Pathier(self.dbpath)
271            print(f"Defaulting to database {self.dbpath}")
272        else:
273            print("Searching for database...")
274            cwd = Pathier.cwd()
275            dbs = list(cwd.glob("*.db"))
276            if len(dbs) == 1:
277                self.dbpath = dbs[0]
278                print(f"Using database {self.dbpath}.")
279            elif dbs:
280                self.dbpath = self._choose_db(dbs)
281            else:
282                print(f"Could not find a .db file in {cwd}.")
283                path = input(
284                    "Enter path to .db file to use or press enter to search again recursively: "
285                )
286                if path:
287                    self.dbpath = Pathier(path)
288                elif not path:
289                    print("Searching recursively...")
290                    dbs = list(cwd.rglob("*.db"))
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("Could not find a .db file.")
298                        self.dbpath = Pathier(input("Enter path to a .db file: "))
299        if not self.dbpath.exists():
300            raise FileNotFoundError(f"{self.dbpath} does not exist.")
301        if not self.dbpath.is_file():
302            raise ValueError(f"{self.dbpath} is not a file.")

Subclass this to create custom ArgShells.

def do_use_db(self, arg: str):
14    def do_use_db(self, arg: str):
15        """Set which database file to use."""
16        dbpath = Pathier(arg)
17        if not dbpath.exists():
18            print(f"{dbpath} does not exist.")
19            print(f"Still using {self.dbpath}")
20        elif not dbpath.is_file():
21            print(f"{dbpath} is not a file.")
22            print(f"Still using {self.dbpath}")
23        else:
24            self.dbpath = dbpath

Set which database file to use.

def do_dbpath(self, arg: str):
26    def do_dbpath(self, arg: str):
27        """Print the .db file in use."""
28        print(self.dbpath)

Print the .db file in use.

@argshell.with_parser(dbparsers.get_backup_parser)
def do_backup(self, args: argshell.argshell.Namespace):
30    @argshell.with_parser(dbparsers.get_backup_parser)
31    def do_backup(self, args: argshell.Namespace):
32        """Create a backup of the current db file."""
33        print(f"Creating a back up for {self.dbpath}...")
34        backup_path = self.dbpath.backup(args.timestamp)
35        print("Creating backup is complete.")
36        print(f"Backup path: {backup_path}")

Create a backup of the current db file.

def do_size(self, arg: str):
38    def do_size(self, arg: str):
39        """Display the size of the the current db file."""
40        print(f"{self.dbpath.name} is {self.dbpath.formatted_size}.")

Display the size of the the current db file.

@argshell.with_parser(dbparsers.get_create_table_parser)
def do_add_table(self, args: argshell.argshell.Namespace):
42    @argshell.with_parser(dbparsers.get_create_table_parser)
43    def do_add_table(self, args: argshell.Namespace):
44        """Add a new table to the database."""
45        with DataBased(self.dbpath) as db:
46            db.create_table(args.table_name, args.columns)

Add a new table to the database.

def do_drop_table(self, arg: str):
48    def do_drop_table(self, arg: str):
49        """Drop the specified table."""
50        with DataBased(self.dbpath) as db:
51            db.drop_table(arg)

Drop the specified table.

@argshell.with_parser(dbparsers.get_add_row_parser, [dbparsers.verify_matching_length])
def do_add_row(self, args: argshell.argshell.Namespace):
53    @argshell.with_parser(
54        dbparsers.get_add_row_parser, [dbparsers.verify_matching_length]
55    )
56    def do_add_row(self, args: argshell.Namespace):
57        """Add a row to a table."""
58        with DataBased(self.dbpath) as db:
59            if db.add_row(args.table_name, args.values, args.columns or None):
60                print(f"Added row to {args.table_name} table successfully.")
61            else:
62                print(f"Failed to add row to {args.table_name} table.")

Add a row to a table.

@argshell.with_parser(dbparsers.get_info_parser)
def do_info(self, args: argshell.argshell.Namespace):
64    @argshell.with_parser(dbparsers.get_info_parser)
65    def do_info(self, args: argshell.Namespace):
66        """Print out the names of the database tables, their columns, and, optionally, the number of rows."""
67        print("Getting database info...")
68        with DataBased(self.dbpath) as db:
69            tables = args.tables or db.get_table_names()
70            info = [
71                {
72                    "Table Name": table,
73                    "Columns": ", ".join(db.get_column_names(table)),
74                    "Number of Rows": db.count(table) if args.rowcount else "n/a",
75                }
76                for table in tables
77            ]
78        print(DataBased.data_to_string(info))

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

@argshell.with_parser(dbparsers.get_lookup_parser, [dbparsers.convert_match_pairs])
def do_show(self, args: argshell.argshell.Namespace):
 80    @argshell.with_parser(dbparsers.get_lookup_parser, [dbparsers.convert_match_pairs])
 81    def do_show(self, args: argshell.Namespace):
 82        """Find and print rows from the database.
 83        Use the -t/--tables, -m/--match_pairs, and -l/--limit flags to limit the search.
 84        Use the -c/--columns flag to limit what columns are printed.
 85        Use the -o/--order_by flag to order the results.
 86        Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs
 87        Pass -h/--help flag for parser help."""
 88        print("Finding records... ")
 89        if len(args.columns) == 0:
 90            args.columns = None
 91        with DataBased(self.dbpath) as db:
 92            tables = args.tables or db.get_table_names()
 93            for table in tables:
 94                results = db.get_rows(
 95                    table,
 96                    args.match_pairs,
 97                    columns_to_return=args.columns,
 98                    order_by=args.order_by,
 99                    limit=args.limit,
100                    exact_match=not args.partial_matching,
101                )
102                db.close()
103                print(f"{len(results)} matching rows in {table} table.")
104                try:
105                    print(DataBased.data_to_string(results))  # type: ignore
106                except Exception as e:
107                    print("Couldn't fit data into a grid.")
108                    print(*results, sep="\n")
109                if results:
110                    print(f"{len(results)} matching rows in {table} table.")
111                print()

Find and print rows from the database. Use the -t/--tables, -m/--match_pairs, and -l/--limit flags to limit the search. Use the -c/--columns flag to limit what columns are printed. Use the -o/--order_by flag to order the results. Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs Pass -h/--help flag for parser help.

@argshell.with_parser(dbparsers.get_lookup_parser, [dbparsers.convert_match_pairs])
def do_count(self, args: argshell.argshell.Namespace):
132    @argshell.with_parser(dbparsers.get_lookup_parser, [dbparsers.convert_match_pairs])
133    def do_count(self, args: argshell.Namespace):
134        """Print the number of rows in the database.
135        Use the -t/--tables flag to limit results to a specific table(s).
136        Use the -m/--match_pairs flag to limit the results to rows matching these criteria.
137        Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs.
138        Pass -h/--help flag for parser help."""
139        print("Counting rows...")
140        with DataBased(self.dbpath) as db:
141            tables = args.tables or db.get_table_names()
142            for table in tables:
143                num_rows = db.count(table, args.match_pairs, not args.partial_matching)
144                print(f"{num_rows} matching rows in {table} table.")

Print the number of rows in the database. Use the -t/--tables flag to limit results to a specific table(s). Use the -m/--match_pairs flag to limit the results to rows matching these criteria. Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs. Pass -h/--help flag for parser help.

def do_query(self, arg: str):
146    def do_query(self, arg: str):
147        """Execute a query against the current database."""
148        print(f"Executing {arg}")
149        with DataBased(self.dbpath) as db:
150            results = db.query(arg)
151        try:
152            try:
153                print(griddy(results))
154            except Exception as e:
155                for result in results:
156                    print(*result, sep="|-|")
157        except Exception as e:
158            print(f"{type(e).__name__}: {e}")
159        print(f"{db.cursor.rowcount} affected rows")

Execute a query against the current database.

@argshell.with_parser(dbparsers.get_update_parser, [dbparsers.convert_match_pairs])
def do_update(self, args: argshell.argshell.Namespace):
161    @argshell.with_parser(dbparsers.get_update_parser, [dbparsers.convert_match_pairs])
162    def do_update(self, args: argshell.Namespace):
163        """Update a column to a new value.
164        Two required args: the column (-c/--column) to update and the value (-v/--value) to update to.
165        Use the -t/--tables flag to limit what tables are updated.
166        Use the -m/--match_pairs flag to specify which rows are updated.
167        Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs.
168        >>> based>update -c username -v big_chungus -t users -m username lil_chungus
169
170        ^will update the username in the users 'table' to 'big_chungus' where the username is currently 'lil_chungus'^"""
171        print("Updating rows...")
172        with DataBased(self.dbpath) as db:
173            tables = args.tables or db.get_table_names()
174            for table in tables:
175                num_updates = db.update(
176                    table,
177                    args.column,
178                    args.new_value,
179                    args.match_pairs,
180                    not args.partial_matching,
181                )
182                print(f"Updated {num_updates} rows in {table} table.")

Update a column to a new value. Two required args: the column (-c/--column) to update and the value (-v/--value) to update to. Use the -t/--tables flag to limit what tables are updated. Use the -m/--match_pairs flag to specify which rows are updated. Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs.

>>> based>update -c username -v big_chungus -t users -m username lil_chungus

^will update the username in the users 'table' to 'big_chungus' where the username is currently 'lil_chungus'^

@argshell.with_parser(dbparsers.get_lookup_parser, [dbparsers.convert_match_pairs])
def do_delete(self, args: argshell.argshell.Namespace):
184    @argshell.with_parser(dbparsers.get_lookup_parser, [dbparsers.convert_match_pairs])
185    def do_delete(self, args: argshell.Namespace):
186        """Delete rows from the database.
187        Use the -t/--tables flag to limit what tables rows are deleted from.
188        Use the -m/--match_pairs flag to specify which rows are deleted.
189        Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs.
190        >>> based>delete -t users -m username chungus -p
191
192        ^will delete all rows in the 'users' table whose username contains 'chungus'^"""
193        print("Deleting records...")
194        with DataBased(self.dbpath) as db:
195            tables = args.tables or db.get_table_names()
196            for table in tables:
197                num_rows = db.delete(table, args.match_pairs, not args.partial_matching)
198                print(f"Deleted {num_rows} rows from {table} table.")

Delete rows from the database. Use the -t/--tables flag to limit what tables rows are deleted from. Use the -m/--match_pairs flag to specify which rows are deleted. Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs.

>>> based>delete -t users -m username chungus -p

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

@argshell.with_parser(dbparsers.get_add_column_parser)
def do_add_column(self, args: argshell.argshell.Namespace):
200    @argshell.with_parser(dbparsers.get_add_column_parser)
201    def do_add_column(self, args: argshell.Namespace):
202        """Add a new column to the specified tables."""
203        with DataBased(self.dbpath) as db:
204            tables = args.tables or db.get_table_names()
205            for table in tables:
206                db.add_column(table, args.column_name, args.type, args.default_value)

Add a new column to the specified tables.

def do_flush_log(self, arg: str):
208    def do_flush_log(self, arg: str):
209        """Clear the log file for this database."""
210        log_path = self.dbpath.with_name(self.dbpath.stem + "db.log")
211        if not log_path.exists():
212            print(f"No log file at path {log_path}")
213        else:
214            print(f"Flushing log...")
215            log_path.write_text("")

Clear the log file for this database.

def do_scan_dbs(self, arg: str):
217    def do_scan_dbs(self, arg: str):
218        """Scan the current working directory for `*.db` files and display them.
219
220        If the command is entered as `based>scan_dbs r`, the scan will be performed recursively."""
221        cwd = Pathier.cwd()
222        if arg.strip() == "r":
223            dbs = cwd.rglob("*.db")
224        else:
225            dbs = cwd.glob("*.db")
226        for db in dbs:
227            print(db.separate(cwd.stem))

Scan the current working directory for *.db files and display them.

If the command is entered as based>scan_dbs r, the scan will be performed recursively.

def do_customize(self, arg: str):
229    def do_customize(self, arg: str):
230        """Generate a template file in the current working directory for creating a custom DBShell class.
231        Expects one argument: the name of the custom dbshell.
232        This will be used to name the generated file as well as several components in the file content."""
233        try:
234            create_shell(arg)
235        except Exception as e:
236            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_vacuum(self, arg: str):
238    def do_vacuum(self, arg: str):
239        """Reduce database disk memory."""
240        starting_size = self.dbpath.size
241        print(f"Database size before vacuuming: {self.dbpath.formatted_size}")
242        print("Vacuuming database...")
243        with DataBased(self.dbpath) 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.")

Reduce database disk memory.

def preloop(self):
266    def preloop(self):
267        """Scan the current directory for a .db file to use.
268        If not found, prompt the user for one or to try again recursively."""
269        if self.dbpath:
270            self.dbpath = Pathier(self.dbpath)
271            print(f"Defaulting to database {self.dbpath}")
272        else:
273            print("Searching for database...")
274            cwd = Pathier.cwd()
275            dbs = list(cwd.glob("*.db"))
276            if len(dbs) == 1:
277                self.dbpath = dbs[0]
278                print(f"Using database {self.dbpath}.")
279            elif dbs:
280                self.dbpath = self._choose_db(dbs)
281            else:
282                print(f"Could not find a .db file in {cwd}.")
283                path = input(
284                    "Enter path to .db file to use or press enter to search again recursively: "
285                )
286                if path:
287                    self.dbpath = Pathier(path)
288                elif not path:
289                    print("Searching recursively...")
290                    dbs = list(cwd.rglob("*.db"))
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("Could not find a .db file.")
298                        self.dbpath = Pathier(input("Enter path to a .db file: "))
299        if not self.dbpath.exists():
300            raise FileNotFoundError(f"{self.dbpath} does not exist.")
301        if not self.dbpath.is_file():
302            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
default
completedefault
completenames
complete
get_names
complete_help
print_topics
columnize
argshell.argshell.ArgShell
do_quit
do_sys
do_help
cmdloop
emptyline
def main():
305def main():
306    DBShell().cmdloop()