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 def do_customize(self, name: str): 111 """Generate a template file in the current working directory for creating a custom DBShell class. 112 Expects one argument: the name of the custom dbshell. 113 This will be used to name the generated file as well as several components in the file content. 114 """ 115 try: 116 create_shell(name) 117 except Exception as e: 118 print(f"{type(e).__name__}: {e}") 119 120 def do_dbpath(self, _: str): 121 """Print the .db file in use.""" 122 print(self.dbpath) 123 124 @argshell.with_parser(dbparsers.get_delete_parser) 125 @time_it() 126 def do_delete(self, args: argshell.Namespace): 127 """Delete rows from the database. 128 129 Syntax: 130 >>> delete {table} {where} 131 >>> based>delete users "username LIKE '%chungus%" 132 133 ^will delete all rows in the 'users' table whose username contains 'chungus'^""" 134 print("Deleting records...") 135 with self._DB() as db: 136 num_rows = db.delete(args.table, args.where) 137 print(f"Deleted {num_rows} rows from {args.table} table.") 138 139 def do_describe(self, tables: str): 140 """Describe each given table or view. If no list is given, all tables and views will be described.""" 141 with self._DB() as db: 142 table_list = tables.split() or (db.tables + db.views) 143 for table in table_list: 144 print(f"<{table}>") 145 print(db.to_grid(db.describe(table))) 146 147 @argshell.with_parser(dbparsers.get_drop_column_parser) 148 def do_drop_column(self, args: argshell.Namespace): 149 """Drop the specified column from the specified table.""" 150 with self._DB() as db: 151 db.drop_column(args.table, args.column) 152 153 def do_drop_table(self, table: str): 154 """Drop the specified table.""" 155 with self._DB() as db: 156 db.drop_table(table) 157 158 @argshell.with_parser(dbparsers.get_dump_parser) 159 @time_it() 160 def do_dump(self, args: argshell.Namespace): 161 """Create `.sql` dump files for the current database.""" 162 date = datetime.now().strftime("%m_%d_%Y_%H_%M_%S") 163 if not args.data_only: 164 print("Dumping schema...") 165 with self._DB() as db: 166 db.dump_schema( 167 Pathier.cwd() / f"{db.name}_schema_{date}.sql", args.tables 168 ) 169 if not args.schema_only: 170 print("Dumping data...") 171 with self._DB() as db: 172 db.dump_data(Pathier.cwd() / f"{db.name}_data_{date}.sql", args.tables) 173 174 def do_flush_log(self, _: str): 175 """Clear the log file for this database.""" 176 log_path = self.dbpath.with_name(self.dbpath.name.replace(".", "") + ".log") 177 if not log_path.exists(): 178 print(f"No log file at path {log_path}") 179 else: 180 print(f"Flushing log...") 181 log_path.write_text("") 182 183 def do_help(self, args: str): 184 """Display help messages.""" 185 super().do_help(args) 186 if args == "": 187 print("Unrecognized commands will be executed as queries.") 188 print( 189 "Use the `query` command explicitly if you don't want to capitalize your key words." 190 ) 191 print("All transactions initiated by commands are committed immediately.") 192 print() 193 194 def do_new_db(self, dbname: str): 195 """Create a new, empty database with the given name.""" 196 dbpath = Pathier(dbname) 197 self.dbpath = dbpath 198 self.prompt = f"{self.dbpath.name}>" 199 200 def do_properties(self, _: str): 201 """See current database property settings.""" 202 for property_ in ["connection_timeout", "detect_types", "enforce_foreign_keys"]: 203 print(f"{property_}: {getattr(self, property_)}") 204 205 @time_it() 206 def do_query(self, query: str): 207 """Execute a query against the current database.""" 208 print(f"Executing {query}") 209 with self._DB() as db: 210 results = db.query(query) 211 self.display(results) 212 print(f"{db.cursor.rowcount} affected rows") 213 214 @argshell.with_parser(dbparsers.get_rename_column_parser) 215 def do_rename_column(self, args: argshell.Namespace): 216 """Rename a column.""" 217 with self._DB() as db: 218 db.rename_column(args.table, args.column, args.new_name) 219 220 @argshell.with_parser(dbparsers.get_rename_table_parser) 221 def do_rename_table(self, args: argshell.Namespace): 222 """Rename a table.""" 223 with self._DB() as db: 224 db.rename_table(args.table, args.new_name) 225 226 def do_restore(self, file: str): 227 """Replace the current db file with the given db backup file.""" 228 backup = Pathier(file.strip('"')) 229 if not backup.exists(): 230 print(f"{backup} does not exist.") 231 else: 232 print(f"Restoring from {file}...") 233 self.dbpath.write_bytes(backup.read_bytes()) 234 print("Restore complete.") 235 236 @argshell.with_parser(dbparsers.get_scan_dbs_parser) 237 def do_scan(self, args: argshell.Namespace): 238 """Scan the current working directory for database files.""" 239 dbs = self._scan(args.extensions, args.recursive) 240 for db in dbs: 241 print(db.separate(Pathier.cwd().stem)) 242 243 @argshell.with_parser(dbparsers.get_schema_parser) 244 @time_it() 245 def do_schema(self, args: argshell.Namespace): 246 """Print out the names of the database tables and views, their columns, and, optionally, the number of rows.""" 247 self._show_tables(args) 248 self._show_views(args) 249 250 @time_it() 251 def do_script(self, path: str): 252 """Execute the given SQL script.""" 253 with self._DB() as db: 254 self.display(db.execute_script(path)) 255 256 @argshell.with_parser(dbparsers.get_select_parser, [dbparsers.select_post_parser]) 257 @time_it() 258 def do_select(self, args: argshell.Namespace): 259 """Execute a SELECT query with the given args.""" 260 print(f"Querying {args.table}... ") 261 with self._DB() as db: 262 rows = db.select( 263 table=args.table, 264 columns=args.columns, 265 joins=args.joins, 266 where=args.where, 267 group_by=args.group_by, 268 having=args.Having, 269 order_by=args.order_by, 270 limit=args.limit, 271 ) 272 print(f"Found {len(rows)} rows:") 273 self.display(rows) 274 print(f"{len(rows)} rows from {args.table}") 275 276 def do_set_connection_timeout(self, seconds: str): 277 """Set database connection timeout to this number of seconds.""" 278 self.connection_timeout = float(seconds) 279 280 def do_set_detect_types(self, should_detect: str): 281 """Pass a `1` to turn on and a `0` to turn off.""" 282 self.detect_types = bool(int(should_detect)) 283 284 def do_set_enforce_foreign_keys(self, should_enforce: str): 285 """Pass a `1` to turn on and a `0` to turn off.""" 286 self.enforce_foreign_keys = bool(int(should_enforce)) 287 288 def do_size(self, _: str): 289 """Display the size of the the current db file.""" 290 print(f"{self.dbpath.name} is {self.dbpath.formatted_size}.") 291 292 @argshell.with_parser(dbparsers.get_schema_parser) 293 @time_it() 294 def do_tables(self, args: argshell.Namespace): 295 """Print out the names of the database tables, their columns, and, optionally, the number of rows.""" 296 self._show_tables(args) 297 298 @argshell.with_parser(dbparsers.get_update_parser) 299 @time_it() 300 def do_update(self, args: argshell.Namespace): 301 """Update a column to a new value. 302 303 Syntax: 304 >>> update {table} {column} {value} {where} 305 >>> based>update users username big_chungus "username = lil_chungus" 306 307 ^will update the username in the users 'table' to 'big_chungus' where the username is currently 'lil_chungus'^ 308 """ 309 print("Updating rows...") 310 with self._DB() as db: 311 num_updates = db.update(args.table, args.column, args.new_value, args.where) 312 print(f"Updated {num_updates} rows in table {args.table}.") 313 314 def do_use(self, dbname: str): 315 """Set which database file to use.""" 316 dbpath = Pathier(dbname) 317 if not dbpath.exists(): 318 print(f"{dbpath} does not exist.") 319 print(f"Still using {self.dbpath}") 320 elif not dbpath.is_file(): 321 print(f"{dbpath} is not a file.") 322 print(f"Still using {self.dbpath}") 323 else: 324 self.dbpath = dbpath 325 self.prompt = f"{self.dbpath.name}>" 326 327 @time_it() 328 def do_vacuum(self, _: str): 329 """Reduce database disk memory.""" 330 print(f"Database size before vacuuming: {self.dbpath.formatted_size}") 331 print("Vacuuming database...") 332 with self._DB() as db: 333 freedspace = db.vacuum() 334 print(f"Database size after vacuuming: {self.dbpath.formatted_size}") 335 print(f"Freed up {Pathier.format_bytes(freedspace)} of disk space.") 336 337 @argshell.with_parser(dbparsers.get_schema_parser) 338 @time_it() 339 def do_views(self, args: argshell.Namespace): 340 """Print out the names of the database views, their columns, and, optionally, the number of rows.""" 341 self._show_views(args) 342 343 # Seat 344 345 def _choose_db(self, options: list[Pathier]) -> Pathier: 346 """Prompt the user to select from a list of files.""" 347 cwd = Pathier.cwd() 348 paths = [path.separate(cwd.stem) for path in options] 349 while True: 350 print( 351 f"DB options:\n{' '.join([f'({i}) {path}' for i, path in enumerate(paths, 1)])}" 352 ) 353 choice = input("Enter the number of the option to use: ") 354 try: 355 index = int(choice) 356 if not 1 <= index <= len(options): 357 print("Choice out of range.") 358 continue 359 return options[index - 1] 360 except Exception as e: 361 print(f"{choice} is not a valid option.") 362 363 def _scan( 364 self, extensions: list[str] = [".sqlite3", ".db"], recursive: bool = False 365 ) -> list[Pathier]: 366 cwd = Pathier.cwd() 367 dbs = [] 368 globber = cwd.glob 369 if recursive: 370 globber = cwd.rglob 371 for extension in extensions: 372 dbs.extend(list(globber(f"*{extension}"))) 373 return dbs 374 375 def preloop(self): 376 """Scan the current directory for a .db file to use. 377 If not found, prompt the user for one or to try again recursively.""" 378 if self.dbpath: 379 self.dbpath = Pathier(self.dbpath) 380 print(f"Defaulting to database {self.dbpath}") 381 else: 382 print("Searching for database...") 383 cwd = Pathier.cwd() 384 dbs = self._scan() 385 if len(dbs) == 1: 386 self.dbpath = dbs[0] 387 print(f"Using database {self.dbpath}.") 388 elif dbs: 389 self.dbpath = self._choose_db(dbs) 390 else: 391 print(f"Could not find a .db file in {cwd}.") 392 path = input( 393 "Enter path to .db file to use or press enter to search again recursively: " 394 ) 395 if path: 396 self.dbpath = Pathier(path) 397 elif not path: 398 print("Searching recursively...") 399 dbs = self._scan(recursive=True) 400 if len(dbs) == 1: 401 self.dbpath = dbs[0] 402 print(f"Using database {self.dbpath}.") 403 elif dbs: 404 self.dbpath = self._choose_db(dbs) 405 else: 406 print("Could not find a .db file.") 407 self.dbpath = Pathier(input("Enter path to a .db file: ")) 408 if not self.dbpath.exists(): 409 raise FileNotFoundError(f"{self.dbpath} does not exist.") 410 if not self.dbpath.is_file(): 411 raise ValueError(f"{self.dbpath} is not a file.") 412 413 414def main(): 415 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 def do_customize(self, name: str): 112 """Generate a template file in the current working directory for creating a custom DBShell class. 113 Expects one argument: the name of the custom dbshell. 114 This will be used to name the generated file as well as several components in the file content. 115 """ 116 try: 117 create_shell(name) 118 except Exception as e: 119 print(f"{type(e).__name__}: {e}") 120 121 def do_dbpath(self, _: str): 122 """Print the .db file in use.""" 123 print(self.dbpath) 124 125 @argshell.with_parser(dbparsers.get_delete_parser) 126 @time_it() 127 def do_delete(self, args: argshell.Namespace): 128 """Delete rows from the database. 129 130 Syntax: 131 >>> delete {table} {where} 132 >>> based>delete users "username LIKE '%chungus%" 133 134 ^will delete all rows in the 'users' table whose username contains 'chungus'^""" 135 print("Deleting records...") 136 with self._DB() as db: 137 num_rows = db.delete(args.table, args.where) 138 print(f"Deleted {num_rows} rows from {args.table} table.") 139 140 def do_describe(self, tables: str): 141 """Describe each given table or view. If no list is given, all tables and views will be described.""" 142 with self._DB() as db: 143 table_list = tables.split() or (db.tables + db.views) 144 for table in table_list: 145 print(f"<{table}>") 146 print(db.to_grid(db.describe(table))) 147 148 @argshell.with_parser(dbparsers.get_drop_column_parser) 149 def do_drop_column(self, args: argshell.Namespace): 150 """Drop the specified column from the specified table.""" 151 with self._DB() as db: 152 db.drop_column(args.table, args.column) 153 154 def do_drop_table(self, table: str): 155 """Drop the specified table.""" 156 with self._DB() as db: 157 db.drop_table(table) 158 159 @argshell.with_parser(dbparsers.get_dump_parser) 160 @time_it() 161 def do_dump(self, args: argshell.Namespace): 162 """Create `.sql` dump files for the current database.""" 163 date = datetime.now().strftime("%m_%d_%Y_%H_%M_%S") 164 if not args.data_only: 165 print("Dumping schema...") 166 with self._DB() as db: 167 db.dump_schema( 168 Pathier.cwd() / f"{db.name}_schema_{date}.sql", args.tables 169 ) 170 if not args.schema_only: 171 print("Dumping data...") 172 with self._DB() as db: 173 db.dump_data(Pathier.cwd() / f"{db.name}_data_{date}.sql", args.tables) 174 175 def do_flush_log(self, _: str): 176 """Clear the log file for this database.""" 177 log_path = self.dbpath.with_name(self.dbpath.name.replace(".", "") + ".log") 178 if not log_path.exists(): 179 print(f"No log file at path {log_path}") 180 else: 181 print(f"Flushing log...") 182 log_path.write_text("") 183 184 def do_help(self, args: str): 185 """Display help messages.""" 186 super().do_help(args) 187 if args == "": 188 print("Unrecognized commands will be executed as queries.") 189 print( 190 "Use the `query` command explicitly if you don't want to capitalize your key words." 191 ) 192 print("All transactions initiated by commands are committed immediately.") 193 print() 194 195 def do_new_db(self, dbname: str): 196 """Create a new, empty database with the given name.""" 197 dbpath = Pathier(dbname) 198 self.dbpath = dbpath 199 self.prompt = f"{self.dbpath.name}>" 200 201 def do_properties(self, _: str): 202 """See current database property settings.""" 203 for property_ in ["connection_timeout", "detect_types", "enforce_foreign_keys"]: 204 print(f"{property_}: {getattr(self, property_)}") 205 206 @time_it() 207 def do_query(self, query: str): 208 """Execute a query against the current database.""" 209 print(f"Executing {query}") 210 with self._DB() as db: 211 results = db.query(query) 212 self.display(results) 213 print(f"{db.cursor.rowcount} affected rows") 214 215 @argshell.with_parser(dbparsers.get_rename_column_parser) 216 def do_rename_column(self, args: argshell.Namespace): 217 """Rename a column.""" 218 with self._DB() as db: 219 db.rename_column(args.table, args.column, args.new_name) 220 221 @argshell.with_parser(dbparsers.get_rename_table_parser) 222 def do_rename_table(self, args: argshell.Namespace): 223 """Rename a table.""" 224 with self._DB() as db: 225 db.rename_table(args.table, args.new_name) 226 227 def do_restore(self, file: str): 228 """Replace the current db file with the given db backup file.""" 229 backup = Pathier(file.strip('"')) 230 if not backup.exists(): 231 print(f"{backup} does not exist.") 232 else: 233 print(f"Restoring from {file}...") 234 self.dbpath.write_bytes(backup.read_bytes()) 235 print("Restore complete.") 236 237 @argshell.with_parser(dbparsers.get_scan_dbs_parser) 238 def do_scan(self, args: argshell.Namespace): 239 """Scan the current working directory for database files.""" 240 dbs = self._scan(args.extensions, args.recursive) 241 for db in dbs: 242 print(db.separate(Pathier.cwd().stem)) 243 244 @argshell.with_parser(dbparsers.get_schema_parser) 245 @time_it() 246 def do_schema(self, args: argshell.Namespace): 247 """Print out the names of the database tables and views, their columns, and, optionally, the number of rows.""" 248 self._show_tables(args) 249 self._show_views(args) 250 251 @time_it() 252 def do_script(self, path: str): 253 """Execute the given SQL script.""" 254 with self._DB() as db: 255 self.display(db.execute_script(path)) 256 257 @argshell.with_parser(dbparsers.get_select_parser, [dbparsers.select_post_parser]) 258 @time_it() 259 def do_select(self, args: argshell.Namespace): 260 """Execute a SELECT query with the given args.""" 261 print(f"Querying {args.table}... ") 262 with self._DB() as db: 263 rows = db.select( 264 table=args.table, 265 columns=args.columns, 266 joins=args.joins, 267 where=args.where, 268 group_by=args.group_by, 269 having=args.Having, 270 order_by=args.order_by, 271 limit=args.limit, 272 ) 273 print(f"Found {len(rows)} rows:") 274 self.display(rows) 275 print(f"{len(rows)} rows from {args.table}") 276 277 def do_set_connection_timeout(self, seconds: str): 278 """Set database connection timeout to this number of seconds.""" 279 self.connection_timeout = float(seconds) 280 281 def do_set_detect_types(self, should_detect: str): 282 """Pass a `1` to turn on and a `0` to turn off.""" 283 self.detect_types = bool(int(should_detect)) 284 285 def do_set_enforce_foreign_keys(self, should_enforce: str): 286 """Pass a `1` to turn on and a `0` to turn off.""" 287 self.enforce_foreign_keys = bool(int(should_enforce)) 288 289 def do_size(self, _: str): 290 """Display the size of the the current db file.""" 291 print(f"{self.dbpath.name} is {self.dbpath.formatted_size}.") 292 293 @argshell.with_parser(dbparsers.get_schema_parser) 294 @time_it() 295 def do_tables(self, args: argshell.Namespace): 296 """Print out the names of the database tables, their columns, and, optionally, the number of rows.""" 297 self._show_tables(args) 298 299 @argshell.with_parser(dbparsers.get_update_parser) 300 @time_it() 301 def do_update(self, args: argshell.Namespace): 302 """Update a column to a new value. 303 304 Syntax: 305 >>> update {table} {column} {value} {where} 306 >>> based>update users username big_chungus "username = lil_chungus" 307 308 ^will update the username in the users 'table' to 'big_chungus' where the username is currently 'lil_chungus'^ 309 """ 310 print("Updating rows...") 311 with self._DB() as db: 312 num_updates = db.update(args.table, args.column, args.new_value, args.where) 313 print(f"Updated {num_updates} rows in table {args.table}.") 314 315 def do_use(self, dbname: str): 316 """Set which database file to use.""" 317 dbpath = Pathier(dbname) 318 if not dbpath.exists(): 319 print(f"{dbpath} does not exist.") 320 print(f"Still using {self.dbpath}") 321 elif not dbpath.is_file(): 322 print(f"{dbpath} is not a file.") 323 print(f"Still using {self.dbpath}") 324 else: 325 self.dbpath = dbpath 326 self.prompt = f"{self.dbpath.name}>" 327 328 @time_it() 329 def do_vacuum(self, _: str): 330 """Reduce database disk memory.""" 331 print(f"Database size before vacuuming: {self.dbpath.formatted_size}") 332 print("Vacuuming database...") 333 with self._DB() as db: 334 freedspace = db.vacuum() 335 print(f"Database size after vacuuming: {self.dbpath.formatted_size}") 336 print(f"Freed up {Pathier.format_bytes(freedspace)} of disk space.") 337 338 @argshell.with_parser(dbparsers.get_schema_parser) 339 @time_it() 340 def do_views(self, args: argshell.Namespace): 341 """Print out the names of the database views, their columns, and, optionally, the number of rows.""" 342 self._show_views(args) 343 344 # Seat 345 346 def _choose_db(self, options: list[Pathier]) -> Pathier: 347 """Prompt the user to select from a list of files.""" 348 cwd = Pathier.cwd() 349 paths = [path.separate(cwd.stem) for path in options] 350 while True: 351 print( 352 f"DB options:\n{' '.join([f'({i}) {path}' for i, path in enumerate(paths, 1)])}" 353 ) 354 choice = input("Enter the number of the option to use: ") 355 try: 356 index = int(choice) 357 if not 1 <= index <= len(options): 358 print("Choice out of range.") 359 continue 360 return options[index - 1] 361 except Exception as e: 362 print(f"{choice} is not a valid option.") 363 364 def _scan( 365 self, extensions: list[str] = [".sqlite3", ".db"], recursive: bool = False 366 ) -> list[Pathier]: 367 cwd = Pathier.cwd() 368 dbs = [] 369 globber = cwd.glob 370 if recursive: 371 globber = cwd.rglob 372 for extension in extensions: 373 dbs.extend(list(globber(f"*{extension}"))) 374 return dbs 375 376 def preloop(self): 377 """Scan the current directory for a .db file to use. 378 If not found, prompt the user for one or to try again recursively.""" 379 if self.dbpath: 380 self.dbpath = Pathier(self.dbpath) 381 print(f"Defaulting to database {self.dbpath}") 382 else: 383 print("Searching for database...") 384 cwd = Pathier.cwd() 385 dbs = self._scan() 386 if len(dbs) == 1: 387 self.dbpath = dbs[0] 388 print(f"Using database {self.dbpath}.") 389 elif dbs: 390 self.dbpath = self._choose_db(dbs) 391 else: 392 print(f"Could not find a .db file in {cwd}.") 393 path = input( 394 "Enter path to .db file to use or press enter to search again recursively: " 395 ) 396 if path: 397 self.dbpath = Pathier(path) 398 elif not path: 399 print("Searching recursively...") 400 dbs = self._scan(recursive=True) 401 if len(dbs) == 1: 402 self.dbpath = dbs[0] 403 print(f"Using database {self.dbpath}.") 404 elif dbs: 405 self.dbpath = self._choose_db(dbs) 406 else: 407 print("Could not find a .db file.") 408 self.dbpath = Pathier(input("Enter path to a .db file: ")) 409 if not self.dbpath.exists(): 410 raise FileNotFoundError(f"{self.dbpath} does not exist.") 411 if not self.dbpath.is_file(): 412 raise ValueError(f"{self.dbpath} is not a file.")
Subclass this to create custom ArgShells.
17 def wrapper(*args, **kwargs) -> Any: 18 timer = Timer(loops) 19 result = None 20 for _ in range(loops): 21 timer.start() 22 result = func(*args, **kwargs) 23 timer.stop() 24 print( 25 f"{func.__name__} {'average ' if loops > 1 else ''}execution time: {timer.average_elapsed_str}" 26 ) 27 return result
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.
17 def wrapper(*args, **kwargs) -> Any: 18 timer = Timer(loops) 19 result = None 20 for _ in range(loops): 21 timer.start() 22 result = func(*args, **kwargs) 23 timer.stop() 24 print( 25 f"{func.__name__} {'average ' if loops > 1 else ''}execution time: {timer.average_elapsed_str}" 26 ) 27 return result
Create a backup of the current db file.
111 def do_customize(self, name: str): 112 """Generate a template file in the current working directory for creating a custom DBShell class. 113 Expects one argument: the name of the custom dbshell. 114 This will be used to name the generated file as well as several components in the file content. 115 """ 116 try: 117 create_shell(name) 118 except Exception as e: 119 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.
17 def wrapper(*args, **kwargs) -> Any: 18 timer = Timer(loops) 19 result = None 20 for _ in range(loops): 21 timer.start() 22 result = func(*args, **kwargs) 23 timer.stop() 24 print( 25 f"{func.__name__} {'average ' if loops > 1 else ''}execution time: {timer.average_elapsed_str}" 26 ) 27 return result
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'^
140 def do_describe(self, tables: str): 141 """Describe each given table or view. If no list is given, all tables and views will be described.""" 142 with self._DB() as db: 143 table_list = tables.split() or (db.tables + db.views) 144 for table in table_list: 145 print(f"<{table}>") 146 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.
148 @argshell.with_parser(dbparsers.get_drop_column_parser) 149 def do_drop_column(self, args: argshell.Namespace): 150 """Drop the specified column from the specified table.""" 151 with self._DB() as db: 152 db.drop_column(args.table, args.column)
Drop the specified column from the specified table.
154 def do_drop_table(self, table: str): 155 """Drop the specified table.""" 156 with self._DB() as db: 157 db.drop_table(table)
Drop the specified table.
17 def wrapper(*args, **kwargs) -> Any: 18 timer = Timer(loops) 19 result = None 20 for _ in range(loops): 21 timer.start() 22 result = func(*args, **kwargs) 23 timer.stop() 24 print( 25 f"{func.__name__} {'average ' if loops > 1 else ''}execution time: {timer.average_elapsed_str}" 26 ) 27 return result
Create .sql
dump files for the current database.
175 def do_flush_log(self, _: str): 176 """Clear the log file for this database.""" 177 log_path = self.dbpath.with_name(self.dbpath.name.replace(".", "") + ".log") 178 if not log_path.exists(): 179 print(f"No log file at path {log_path}") 180 else: 181 print(f"Flushing log...") 182 log_path.write_text("")
Clear the log file for this database.
184 def do_help(self, args: str): 185 """Display help messages.""" 186 super().do_help(args) 187 if args == "": 188 print("Unrecognized commands will be executed as queries.") 189 print( 190 "Use the `query` command explicitly if you don't want to capitalize your key words." 191 ) 192 print("All transactions initiated by commands are committed immediately.") 193 print()
Display help messages.
195 def do_new_db(self, dbname: str): 196 """Create a new, empty database with the given name.""" 197 dbpath = Pathier(dbname) 198 self.dbpath = dbpath 199 self.prompt = f"{self.dbpath.name}>"
Create a new, empty database with the given name.
201 def do_properties(self, _: str): 202 """See current database property settings.""" 203 for property_ in ["connection_timeout", "detect_types", "enforce_foreign_keys"]: 204 print(f"{property_}: {getattr(self, property_)}")
See current database property settings.
17 def wrapper(*args, **kwargs) -> Any: 18 timer = Timer(loops) 19 result = None 20 for _ in range(loops): 21 timer.start() 22 result = func(*args, **kwargs) 23 timer.stop() 24 print( 25 f"{func.__name__} {'average ' if loops > 1 else ''}execution time: {timer.average_elapsed_str}" 26 ) 27 return result
Execute a query against the current database.
215 @argshell.with_parser(dbparsers.get_rename_column_parser) 216 def do_rename_column(self, args: argshell.Namespace): 217 """Rename a column.""" 218 with self._DB() as db: 219 db.rename_column(args.table, args.column, args.new_name)
Rename a column.
221 @argshell.with_parser(dbparsers.get_rename_table_parser) 222 def do_rename_table(self, args: argshell.Namespace): 223 """Rename a table.""" 224 with self._DB() as db: 225 db.rename_table(args.table, args.new_name)
Rename a table.
227 def do_restore(self, file: str): 228 """Replace the current db file with the given db backup file.""" 229 backup = Pathier(file.strip('"')) 230 if not backup.exists(): 231 print(f"{backup} does not exist.") 232 else: 233 print(f"Restoring from {file}...") 234 self.dbpath.write_bytes(backup.read_bytes()) 235 print("Restore complete.")
Replace the current db file with the given db backup file.
237 @argshell.with_parser(dbparsers.get_scan_dbs_parser) 238 def do_scan(self, args: argshell.Namespace): 239 """Scan the current working directory for database files.""" 240 dbs = self._scan(args.extensions, args.recursive) 241 for db in dbs: 242 print(db.separate(Pathier.cwd().stem))
Scan the current working directory for database files.
17 def wrapper(*args, **kwargs) -> Any: 18 timer = Timer(loops) 19 result = None 20 for _ in range(loops): 21 timer.start() 22 result = func(*args, **kwargs) 23 timer.stop() 24 print( 25 f"{func.__name__} {'average ' if loops > 1 else ''}execution time: {timer.average_elapsed_str}" 26 ) 27 return result
Print out the names of the database tables and views, their columns, and, optionally, the number of rows.
17 def wrapper(*args, **kwargs) -> Any: 18 timer = Timer(loops) 19 result = None 20 for _ in range(loops): 21 timer.start() 22 result = func(*args, **kwargs) 23 timer.stop() 24 print( 25 f"{func.__name__} {'average ' if loops > 1 else ''}execution time: {timer.average_elapsed_str}" 26 ) 27 return result
Execute the given SQL script.
17 def wrapper(*args, **kwargs) -> Any: 18 timer = Timer(loops) 19 result = None 20 for _ in range(loops): 21 timer.start() 22 result = func(*args, **kwargs) 23 timer.stop() 24 print( 25 f"{func.__name__} {'average ' if loops > 1 else ''}execution time: {timer.average_elapsed_str}" 26 ) 27 return result
Execute a SELECT query with the given args.
277 def do_set_connection_timeout(self, seconds: str): 278 """Set database connection timeout to this number of seconds.""" 279 self.connection_timeout = float(seconds)
Set database connection timeout to this number of seconds.
281 def do_set_detect_types(self, should_detect: str): 282 """Pass a `1` to turn on and a `0` to turn off.""" 283 self.detect_types = bool(int(should_detect))
Pass a 1
to turn on and a 0
to turn off.
285 def do_set_enforce_foreign_keys(self, should_enforce: str): 286 """Pass a `1` to turn on and a `0` to turn off.""" 287 self.enforce_foreign_keys = bool(int(should_enforce))
Pass a 1
to turn on and a 0
to turn off.
289 def do_size(self, _: str): 290 """Display the size of the the current db file.""" 291 print(f"{self.dbpath.name} is {self.dbpath.formatted_size}.")
Display the size of the the current db file.
17 def wrapper(*args, **kwargs) -> Any: 18 timer = Timer(loops) 19 result = None 20 for _ in range(loops): 21 timer.start() 22 result = func(*args, **kwargs) 23 timer.stop() 24 print( 25 f"{func.__name__} {'average ' if loops > 1 else ''}execution time: {timer.average_elapsed_str}" 26 ) 27 return result
Print out the names of the database tables, their columns, and, optionally, the number of rows.
17 def wrapper(*args, **kwargs) -> Any: 18 timer = Timer(loops) 19 result = None 20 for _ in range(loops): 21 timer.start() 22 result = func(*args, **kwargs) 23 timer.stop() 24 print( 25 f"{func.__name__} {'average ' if loops > 1 else ''}execution time: {timer.average_elapsed_str}" 26 ) 27 return result
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'^
315 def do_use(self, dbname: str): 316 """Set which database file to use.""" 317 dbpath = Pathier(dbname) 318 if not dbpath.exists(): 319 print(f"{dbpath} does not exist.") 320 print(f"Still using {self.dbpath}") 321 elif not dbpath.is_file(): 322 print(f"{dbpath} is not a file.") 323 print(f"Still using {self.dbpath}") 324 else: 325 self.dbpath = dbpath 326 self.prompt = f"{self.dbpath.name}>"
Set which database file to use.
17 def wrapper(*args, **kwargs) -> Any: 18 timer = Timer(loops) 19 result = None 20 for _ in range(loops): 21 timer.start() 22 result = func(*args, **kwargs) 23 timer.stop() 24 print( 25 f"{func.__name__} {'average ' if loops > 1 else ''}execution time: {timer.average_elapsed_str}" 26 ) 27 return result
Reduce database disk memory.
17 def wrapper(*args, **kwargs) -> Any: 18 timer = Timer(loops) 19 result = None 20 for _ in range(loops): 21 timer.start() 22 result = func(*args, **kwargs) 23 timer.stop() 24 print( 25 f"{func.__name__} {'average ' if loops > 1 else ''}execution time: {timer.average_elapsed_str}" 26 ) 27 return result
Print out the names of the database views, their columns, and, optionally, the number of rows.
376 def preloop(self): 377 """Scan the current directory for a .db file to use. 378 If not found, prompt the user for one or to try again recursively.""" 379 if self.dbpath: 380 self.dbpath = Pathier(self.dbpath) 381 print(f"Defaulting to database {self.dbpath}") 382 else: 383 print("Searching for database...") 384 cwd = Pathier.cwd() 385 dbs = self._scan() 386 if len(dbs) == 1: 387 self.dbpath = dbs[0] 388 print(f"Using database {self.dbpath}.") 389 elif dbs: 390 self.dbpath = self._choose_db(dbs) 391 else: 392 print(f"Could not find a .db file in {cwd}.") 393 path = input( 394 "Enter path to .db file to use or press enter to search again recursively: " 395 ) 396 if path: 397 self.dbpath = Pathier(path) 398 elif not path: 399 print("Searching recursively...") 400 dbs = self._scan(recursive=True) 401 if len(dbs) == 1: 402 self.dbpath = dbs[0] 403 print(f"Using database {self.dbpath}.") 404 elif dbs: 405 self.dbpath = self._choose_db(dbs) 406 else: 407 print("Could not find a .db file.") 408 self.dbpath = Pathier(input("Enter path to a .db file: ")) 409 if not self.dbpath.exists(): 410 raise FileNotFoundError(f"{self.dbpath} does not exist.") 411 if not self.dbpath.is_file(): 412 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