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