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 print(f"Database size after vacuuming: {self.dbpath.size(True)}") 243 print(f"Freed up {Pathier.format_size(starting_size - self.dbpath.size())} of disk space.") # type: ignore 244 245 def _choose_db(self, options: list[Pathier]) -> Pathier: 246 """Prompt the user to select from a list of files.""" 247 cwd = Pathier.cwd() 248 paths = [path.separate(cwd.stem) for path in options] 249 while True: 250 print( 251 f"DB options:\n{' '.join([f'({i}) {path}' for i,path in enumerate(paths,1)])}" 252 ) 253 choice = input("Enter the number of the option to use: ") 254 try: 255 index = int(choice) 256 if not 1 <= index <= len(options): 257 print("Choice out of range.") 258 continue 259 return options[index - 1] 260 except Exception as e: 261 print(f"{choice} is not a valid option.") 262 263 def preloop(self): 264 """Scan the current directory for a .db file to use. 265 If not found, prompt the user for one or to try again recursively.""" 266 if self.dbpath: 267 self.dbpath = Pathier(self.dbpath) 268 print(f"Defaulting to database {self.dbpath}") 269 else: 270 print("Searching for database...") 271 cwd = Pathier.cwd() 272 dbs = list(cwd.glob("*.db")) 273 if len(dbs) == 1: 274 self.dbpath = dbs[0] 275 print(f"Using database {self.dbpath}.") 276 elif dbs: 277 self.dbpath = self._choose_db(dbs) 278 else: 279 print(f"Could not find a .db file in {cwd}.") 280 path = input( 281 "Enter path to .db file to use or press enter to search again recursively: " 282 ) 283 if path: 284 self.dbpath = Pathier(path) 285 elif not path: 286 print("Searching recursively...") 287 dbs = list(cwd.rglob("*.db")) 288 if len(dbs) == 1: 289 self.dbpath = dbs[0] 290 print(f"Using database {self.dbpath}.") 291 elif dbs: 292 self.dbpath = self._choose_db(dbs) 293 else: 294 print("Could not find a .db file.") 295 self.dbpath = Pathier(input("Enter path to a .db file: ")) 296 if not self.dbpath.exists(): 297 raise FileNotFoundError(f"{self.dbpath} does not exist.") 298 if not self.dbpath.is_file(): 299 raise ValueError(f"{self.dbpath} is not a file.") 300 301 302def main(): 303 DBShell().cmdloop()
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 print(f"Database size after vacuuming: {self.dbpath.size(True)}") 244 print(f"Freed up {Pathier.format_size(starting_size - self.dbpath.size())} of disk space.") # type: ignore 245 246 def _choose_db(self, options: list[Pathier]) -> Pathier: 247 """Prompt the user to select from a list of files.""" 248 cwd = Pathier.cwd() 249 paths = [path.separate(cwd.stem) for path in options] 250 while True: 251 print( 252 f"DB options:\n{' '.join([f'({i}) {path}' for i,path in enumerate(paths,1)])}" 253 ) 254 choice = input("Enter the number of the option to use: ") 255 try: 256 index = int(choice) 257 if not 1 <= index <= len(options): 258 print("Choice out of range.") 259 continue 260 return options[index - 1] 261 except Exception as e: 262 print(f"{choice} is not a valid option.") 263 264 def preloop(self): 265 """Scan the current directory for a .db file to use. 266 If not found, prompt the user for one or to try again recursively.""" 267 if self.dbpath: 268 self.dbpath = Pathier(self.dbpath) 269 print(f"Defaulting to database {self.dbpath}") 270 else: 271 print("Searching for database...") 272 cwd = Pathier.cwd() 273 dbs = list(cwd.glob("*.db")) 274 if len(dbs) == 1: 275 self.dbpath = dbs[0] 276 print(f"Using database {self.dbpath}.") 277 elif dbs: 278 self.dbpath = self._choose_db(dbs) 279 else: 280 print(f"Could not find a .db file in {cwd}.") 281 path = input( 282 "Enter path to .db file to use or press enter to search again recursively: " 283 ) 284 if path: 285 self.dbpath = Pathier(path) 286 elif not path: 287 print("Searching recursively...") 288 dbs = list(cwd.rglob("*.db")) 289 if len(dbs) == 1: 290 self.dbpath = dbs[0] 291 print(f"Using database {self.dbpath}.") 292 elif dbs: 293 self.dbpath = self._choose_db(dbs) 294 else: 295 print("Could not find a .db file.") 296 self.dbpath = Pathier(input("Enter path to a .db file: ")) 297 if not self.dbpath.exists(): 298 raise FileNotFoundError(f"{self.dbpath} does not exist.") 299 if not self.dbpath.is_file(): 300 raise ValueError(f"{self.dbpath} is not a file.")
Subclass this to create custom ArgShells.
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.
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.
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.
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.
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.
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.
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.
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.
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))
Search and return any rows containg the searched substring in any of its columns. Use the -t/--tables flag to limit the search to a specific table(s). Use the -c/--columns flag to limit the search to a specific column(s).
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.
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.
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'^
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'^
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.
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.
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.
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.
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 print(f"Database size after vacuuming: {self.dbpath.size(True)}") 244 print(f"Freed up {Pathier.format_size(starting_size - self.dbpath.size())} of disk space.") # type: ignore
Reduce database disk memory.
264 def preloop(self): 265 """Scan the current directory for a .db file to use. 266 If not found, prompt the user for one or to try again recursively.""" 267 if self.dbpath: 268 self.dbpath = Pathier(self.dbpath) 269 print(f"Defaulting to database {self.dbpath}") 270 else: 271 print("Searching for database...") 272 cwd = Pathier.cwd() 273 dbs = list(cwd.glob("*.db")) 274 if len(dbs) == 1: 275 self.dbpath = dbs[0] 276 print(f"Using database {self.dbpath}.") 277 elif dbs: 278 self.dbpath = self._choose_db(dbs) 279 else: 280 print(f"Could not find a .db file in {cwd}.") 281 path = input( 282 "Enter path to .db file to use or press enter to search again recursively: " 283 ) 284 if path: 285 self.dbpath = Pathier(path) 286 elif not path: 287 print("Searching recursively...") 288 dbs = list(cwd.rglob("*.db")) 289 if len(dbs) == 1: 290 self.dbpath = dbs[0] 291 print(f"Using database {self.dbpath}.") 292 elif dbs: 293 self.dbpath = self._choose_db(dbs) 294 else: 295 print("Could not find a .db file.") 296 self.dbpath = Pathier(input("Enter path to a .db file: ")) 297 if not self.dbpath.exists(): 298 raise FileNotFoundError(f"{self.dbpath} does not exist.") 299 if not self.dbpath.is_file(): 300 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