databased.dbshell

  1import argshell
  2from griddle import griddy
  3from pathier import Pathier
  4
  5from databased import DataBased, 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.size(True)}.")
 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                print()
109
110    @argshell.with_parser(dbparsers.get_search_parser)
111    def do_search(self, args: argshell.Namespace):
112        """Search and return any rows containg the searched substring in any of its columns.
113        Use the -t/--tables flag to limit the search to a specific table(s).
114        Use the -c/--columns flag to limit the search to a specific column(s)."""
115        print(f"Searching for {args.search_string}...")
116        with DataBased(self.dbpath) as db:
117            tables = args.tables or db.get_table_names()
118            for table in tables:
119                columns = args.columns or db.get_column_names(table)
120                matcher = " OR ".join(
121                    f'{column} LIKE "%{args.search_string}%"' for column in columns
122                )
123                query = f"SELECT * FROM {table} WHERE {matcher};"
124                results = db.query(query)
125                results = [db._get_dict(table, result) for result in results]
126                print(f"Found {len(results)} results in {table} table.")
127                print(DataBased.data_to_string(results))
128
129    @argshell.with_parser(dbparsers.get_lookup_parser, [dbparsers.convert_match_pairs])
130    def do_count(self, args: argshell.Namespace):
131        """Print the number of rows in the database.
132        Use the -t/--tables flag to limit results to a specific table(s).
133        Use the -m/--match_pairs flag to limit the results to rows matching these criteria.
134        Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs.
135        Pass -h/--help flag for parser help."""
136        print("Counting rows...")
137        with DataBased(self.dbpath) as db:
138            tables = args.tables or db.get_table_names()
139            for table in tables:
140                num_rows = db.count(table, args.match_pairs, not args.partial_matching)
141                print(f"{num_rows} matching rows in {table} table.")
142
143    def do_query(self, arg: str):
144        """Execute a query against the current database."""
145        print(f"Executing {arg}")
146        with DataBased(self.dbpath) as db:
147            results = db.query(arg)
148        try:
149            try:
150                print(griddy(results))
151            except Exception as e:
152                for result in results:
153                    print(*result, sep="|-|")
154        except Exception as e:
155            print(f"{type(e).__name__}: {e}")
156        print(f"{db.cursor.rowcount} affected rows")
157
158    @argshell.with_parser(dbparsers.get_update_parser, [dbparsers.convert_match_pairs])
159    def do_update(self, args: argshell.Namespace):
160        """Update a column to a new value.
161        Two required args: the column (-c/--column) to update and the value (-v/--value) to update to.
162        Use the -t/--tables flag to limit what tables are updated.
163        Use the -m/--match_pairs flag to specify which rows are updated.
164        Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs.
165        >>> based>update -c username -v big_chungus -t users -m username lil_chungus
166
167        ^will update the username in the users 'table' to 'big_chungus' where the username is currently 'lil_chungus'^"""
168        print("Updating rows...")
169        with DataBased(self.dbpath) as db:
170            tables = args.tables or db.get_table_names()
171            for table in tables:
172                num_updates = db.update(
173                    table,
174                    args.column,
175                    args.new_value,
176                    args.match_pairs,
177                    not args.partial_matching,
178                )
179                print(f"Updated {num_updates} rows in {table} table.")
180
181    @argshell.with_parser(dbparsers.get_lookup_parser, [dbparsers.convert_match_pairs])
182    def do_delete(self, args: argshell.Namespace):
183        """Delete rows from the database.
184        Use the -t/--tables flag to limit what tables rows are deleted from.
185        Use the -m/--match_pairs flag to specify which rows are deleted.
186        Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs.
187        >>> based>delete -t users -m username chungus -p
188
189        ^will delete all rows in the 'users' table whose username contains 'chungus'^"""
190        print("Deleting records...")
191        with DataBased(self.dbpath) as db:
192            tables = args.tables or db.get_table_names()
193            for table in tables:
194                num_rows = db.delete(table, args.match_pairs, not args.partial_matching)
195                print(f"Deleted {num_rows} rows from {table} table.")
196
197    @argshell.with_parser(dbparsers.get_add_column_parser)
198    def do_add_column(self, args: argshell.Namespace):
199        """Add a new column to the specified tables."""
200        with DataBased(self.dbpath) as db:
201            tables = args.tables or db.get_table_names()
202            for table in tables:
203                db.add_column(table, args.column_name, args.type, args.default_value)
204
205    def do_flush_log(self, arg: str):
206        """Clear the log file for this database."""
207        log_path = self.dbpath.with_name(self.dbpath.stem + "db.log")
208        if not log_path.exists():
209            print(f"No log file at path {log_path}")
210        else:
211            print(f"Flushing log...")
212            log_path.write_text("")
213
214    def do_scan_dbs(self, arg: str):
215        """Scan the current working directory for `*.db` files and display them.
216
217        If the command is entered as `based>scan_dbs r`, the scan will be performed recursively."""
218        cwd = Pathier.cwd()
219        if arg.strip() == "r":
220            dbs = cwd.rglob("*.db")
221        else:
222            dbs = cwd.glob("*.db")
223        for db in dbs:
224            print(db.separate(cwd.stem))
225
226    def do_customize(self, arg: str):
227        """Generate a template file in the current working directory for creating a custom DBShell class.
228        Expects one argument: the name of the custom dbshell.
229        This will be used to name the generated file as well as several components in the file content."""
230        custom_file = (Pathier.cwd() / arg.replace(" ", "_")).with_suffix(".py")
231        if custom_file.exists():
232            print(f"Error: {custom_file.name} already exists in this location.")
233        else:
234            variable_name = "_".join(word for word in arg.lower().split())
235            class_name = "".join(word.capitalize() for word in arg.split())
236            content = (Pathier(__file__).parent / "customshell.py").read_text()
237            content = content.replace("CustomShell", class_name)
238            content = content.replace("customshell", variable_name)
239            custom_file.write_text(content)
240
241    def do_vacuum(self, arg: str):
242        """Reduce database disk memory."""
243        starting_size = self.dbpath.size()
244        print(f"Database size before vacuuming: {self.dbpath.size(True)}")
245        print("Vacuuming database...")
246        with DataBased(self.dbpath) as db:
247            db.vacuum()
248        print(f"Database size after vacuuming: {self.dbpath.size(True)}")
249        print(f"Freed up {Pathier.format_size(starting_size - self.dbpath.size())} of disk space.")  # type: ignore
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 preloop(self):
270        """Scan the current directory for a .db file to use.
271        If not found, prompt the user for one or to try again recursively."""
272        if self.dbpath:
273            self.dbpath = Pathier(self.dbpath)
274            print(f"Defaulting to database {self.dbpath}")
275        else:
276            print("Searching for database...")
277            cwd = Pathier.cwd()
278            dbs = list(cwd.glob("*.db"))
279            if len(dbs) == 1:
280                self.dbpath = dbs[0]
281                print(f"Using database {self.dbpath}.")
282            elif dbs:
283                self.dbpath = self._choose_db(dbs)
284            else:
285                print(f"Could not find a .db file in {cwd}.")
286                path = input(
287                    "Enter path to .db file to use or press enter to search again recursively: "
288                )
289                if path:
290                    self.dbpath = Pathier(path)
291                elif not path:
292                    print("Searching recursively...")
293                    dbs = list(cwd.rglob("*.db"))
294                    if len(dbs) == 1:
295                        self.dbpath = dbs[0]
296                        print(f"Using database {self.dbpath}.")
297                    elif dbs:
298                        self.dbpath = self._choose_db(dbs)
299                    else:
300                        print("Could not find a .db file.")
301                        self.dbpath = Pathier(input("Enter path to a .db file: "))
302        if not self.dbpath.exists():
303            raise FileNotFoundError(f"{self.dbpath} does not exist.")
304        if not self.dbpath.is_file():
305            raise ValueError(f"{self.dbpath} is not a file.")
306
307
308def main():
309    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.size(True)}.")
 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                print()
