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_flush_log(self, _: str): 117 """Clear the log file for this database.""" 118 log_path = self.dbpath.with_name(self.dbpath.name.replace(".", "") + ".log") 119 if not log_path.exists(): 120 print(f"No log file at path {log_path}") 121 else: 122 print(f"Flushing log...") 123 log_path.write_text("") 124 125 def do_help(self, args: str): 126 """Display help messages.""" 127 super().do_help(args) 128 if args == "": 129 print("Unrecognized commands will be executed as queries.") 130 print( 131 "Use the `query` command explicitly if you don't want to capitalize your key words." 132 ) 133 print("All transactions initiated by commands are committed immediately.") 134 print() 135 136 def do_properties(self, _: str): 137 """See current database property settings.""" 138 for property_ in ["connection_timeout", "detect_types", "enforce_foreign_keys"]: 139 print(f"{property_}: {getattr(self, property_)}") 140 141 def do_query(self, query: str): 142 """Execute a query against the current database.""" 143 print(f"Executing {query}") 144 with self._DB() as db: 145 results = db.query(query) 146 self.display(results) 147 print(f"{db.cursor.rowcount} affected rows") 148 149 @argshell.with_parser(dbparsers.get_rename_column_parser) 150 def do_rename_column(self, args: argshell.Namespace): 151 """Rename a column.""" 152 with self._DB() as db: 153 db.rename_column(args.table, args.column, args.new_name) 154 155 @argshell.with_parser(dbparsers.get_rename_table_parser) 156 def do_rename_table(self, args: argshell.Namespace): 157 """Rename a table.""" 158 with self._DB() as db: 159 db.rename_table(args.table, args.new_name) 160 161 def do_restore(self, file: str): 162 """Replace the current db file with the given db backup file.""" 163 backup = Pathier(file.strip('"')) 164 if not backup.exists(): 165 print(f"{backup} does not exist.") 166 else: 167 print(f"Restoring from {file}...") 168 self.dbpath.write_bytes(backup.read_bytes()) 169 print("Restore complete.") 170 171 @argshell.with_parser(dbparsers.get_scan_dbs_parser) 172 def do_scan(self, args: argshell.Namespace): 173 """Scan the current working directory for database files.""" 174 dbs = self._scan(args.extensions, args.recursive) 175 for db in dbs: 176 print(db.separate(Pathier.cwd().stem)) 177 178 @argshell.with_parser(dbparsers.get_schema_parser) 179 def do_schema(self, args: argshell.Namespace): 180 """Print out the names of the database tables, their columns, and, optionally, the number of rows.""" 181 print("Getting database schema...") 182 with self._DB() as db: 183 tables = args.tables or db.tables 184 info = [ 185 { 186 "Table Name": table, 187 "Columns": ", ".join(db.get_columns(table)), 188 "Number of Rows": db.count(table) if args.rowcount else "n/a", 189 } 190 for table in tables 191 ] 192 self.display(info) 193 194 @argshell.with_parser(dbparsers.get_select_parser, [dbparsers.select_post_parser]) 195 def do_select(self, args: argshell.Namespace): 196 """Execute a SELECT query with the given args.""" 197 print(f"Querying {args.table}... ") 198 with self._DB() as db: 199 rows = db.select( 200 table=args.table, 201 columns=args.columns, 202 joins=args.joins, 203 where=args.where, 204 group_by=args.group_by, 205 having=args.Having, 206 order_by=args.order_by, 207 limit=args.limit, 208 ) 209 print(f"Found {len(rows)} rows:") 210 self.display(rows) 211 print(f"{len(rows)} rows from {args.table}") 212 213 def do_set_connection_timeout(self, seconds: str): 214 """Set database connection timeout to this number of seconds.""" 215 self.connection_timeout = float(seconds) 216 217 def do_set_detect_types(self, should_detect: str): 218 """Pass a `1` to turn on and a `0` to turn off.""" 219 self.detect_types = bool(int(should_detect)) 220 221 def do_set_enforce_foreign_keys(self, should_enforce: str): 222 """Pass a `1` to turn on and a `0` to turn off.""" 223 self.enforce_foreign_keys = bool(int(should_enforce)) 224 225 def do_size(self, _: str): 226 """Display the size of the the current db file.""" 227 print(f"{self.dbpath.name} is {self.dbpath.formatted_size}.") 228 229 @argshell.with_parser(dbparsers.get_update_parser) 230 def do_update(self, args: argshell.Namespace): 231 """Update a column to a new value. 232 233 Syntax: 234 >>> update {table} {column} {value} {where} 235 >>> based>update users username big_chungus "username = lil_chungus" 236 237 ^will update the username in the users 'table' to 'big_chungus' where the username is currently 'lil_chungus'^ 238 """ 239 print("Updating rows...") 240 with self._DB() as db: 241 num_updates = db.update(args.table, args.column, args.new_value, args.where) 242 print(f"Updated {num_updates} rows in table {args.table}.") 243 244 def do_use(self, arg: str): 245 """Set which database file to use.""" 246 dbpath = Pathier(arg) 247 if not dbpath.exists(): 248 print(f"{dbpath} does not exist.") 249 print(f"Still using {self.dbpath}") 250 elif not dbpath.is_file(): 251 print(f"{dbpath} is not a file.") 252 print(f"Still using {self.dbpath}") 253 else: 254 self.dbpath = dbpath 255 self.prompt = f"{self.dbpath.name}>" 256 257 def do_vacuum(self, _: str): 258 """Reduce database disk memory.""" 259 print(f"Database size before vacuuming: {self.dbpath.formatted_size}") 260 print("Vacuuming database...") 261 with self._DB() as db: 262 freedspace = db.vacuum() 263 print(f"Database size after vacuuming: {self.dbpath.formatted_size}") 264 print(f"Freed up {Pathier.format_bytes(freedspace)} of disk space.") 265 266 # Seat 267 268 def _choose_db(self, options: list[Pathier]) -> Pathier: 269 """Prompt the user to select from a list of files.""" 270 cwd = Pathier.cwd() 271 paths = [path.separate(cwd.stem) for path in options] 272 while True: 273 print( 274 f"DB options:\n{' '.join([f'({i}) {path}' for i, path in enumerate(paths, 1)])}" 275 ) 276 choice = input("Enter the number of the option to use: ") 277 try: 278 index = int(choice) 279 if not 1 <= index <= len(options): 280 print("Choice out of range.") 281 continue 282 return options[index - 1] 283 except Exception as e: 284 print(f"{choice} is not a valid option.") 285 286 def _scan( 287 self, extensions: list[str] = [".sqlite3", ".db"], recursive: bool = False 288 ) -> list[Pathier]: 289 cwd = Pathier.cwd() 290 dbs = [] 291 globber = cwd.glob 292 if recursive: 293 globber = cwd.rglob 294 for extension in extensions: 295 dbs.extend(list(globber(f"*{extension}"))) 296 return dbs 297 298 def preloop(self): 299 """Scan the current directory for a .db file to use. 300 If not found, prompt the user for one or to try again recursively.""" 301 if self.dbpath: 302 self.dbpath = Pathier(self.dbpath) 303 print(f"Defaulting to database {self.dbpath}") 304 else: 305 print("Searching for database...") 306 cwd = Pathier.cwd() 307 dbs = self._scan() 308 if len(dbs) == 1: 309 self.dbpath = dbs[0] 310 print(f"Using database {self.dbpath}.") 311 elif dbs: 312 self.dbpath = self._choose_db(dbs) 313 else: 314 print(f"Could not find a .db file in {cwd}.") 315 path = input( 316 "Enter path to .db file to use or press enter to search again recursively: " 317 ) 318 if path: 319 self.dbpath = Pathier(path) 320 elif not path: 321 print("Searching recursively...") 322 dbs = self._scan(recursive=True) 323 if len(dbs) == 1: 324 self.dbpath = dbs[0] 325 print(f"Using database {self.dbpath}.") 326 elif dbs: 327 self.dbpath = self._choose_db(dbs) 328 else: 329 print("Could not find a .db file.") 330 self.dbpath = Pathier(input("Enter path to a .db file: ")) 331 if not self.dbpath.exists(): 332 raise FileNotFoundError(f"{self.dbpath} does not exist.") 333 if not self.dbpath.is_file(): 334 raise ValueError(f"{self.dbpath} is not a file.") 335 336 337def main(): 338 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_flush_log(self, _: str): 118 """Clear the log file for this database.""" 119 log_path = self.dbpath.with_name(self.dbpath.name.replace(".", "") + ".log") 120 if not log_path.exists(): 121 print(f"No log file at path {log_path}") 122 else: 123 print(f"Flushing log...") 124 log_path.write_text("") 125 126 def do_help(self, args: str): 127 """Display help messages.""" 128 super().do_help(args) 129 if args == "": 130 print("Unrecognized commands will be executed as queries.") 131 print( 132 "Use the `query` command explicitly if you don't want to capitalize your key words." 133 ) 134 print("All transactions initiated by commands are committed immediately.") 135 print() 136 137 def do_properties(self, _: str): 138 """See current database property settings.""" 139 for property_ in ["connection_timeout", "detect_types", "enforce_foreign_keys"]: 140 print(f"{property_}: {getattr(self, property_)}") 141 142 def do_query(self, query: str): 143 """Execute a query against the current database.""" 144 print(f"Executing {query}") 145 with self._DB() as db: 146 results = db.query(query) 147 self.display(results) 148 print(f"{db.cursor.rowcount} affected rows") 149 150 @argshell.with_parser(dbparsers.get_rename_column_parser) 151 def do_rename_column(self, args: argshell.Namespace): 152 """Rename a column.""" 153 with self._DB() as db: 154 db.rename_column(args.table, args.column, args.new_name) 155 156 @argshell.with_parser(dbparsers.get_rename_table_parser) 157 def do_rename_table(self, args: argshell.Namespace): 158 """Rename a table.""" 159 with self._DB() as db: 160 db.rename_table(args.table, args.new_name) 161 162 def do_restore(self, file: str): 163 """Replace the current db file with the given db backup file.""" 164 backup = Pathier(file.strip('"')) 165 if not backup.exists(): 166 print(f"{backup} does not exist.") 167 else: 168 print(f"Restoring from {file}...") 169 self.dbpath.write_bytes(backup.read_bytes()) 170 print("Restore complete.") 171 172 @argshell.with_parser(dbparsers.get_scan_dbs_parser) 173 def do_scan(self, args: argshell.Namespace): 174 """Scan the current working directory for database files.""" 175 dbs = self._scan(args.extensions, args.recursive) 176 for db in dbs: 177 print(db.separate(Pathier.cwd().stem)) 178 179 @argshell.with_parser(dbparsers.get_schema_parser) 180 def do_schema(self, args: argshell.Namespace): 181 """Print out the names of the database tables, their columns, and, optionally, the number of rows.""" 182 print("Getting database schema...") 183 with self._DB() as db: 184 tables = args.tables or db.tables 185 info = [ 186 { 187 "Table Name": table, 188 "Columns": ", ".join(db.get_columns(table)), 189 "Number of Rows": db.count(table) if args.rowcount else "n/a", 190 } 191 for table in tables 192 ] 193 self.display(info) 194 195 @argshell.with_parser(dbparsers.get_select_parser, [dbparsers.select_post_parser]) 196 def do_select(self, args: argshell.Namespace): 197 """Execute a SELECT query with the given args.""" 198 print(f"Querying {args.table}... ") 199 with self._DB() as db: 200 rows = db.select( 201 table=args.table, 202 columns=args.columns, 203 joins=args.joins, 204 where=args.where, 205 group_by=args.group_by, 206 having=args.Having, 207 order_by=args.order_by, 208 limit=args.limit, 209 ) 210 print(f"Found {len(rows)} rows:") 211 self.display(rows) 212 print(f"{len(rows)} rows from {args.table}") 213 214 def do_set_connection_timeout(self, seconds: str): 215 """Set database connection timeout to this number of seconds.""" 216 self.connection_timeout = float(seconds) 217 218 def do_set_detect_types(self, should_detect: str): 219 """Pass a `1` to turn on and a `0` to turn off.""" 220 self.detect_types = bool(int(should_detect)) 221 222 def do_set_enforce_foreign_keys(self, should_enforce: str): 223 """Pass a `1` to turn on and a `0` to turn off.""" 224 self.enforce_foreign_keys = bool(int(should_enforce)) 225 226 def do_size(self, _: str): 227 """Display the size of the the current db file.""" 228 print(f"{self.dbpath.name} is {self.dbpath.formatted_size}.") 229 230 @argshell.with_parser(dbparsers.get_update_parser) 231 def do_update(self, args: argshell.Namespace): 232 """Update a column to a new value. 233 234 Syntax: 235 >>> update {table} {column} {value} {where} 236 >>> based>update users username big_chungus "username = lil_chungus" 237 238 ^will update the username in the users 'table' to 'big_chungus' where the username is currently 'lil_chungus'^ 239 """ 240 print("Updating rows...") 241 with self._DB() as db: 242 num_updates = db.update(args.table, args.column, args.new_value, args.where) 243 print(f"Updated {num_updates} rows in table {args.table}.") 244 245 def do_use(self, arg: str): 246 """Set which database file to use.""" 247 dbpath = Pathier(arg) 248 if not dbpath.exists(): 249 print(f"{dbpath} does not exist.") 250 print(f"Still using {self.dbpath}") 251 elif not dbpath.is_file(): 252 print(f"{dbpath} is not a file.") 253 print(f"Still using {self.dbpath}") 254 else: 255 self.dbpath = dbpath 256 self.prompt = f"{self.dbpath.name}>" 257 258 def do_vacuum(self, _: str): 259 """Reduce database disk memory.""" 260 print(f"Database size before vacuuming: {self.dbpath.formatted_size}") 261 print("Vacuuming database...") 262 with self._DB() as db: 263 freedspace = db.vacuum() 264 print(f"Database size after vacuuming: {self.dbpath.formatted_size}") 265 print(f"Freed up {Pathier.format_bytes(freedspace)} of disk space.") 266 267 # Seat 268 269 def _choose_db(self, options: list[Pathier]) -> Pathier: 270 """Prompt the user to select from a list of files.""" 271 cwd = Pathier.cwd() 272 paths = [path.separate(cwd.stem) for path in options] 273 while True: 274 print( 275 f"DB options:\n{' '.join([f'({i}) {path}' for i, path in enumerate(paths, 1)])}" 276 ) 277 choice = input("Enter the number of the option to use: ") 278 try: 279 index = int(choice) 280 if not 1 <= index <= len(options): 281 print("Choice out of range.") 282 continue 283 return options[index - 1] 284 except Exception as e: 285 print(f"{choice} is not a valid option.") 286 287 def _scan( 288 self, extensions: list[str] = [".sqlite3", ".db"], recursive: bool = False 289 ) -> list[Pathier]: 290 cwd = Pathier.cwd() 291 dbs = [] 292 globber = cwd.glob 293 if recursive: 294 globber = cwd.rglob 295 for extension in extensions: 296 dbs.extend(list(globber(f"*{extension}"))) 297 return dbs 298 299 def preloop(self): 300 """Scan the current directory for a .db file to use. 301 If not found, prompt the user for one or to try again recursively.""" 302 if self.dbpath: 303 self.dbpath = Pathier(self.dbpath) 304 print(f"Defaulting to database {self.dbpath}") 305 else: 306 print("Searching for database...") 307 cwd = Pathier.cwd() 308 dbs = self._scan() 309 if len(dbs) == 1: 310 self.dbpath = dbs[0] 311 print(f"Using database {self.dbpath}.") 312 elif dbs: 313 self.dbpath = self._choose_db(dbs) 314 else: 315 print(f"Could not find a .db file in {cwd}.") 316 path = input( 317 "Enter path to .db file to use or press enter to search again recursively: " 318 ) 319 if path: 320 self.dbpath = Pathier(path) 321 elif not path: 322 print("Searching recursively...") 323 dbs = self._scan(recursive=True) 324 if len(dbs) == 1: 325 self.dbpath = dbs[0] 326 print(f"Using database {self.dbpath}.") 327 elif dbs: 328 self.dbpath = self._choose_db(dbs) 329 else: 330 print("Could not find a .db file.") 331 self.dbpath = Pathier(input("Enter path to a .db file: ")) 332 if not self.dbpath.exists(): 333 raise FileNotFoundError(f"{self.dbpath} does not exist.") 334 if not self.dbpath.is_file(): 335 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_flush_log(self, _: str): 118 """Clear the log file for this database.""" 119 log_path = self.dbpath.with_name(self.dbpath.name.replace(".", "") + ".log") 120 if not log_path.exists(): 121 print(f"No log file at path {log_path}") 122 else: 123 print(f"Flushing log...") 124 log_path.write_text("")
Clear the log file for this database.
126 def do_help(self, args: str): 127 """Display help messages.""" 128 super().do_help(args) 129 if args == "": 130 print("Unrecognized commands will be executed as queries.") 131 print( 132 "Use the `query` command explicitly if you don't want to capitalize your key words." 133 ) 134 print("All transactions initiated by commands are committed immediately.") 135 print()
Display help messages.
137 def do_properties(self, _: str): 138 """See current database property settings.""" 139 for property_ in ["connection_timeout", "detect_types", "enforce_foreign_keys"]: 140 print(f"{property_}: {getattr(self, property_)}")
See current database property settings.
142 def do_query(self, query: str): 143 """Execute a query against the current database.""" 144 print(f"Executing {query}") 145 with self._DB() as db: 146 results = db.query(query) 147 self.display(results) 148 print(f"{db.cursor.rowcount} affected rows")
Execute a query against the current database.
150 @argshell.with_parser(dbparsers.get_rename_column_parser) 151 def do_rename_column(self, args: argshell.Namespace): 152 """Rename a column.""" 153 with self._DB() as db: 154 db.rename_column(args.table, args.column, args.new_name)
Rename a column.
156 @argshell.with_parser(dbparsers.get_rename_table_parser) 157 def do_rename_table(self, args: argshell.Namespace): 158 """Rename a table.""" 159 with self._DB() as db: 160 db.rename_table(args.table, args.new_name)
Rename a table.
162 def do_restore(self, file: str): 163 """Replace the current db file with the given db backup file.""" 164 backup = Pathier(file.strip('"')) 165 if not backup.exists(): 166 print(f"{backup} does not exist.") 167 else: 168 print(f"Restoring from {file}...") 169 self.dbpath.write_bytes(backup.read_bytes()) 170 print("Restore complete.")
Replace the current db file with the given db backup file.
172 @argshell.with_parser(dbparsers.get_scan_dbs_parser) 173 def do_scan(self, args: argshell.Namespace): 174 """Scan the current working directory for database files.""" 175 dbs = self._scan(args.extensions, args.recursive) 176 for db in dbs: 177 print(db.separate(Pathier.cwd().stem))
Scan the current working directory for database files.
179 @argshell.with_parser(dbparsers.get_schema_parser) 180 def do_schema(self, args: argshell.Namespace): 181 """Print out the names of the database tables, their columns, and, optionally, the number of rows.""" 182 print("Getting database schema...") 183 with self._DB() as db: 184 tables = args.tables or db.tables 185 info = [ 186 { 187 "Table Name": table, 188 "Columns": ", ".join(db.get_columns(table)), 189 "Number of Rows": db.count(table) if args.rowcount else "n/a", 190 } 191 for table in tables 192 ] 193 self.display(info)
Print out the names of the database tables, their columns, and, optionally, the number of rows.
195 @argshell.with_parser(dbparsers.get_select_parser, [dbparsers.select_post_parser]) 196 def do_select(self, args: argshell.Namespace): 197 """Execute a SELECT query with the given args.""" 198 print(f"Querying {args.table}... ") 199 with self._DB() as db: 200 rows = db.select( 201 table=args.table, 202 columns=args.columns, 203 joins=args.joins, 204 where=args.where, 205 group_by=args.group_by, 206 having=args.Having, 207 order_by=args.order_by, 208 limit=args.limit, 209 ) 210 print(f"Found {len(rows)} rows:") 211 self.display(rows) 212 print(f"{len(rows)} rows from {args.table}")
Execute a SELECT query with the given args.
214 def do_set_connection_timeout(self, seconds: str): 215 """Set database connection timeout to this number of seconds.""" 216 self.connection_timeout = float(seconds)
Set database connection timeout to this number of seconds.
218 def do_set_detect_types(self, should_detect: str): 219 """Pass a `1` to turn on and a `0` to turn off.""" 220 self.detect_types = bool(int(should_detect))
Pass a 1
to turn on and a 0
to turn off.
222 def do_set_enforce_foreign_keys(self, should_enforce: str): 223 """Pass a `1` to turn on and a `0` to turn off.""" 224 self.enforce_foreign_keys = bool(int(should_enforce))
Pass a 1
to turn on and a 0
to turn off.
226 def do_size(self, _: str): 227 """Display the size of the the current db file.""" 228 print(f"{self.dbpath.name} is {self.dbpath.formatted_size}.")
Display the size of the the current db file.
230 @argshell.with_parser(dbparsers.get_update_parser) 231 def do_update(self, args: argshell.Namespace): 232 """Update a column to a new value. 233 234 Syntax: 235 >>> update {table} {column} {value} {where} 236 >>> based>update users username big_chungus "username = lil_chungus" 237 238 ^will update the username in the users 'table' to 'big_chungus' where the username is currently 'lil_chungus'^ 239 """ 240 print("Updating rows...") 241 with self._DB() as db: 242 num_updates = db.update(args.table, args.column, args.new_value, args.where) 243 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'^
245 def do_use(self, arg: str): 246 """Set which database file to use.""" 247 dbpath = Pathier(arg) 248 if not dbpath.exists(): 249 print(f"{dbpath} does not exist.") 250 print(f"Still using {self.dbpath}") 251 elif not dbpath.is_file(): 252 print(f"{dbpath} is not a file.") 253 print(f"Still using {self.dbpath}") 254 else: 255 self.dbpath = dbpath 256 self.prompt = f"{self.dbpath.name}>"
Set which database file to use.
258 def do_vacuum(self, _: str): 259 """Reduce database disk memory.""" 260 print(f"Database size before vacuuming: {self.dbpath.formatted_size}") 261 print("Vacuuming database...") 262 with self._DB() as db: 263 freedspace = db.vacuum() 264 print(f"Database size after vacuuming: {self.dbpath.formatted_size}") 265 print(f"Freed up {Pathier.format_bytes(freedspace)} of disk space.")
Reduce database disk memory.
299 def preloop(self): 300 """Scan the current directory for a .db file to use. 301 If not found, prompt the user for one or to try again recursively.""" 302 if self.dbpath: 303 self.dbpath = Pathier(self.dbpath) 304 print(f"Defaulting to database {self.dbpath}") 305 else: 306 print("Searching for database...") 307 cwd = Pathier.cwd() 308 dbs = self._scan() 309 if len(dbs) == 1: 310 self.dbpath = dbs[0] 311 print(f"Using database {self.dbpath}.") 312 elif dbs: 313 self.dbpath = self._choose_db(dbs) 314 else: 315 print(f"Could not find a .db file in {cwd}.") 316 path = input( 317 "Enter path to .db file to use or press enter to search again recursively: " 318 ) 319 if path: 320 self.dbpath = Pathier(path) 321 elif not path: 322 print("Searching recursively...") 323 dbs = self._scan(recursive=True) 324 if len(dbs) == 1: 325 self.dbpath = dbs[0] 326 print(f"Using database {self.dbpath}.") 327 elif dbs: 328 self.dbpath = self._choose_db(dbs) 329 else: 330 print("Could not find a .db file.") 331 self.dbpath = Pathier(input("Enter path to a .db file: ")) 332 if not self.dbpath.exists(): 333 raise FileNotFoundError(f"{self.dbpath} does not exist.") 334 if not self.dbpath.is_file(): 335 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