databased.dbshell
1from datetime import datetime 2 3import argshell 4from griddle import griddy 5from noiftimer import time_it 6from pathier import Pathier, Pathish 7 8from databased import Databased, __version__, dbparsers 9from databased.create_shell import create_shell 10 11 12class DBShell(argshell.ArgShell): 13 _dbpath: Pathier = None # type: ignore 14 connection_timeout: float = 10 15 detect_types: bool = True 16 enforce_foreign_keys: bool = True 17 intro = f"Starting dbshell v{__version__} (enter help or ? for arg info)...\n" 18 prompt = f"based>" 19 20 @property 21 def dbpath(self) -> Pathier: 22 return self._dbpath 23 24 @dbpath.setter 25 def dbpath(self, path: Pathish): 26 self._dbpath = Pathier(path) 27 self.prompt = f"{self._dbpath.name}>" 28 29 def _DB(self) -> Databased: 30 return Databased( 31 self.dbpath, 32 self.connection_timeout, 33 self.detect_types, 34 self.enforce_foreign_keys, 35 ) 36 37 @time_it() 38 def default(self, line: str): 39 line = line.strip("_") 40 with self._DB() as db: 41 self.display(db.query(line)) 42 43 def display(self, data: list[dict]): 44 """Print row data to terminal in a grid.""" 45 try: 46 print(griddy(data, "keys")) 47 except Exception as e: 48 print("Could not fit data into grid :(") 49 print(e) 50 51 # Seat 52 53 def _show_tables(self, args: argshell.Namespace): 54 with self._DB() as db: 55 if args.tables: 56 tables = [table for table in args.tables if table in db.tables] 57 else: 58 tables = db.tables 59 if tables: 60 print("Getting database tables...") 61 info = [ 62 { 63 "Table Name": table, 64 "Columns": ", ".join(db.get_columns(table)), 65 "Number of Rows": db.count(table) if args.rowcount else "n/a", 66 } 67 for table in tables 68 ] 69 self.display(info) 70 71 def _show_views(self, args: argshell.Namespace): 72 with self._DB() as db: 73 if args.tables: 74 views = [view for view in args.tables if view in db.views] 75 else: 76 views = db.views 77 if views: 78 print("Getting database views...") 79 info = [ 80 { 81 "View Name": view, 82 "Columns": ", ".join(db.get_columns(view)), 83 "Number of Rows": db.count(view) if args.rowcount else "n/a", 84 } 85 for view in views 86 ] 87 self.display(info) 88 89 @argshell.with_parser(dbparsers.get_add_column_parser) 90 def do_add_column(self, args: argshell.Namespace): 91 """Add a new column to the specified tables.""" 92 with self._DB() as db: 93 db.add_column(args.table, args.column_def) 94 95 @argshell.with_parser(dbparsers.get_add_table_parser) 96 def do_add_table(self, args: argshell.Namespace): 97 """Add a new table to the database.""" 98 with self._DB() as db: 99 db.create_table(args.table, *args.columns) 100 101 @argshell.with_parser(dbparsers.get_backup_parser) 102 @time_it() 103 def do_backup(self, args: argshell.Namespace): 104 """Create a backup of the current db file.""" 105 print(f"Creating a back up for {self.dbpath}...") 106 backup_path = self.dbpath.backup(args.timestamp) 107 print("Creating backup is complete.") 108 print(f"Backup path: {backup_path}") 109 110 @argshell.with_parser(dbparsers.get_count_parser) 111 @time_it() 112 def do_count(self, args: argshell.Namespace): 113 """Count the number of matching records.""" 114 with self._DB() as db: 115 count = db.count(args.table, args.column, args.where, args.distinct) 116 self.display( 117 [ 118 { 119 "Table": args.table, 120 "Column": args.column, 121 "Distinct": args.distinct, 122 "Where": args.where, 123 "Count": count, 124 } 125 ] 126 ) 127 128 def do_customize(self, name: str): 129 """Generate a template file in the current working directory for creating a custom DBShell class. 130 Expects one argument: the name of the custom dbshell. 131 This will be used to name the generated file as well as several components in the file content. 132 """ 133 try: 134 create_shell(name) 135 except Exception as e: 136 print(f"{type(e).__name__}: {e}") 137 138 def do_dbpath(self, _: str): 139 """Print the .db file in use.""" 140 print(self.dbpath) 141 142 @argshell.with_parser(dbparsers.get_delete_parser) 143 @time_it() 144 def do_delete(self, args: argshell.Namespace): 145 """Delete rows from the database. 146 147 Syntax: 148 >>> delete {table} {where} 149 >>> based>delete users "username LIKE '%chungus%" 150 151 ^will delete all rows in the 'users' table whose username contains 'chungus'^""" 152 print("Deleting records...") 153 with self._DB() as db: 154 num_rows = db.delete(args.table, args.where) 155 print(f"Deleted {num_rows} rows from {args.table} table.") 156 157 def do_describe(self, tables: str): 158 """Describe each given table or view. If no list is given, all tables and views will be described.""" 159 with self._DB() as db: 160 table_list = tables.split() or (db.tables + db.views) 161 for table in table_list: 162 print(f"<{table}>") 163 print(db.to_grid(db.describe(table))) 164 165 @argshell.with_parser(dbparsers.get_drop_column_parser) 166 def do_drop_column(self, args: argshell.Namespace): 167 """Drop the specified column from the specified table.""" 168 with self._DB() as db: 169 db.drop_column(args.table, args.column) 170 171 def do_drop_table(self, table: str): 172 """Drop the specified table.""" 173 with self._DB() as db: 174 db.drop_table(table) 175 176 @argshell.with_parser(dbparsers.get_dump_parser) 177 @time_it() 178 def do_dump(self, args: argshell.Namespace): 179 """Create `.sql` dump files for the current database.""" 180 date = datetime.now().strftime("%m_%d_%Y_%H_%M_%S") 181 if not args.data_only: 182 print("Dumping schema...") 183 with self._DB() as db: 184 db.dump_schema( 185 Pathier.cwd() / f"{db.name}_schema_{date}.sql", args.tables 186 ) 187 if not args.schema_only: 188 print("Dumping data...") 189 with self._DB() as db: 190 db.dump_data(Pathier.cwd() / f"{db.name}_data_{date}.sql", args.tables) 191 192 def do_flush_log(self, _: str): 193 """Clear the log file for this database.""" 194 log_path = self.dbpath.with_name(self.dbpath.name.replace(".", "") + ".log") 195 if not log_path.exists(): 196 print(f"No log file at path {log_path}") 197 else: 198 print(f"Flushing log...") 199 log_path.write_text("") 200 201 def do_help(self, args: str): 202 """Display help messages.""" 203 super().do_help(args) 204 if args == "": 205 print("Unrecognized commands will be executed as queries.") 206 print( 207 "Use the `query` command explicitly if you don't want to capitalize your key words." 208 ) 209 print("All transactions initiated by commands are committed immediately.") 210 print() 211 212 def do_new_db(self, dbname: str): 213 """Create a new, empty database with the given name.""" 214 dbpath = Pathier(dbname) 215 self.dbpath = dbpath 216 self.prompt = f"{self.dbpath.name}>" 217 218 def do_properties(self, _: str): 219 """See current database property settings.""" 220 for property_ in ["connection_timeout", "detect_types", "enforce_foreign_keys"]: 221 print(f"{property_}: {getattr(self, property_)}") 222 223 @time_it() 224 def do_query(self, query: str): 225 """Execute a query against the current database.""" 226 print(f"Executing {query}") 227 with self._DB() as db: 228 results = db.query(query) 229 self.display(results) 230 print(f"{db.cursor.rowcount} affected rows") 231 232 @argshell.with_parser(dbparsers.get_rename_column_parser) 233 def do_rename_column(self, args: argshell.Namespace): 234 """Rename a column.""" 235 with self._DB() as db: 236 db.rename_column(args.table, args.column, args.new_name) 237 238 @argshell.with_parser(dbparsers.get_rename_table_parser) 239 def do_rename_table(self, args: argshell.Namespace): 240 """Rename a table.""" 241 with self._DB() as db: 242 db.rename_table(args.table, args.new_name) 243 244 def do_restore(self, file: str): 245 """Replace the current db file with the given db backup file.""" 246 backup = Pathier(file.strip('"')) 247 if not backup.exists(): 248 print(f"{backup} does not exist.") 249 else: 250 print(f"Restoring from {file}...") 251 self.dbpath.write_bytes(backup.read_bytes()) 252 print("Restore complete.") 253 254 @argshell.with_parser(dbparsers.get_scan_dbs_parser) 255 def do_scan(self, args: argshell.Namespace): 256 """Scan the current working directory for database files.""" 257 dbs = self._scan(args.extensions, args.recursive) 258 for db in dbs: 259 print(db.separate(Pathier.cwd().stem)) 260 261 @argshell.with_parser(dbparsers.get_schema_parser) 262 @time_it() 263 def do_schema(self, args: argshell.Namespace): 264 """Print out the names of the database tables and views, their columns, and, optionally, the number of rows.""" 265 self._show_tables(args) 266 self._show_views(args) 267 268 @time_it() 269 def do_script(self, path: str): 270 """Execute the given SQL script.""" 271 with self._DB() as db: 272 self.display(db.execute_script(path)) 273 274 @argshell.with_parser(dbparsers.get_select_parser, [dbparsers.select_post_parser]) 275 @time_it() 276 def do_select(self, args: argshell.Namespace): 277 """Execute a SELECT query with the given args.""" 278 print(f"Querying {args.table}... ") 279 with self._DB() as db: 280 rows = db.select( 281 table=args.table, 282 columns=args.columns, 283 joins=args.joins, 284 where=args.where, 285 group_by=args.group_by, 286 having=args.Having, 287 order_by=args.order_by, 288 limit=args.limit, 289 exclude_columns=args.exclude_columns, 290 ) 291 print(f"Found {len(rows)} rows:") 292 self.display(rows) 293 print(f"{len(rows)} rows from {args.table}") 294 295 def do_set_connection_timeout(self, seconds: str): 296 """Set database connection timeout to this number of seconds.""" 297 self.connection_timeout = float(seconds) 298 299 def do_set_detect_types(self, should_detect: str): 300 """Pass a `1` to turn on and a `0` to turn off.""" 301 self.detect_types = bool(int(should_detect)) 302 303 def do_set_enforce_foreign_keys(self, should_enforce: str): 304 """Pass a `1` to turn on and a `0` to turn off.""" 305 self.enforce_foreign_keys = bool(int(should_enforce)) 306 307 def do_size(self, _: str): 308 """Display the size of the the current db file.""" 309 print(f"{self.dbpath.name} is {self.dbpath.formatted_size}.") 310 311 @argshell.with_parser(dbparsers.get_schema_parser) 312 @time_it() 313 def do_tables(self, args: argshell.Namespace): 314 """Print out the names of the database tables, their columns, and, optionally, the number of rows.""" 315 self._show_tables(args) 316 317 @argshell.with_parser(dbparsers.get_update_parser) 318 @time_it() 319 def do_update(self, args: argshell.Namespace): 320 """Update a column to a new value. 321 322 Syntax: 323 >>> update {table} {column} {value} {where} 324 >>> based>update users username big_chungus "username = lil_chungus" 325 326 ^will update the username in the users 'table' to 'big_chungus' where the username is currently 'lil_chungus'^ 327 """ 328 print("Updating rows...") 329 with self._DB() as db: 330 num_updates = db.update(args.table, args.column, args.new_value, args.where) 331 print(f"Updated {num_updates} rows in table {args.table}.") 332 333 def do_use(self, dbname: str): 334 """Set which database file to use.""" 335 dbpath = Pathier(dbname) 336 if not dbpath.exists(): 337 print(f"{dbpath} does not exist.") 338 print(f"Still using {self.dbpath}") 339 elif not dbpath.is_file(): 340 print(f"{dbpath} is not a file.") 341 print(f"Still using {self.dbpath}") 342 else: 343 self.dbpath = dbpath 344 self.prompt = f"{self.dbpath.name}>" 345 346 @time_it() 347 def do_vacuum(self, _: str): 348 """Reduce database disk memory.""" 349 print(f"Database size before vacuuming: {self.dbpath.formatted_size}") 350 print("Vacuuming database...") 351 with self._DB() as db: 352 freedspace = db.vacuum() 353 print(f"Database size after vacuuming: {self.dbpath.formatted_size}") 354 print(f"Freed up {Pathier.format_bytes(freedspace)} of disk space.") 355 356 @argshell.with_parser(dbparsers.get_schema_parser) 357 @time_it() 358 def do_views(self, args: argshell.Namespace): 359 """Print out the names of the database views, their columns, and, optionally, the number of rows.""" 360 self._show_views(args) 361 362 # Seat 363 364 def _choose_db(self, options: list[Pathier]) -> Pathier: 365 """Prompt the user to select from a list of files.""" 366 cwd = Pathier.cwd() 367 paths = [path.separate(cwd.stem) for path in options] 368 while True: 369 print( 370 f"DB options:\n{' '.join([f'({i}) {path}' for i, path in enumerate(paths, 1)])}" 371 ) 372 choice = input("Enter the number of the option to use: ") 373 try: 374 index = int(choice) 375 if not 1 <= index <= len(options): 376 print("Choice out of range.") 377 continue 378 return options[index - 1] 379 except Exception as e: 380 print(f"{choice} is not a valid option.") 381 382 def _scan( 383 self, extensions: list[str] = [".sqlite3", ".db"], recursive: bool = False 384 ) -> list[Pathier]: 385 cwd = Pathier.cwd() 386 dbs = [] 387 globber = cwd.glob 388 if recursive: 389 globber = cwd.rglob 390 for extension in extensions: 391 dbs.extend(list(globber(f"*{extension}"))) 392 return dbs 393 394 def preloop(self): 395 """Scan the current directory for a .db file to use. 396 If not found, prompt the user for one or to try again recursively.""" 397 if self.dbpath: 398 self.dbpath = Pathier(self.dbpath) 399 print(f"Defaulting to database {self.dbpath}") 400 else: 401 print("Searching for database...") 402 cwd = Pathier.cwd() 403 dbs = self._scan() 404 if len(dbs) == 1: 405 self.dbpath = dbs[0] 406 print(f"Using database {self.dbpath}.") 407 elif dbs: 408 self.dbpath = self._choose_db(dbs) 409 else: 410 print(f"Could not find a .db file in {cwd}.") 411 path = input( 412 "Enter path to .db file to use or press enter to search again recursively: " 413 ) 414 if path: 415 self.dbpath = Pathier(path) 416 elif not path: 417 print("Searching recursively...") 418 dbs = self._scan(recursive=True) 419 if len(dbs) == 1: 420 self.dbpath = dbs[0] 421 print(f"Using database {self.dbpath}.") 422 elif dbs: 423 self.dbpath = self._choose_db(dbs) 424 else: 425 print("Could not find a .db file.") 426 self.dbpath = Pathier(input("Enter path to a .db file: ")) 427 if not self.dbpath.exists(): 428 raise FileNotFoundError(f"{self.dbpath} does not exist.") 429 if not self.dbpath.is_file(): 430 raise ValueError(f"{self.dbpath} is not a file.") 431 432 433def main(): 434 DBShell().cmdloop()
13class DBShell(argshell.ArgShell): 14 _dbpath: Pathier = None # type: ignore 15 connection_timeout: float = 10 16 detect_types: bool = True 17 enforce_foreign_keys: bool = True 18 intro = f"Starting dbshell v{__version__} (enter help or ? for arg info)...\n" 19 prompt = f"based>" 20 21 @property 22 def dbpath(self) -> Pathier: 23 return self._dbpath 24 25 @dbpath.setter 26 def dbpath(self, path: Pathish): 27 self._dbpath = Pathier(path) 28 self.prompt = f"{self._dbpath.name}>" 29 30 def _DB(self) -> Databased: 31 return Databased( 32 self.dbpath, 33 self.connection_timeout, 34 self.detect_types, 35 self.enforce_foreign_keys, 36 ) 37 38 @time_it() 39 def default(self, line: str): 40 line = line.strip("_") 41 with self._DB() as db: 42 self.display(db.query(line)) 43 44 def display(self, data: list[dict]): 45 """Print row data to terminal in a grid.""" 46 try: 47 print(griddy(data, "keys")) 48 except Exception as e: 49 print("Could not fit data into grid :(") 50 print(e) 51 52 # Seat 53 54 def _show_tables(self, args: argshell.Namespace): 55 with self._DB() as db: 56 if args.tables: 57 tables = [table for table in args.tables if table in db.tables] 58 else: 59 tables = db.tables 60 if tables: 61 print("Getting database tables...") 62 info = [ 63 { 64 "Table Name": table, 65 "Columns": ", ".join(db.get_columns(table)), 66 "Number of Rows": db.count(table) if args.rowcount else "n/a", 67 } 68 for table in tables 69 ] 70 self.display(info) 71 72 def _show_views(self, args: argshell.Namespace): 73 with self._DB() as db: 74 if args.tables: 75 views = [view for view in args.tables if view in db.views] 76 else: 77 views = db.views 78 if views: 79 print("Getting database views...") 80 info = [ 81 { 82 "View Name": view, 83 "Columns": ", ".join(db.get_columns(view)), 84 "Number of Rows": db.count(view) if args.rowcount else "n/a", 85 } 86 for view in views 87 ] 88 self.display(info) 89 90 @argshell.with_parser(dbparsers.get_add_column_parser) 91 def do_add_column(self, args: argshell.Namespace): 92 """Add a new column to the specified tables.""" 93 with self._DB() as db: 94 db.add_column(args.table, args.column_def) 95 96 @argshell.with_parser(dbparsers.get_add_table_parser) 97 def do_add_table(self, args: argshell.Namespace): 98 """Add a new table to the database.""" 99 with self._DB() as db: 100 db.create_table(args.table, *args.columns) 101 102 @argshell.with_parser(dbparsers.get_backup_parser) 103 @time_it() 104 def do_backup(self, args: argshell.Namespace): 105 """Create a backup of the current db file.""" 106 print(f"Creating a back up for {self.dbpath}...") 107 backup_path = self.dbpath.backup(args.timestamp) 108 print("Creating backup is complete.") 109 print(f"Backup path: {backup_path}") 110 111 @argshell.with_parser(dbparsers.get_count_parser) 112 @time_it() 113 def do_count(self, args: argshell.Namespace): 114 """Count the number of matching records.""" 115 with self._DB() as db: 116 count = db.count(args.table, args.column, args.where, args.distinct) 117 self.display( 118 [ 119 { 120 "Table": args.table, 121 "Column": args.column, 122 "Distinct": args.distinct, 123 "Where": args.where, 124 "Count": count, 125 } 126 ] 127 ) 128 129 def do_customize(self, name: str): 130 """Generate a template file in the current working directory for creating a custom DBShell class. 131 Expects one argument: the name of the custom dbshell. 132 This will be used to name the generated file as well as several components in the file content. 133 """ 134 try: 135 create_shell(name) 136 except Exception as e: 137 print(f"{type(e).__name__}: {e}") 138 139 def do_dbpath(self, _: str): 140 """Print the .db file in use.""" 141 print(self.dbpath) 142 143 @argshell.with_parser(dbparsers.get_delete_parser) 144 @time_it() 145 def do_delete(self, args: argshell.Namespace): 146 """Delete rows from the database. 147 148 Syntax: 149 >>> delete {table} {where} 150 >>> based>delete users "username LIKE '%chungus%" 151 152 ^will delete all rows in the 'users' table whose username contains 'chungus'^""" 153 print("Deleting records...") 154 with self._DB() as db: 155 num_rows = db.delete(args.table, args.where) 156 print(f"Deleted {num_rows} rows from {args.table} table.") 157 158 def do_describe(self, tables: str): 159 """Describe each given table or view. If no list is given, all tables and views will be described.""" 160 with self._DB() as db: 161 table_list = tables.split() or (db.tables + db.views) 162 for table in table_list: 163 print(f"<{table}>") 164 print(db.to_grid(db.describe(table))) 165 166 @argshell.with_parser(dbparsers.get_drop_column_parser) 167 def do_drop_column(self, args: argshell.Namespace): 168 """Drop the specified column from the specified table.""" 169 with self._DB() as db: 170 db.drop_column(args.table, args.column) 171 172 def do_drop_table(self, table: str): 173 """Drop the specified table.""" 174 with self._DB() as db: 175 db.drop_table(table) 176 177 @argshell.with_parser(dbparsers.get_dump_parser) 178 @time_it() 179 def do_dump(self, args: argshell.Namespace): 180 """Create `.sql` dump files for the current database.""" 181 date = datetime.now().strftime("%m_%d_%Y_%H_%M_%S") 182 if not args.data_only: 183 print("Dumping schema...") 184 with self._DB() as db: 185 db.dump_schema( 186 Pathier.cwd() / f"{db.name}_schema_{date}.sql", args.tables 187 ) 188 if not args.schema_only: 189 print("Dumping data...") 190 with self._DB() as db: 191 db.dump_data(Pathier.cwd() / f"{db.name}_data_{date}.sql", args.tables) 192 193 def do_flush_log(self, _: str): 194 """Clear the log file for this database.""" 195 log_path = self.dbpath.with_name(self.dbpath.name.replace(".", "") + ".log") 196 if not log_path.exists(): 197 print(f"No log file at path {log_path}") 198 else: 199 print(f"Flushing log...") 200 log_path.write_text("") 201 202 def do_help(self, args: str): 203 """Display help messages.""" 204 super().do_help(args) 205 if args == "": 206 print("Unrecognized commands will be executed as queries.") 207 print( 208 "Use the `query` command explicitly if you don't want to capitalize your key words." 209 ) 210 print("All transactions initiated by commands are committed immediately.") 211 print() 212 213 def do_new_db(self, dbname: str): 214 """Create a new, empty database with the given name.""" 215 dbpath = Pathier(dbname) 216 self.dbpath = dbpath 217 self.prompt = f"{self.dbpath.name}>" 218 219 def do_properties(self, _: str): 220 """See current database property settings.""" 221 for property_ in ["connection_timeout", "detect_types", "enforce_foreign_keys"]: 222 print(f"{property_}: {getattr(self, property_)}") 223 224 @time_it() 225 def do_query(self, query: str): 226 """Execute a query against the current database.""" 227 print(f"Executing {query}") 228 with self._DB() as db: 229 results = db.query(query) 230 self.display(results) 231 print(f"{db.cursor.rowcount} affected rows") 232 233 @argshell.with_parser(dbparsers.get_rename_column_parser) 234 def do_rename_column(self, args: argshell.Namespace): 235 """Rename a column.""" 236 with self._DB() as db: 237 db.rename_column(args.table, args.column, args.new_name) 238 239 @argshell.with_parser(dbparsers.get_rename_table_parser) 240 def do_rename_table(self, args: argshell.Namespace): 241 """Rename a table.""" 242 with self._DB() as db: 243 db.rename_table(args.table, args.new_name) 244 245 def do_restore(self, file: str): 246 """Replace the current db file with the given db backup file.""" 247 backup = Pathier(file.strip('"')) 248 if not backup.exists(): 249 print(f"{backup} does not exist.") 250 else: 251 print(f"Restoring from {file}...") 252 self.dbpath.write_bytes(backup.read_bytes()) 253 print("Restore complete.") 254 255 @argshell.with_parser(dbparsers.get_scan_dbs_parser) 256 def do_scan(self, args: argshell.Namespace): 257 """Scan the current working directory for database files.""" 258 dbs = self._scan(args.extensions, args.recursive) 259 for db in dbs: 260 print(db.separate(Pathier.cwd().stem)) 261 262 @argshell.with_parser(dbparsers.get_schema_parser) 263 @time_it() 264 def do_schema(self, args: argshell.Namespace): 265 """Print out the names of the database tables and views, their columns, and, optionally, the number of rows.""" 266 self._show_tables(args) 267 self._show_views(args) 268 269 @time_it() 270 def do_script(self, path: str): 271 """Execute the given SQL script.""" 272 with self._DB() as db: 273 self.display(db.execute_script(path)) 274 275 @argshell.with_parser(dbparsers.get_select_parser, [dbparsers.select_post_parser]) 276 @time_it() 277 def do_select(self, args: argshell.Namespace): 278 """Execute a SELECT query with the given args.""" 279 print(f"Querying {args.table}... ") 280 with self._DB() as db: 281 rows = db.select( 282 table=args.table, 283 columns=args.columns, 284 joins=args.joins, 285 where=args.where, 286 group_by=args.group_by, 287 having=args.Having, 288 order_by=args.order_by, 289 limit=args.limit, 290 exclude_columns=args.exclude_columns, 291 ) 292 print(f"Found {len(rows)} rows:") 293 self.display(rows) 294 print(f"{len(rows)} rows from {args.table}") 295 296 def do_set_connection_timeout(self, seconds: str): 297 """Set database connection timeout to this number of seconds.""" 298 self.connection_timeout = float(seconds) 299 300 def do_set_detect_types(self, should_detect: str): 301 """Pass a `1` to turn on and a `0` to turn off.""" 302 self.detect_types = bool(int(should_detect)) 303 304 def do_set_enforce_foreign_keys(self, should_enforce: str): 305 """Pass a `1` to turn on and a `0` to turn off.""" 306 self.enforce_foreign_keys = bool(int(should_enforce)) 307 308 def do_size(self, _: str): 309 """Display the size of the the current db file.""" 310 print(f"{self.dbpath.name} is {self.dbpath.formatted_size}.") 311 312 @argshell.with_parser(dbparsers.get_schema_parser) 313 @time_it() 314 def do_tables(self, args: argshell.Namespace): 315 """Print out the names of the database tables, their columns, and, optionally, the number of rows.""" 316 self._show_tables(args) 317 318 @argshell.with_parser(dbparsers.get_update_parser) 319 @time_it() 320 def do_update(self, args: argshell.Namespace): 321 """Update a column to a new value. 322 323 Syntax: 324 >>> update {table} {column} {value} {where} 325 >>> based>update users username big_chungus "username = lil_chungus" 326 327 ^will update the username in the users 'table' to 'big_chungus' where the username is currently 'lil_chungus'^ 328 """ 329 print("Updating rows...") 330 with self._DB() as db: 331 num_updates = db.update(args.table, args.column, args.new_value, args.where) 332 print(f"Updated {num_updates} rows in table {args.table}.") 333 334 def do_use(self, dbname: str): 335 """Set which database file to use.""" 336 dbpath = Pathier(dbname) 337 if not dbpath.exists(): 338 print(f"{dbpath} does not exist.") 339 print(f"Still using {self.dbpath}") 340 elif not dbpath.is_file(): 341 print(f"{dbpath} is not a file.") 342 print(f"Still using {self.dbpath}") 343 else: 344 self.dbpath = dbpath 345 self.prompt = f"{self.dbpath.name}>" 346 347 @time_it() 348 def do_vacuum(self, _: str): 349 """Reduce database disk memory.""" 350 print(f"Database size before vacuuming: {self.dbpath.formatted_size}") 351 print("Vacuuming database...") 352 with self._DB() as db: 353 freedspace = db.vacuum() 354 print(f"Database size after vacuuming: {self.dbpath.formatted_size}") 355 print(f"Freed up {Pathier.format_bytes(freedspace)} of disk space.") 356 357 @argshell.with_parser(dbparsers.get_schema_parser) 358 @time_it() 359 def do_views(self, args: argshell.Namespace): 360 """Print out the names of the database views, their columns, and, optionally, the number of rows.""" 361 self._show_views(args) 362 363 # Seat 364 365 def _choose_db(self, options: list[Pathier]) -> Pathier: 366 """Prompt the user to select from a list of files.""" 367 cwd = Pathier.cwd() 368 paths = [path.separate(cwd.stem) for path in options] 369 while True: 370 print( 371 f"DB options:\n{' '.join([f'({i}) {path}' for i, path in enumerate(paths, 1)])}" 372 ) 373 choice = input("Enter the number of the option to use: ") 374 try: 375 index = int(choice) 376 if not 1 <= index <= len(options): 377 print("Choice out of range.") 378 continue 379 return options[index - 1] 380 except Exception as e: 381 print(f"{choice} is not a valid option.") 382 383 def _scan( 384 self, extensions: list[str] = [".sqlite3", ".db"], recursive: bool = False 385 ) -> list[Pathier]: 386 cwd = Pathier.cwd() 387 dbs = [] 388 globber = cwd.glob 389 if recursive: 390 globber = cwd.rglob 391 for extension in extensions: 392 dbs.extend(list(globber(f"*{extension}"))) 393 return dbs 394 395 def preloop(self): 396 """Scan the current directory for a .db file to use. 397 If not found, prompt the user for one or to try again recursively.""" 398 if self.dbpath: 399 self.dbpath = Pathier(self.dbpath) 400 print(f"Defaulting to database {self.dbpath}") 401 else: 402 print("Searching for database...") 403 cwd = Pathier.cwd() 404 dbs = self._scan() 405 if len(dbs) == 1: 406 self.dbpath = dbs[0] 407 print(f"Using database {self.dbpath}.") 408 elif dbs: 409 self.dbpath = self._choose_db(dbs) 410 else: 411 print(f"Could not find a .db file in {cwd}.") 412 path = input( 413 "Enter path to .db file to use or press enter to search again recursively: " 414 ) 415 if path: 416 self.dbpath = Pathier(path) 417 elif not path: 418 print("Searching recursively...") 419 dbs = self._scan(recursive=True) 420 if len(dbs) == 1: 421 self.dbpath = dbs[0] 422 print(f"Using database {self.dbpath}.") 423 elif dbs: 424 self.dbpath = self._choose_db(dbs) 425 else: 426 print("Could not find a .db file.") 427 self.dbpath = Pathier(input("Enter path to a .db file: ")) 428 if not self.dbpath.exists(): 429 raise FileNotFoundError(f"{self.dbpath} does not exist.") 430 if not self.dbpath.is_file(): 431 raise ValueError(f"{self.dbpath} is not a file.")
Subclass this to create custom ArgShells.
38 @time_it() 39 def default(self, line: str): 40 line = line.strip("_") 41 with self._DB() as db: 42 self.display(db.query(line))
Called on an input line when the command prefix is not recognized.
If this method is not overridden, it prints an error message and returns.
44 def display(self, data: list[dict]): 45 """Print row data to terminal in a grid.""" 46 try: 47 print(griddy(data, "keys")) 48 except Exception as e: 49 print("Could not fit data into grid :(") 50 print(e)
Print row data to terminal in a grid.
90 @argshell.with_parser(dbparsers.get_add_column_parser) 91 def do_add_column(self, args: argshell.Namespace): 92 """Add a new column to the specified tables.""" 93 with self._DB() as db: 94 db.add_column(args.table, args.column_def)
Add a new column to the specified tables.
96 @argshell.with_parser(dbparsers.get_add_table_parser) 97 def do_add_table(self, args: argshell.Namespace): 98 """Add a new table to the database.""" 99 with self._DB() as db: 100 db.create_table(args.table, *args.columns)
Add a new table to the database.
102 @argshell.with_parser(dbparsers.get_backup_parser) 103 @time_it() 104 def do_backup(self, args: argshell.Namespace): 105 """Create a backup of the current db file.""" 106 print(f"Creating a back up for {self.dbpath}...") 107 backup_path = self.dbpath.backup(args.timestamp) 108 print("Creating backup is complete.") 109 print(f"Backup path: {backup_path}")
Create a backup of the current db file.
111 @argshell.with_parser(dbparsers.get_count_parser) 112 @time_it() 113 def do_count(self, args: argshell.Namespace): 114 """Count the number of matching records.""" 115 with self._DB() as db: 116 count = db.count(args.table, args.column, args.where, args.distinct) 117 self.display( 118 [ 119 { 120 "Table": args.table, 121 "Column": args.column, 122 "Distinct": args.distinct, 123 "Where": args.where, 124 "Count": count, 125 } 126 ] 127 )
Count the number of matching records.
129 def do_customize(self, name: str): 130 """Generate a template file in the current working directory for creating a custom DBShell class. 131 Expects one argument: the name of the custom dbshell. 132 This will be used to name the generated file as well as several components in the file content. 133 """ 134 try: 135 create_shell(name) 136 except Exception as e: 137 print(f"{type(e).__name__}: {e}")
Generate a template file in the current working directory for creating a custom DBShell class. Expects one argument: the name of the custom dbshell. This will be used to name the generated file as well as several components in the file content.
143 @argshell.with_parser(dbparsers.get_delete_parser) 144 @time_it() 145 def do_delete(self, args: argshell.Namespace): 146 """Delete rows from the database. 147 148 Syntax: 149 >>> delete {table} {where} 150 >>> based>delete users "username LIKE '%chungus%" 151 152 ^will delete all rows in the 'users' table whose username contains 'chungus'^""" 153 print("Deleting records...") 154 with self._DB() as db: 155 num_rows = db.delete(args.table, args.where) 156 print(f"Deleted {num_rows} rows from {args.table} table.")
Delete rows from the database.
Syntax:
>>> delete {table} {where}
>>> based>delete users "username LIKE '%chungus%"
^will delete all rows in the 'users' table whose username contains 'chungus'^
158 def do_describe(self, tables: str): 159 """Describe each given table or view. If no list is given, all tables and views will be described.""" 160 with self._DB() as db: 161 table_list = tables.split() or (db.tables + db.views) 162 for table in table_list: 163 print(f"<{table}>") 164 print(db.to_grid(db.describe(table)))
Describe each given table or view. If no list is given, all tables and views will be described.
166 @argshell.with_parser(dbparsers.get_drop_column_parser) 167 def do_drop_column(self, args: argshell.Namespace): 168 """Drop the specified column from the specified table.""" 169 with self._DB() as db: 170 db.drop_column(args.table, args.column)
Drop the specified column from the specified table.
172 def do_drop_table(self, table: str): 173 """Drop the specified table.""" 174 with self._DB() as db: 175 db.drop_table(table)
Drop the specified table.
177 @argshell.with_parser(dbparsers.get_dump_parser) 178 @time_it() 179 def do_dump(self, args: argshell.Namespace): 180 """Create `.sql` dump files for the current database.""" 181 date = datetime.now().strftime("%m_%d_%Y_%H_%M_%S") 182 if not args.data_only: 183 print("Dumping schema...") 184 with self._DB() as db: 185 db.dump_schema( 186 Pathier.cwd() / f"{db.name}_schema_{date}.sql", args.tables 187 ) 188 if not args.schema_only: 189 print("Dumping data...") 190 with self._DB() as db: 191 db.dump_data(Pathier.cwd() / f"{db.name}_data_{date}.sql", args.tables)
Create .sql
dump files for the current database.
193 def do_flush_log(self, _: str): 194 """Clear the log file for this database.""" 195 log_path = self.dbpath.with_name(self.dbpath.name.replace(".", "") + ".log") 196 if not log_path.exists(): 197 print(f"No log file at path {log_path}") 198 else: 199 print(f"Flushing log...") 200 log_path.write_text("")
Clear the log file for this database.
202 def do_help(self, args: str): 203 """Display help messages.""" 204 super().do_help(args) 205 if args == "": 206 print("Unrecognized commands will be executed as queries.") 207 print( 208 "Use the `query` command explicitly if you don't want to capitalize your key words." 209 ) 210 print("All transactions initiated by commands are committed immediately.") 211 print()
Display help messages.
213 def do_new_db(self, dbname: str): 214 """Create a new, empty database with the given name.""" 215 dbpath = Pathier(dbname) 216 self.dbpath = dbpath 217 self.prompt = f"{self.dbpath.name}>"
Create a new, empty database with the given name.
219 def do_properties(self, _: str): 220 """See current database property settings.""" 221 for property_ in ["connection_timeout", "detect_types", "enforce_foreign_keys"]: 222 print(f"{property_}: {getattr(self, property_)}")
See current database property settings.
224 @time_it() 225 def do_query(self, query: str): 226 """Execute a query against the current database.""" 227 print(f"Executing {query}") 228 with self._DB() as db: 229 results = db.query(query) 230 self.display(results) 231 print(f"{db.cursor.rowcount} affected rows")
Execute a query against the current database.
233 @argshell.with_parser(dbparsers.get_rename_column_parser) 234 def do_rename_column(self, args: argshell.Namespace): 235 """Rename a column.""" 236 with self._DB() as db: 237 db.rename_column(args.table, args.column, args.new_name)
Rename a column.
239 @argshell.with_parser(dbparsers.get_rename_table_parser) 240 def do_rename_table(self, args: argshell.Namespace): 241 """Rename a table.""" 242 with self._DB() as db: 243 db.rename_table(args.table, args.new_name)
Rename a table.
245 def do_restore(self, file: str): 246 """Replace the current db file with the given db backup file.""" 247 backup = Pathier(file.strip('"')) 248 if not backup.exists(): 249 print(f"{backup} does not exist.") 250 else: 251 print(f"Restoring from {file}...") 252 self.dbpath.write_bytes(backup.read_bytes()) 253 print("Restore complete.")
Replace the current db file with the given db backup file.
255 @argshell.with_parser(dbparsers.get_scan_dbs_parser) 256 def do_scan(self, args: argshell.Namespace): 257 """Scan the current working directory for database files.""" 258 dbs = self._scan(args.extensions, args.recursive) 259 for db in dbs: 260 print(db.separate(Pathier.cwd().stem))
Scan the current working directory for database files.
262 @argshell.with_parser(dbparsers.get_schema_parser) 263 @time_it() 264 def do_schema(self, args: argshell.Namespace): 265 """Print out the names of the database tables and views, their columns, and, optionally, the number of rows.""" 266 self._show_tables(args) 267 self._show_views(args)
Print out the names of the database tables and views, their columns, and, optionally, the number of rows.
269 @time_it() 270 def do_script(self, path: str): 271 """Execute the given SQL script.""" 272 with self._DB() as db: 273 self.display(db.execute_script(path))
Execute the given SQL script.
275 @argshell.with_parser(dbparsers.get_select_parser, [dbparsers.select_post_parser]) 276 @time_it() 277 def do_select(self, args: argshell.Namespace): 278 """Execute a SELECT query with the given args.""" 279 print(f"Querying {args.table}... ") 280 with self._DB() as db: 281 rows = db.select( 282 table=args.table, 283 columns=args.columns, 284 joins=args.joins, 285 where=args.where, 286 group_by=args.group_by, 287 having=args.Having, 288 order_by=args.order_by, 289 limit=args.limit, 290 exclude_columns=args.exclude_columns, 291 ) 292 print(f"Found {len(rows)} rows:") 293 self.display(rows) 294 print(f"{len(rows)} rows from {args.table}")
Execute a SELECT query with the given args.
296 def do_set_connection_timeout(self, seconds: str): 297 """Set database connection timeout to this number of seconds.""" 298 self.connection_timeout = float(seconds)
Set database connection timeout to this number of seconds.
300 def do_set_detect_types(self, should_detect: str): 301 """Pass a `1` to turn on and a `0` to turn off.""" 302 self.detect_types = bool(int(should_detect))
Pass a 1
to turn on and a 0
to turn off.
304 def do_set_enforce_foreign_keys(self, should_enforce: str): 305 """Pass a `1` to turn on and a `0` to turn off.""" 306 self.enforce_foreign_keys = bool(int(should_enforce))
Pass a 1
to turn on and a 0
to turn off.
308 def do_size(self, _: str): 309 """Display the size of the the current db file.""" 310 print(f"{self.dbpath.name} is {self.dbpath.formatted_size}.")
Display the size of the the current db file.
312 @argshell.with_parser(dbparsers.get_schema_parser) 313 @time_it() 314 def do_tables(self, args: argshell.Namespace): 315 """Print out the names of the database tables, their columns, and, optionally, the number of rows.""" 316 self._show_tables(args)
Print out the names of the database tables, their columns, and, optionally, the number of rows.
318 @argshell.with_parser(dbparsers.get_update_parser) 319 @time_it() 320 def do_update(self, args: argshell.Namespace): 321 """Update a column to a new value. 322 323 Syntax: 324 >>> update {table} {column} {value} {where} 325 >>> based>update users username big_chungus "username = lil_chungus" 326 327 ^will update the username in the users 'table' to 'big_chungus' where the username is currently 'lil_chungus'^ 328 """ 329 print("Updating rows...") 330 with self._DB() as db: 331 num_updates = db.update(args.table, args.column, args.new_value, args.where) 332 print(f"Updated {num_updates} rows in table {args.table}.")
Update a column to a new value.
Syntax:
>>> update {table} {column} {value} {where}
>>> based>update users username big_chungus "username = lil_chungus"
^will update the username in the users 'table' to 'big_chungus' where the username is currently 'lil_chungus'^
334 def do_use(self, dbname: str): 335 """Set which database file to use.""" 336 dbpath = Pathier(dbname) 337 if not dbpath.exists(): 338 print(f"{dbpath} does not exist.") 339 print(f"Still using {self.dbpath}") 340 elif not dbpath.is_file(): 341 print(f"{dbpath} is not a file.") 342 print(f"Still using {self.dbpath}") 343 else: 344 self.dbpath = dbpath 345 self.prompt = f"{self.dbpath.name}>"
Set which database file to use.
347 @time_it() 348 def do_vacuum(self, _: str): 349 """Reduce database disk memory.""" 350 print(f"Database size before vacuuming: {self.dbpath.formatted_size}") 351 print("Vacuuming database...") 352 with self._DB() as db: 353 freedspace = db.vacuum() 354 print(f"Database size after vacuuming: {self.dbpath.formatted_size}") 355 print(f"Freed up {Pathier.format_bytes(freedspace)} of disk space.")
Reduce database disk memory.
357 @argshell.with_parser(dbparsers.get_schema_parser) 358 @time_it() 359 def do_views(self, args: argshell.Namespace): 360 """Print out the names of the database views, their columns, and, optionally, the number of rows.""" 361 self._show_views(args)
Print out the names of the database views, their columns, and, optionally, the number of rows.
395 def preloop(self): 396 """Scan the current directory for a .db file to use. 397 If not found, prompt the user for one or to try again recursively.""" 398 if self.dbpath: 399 self.dbpath = Pathier(self.dbpath) 400 print(f"Defaulting to database {self.dbpath}") 401 else: 402 print("Searching for database...") 403 cwd = Pathier.cwd() 404 dbs = self._scan() 405 if len(dbs) == 1: 406 self.dbpath = dbs[0] 407 print(f"Using database {self.dbpath}.") 408 elif dbs: 409 self.dbpath = self._choose_db(dbs) 410 else: 411 print(f"Could not find a .db file in {cwd}.") 412 path = input( 413 "Enter path to .db file to use or press enter to search again recursively: " 414 ) 415 if path: 416 self.dbpath = Pathier(path) 417 elif not path: 418 print("Searching recursively...") 419 dbs = self._scan(recursive=True) 420 if len(dbs) == 1: 421 self.dbpath = dbs[0] 422 print(f"Using database {self.dbpath}.") 423 elif dbs: 424 self.dbpath = self._choose_db(dbs) 425 else: 426 print("Could not find a .db file.") 427 self.dbpath = Pathier(input("Enter path to a .db file: ")) 428 if not self.dbpath.exists(): 429 raise FileNotFoundError(f"{self.dbpath} does not exist.") 430 if not self.dbpath.is_file(): 431 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
- completedefault
- completenames
- complete
- get_names
- complete_help
- print_topics
- columnize
- argshell.argshell.ArgShell
- do_quit
- do_sys
- cmdloop
- emptyline