110
111    @argshell.with_parser(dbparsers.get_search_parser)
112    def do_search(self, args: argshell.Namespace):
113        """Search and return any rows containg the searched substring in any of its columns.
114        Use the -t/--tables flag to limit the search to a specific table(s).
115        Use the -c/--columns flag to limit the search to a specific column(s)."""
116        print(f"Searching for {args.search_string}...")
117        with DataBased(self.dbpath) as db:
118            tables = args.tables or db.get_table_names()
119            for table in tables:
120                columns = args.columns or db.get_column_names(table)
121                matcher = " OR ".join(
122                    f'{column} LIKE "%{args.search_string}%"' for column in columns
123                )
124                query = f"SELECT * FROM {table} WHERE {matcher};"
125                results = db.query(query)
126                results = [db._get_dict(table, result) for result in results]
127                print(f"Found {len(results)} results in {table} table.")
128                print(DataBased.data_to_string(results))
129
130    @argshell.with_parser(dbparsers.get_lookup_parser, [dbparsers.convert_match_pairs])
131    def do_count(self, args: argshell.Namespace):
132        """Print the number of rows in the database.
133        Use the -t/--tables flag to limit results to a specific table(s).
134        Use the -m/--match_pairs flag to limit the results to rows matching these criteria.
135        Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs.
136        Pass -h/--help flag for parser help."""
137        print("Counting rows...")
138        with DataBased(self.dbpath) as db:
139            tables = args.tables or db.get_table_names()
140            for table in tables:
141                num_rows = db.count(table, args.match_pairs, not args.partial_matching)
142                print(f"{num_rows} matching rows in {table} table.")
143
144    def do_query(self, arg: str):
145        """Execute a query against the current database."""
146        print(f"Executing {arg}")
147        with DataBased(self.dbpath) as db:
148            results = db.query(arg)
149        try:
150            try:
151                print(griddy(results))
152            except Exception as e:
153                for result in results:
154                    print(*result, sep="|-|")
155        except Exception as e:
156            print(f"{type(e).__name__}: {e}")
157        print(f"{db.cursor.rowcount} affected rows")
158
159    @argshell.with_parser(dbparsers.get_update_parser, [dbparsers.convert_match_pairs])
160    def do_update(self, args: argshell.Namespace):
161        """Update a column to a new value.
162        Two required args: the column (-c/--column) to update and the value (-v/--value) to update to.
163        Use the -t/--tables flag to limit what tables are updated.
164        Use the -m/--match_pairs flag to specify which rows are updated.
165        Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs.
166        >>> based>update -c username -v big_chungus -t users -m username lil_chungus
167
168        ^will update the username in the users 'table' to 'big_chungus' where the username is currently 'lil_chungus'^"""
169        print("Updating rows...")
170        with DataBased(self.dbpath) as db:
171            tables = args.tables or db.get_table_names()
172            for table in tables:
173                num_updates = db.update(
174                    table,
175                    args.column,
176                    args.new_value,
177                    args.match_pairs,
178                    not args.partial_matching,
179                )
180                print(f"Updated {num_updates} rows in {table} table.")
181
182    @argshell.with_parser(dbparsers.get_lookup_parser, [dbparsers.convert_match_pairs])
183    def do_delete(self, args: argshell.Namespace):
184        """Delete rows from the database.
185        Use the -t/--tables flag to limit what tables rows are deleted from.
186        Use the -m/--match_pairs flag to specify which rows are deleted.
187        Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs.
188        >>> based>delete -t users -m username chungus -p
189
190        ^will delete all rows in the 'users' table whose username contains 'chungus'^"""
191        print("Deleting records...")
192        with DataBased(self.dbpath) as db:
193            tables = args.tables or db.get_table_names()
194            for table in tables:
195                num_rows = db.delete(table, args.match_pairs, not args.partial_matching)
196                print(f"Deleted {num_rows} rows from {table} table.")
197
198    @argshell.with_parser(dbparsers.get_add_column_parser)
199    def do_add_column(self, args: argshell.Namespace):
200        """Add a new column to the specified tables."""
201        with DataBased(self.dbpath) as db:
202            tables = args.tables or db.get_table_names()
203            for table in tables:
204                db.add_column(table, args.column_name, args.type, args.default_value)
205
206    def do_flush_log(self, arg: str):
207        """Clear the log file for this database."""
208        log_path = self.dbpath.with_name(self.dbpath.stem + "db.log")
209        if not log_path.exists():
210            print(f"No log file at path {log_path}")
211        else:
212            print(f"Flushing log...")
213            log_path.write_text("")
214
215    def do_scan_dbs(self, arg: str):
216        """Scan the current working directory for `*.db` files and display them.
217
218        If the command is entered as `based>scan_dbs r`, the scan will be performed recursively."""
219        cwd = Pathier.cwd()
220        if arg.strip() == "r":
221            dbs = cwd.rglob("*.db")
222        else:
223            dbs = cwd.glob("*.db")
224        for db in dbs:
225            print(db.separate(cwd.stem))
226
227    def do_customize(self, arg: str):
228        """Generate a template file in the current working directory for creating a custom DBShell class.
229        Expects one argument: the name of the custom dbshell.
230        This will be used to name the generated file as well as several components in the file content."""
231        custom_file = (Pathier.cwd() / arg.replace(" ", "_")).with_suffix(".py")
232        if custom_file.exists():
233            print(f"Error: {custom_file.name} already exists in this location.")
234        else:
235            variable_name = "_".join(word for word in arg.lower().split())
236            class_name = "".join(word.capitalize() for word in arg.split())
237            content = (Pathier(__file__).parent / "customshell.py").read_text()
238            content = content.replace("CustomShell", class_name)
239            content = content.replace("customshell", variable_name)
240            custom_file.write_text(content)
241
242    def do_vacuum(self, arg: str):
243        """Reduce database disk memory."""
244        starting_size = self.dbpath.size()
245        print(f"Database size before vacuuming: {self.dbpath.size(True)}")
246        print("Vacuuming database...")
247        with DataBased(self.dbpath) as db:
248            db.vacuum()
249        print(f"Database size after vacuuming: {self.dbpath.size(True)}")
250        print(f"Freed up {Pathier.format_size(starting_size - self.dbpath.size())} of disk space.")  # type: ignore
251
252    def _choose_db(self, options: list[Pathier]) -> Pathier:
253        """Prompt the user to select from a list of files."""
254        cwd = Pathier.cwd()
255        paths = [path.separate(cwd.stem) for path in options]
256        while True:
257            print(
258                f"DB options:\n{' '.join([f'({i}) {path}' for i,path in enumerate(paths,1)])}"
259            )
260            choice = input("Enter the number of the option to use: ")
261            try:
262                index = int(choice)
263                if not 1 <= index <= len(options):
264                    print("Choice out of range.")
265                    continue
266                return options[index - 1]
267            except Exception as e:
268                print(f"{choice} is not a valid option.")
269
270    def preloop(self):
271        """Scan the current directory for a .db file to use.
272        If not found, prompt the user for one or to try again recursively."""
273        if self.dbpath:
274            self.dbpath = Pathier(self.dbpath)
275            print(f"Defaulting to database {self.dbpath}")
276        else:
277            print("Searching for database...")
278            cwd = Pathier.cwd()
279            dbs = list(cwd.glob("*.db"))
280            if len(dbs) == 1:
281                self.dbpath = dbs[0]
282                print(f"Using database {self.dbpath}.")
283            elif dbs:
284                self.dbpath = self._choose_db(dbs)
285            else:
286                print(f"Could not find a .db file in {cwd}.")
287                path = input(
288                    "Enter path to .db file to use or press enter to search again recursively: "
289                )
290                if path:
291                    self.dbpath = Pathier(path)
292                elif not path:
293                    print("Searching recursively...")
294                    dbs = list(cwd.rglob("*.db"))
295                    if len(dbs) == 1:
296                        self.dbpath = dbs[0]
297                        print(f"Using database {self.dbpath}.")
298                    elif dbs:
299                        self.dbpath = self._choose_db(dbs)
300                    else:
301                        print("Could not find a .db file.")
302                        self.dbpath = Pathier(input("Enter path to a .db file: "))
303        if not self.dbpath.exists():
304            raise FileNotFoundError(f"{self.dbpath} does not exist.")
305        if not self.dbpath.is_file():
306            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.size(True)}.")

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                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):
130    @argshell.with_parser(dbparsers.get_lookup_parser, [dbparsers.convert_match_pairs])
131    def do_count(self, args: argshell.Namespace):
132        """Print the number of rows in the database.
133        Use the -t/--tables flag to limit results to a specific table(s).
134        Use the -m/--match_pairs flag to limit the results to rows matching these criteria.
135        Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs.
136        Pass -h/--help flag for parser help."""
137        print("Counting rows...")
138        with DataBased(self.dbpath) as db:
139            tables = args.tables or db.get_table_names()
140            for table in tables:
141                num_rows = db.count(table, args.match_pairs, not args.partial_matching)
142                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):
144    def do_query(self, arg: str):
145        """Execute a query against the current database."""
146        print(f"Executing {arg}")
147        with DataBased(self.dbpath) as db:
148            results = db.query(arg)
149        try:
150            try:
151                print(griddy(results))
152            except Exception as e:
153                for result in results:
154                    print(*result, sep="|-|")
155        except Exception as e:
156            print(f"{type(e).__name__}: {e}")
157        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):
159    @argshell.with_parser(dbparsers.get_update_parser, [dbparsers.convert_match_pairs])
160    def do_update(self, args: argshell.Namespace):
161        """Update a column to a new value.
162        Two required args: the column (-c/--column) to update and the value (-v/--value) to update to.
163        Use the -t/--tables flag to limit what tables are updated.
164        Use the -m/--match_pairs flag to specify which rows are updated.
165        Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs.
166        >>> based>update -c username -v big_chungus -t users -m username lil_chungus
167
168        ^will update the username in the users 'table' to 'big_chungus' where the username is currently 'lil_chungus'^"""
169        print("Updating rows...")
170        with DataBased(self.dbpath) as db:
171            tables = args.tables or db.get_table_names()
172            for table in tables:
173                num_updates = db.update(
174                    table,
175                    args.column,
176                    args.new_value,
177                    args.match_pairs,
178                    not args.partial_matching,
179                )
180                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):
182    @argshell.with_parser(dbparsers.get_lookup_parser, [dbparsers.convert_match_pairs])
183    def do_delete(self, args: argshell.Namespace):
184        """Delete rows from the database.
185        Use the -t/--tables flag to limit what tables rows are deleted from.
186        Use the -m/--match_pairs flag to specify which rows are deleted.
187        Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs.
188        >>> based>delete -t users -m username chungus -p
189
190        ^will delete all rows in the 'users' table whose username contains 'chungus'^"""
191        print("Deleting records...")
192        with DataBased(self.dbpath) as db:
193            tables = args.tables or db.get_table_names()
194            for table in tables:
195                num_rows = db.delete(table, args.match_pairs, not args.partial_matching)
196                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):
198    @argshell.with_parser(dbparsers.get_add_column_parser)
199    def do_add_column(self, args: argshell.Namespace):
200        """Add a new column to the specified tables."""
201        with DataBased(self.dbpath) as db:
202            tables = args.tables or db.get_table_names()
203            for table in tables:
204                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):
206    def do_flush_log(self, arg: str):
207        """Clear the log file for this database."""
208        log_path = self.dbpath.with_name(self.dbpath.stem + "db.log")
209        if not log_path.exists():
210            print(f"No log file at path {log_path}")
211        else:
212            print(f"Flushing log...")
213            log_path.write_text("")

