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                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        custom_file = (Pathier.cwd() / arg.replace(" ", "_")).with_suffix(".py")
233        if custom_file.exists():
234            print(f"Error: {custom_file.name} already exists in this location.")
235        else:
236            variable_name = "_".join(word for word in arg.lower().split())
237            class_name = "".join(word.capitalize() for word in arg.split())
238            content = (Pathier(__file__).parent / "customshell.py").read_text()
239            content = content.replace("CustomShell", class_name)
240            content = content.replace("customshell", variable_name)
241            custom_file.write_text(content)
242
243    def do_vacuum(self, arg: str):
244        """Reduce database disk memory."""
245        starting_size = self.dbpath.size()
246        print(f"Database size before vacuuming: {self.dbpath.size(True)}")
247        print("Vacuuming database...")
248        with DataBased(self.dbpath) as db:
249            db.vacuum()
250        print(f"Database size after vacuuming: {self.dbpath.size(True)}")
251        print(f"Freed up {Pathier.format_size(starting_size - self.dbpath.size())} of disk space.")  # type: ignore
252
253    def _choose_db(self, options: list[Pathier]) -> Pathier:
254        """Prompt the user to select from a list of files."""
255        cwd = Pathier.cwd()
256        paths = [path.separate(cwd.stem) for path in options]
257        while True:
258            print(
259                f"DB options:\n{' '.join([f'({i}) {path}' for i,path in enumerate(paths,1)])}"
260            )
261            choice = input("Enter the number of the option to use: ")
262            try:
263                index = int(choice)
264                if not 1 <= index <= len(options):
265                    print("Choice out of range.")
266                    continue
267                return options[index - 1]
268            except Exception as e:
269                print(f"{choice} is not a valid option.")
270
271    def preloop(self):
272        """Scan the current directory for a .db file to use.
273        If not found, prompt the user for one or to try again recursively."""
274        if self.dbpath:
275            self.dbpath = Pathier(self.dbpath)
276            print(f"Defaulting to database {self.dbpath}")
277        else:
278            print("Searching for database...")
279            cwd = Pathier.cwd()
280            dbs = list(cwd.glob("*.db"))
281            if len(dbs) == 1:
282                self.dbpath = dbs[0]
283                print(f"Using database {self.dbpath}.")
284            elif dbs:
285                self.dbpath = self._choose_db(dbs)
286            else:
287                print(f"Could not find a .db file in {cwd}.")
288                path = input(
289                    "Enter path to .db file to use or press enter to search again recursively: "
290                )
291                if path:
292                    self.dbpath = Pathier(path)
293                elif not path:
294                    print("Searching recursively...")
295                    dbs = list(cwd.rglob("*.db"))
296                    if len(dbs) == 1:
297                        self.dbpath = dbs[0]
298                        print(f"Using database {self.dbpath}.")
299                    elif dbs:
300                        self.dbpath = self._choose_db(dbs)
301                    else:
302                        print("Could not find a .db file.")
303                        self.dbpath = Pathier(input("Enter path to a .db file: "))
304        if not self.dbpath.exists():
305            raise FileNotFoundError(f"{self.dbpath} does not exist.")
306        if not self.dbpath.is_file():
307            raise ValueError(f"{self.dbpath} is not a file.")
308
309
310def main():
311    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                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        custom_file = (Pathier.cwd() / arg.replace(" ", "_")).with_suffix(".py")
234        if custom_file.exists():
235            print(f"Error: {custom_file.name} already exists in this location.")
236        else:
237            variable_name = "_".join(word for word in arg.lower().split())
238            class_name = "".join(word.capitalize() for word in arg.split())
239            content = (Pathier(__file__).parent / "customshell.py").read_text()
240            content = content.replace("CustomShell", class_name)
241            content = content.replace("customshell", variable_name)
242            custom_file.write_text(content)
243
244    def do_vacuum(self, arg: str):
245        """Reduce database disk memory."""
246        starting_size = self.dbpath.size()
247        print(f"Database size before vacuuming: {self.dbpath.size(True)}")
248        print("Vacuuming database...")
249        with DataBased(self.dbpath) as db:
250            db.vacuum()
251        print(f"Database size after vacuuming: {self.dbpath.size(True)}")
252        print(f"Freed up {Pathier.format_size(starting_size - self.dbpath.size())} of disk space.")  # type: ignore
253
254    def _choose_db(self, options: list[Pathier]) -> Pathier:
255        """Prompt the user to select from a list of files."""
256        cwd = Pathier.cwd()
257        paths = [path.separate(cwd.stem) for path in options]
258        while True:
259            print(
260                f"DB options:\n{' '.join([f'({i}) {path}' for i,path in enumerate(paths,1)])}"
261            )
262            choice = input("Enter the number of the option to use: ")
263            try:
264                index = int(choice)
265                if not 1 <= index <= len(options):
266                    print("Choice out of range.")
267                    continue
268                return options[index - 1]
269            except Exception as e:
270                print(f"{choice} is not a valid option.")
271
272    def preloop(self):
273        """Scan the current directory for a .db file to use.
274        If not found, prompt the user for one or to try again recursively."""
275        if self.dbpath:
276            self.dbpath = Pathier(self.dbpath)
277            print(f"Defaulting to database {self.dbpath}")
278        else:
279            print("Searching for database...")
280            cwd = Pathier.cwd()
281            dbs = list(cwd.glob("*.db"))
282            if len(dbs) == 1:
283                self.dbpath = dbs[0]
284                print(f"Using database {self.dbpath}.")
285            elif dbs:
286                self.dbpath = self._choose_db(dbs)
287            else:
288                print(f"Could not find a .db file in {cwd}.")
289                path = input(
290                    "Enter path to .db file to use or press enter to search again recursively: "
291                )
292                if path:
293                    self.dbpath = Pathier(path)
294                elif not path:
295                    print("Searching recursively...")
296                    dbs = list(cwd.rglob("*.db"))
297                    if len(dbs) == 1:
298                        self.dbpath = dbs[0]
299                        print(f"Using database {self.dbpath}.")
300                    elif dbs:
301                        self.dbpath = self._choose_db(dbs)
302                    else:
303                        print("Could not find a .db file.")
304                        self.dbpath = Pathier(input("Enter path to a .db file: "))
305        if not self.dbpath.exists():
306            raise FileNotFoundError(f"{self.dbpath} does not exist.")
307        if not self.dbpath.is_file():
308            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                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        custom_file = (Pathier.cwd() / arg.replace(" ", "_")).with_suffix(".py")
234        if custom_file.exists():
235            print(f"Error: {custom_file.name} already exists in this location.")
236        else:
237            variable_name = "_".join(word for word in arg.lower().split())
238            class_name = "".join(word.capitalize() for word in arg.split())
239            content = (Pathier(__file__).parent / "customshell.py").read_text()
240            content = content.replace("CustomShell", class_name)
241            content = content.replace("customshell", variable_name)
242            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):
244    def do_vacuum(self, arg: str):
245        """Reduce database disk memory."""
246        starting_size = self.dbpath.size()
247        print(f"Database size before vacuuming: {self.dbpath.size(True)}")
248        print("Vacuuming database...")
249        with DataBased(self.dbpath) as db:
250            db.vacuum()
251        print(f"Database size after vacuuming: {self.dbpath.size(True)}")
252        print(f"Freed up {Pathier.format_size(starting_size - self.dbpath.size())} of disk space.")  # type: ignore

Reduce database disk memory.

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