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