Clear the log file for this database.

def do_scan_dbs(self, arg: str):
215    def do_scan_dbs(self, arg: str):
216        """Scan the current working directory for `*.db` files and display them.
217
218        If the command is entered as `based>scan_dbs r`, the scan will be performed recursively."""
219        cwd = Pathier.cwd()
220        if arg.strip() == "r":
221            dbs = cwd.rglob("*.db")
222        else:
223            dbs = cwd.glob("*.db")
224        for db in dbs:
225            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):
227    def do_customize(self, arg: str):
228        """Generate a template file in the current working directory for creating a custom DBShell class.
229        Expects one argument: the name of the custom dbshell.
230        This will be used to name the generated file as well as several components in the file content."""
231        custom_file = (Pathier.cwd() / arg.replace(" ", "_")).with_suffix(".py")
232        if custom_file.exists():
233            print(f"Error: {custom_file.name} already exists in this location.")
234        else:
235            variable_name = "_".join(word for word in arg.lower().split())
236            class_name = "".join(word.capitalize() for word in arg.split())
237            content = (Pathier(__file__).parent / "customshell.py").read_text()
238            content = content.replace("CustomShell", class_name)
239            content = content.replace("customshell", variable_name)
240            custom_file.write_text(content)

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):
242    def do_vacuum(self, arg: str):
243        """Reduce database disk memory."""
244        starting_size = self.dbpath.size()
245        print(f"Database size before vacuuming: {self.dbpath.size(True)}")
246        print("Vacuuming database...")
247        with DataBased(self.dbpath) as db:
248            db.vacuum()
249        print(f"Database size after vacuuming: {self.dbpath.size(True)}")
250        print(f"Freed up {Pathier.format_size(starting_size - self.dbpath.size())} of disk space.")  # type: ignore

