databased.dbshell

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

Subclass this to create custom ArgShells.

def do_use_db(self, arg: str):
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

Set which database file to use.

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

Print the .db file in use.

@argshell.with_parser(dbparsers.get_backup_parser)
def do_backup(self, args: argshell.argshell.Namespace):
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}")

Create a backup of the current db file.

def do_size(self, arg: str):
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)}.")

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):
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)

Add a new table to the database.

def do_drop_table(self, arg: str):
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)

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):
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.")

Add a row to a table.

@argshell.with_parser(dbparsers.get_info_parser)
def do_info(self, args: argshell.argshell.Namespace):
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))

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):
 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()

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):
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.")

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):
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            for result in results:
150                print(*result, sep="|-|")
151            print(f"{db.cursor.rowcount} affected rows")
152        except Exception as e:
153            print(f"{type(e).__name__}: {e}")

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):
155    @argshell.with_parser(dbparsers.get_update_parser, [dbparsers.convert_match_pairs])
156    def do_update(self, args: argshell.Namespace):
157        """Update a column to a new value.
158        Two required args: the column (-c/--column) to update and the value (-v/--value) to update to.
159        Use the -t/--tables flag to limit what tables are updated.
160        Use the -m/--match_pairs flag to specify which rows are updated.
161        Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs.
162        >>> based>update -c username -v big_chungus -t users -m username lil_chungus
163
164        ^will update the username in the users 'table' to 'big_chungus' where the username is currently 'lil_chungus'^"""
165        print("Updating rows...")
166        with DataBased(self.dbpath) as db:
167            tables = args.tables or db.get_table_names()
168            for table in tables:
169                num_updates = db.update(
170                    table,
171                    args.column,
172                    args.new_value,
173                    args.match_pairs,
174                    not args.partial_matching,
175                )
176                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):
178    @argshell.with_parser(dbparsers.get_lookup_parser, [dbparsers.convert_match_pairs])
179    def do_delete(self, args: argshell.Namespace):
180        """Delete rows from the database.
181        Use the -t/--tables flag to limit what tables rows are deleted from.
182        Use the -m/--match_pairs flag to specify which rows are deleted.
183        Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs.
184        >>> based>delete -t users -m username chungus -p
185
186        ^will delete all rows in the 'users' table whose username contains 'chungus'^"""
187        print("Deleting records...")
188        with DataBased(self.dbpath) as db:
189            tables = args.tables or db.get_table_names()
190            for table in tables:
191                num_rows = db.delete(table, args.match_pairs, not args.partial_matching)
192                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):
194    @argshell.with_parser(dbparsers.get_add_column_parser)
195    def do_add_column(self, args: argshell.Namespace):
196        """Add a new column to the specified tables."""
197        with DataBased(self.dbpath) as db:
198            tables = args.tables or db.get_table_names()
199            for table in tables:
200                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):
202    def do_flush_log(self, arg: str):
203        """Clear the log file for this database."""
204        log_path = self.dbpath.with_name(self.dbpath.stem + "db.log")
205        if not log_path.exists():
206            print(f"No log file at path {log_path}")
207        else:
208            print(f"Flushing log...")
209            log_path.write_text("")

Clear the log file for this database.

def do_scan_dbs(self, arg: str):
211    def do_scan_dbs(self, arg: str):
212        """Scan the current working directory for `*.db` files and display them.
213
214        If the command is entered as `based>scan_dbs r`, the scan will be performed recursively."""
215        cwd = Pathier.cwd()
216        if arg.strip() == "r":
217            dbs = cwd.rglob("*.db")
218        else:
219            dbs = cwd.glob("*.db")
220        for db in dbs:
221            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):
223    def do_customize(self, arg: str):
224        """Generate a template file in the current working directory for creating a custom DBShell class.
225        Expects one argument: the name of the custom dbshell.
226        This will be used to name the generated file as well as several components in the file content."""
227        custom_file = (Pathier.cwd() / arg.replace(" ", "_")).with_suffix(".py")
228        if custom_file.exists():
229            print(f"Error: {custom_file.name} already exists in this location.")
230        else:
231            variable_name = "_".join(word for word in arg.lower().split())
232            class_name = "".join(word.capitalize() for word in arg.split())
233            content = (Pathier(__file__).parent / "customshell.py").read_text()
234            content = content.replace("CustomShell", class_name)
235            content = content.replace("customshell", variable_name)
236            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):
238    def do_vacuum(self, arg: str):
239        """Reduce database disk memory."""
240        starting_size = self.dbpath.size()
241        print(f"Database size before vacuuming: {self.dbpath.size(True)}")
242        print("Vacuuming database...")
243        with DataBased(self.dbpath) as db:
244            db.vacuum()
245        print(f"Database size after vacuuming: {self.dbpath.size(True)}")
246        print(f"Freed up {Pathier.format_size(starting_size - self.dbpath.size())} of disk space.")  # type: ignore

Reduce database disk memory.

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

Scan the current directory for a .db file to use. If not found, prompt the user for one or to try again recursively.

Inherited Members
cmd.Cmd
Cmd
precmd
postcmd
postloop
parseline
onecmd
default
completedefault
completenames
complete
get_names
complete_help
print_topics
columnize
argshell.argshell.ArgShell
do_quit
do_sys
do_help
cmdloop
emptyline
def main():
305def main():
306    DBShell().cmdloop()