Reduce database disk memory.

def preloop(self):
270    def preloop(self):
271        """Scan the current directory for a .db file to use.
272        If not found, prompt the user for one or to try again recursively."""
273        if self.dbpath:
274            self.dbpath = Pathier(self.dbpath)
275            print(f"Defaulting to database {self.dbpath}")
276        else:
277            print("Searching for database...")
278            cwd = Pathier.cwd()
279            dbs = list(cwd.glob("*.db"))
280            if len(dbs) == 1:
281                self.dbpath = dbs[0]
282                print(f"Using database {self.dbpath}.")
283            elif dbs:
284                self.dbpath = self._choose_db(dbs)
285            else:
286                print(f"Could not find a .db file in {cwd}.")
287                path = input(
288                    "Enter path to .db file to use or press enter to search again recursively: "
289                )
290                if path:
291                    self.dbpath = Pathier(path)
292                elif not path:
293                    print("Searching recursively...")
294                    dbs = list(cwd.rglob("*.db"))
295                    if len(dbs) == 1:
296                        self.dbpath = dbs[0]
297                        print(f"Using database {self.dbpath}.")
298                    elif dbs:
299                        self.dbpath = self._choose_db(dbs)
300                    else:
301                        print("Could not find a .db file.")
302                        self.dbpath = Pathier(input("Enter path to a .db file: "))
303        if not self.dbpath.exists():
304            raise FileNotFoundError(f"{self.dbpath} does not exist.")
305        if not self.dbpath.is_file():
306            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():
309def main():
310    DBShell().cmdloop()