databased.dbshell

  1import argshell
  2from pathier import Pathier
  3
  4from databased import DataBased, dbparsers
  5
  6
  7class DBShell(argshell.ArgShell):
  8    intro = "Starting dbshell (enter help or ? for arg info)..."
  9    prompt = "based>"
 10    dbpath: Pathier = None  # type: ignore
 11
 12    def do_use_db(self, arg: str):
 13        """Set which database file to use."""
 14        dbpath = Pathier(arg)
 15        if not dbpath.exists():
 16            print(f"{dbpath} does not exist.")
 17            print(f"Still using {self.dbpath}")
 18        elif not dbpath.is_file():
 19            print(f"{dbpath} is not a file.")
 20            print(f"Still using {self.dbpath}")
 21        else:
 22            self.dbpath = dbpath
 23
 24    def do_dbpath(self, arg: str):
 25        """Print the .db file in use."""
 26        print(self.dbpath)
 27
 28    def do_backup(self, arg: str):
 29        """Create a backup of the current db file."""
 30        print(f"Creating a back up for {self.dbpath}...")
 31        backup_path = self.dbpath.with_stem(f"{self.dbpath.stem}_bckup")
 32        self.dbpath.copy(backup_path, True)
 33        print("Creating backup is complete.")
 34        print(f"Backup path: {backup_path}")
 35
 36    def do_size(self, arg: str):
 37        """Display the size of the the current db file."""
 38        print(f"{self.dbpath.name} is {self.dbpath.size(True)}.")
 39
 40    @argshell.with_parser(dbparsers.get_create_table_parser)
 41    def do_add_table(self, args: argshell.Namespace):
 42        """Add a new table to the database."""
 43        with DataBased(self.dbpath) as db:
 44            db.create_table(args.table_name, args.columns)
 45
 46    def do_drop_table(self, arg: str):
 47        """Drop the specified table."""
 48        with DataBased(self.dbpath) as db:
 49            db.drop_table(arg)
 50
 51    @argshell.with_parser(
 52        dbparsers.get_add_row_parser, [dbparsers.verify_matching_length]
 53    )
 54    def do_add_row(self, args: argshell.Namespace):
 55        """Add a row to a table."""
 56        with DataBased(self.dbpath) as db:
 57            if db.add_row(args.table_name, args.values, args.columns or None):
 58                print(f"Added row to {args.table_name} table successfully.")
 59            else:
 60                print(f"Failed to add row to {args.table_name} table.")
 61
 62    @argshell.with_parser(dbparsers.get_info_parser)
 63    def do_info(self, args: argshell.Namespace):
 64        """Print out the names of the database tables, their columns, and, optionally, the number of rows."""
 65        print("Getting database info...")
 66        with DataBased(self.dbpath) as db:
 67            tables = args.tables or db.get_table_names()
 68            info = [
 69                {
 70                    "Table Name": table,
 71                    "Columns": ", ".join(db.get_column_names(table)),
 72                    "Number of Rows": db.count(table) if args.rowcount else "n/a",
 73                }
 74                for table in tables
 75            ]
 76        print(DataBased.data_to_string(info))
 77
 78    @argshell.with_parser(dbparsers.get_lookup_parser, [dbparsers.convert_match_pairs])
 79    def do_show(self, args: argshell.Namespace):
 80        """Find and print rows from the database.
 81        Use the -t/--tables, -m/--match_pairs, and -l/--limit flags to limit the search.
 82        Use the -c/--columns flag to limit what columns are printed.
 83        Use the -o/--order_by flag to order the results.
 84        Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs
 85        Pass -h/--help flag for parser help."""
 86        print("Finding records... ")
 87        if len(args.columns) == 0:
 88            args.columns = None
 89        with DataBased(self.dbpath) as db:
 90            tables = args.tables or db.get_table_names()
 91            for table in tables:
 92                results = db.get_rows(
 93                    table,
 94                    args.match_pairs,
 95                    columns_to_return=args.columns,
 96                    order_by=args.order_by,
 97                    limit=args.limit,
 98                    exact_match=not args.partial_matching,
 99                )
100                db.close()
101                print(f"{len(results)} matching rows in {table} table:")
102                try:
103                    print(DataBased.data_to_string(results))  # type: ignore
104                except Exception as e:
105                    print("Couldn't fit data into a grid.")
106                    print(*results, sep="\n")
107                print()
108
109    @argshell.with_parser(dbparsers.get_search_parser)
110    def do_search(self, args: argshell.Namespace):
111        """Search and return any rows containg the searched substring in any of its columns.
112        Use the -t/--tables flag to limit the search to a specific table(s).
113        Use the -c/--columns flag to limit the search to a specific column(s)."""
114        print(f"Searching for {args.search_string}...")
115        with DataBased(self.dbpath) as db:
116            tables = args.tables or db.get_table_names()
117            for table in tables:
118                columns = args.columns or db.get_column_names(table)
119                matcher = " OR ".join(
120                    f'{column} LIKE "%{args.search_string}%"' for column in columns
121                )
122                query = f"SELECT * FROM {table} WHERE {matcher};"
123                results = db.query(query)
124                results = [db._get_dict(table, result) for result in results]
125                print(f"Found {len(results)} results in {table} table.")
126                print(DataBased.data_to_string(results))
127
128    @argshell.with_parser(dbparsers.get_lookup_parser, [dbparsers.convert_match_pairs])
129    def do_count(self, args: argshell.Namespace):
130        """Print the number of rows in the database.
131        Use the -t/--tables flag to limit results to a specific table(s).
132        Use the -m/--match_pairs flag to limit the results to rows matching these criteria.
133        Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs.
134        Pass -h/--help flag for parser help."""
135        print("Counting rows...")
136        with DataBased(self.dbpath) as db:
137            tables = args.tables or db.get_table_names()
138            for table in tables:
139                num_rows = db.count(table, args.match_pairs, not args.partial_matching)
140                print(f"{num_rows} matching rows in {table} table.")
141
142    def do_query(self, arg: str):
143        """Execute a query against the current database."""
144        print(f"Executing {arg}")
145        with DataBased(self.dbpath) as db:
146            results = db.query(arg)
147        try:
148            for result in results:
149                print(*result, sep="|-|")
150            print(f"{db.cursor.rowcount} affected rows")
151        except Exception as e:
152            print(f"{type(e).__name__}: {e}")
153
154    @argshell.with_parser(dbparsers.get_update_parser, [dbparsers.convert_match_pairs])
155    def do_update(self, args: argshell.Namespace):
156        """Update a column to a new value.
157        Two required args: the column (-c/--column) to update and the value (-v/--value) to update to.
158        Use the -t/--tables flag to limit what tables are updated.
159        Use the -m/--match_pairs flag to specify which rows are updated.
160        Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs.
161        >>> based>update -c username -v big_chungus -t users -m username lil_chungus
162
163        ^will update the username in the users 'table' to 'big_chungus' where the username is currently 'lil_chungus'^"""
164        print("Updating rows...")
165        with DataBased(self.dbpath) as db:
166            tables = args.tables or db.get_table_names()
167            for table in tables:
168                num_updates = db.update(
169                    table,
170                    args.column,
171                    args.new_value,
172                    args.match_pairs,
173                    not args.partial_matching,
174                )
175                print(f"Updated {num_updates} rows in {table} table.")
176
177    @argshell.with_parser(dbparsers.get_lookup_parser, [dbparsers.convert_match_pairs])
178    def do_delete(self, args: argshell.Namespace):
179        """Delete rows from the database.
180        Use the -t/--tables flag to limit what tables rows are deleted from.
181        Use the -m/--match_pairs flag to specify which rows are deleted.
182        Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs.
183        >>> based>delete -t users -m username chungus -p
184
185        ^will delete all rows in the 'users' table whose username contains 'chungus'^"""
186        print("Deleting records...")
187        with DataBased(self.dbpath) as db:
188            tables = args.tables or db.get_table_names()
189            for table in tables:
190                num_rows = db.delete(table, args.match_pairs, not args.partial_matching)
191                print(f"Deleted {num_rows} rows from {table} table.")
192
193    @argshell.with_parser(dbparsers.get_add_column_parser)
194    def do_add_column(self, args: argshell.Namespace):
195        """Add a new column to the specified tables."""
196        with DataBased(self.dbpath) as db:
197            tables = args.tables or db.get_table_names()
198            for table in tables:
199                db.add_column(table, args.column_name, args.type, args.default_value)
200
201    def do_flush_log(self, arg: str):
202        """Clear the log file for this database."""
203        log_path = self.dbpath.with_name(self.dbpath.stem + "db.log")
204        if not log_path.exists():
205            print(f"No log file at path {log_path}")
206        else:
207            print(f"Flushing log...")
208            log_path.write_text("")
209
210    def do_scan_dbs(self, arg: str):
211        """Scan the current working directory for `*.db` files and display them.
212
213        If the command is entered as `based>scan_dbs r`, the scan will be performed recursively."""
214        cwd = Pathier.cwd()
215        if arg.strip() == "r":
216            dbs = cwd.rglob("*.db")
217        else:
218            dbs = cwd.glob("*.db")
219        for db in dbs:
220            print(db.separate(cwd.stem))
221
222    def do_customize(self, arg: str):
223        """Generate a template file in the current working directory for creating a custom DBShell class.
224        Expects one argument: the name of the custom dbshell.
225        This will be used to name the generated file as well as several components in the file content."""
226        custom_file = (Pathier.cwd() / arg.replace(" ", "_")).with_suffix(".py")
227        if custom_file.exists():
228            print(f"Error: {custom_file.name} already exists in this location.")
229        else:
230            variable_name = "_".join(word for word in arg.lower().split())
231            class_name = "".join(word.capitalize() for word in arg.split())
232            content = (Pathier(__file__).parent / "customshell.py").read_text()
233            content = content.replace("CustomShell", class_name)
234            content = content.replace("customshell", variable_name)
235            custom_file.write_text(content)
236
237    def _choose_db(self, options: list[Pathier]) -> Pathier:
238        """Prompt the user to select from a list of files."""
239        cwd = Pathier.cwd()
240        paths = [path.separate(cwd.stem) for path in options]
241        while True:
242            print(
243                f"DB options:\n{' '.join([f'({i}) {path}' for i,path in enumerate(paths,1)])}"
244            )
245            choice = input("Enter the number of the option to use: ")
246            try:
247                index = int(choice)
248                if not 1 <= index <= len(options):
249                    print("Choice out of range.")
250                    continue
251                return options[index - 1]
252            except Exception as e:
253                print(f"{choice} is not a valid option.")
254
255    def preloop(self):
256        """Scan the current directory for a .db file to use.
257        If not found, prompt the user for one or to try again recursively."""
258        if self.dbpath:
259            self.dbpath = Pathier(self.dbpath)
260            print(f"Defaulting to database {self.dbpath}")
261        else:
262            print("Searching for database...")
263            cwd = Pathier.cwd()
264            dbs = list(cwd.glob("*.db"))
265            if len(dbs) == 1:
266                self.dbpath = dbs[0]
267                print(f"Using database {self.dbpath}.")
268            elif dbs:
269                self.dbpath = self._choose_db(dbs)
270            else:
271                print(f"Could not find a .db file in {cwd}.")
272                path = input(
273                    "Enter path to .db file to use or press enter to search again recursively: "
274                )
275                if path:
276                    self.dbpath = Pathier(path)
277                elif not path:
278                    print("Searching recursively...")
279                    dbs = list(cwd.rglob("*.db"))
280                    if len(dbs) == 1:
281                        self.dbpath = dbs[0]
282                        print(f"Using database {self.dbpath}.")
283                    elif dbs:
284                        self.dbpath = self._choose_db(dbs)
285                    else:
286                        print("Could not find a .db file.")
287                        self.dbpath = Pathier(input("Enter path to a .db file: "))
288        if not self.dbpath.exists():
289            raise FileNotFoundError(f"{self.dbpath} does not exist.")
290        if not self.dbpath.is_file():
291            raise ValueError(f"{self.dbpath} is not a file.")
292
293
294def main():
295    DBShell().cmdloop()
class DBShell(argshell.argshell.ArgShell):
  8class DBShell(argshell.ArgShell):
  9    intro = "Starting dbshell (enter help or ? for arg info)..."
 10    prompt = "based>"
 11    dbpath: Pathier = None  # type: ignore
 12
 13    def do_use_db(self, arg: str):
 14        """Set which database file to use."""
 15        dbpath = Pathier(arg)
 16        if not dbpath.exists():
 17            print(f"{dbpath} does not exist.")
 18            print(f"Still using {self.dbpath}")
 19        elif not dbpath.is_file():
 20            print(f"{dbpath} is not a file.")
 21            print(f"Still using {self.dbpath}")
 22        else:
 23            self.dbpath = dbpath
 24
 25    def do_dbpath(self, arg: str):
 26        """Print the .db file in use."""
 27        print(self.dbpath)
 28
 29    def do_backup(self, arg: str):
 30        """Create a backup of the current db file."""
 31        print(f"Creating a back up for {self.dbpath}...")
 32        backup_path = self.dbpath.with_stem(f"{self.dbpath.stem}_bckup")
 33        self.dbpath.copy(backup_path, True)
 34        print("Creating backup is complete.")
 35        print(f"Backup path: {backup_path}")
 36
 37    def do_size(self, arg: str):
 38        """Display the size of the the current db file."""
 39        print(f"{self.dbpath.name} is {self.dbpath.size(True)}.")
 40
 41    @argshell.with_parser(dbparsers.get_create_table_parser)
 42    def do_add_table(self, args: argshell.Namespace):
 43        """Add a new table to the database."""
 44        with DataBased(self.dbpath) as db:
 45            db.create_table(args.table_name, args.columns)
 46
 47    def do_drop_table(self, arg: str):
 48        """Drop the specified table."""
 49        with DataBased(self.dbpath) as db:
 50            db.drop_table(arg)
 51
 52    @argshell.with_parser(
 53        dbparsers.get_add_row_parser, [dbparsers.verify_matching_length]
 54    )
 55    def do_add_row(self, args: argshell.Namespace):
 56        """Add a row to a table."""
 57        with DataBased(self.dbpath) as db:
 58            if db.add_row(args.table_name, args.values, args.columns or None):
 59                print(f"Added row to {args.table_name} table successfully.")
 60            else:
 61                print(f"Failed to add row to {args.table_name} table.")
 62
 63    @argshell.with_parser(dbparsers.get_info_parser)
 64    def do_info(self, args: argshell.Namespace):
 65        """Print out the names of the database tables, their columns, and, optionally, the number of rows."""
 66        print("Getting database info...")
 67        with DataBased(self.dbpath) as db:
 68            tables = args.tables or db.get_table_names()
 69            info = [
 70                {
 71                    "Table Name": table,
 72                    "Columns": ", ".join(db.get_column_names(table)),
 73                    "Number of Rows": db.count(table) if args.rowcount else "n/a",
 74                }
 75                for table in tables
 76            ]
 77        print(DataBased.data_to_string(info))
 78
 79    @argshell.with_parser(dbparsers.get_lookup_parser, [dbparsers.convert_match_pairs])
 80    def do_show(self, args: argshell.Namespace):
 81        """Find and print rows from the database.
 82        Use the -t/--tables, -m/--match_pairs, and -l/--limit flags to limit the search.
 83        Use the -c/--columns flag to limit what columns are printed.
 84        Use the -o/--order_by flag to order the results.
 85        Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs
 86        Pass -h/--help flag for parser help."""
 87        print("Finding records... ")
 88        if len(args.columns) == 0:
 89            args.columns = None
 90        with DataBased(self.dbpath) as db:
 91            tables = args.tables or db.get_table_names()
 92            for table in tables:
 93                results = db.get_rows(
 94                    table,
 95                    args.match_pairs,
 96                    columns_to_return=args.columns,
 97                    order_by=args.order_by,
 98                    limit=args.limit,
 99                    exact_match=not args.partial_matching,
100                )
101                db.close()
102                print(f"{len(results)} matching rows in {table} table:")
103                try:
104                    print(DataBased.data_to_string(results))  # type: ignore
105                except Exception as e:
106                    print("Couldn't fit data into a grid.")
107                    print(*results, sep="\n")
108                print()
109
110    @argshell.with_parser(dbparsers.get_search_parser)
111    def do_search(self, args: argshell.Namespace):
112        """Search and return any rows containg the searched substring in any of its columns.
113        Use the -t/--tables flag to limit the search to a specific table(s).
114        Use the -c/--columns flag to limit the search to a specific column(s)."""
115        print(f"Searching for {args.search_string}...")
116        with DataBased(self.dbpath) as db:
117            tables = args.tables or db.get_table_names()
118            for table in tables:
119                columns = args.columns or db.get_column_names(table)
120                matcher = " OR ".join(
121                    f'{column} LIKE "%{args.search_string}%"' for column in columns
122                )
123                query = f"SELECT * FROM {table} WHERE {matcher};"
124                results = db.query(query)
125                results = [db._get_dict(table, result) for result in results]
126                print(f"Found {len(results)} results in {table} table.")
127                print(DataBased.data_to_string(results))
128
129    @argshell.with_parser(dbparsers.get_lookup_parser, [dbparsers.convert_match_pairs])
130    def do_count(self, args: argshell.Namespace):
131        """Print the number of rows in the database.
132        Use the -t/--tables flag to limit results to a specific table(s).
133        Use the -m/--match_pairs flag to limit the results to rows matching these criteria.
134        Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs.
135        Pass -h/--help flag for parser help."""
136        print("Counting rows...")
137        with DataBased(self.dbpath) as db:
138            tables = args.tables or db.get_table_names()
139            for table in tables:
140                num_rows = db.count(table, args.match_pairs, not args.partial_matching)
141                print(f"{num_rows} matching rows in {table} table.")
142
143    def do_query(self, arg: str):
144        """Execute a query against the current database."""
145        print(f"Executing {arg}")
146        with DataBased(self.dbpath) as db:
147            results = db.query(arg)
148        try:
149            for result in results:
150                print(*result, sep="|-|")
151            print(f"{db.cursor.rowcount} affected rows")
152        except Exception as e:
153            print(f"{type(e).__name__}: {e}")
154
155    @argshell.with_parser(dbparsers.get_update_parser, [dbparsers.convert_match_pairs])
156    def do_update(self, args: argshell.Namespace):
157        """Update a column to a new value.
158        Two required args: the column (-c/--column) to update and the value (-v/--value) to update to.
159        Use the -t/--tables flag to limit what tables are updated.
160        Use the -m/--match_pairs flag to specify which rows are updated.
161        Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs.
162        >>> based>update -c username -v big_chungus -t users -m username lil_chungus
163
164        ^will update the username in the users 'table' to 'big_chungus' where the username is currently 'lil_chungus'^"""
165        print("Updating rows...")
166        with DataBased(self.dbpath) as db:
167            tables = args.tables or db.get_table_names()
168            for table in tables:
169                num_updates = db.update(
170                    table,
171                    args.column,
172                    args.new_value,
173                    args.match_pairs,
174                    not args.partial_matching,
175                )
176                print(f"Updated {num_updates} rows in {table} table.")
177
178    @argshell.with_parser(dbparsers.get_lookup_parser, [dbparsers.convert_match_pairs])
179    def do_delete(self, args: argshell.Namespace):
180        """Delete rows from the database.
181        Use the -t/--tables flag to limit what tables rows are deleted from.
182        Use the -m/--match_pairs flag to specify which rows are deleted.
183        Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs.
184        >>> based>delete -t users -m username chungus -p
185
186        ^will delete all rows in the 'users' table whose username contains 'chungus'^"""
187        print("Deleting records...")
188        with DataBased(self.dbpath) as db:
189            tables = args.tables or db.get_table_names()
190            for table in tables:
191                num_rows = db.delete(table, args.match_pairs, not args.partial_matching)
192                print(f"Deleted {num_rows} rows from {table} table.")
193
194    @argshell.with_parser(dbparsers.get_add_column_parser)
195    def do_add_column(self, args: argshell.Namespace):
196        """Add a new column to the specified tables."""
197        with DataBased(self.dbpath) as db:
198            tables = args.tables or db.get_table_names()
199            for table in tables:
200                db.add_column(table, args.column_name, args.type, args.default_value)
201
202    def do_flush_log(self, arg: str):
203        """Clear the log file for this database."""
204        log_path = self.dbpath.with_name(self.dbpath.stem + "db.log")
205        if not log_path.exists():
206            print(f"No log file at path {log_path}")
207        else:
208            print(f"Flushing log...")
209            log_path.write_text("")
210
211    def do_scan_dbs(self, arg: str):
212        """Scan the current working directory for `*.db` files and display them.
213
214        If the command is entered as `based>scan_dbs r`, the scan will be performed recursively."""
215        cwd = Pathier.cwd()
216        if arg.strip() == "r":
217            dbs = cwd.rglob("*.db")
218        else:
219            dbs = cwd.glob("*.db")
220        for db in dbs:
221            print(db.separate(cwd.stem))
222
223    def do_customize(self, arg: str):
224        """Generate a template file in the current working directory for creating a custom DBShell class.
225        Expects one argument: the name of the custom dbshell.
226        This will be used to name the generated file as well as several components in the file content."""
227        custom_file = (Pathier.cwd() / arg.replace(" ", "_")).with_suffix(".py")
228        if custom_file.exists():
229            print(f"Error: {custom_file.name} already exists in this location.")
230        else:
231            variable_name = "_".join(word for word in arg.lower().split())
232            class_name = "".join(word.capitalize() for word in arg.split())
233            content = (Pathier(__file__).parent / "customshell.py").read_text()
234            content = content.replace("CustomShell", class_name)
235            content = content.replace("customshell", variable_name)
236            custom_file.write_text(content)
237
238    def _choose_db(self, options: list[Pathier]) -> Pathier:
239        """Prompt the user to select from a list of files."""
240        cwd = Pathier.cwd()
241        paths = [path.separate(cwd.stem) for path in options]
242        while True:
243            print(
244                f"DB options:\n{' '.join([f'({i}) {path}' for i,path in enumerate(paths,1)])}"
245            )
246            choice = input("Enter the number of the option to use: ")
247            try:
248                index = int(choice)
249                if not 1 <= index <= len(options):
250                    print("Choice out of range.")
251                    continue
252                return options[index - 1]
253            except Exception as e:
254                print(f"{choice} is not a valid option.")
255
256    def preloop(self):
257        """Scan the current directory for a .db file to use.
258        If not found, prompt the user for one or to try again recursively."""
259        if self.dbpath:
260            self.dbpath = Pathier(self.dbpath)
261            print(f"Defaulting to database {self.dbpath}")
262        else:
263            print("Searching for database...")
264            cwd = Pathier.cwd()
265            dbs = list(cwd.glob("*.db"))
266            if len(dbs) == 1:
267                self.dbpath = dbs[0]
268                print(f"Using database {self.dbpath}.")
269            elif dbs:
270                self.dbpath = self._choose_db(dbs)
271            else:
272                print(f"Could not find a .db file in {cwd}.")
273                path = input(
274                    "Enter path to .db file to use or press enter to search again recursively: "
275                )
276                if path:
277                    self.dbpath = Pathier(path)
278                elif not path:
279                    print("Searching recursively...")
280                    dbs = list(cwd.rglob("*.db"))
281                    if len(dbs) == 1:
282                        self.dbpath = dbs[0]
283                        print(f"Using database {self.dbpath}.")
284                    elif dbs:
285                        self.dbpath = self._choose_db(dbs)
286                    else:
287                        print("Could not find a .db file.")
288                        self.dbpath = Pathier(input("Enter path to a .db file: "))
289        if not self.dbpath.exists():
290            raise FileNotFoundError(f"{self.dbpath} does not exist.")
291        if not self.dbpath.is_file():
292            raise ValueError(f"{self.dbpath} is not a file.")

Subclass this to create custom ArgShells.

def do_use_db(self, arg: str):
13    def do_use_db(self, arg: str):
14        """Set which database file to use."""
15        dbpath = Pathier(arg)
16        if not dbpath.exists():
17            print(f"{dbpath} does not exist.")
18            print(f"Still using {self.dbpath}")
19        elif not dbpath.is_file():
20            print(f"{dbpath} is not a file.")
21            print(f"Still using {self.dbpath}")
22        else:
23            self.dbpath = dbpath

Set which database file to use.

def do_dbpath(self, arg: str):
25    def do_dbpath(self, arg: str):
26        """Print the .db file in use."""
27        print(self.dbpath)

Print the .db file in use.

def do_backup(self, arg: str):
29    def do_backup(self, arg: str):
30        """Create a backup of the current db file."""
31        print(f"Creating a back up for {self.dbpath}...")
32        backup_path = self.dbpath.with_stem(f"{self.dbpath.stem}_bckup")
33        self.dbpath.copy(backup_path, True)
34        print("Creating backup is complete.")
35        print(f"Backup path: {backup_path}")

Create a backup of the current db file.

def do_size(self, arg: str):
37    def do_size(self, arg: str):
38        """Display the size of the the current db file."""
39        print(f"{self.dbpath.name} is {self.dbpath.size(True)}.")

Display the size of the the current db file.

@argshell.with_parser(dbparsers.get_create_table_parser)
def do_add_table(self, args: argshell.argshell.Namespace):
41    @argshell.with_parser(dbparsers.get_create_table_parser)
42    def do_add_table(self, args: argshell.Namespace):
43        """Add a new table to the database."""
44        with DataBased(self.dbpath) as db:
45            db.create_table(args.table_name, args.columns)

Add a new table to the database.

def do_drop_table(self, arg: str):
47    def do_drop_table(self, arg: str):
48        """Drop the specified table."""
49        with DataBased(self.dbpath) as db:
50            db.drop_table(arg)

Drop the specified table.

@argshell.with_parser(dbparsers.get_add_row_parser, [dbparsers.verify_matching_length])
def do_add_row(self, args: argshell.argshell.Namespace):
52    @argshell.with_parser(
53        dbparsers.get_add_row_parser, [dbparsers.verify_matching_length]
54    )
55    def do_add_row(self, args: argshell.Namespace):
56        """Add a row to a table."""
57        with DataBased(self.dbpath) as db:
58            if db.add_row(args.table_name, args.values, args.columns or None):
59                print(f"Added row to {args.table_name} table successfully.")
60            else:
61                print(f"Failed to add row to {args.table_name} table.")

Add a row to a table.

@argshell.with_parser(dbparsers.get_info_parser)
def do_info(self, args: argshell.argshell.Namespace):
63    @argshell.with_parser(dbparsers.get_info_parser)
64    def do_info(self, args: argshell.Namespace):
65        """Print out the names of the database tables, their columns, and, optionally, the number of rows."""
66        print("Getting database info...")
67        with DataBased(self.dbpath) as db:
68            tables = args.tables or db.get_table_names()
69            info = [
70                {
71                    "Table Name": table,
72                    "Columns": ", ".join(db.get_column_names(table)),
73                    "Number of Rows": db.count(table) if args.rowcount else "n/a",
74                }
75                for table in tables
76            ]
77        print(DataBased.data_to_string(info))

Print out the names of the database tables, their columns, and, optionally, the number of rows.

@argshell.with_parser(dbparsers.get_lookup_parser, [dbparsers.convert_match_pairs])
def do_show(self, args: argshell.argshell.Namespace):
 79    @argshell.with_parser(dbparsers.get_lookup_parser, [dbparsers.convert_match_pairs])
 80    def do_show(self, args: argshell.Namespace):
 81        """Find and print rows from the database.
 82        Use the -t/--tables, -m/--match_pairs, and -l/--limit flags to limit the search.
 83        Use the -c/--columns flag to limit what columns are printed.
 84        Use the -o/--order_by flag to order the results.
 85        Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs
 86        Pass -h/--help flag for parser help."""
 87        print("Finding records... ")
 88        if len(args.columns) == 0:
 89            args.columns = None
 90        with DataBased(self.dbpath) as db:
 91            tables = args.tables or db.get_table_names()
 92            for table in tables:
 93                results = db.get_rows(
 94                    table,
 95                    args.match_pairs,
 96                    columns_to_return=args.columns,
 97                    order_by=args.order_by,
 98                    limit=args.limit,
 99                    exact_match=not args.partial_matching,
100                )
101                db.close()
102                print(f"{len(results)} matching rows in {table} table:")
103                try:
104                    print(DataBased.data_to_string(results))  # type: ignore
105                except Exception as e:
106                    print("Couldn't fit data into a grid.")
107                    print(*results, sep="\n")
108                print()

Find and print rows from the database. Use the -t/--tables, -m/--match_pairs, and -l/--limit flags to limit the search. Use the -c/--columns flag to limit what columns are printed. Use the -o/--order_by flag to order the results. Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs Pass -h/--help flag for parser help.

@argshell.with_parser(dbparsers.get_lookup_parser, [dbparsers.convert_match_pairs])
def do_count(self, args: argshell.argshell.Namespace):
129    @argshell.with_parser(dbparsers.get_lookup_parser, [dbparsers.convert_match_pairs])
130    def do_count(self, args: argshell.Namespace):
131        """Print the number of rows in the database.
132        Use the -t/--tables flag to limit results to a specific table(s).
133        Use the -m/--match_pairs flag to limit the results to rows matching these criteria.
134        Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs.
135        Pass -h/--help flag for parser help."""
136        print("Counting rows...")
137        with DataBased(self.dbpath) as db:
138            tables = args.tables or db.get_table_names()
139            for table in tables:
140                num_rows = db.count(table, args.match_pairs, not args.partial_matching)
141                print(f"{num_rows} matching rows in {table} table.")

Print the number of rows in the database. Use the -t/--tables flag to limit results to a specific table(s). Use the -m/--match_pairs flag to limit the results to rows matching these criteria. Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs. Pass -h/--help flag for parser help.

def do_query(self, arg: str):
143    def do_query(self, arg: str):
144        """Execute a query against the current database."""
145        print(f"Executing {arg}")
146        with DataBased(self.dbpath) as db:
147            results = db.query(arg)
148        try:
149            for result in results:
150                print(*result, sep="|-|")
151            print(f"{db.cursor.rowcount} affected rows")
152        except Exception as e:
153            print(f"{type(e).__name__}: {e}")

Execute a query against the current database.

@argshell.with_parser(dbparsers.get_update_parser, [dbparsers.convert_match_pairs])
def do_update(self, args: argshell.argshell.Namespace):
155    @argshell.with_parser(dbparsers.get_update_parser, [dbparsers.convert_match_pairs])
156    def do_update(self, args: argshell.Namespace):
157        """Update a column to a new value.
158        Two required args: the column (-c/--column) to update and the value (-v/--value) to update to.
159        Use the -t/--tables flag to limit what tables are updated.
160        Use the -m/--match_pairs flag to specify which rows are updated.
161        Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs.
162        >>> based>update -c username -v big_chungus -t users -m username lil_chungus
163
164        ^will update the username in the users 'table' to 'big_chungus' where the username is currently 'lil_chungus'^"""
165        print("Updating rows...")
166        with DataBased(self.dbpath) as db:
167            tables = args.tables or db.get_table_names()
168            for table in tables:
169                num_updates = db.update(
170                    table,
171                    args.column,
172                    args.new_value,
173                    args.match_pairs,
174                    not args.partial_matching,
175                )
176                print(f"Updated {num_updates} rows in {table} table.")

Update a column to a new value. Two required args: the column (-c/--column) to update and the value (-v/--value) to update to. Use the -t/--tables flag to limit what tables are updated. Use the -m/--match_pairs flag to specify which rows are updated. Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs.

>>> based>update -c username -v big_chungus -t users -m username lil_chungus

^will update the username in the users 'table' to 'big_chungus' where the username is currently 'lil_chungus'^

@argshell.with_parser(dbparsers.get_lookup_parser, [dbparsers.convert_match_pairs])
def do_delete(self, args: argshell.argshell.Namespace):
178    @argshell.with_parser(dbparsers.get_lookup_parser, [dbparsers.convert_match_pairs])
179    def do_delete(self, args: argshell.Namespace):
180        """Delete rows from the database.
181        Use the -t/--tables flag to limit what tables rows are deleted from.
182        Use the -m/--match_pairs flag to specify which rows are deleted.
183        Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs.
184        >>> based>delete -t users -m username chungus -p
185
186        ^will delete all rows in the 'users' table whose username contains 'chungus'^"""
187        print("Deleting records...")
188        with DataBased(self.dbpath) as db:
189            tables = args.tables or db.get_table_names()
190            for table in tables:
191                num_rows = db.delete(table, args.match_pairs, not args.partial_matching)
192                print(f"Deleted {num_rows} rows from {table} table.")

Delete rows from the database. Use the -t/--tables flag to limit what tables rows are deleted from. Use the -m/--match_pairs flag to specify which rows are deleted. Use the -p/--partial_matching flag to enable substring matching on -m/--match_pairs.

>>> based>delete -t users -m username chungus -p

^will delete all rows in the 'users' table whose username contains 'chungus'^

@argshell.with_parser(dbparsers.get_add_column_parser)
def do_add_column(self, args: argshell.argshell.Namespace):
194    @argshell.with_parser(dbparsers.get_add_column_parser)
195    def do_add_column(self, args: argshell.Namespace):
196        """Add a new column to the specified tables."""
197        with DataBased(self.dbpath) as db:
198            tables = args.tables or db.get_table_names()
199            for table in tables:
200                db.add_column(table, args.column_name, args.type, args.default_value)

Add a new column to the specified tables.

def do_flush_log(self, arg: str):
202    def do_flush_log(self, arg: str):
203        """Clear the log file for this database."""
204        log_path = self.dbpath.with_name(self.dbpath.stem + "db.log")
205        if not log_path.exists():
206            print(f"No log file at path {log_path}")
207        else:
208            print(f"Flushing log...")
209            log_path.write_text("")

Clear the log file for this database.

def do_scan_dbs(self, arg: str):
211    def do_scan_dbs(self, arg: str):
212        """Scan the current working directory for `*.db` files and display them.
213
214        If the command is entered as `based>scan_dbs r`, the scan will be performed recursively."""
215        cwd = Pathier.cwd()
216        if arg.strip() == "r":
217            dbs = cwd.rglob("*.db")
218        else:
219            dbs = cwd.glob("*.db")
220        for db in dbs:
221            print(db.separate(cwd.stem))

Scan the current working directory for *.db files and display them.

If the command is entered as based>scan_dbs r, the scan will be performed recursively.

def do_customize(self, arg: str):
223    def do_customize(self, arg: str):
224        """Generate a template file in the current working directory for creating a custom DBShell class.
225        Expects one argument: the name of the custom dbshell.
226        This will be used to name the generated file as well as several components in the file content."""
227        custom_file = (Pathier.cwd() / arg.replace(" ", "_")).with_suffix(".py")
228        if custom_file.exists():
229            print(f"Error: {custom_file.name} already exists in this location.")
230        else:
231            variable_name = "_".join(word for word in arg.lower().split())
232            class_name = "".join(word.capitalize() for word in arg.split())
233            content = (Pathier(__file__).parent / "customshell.py").read_text()
234            content = content.replace("CustomShell", class_name)
235            content = content.replace("customshell", variable_name)
236            custom_file.write_text(content)

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.

def preloop(self):
256    def preloop(self):
257        """Scan the current directory for a .db file to use.
258        If not found, prompt the user for one or to try again recursively."""
259        if self.dbpath:
260            self.dbpath = Pathier(self.dbpath)
261            print(f"Defaulting to database {self.dbpath}")
262        else:
263            print("Searching for database...")
264            cwd = Pathier.cwd()
265            dbs = list(cwd.glob("*.db"))
266            if len(dbs) == 1:
267                self.dbpath = dbs[0]
268                print(f"Using database {self.dbpath}.")
269            elif dbs:
270                self.dbpath = self._choose_db(dbs)
271            else:
272                print(f"Could not find a .db file in {cwd}.")
273                path = input(
274                    "Enter path to .db file to use or press enter to search again recursively: "
275                )
276                if path:
277                    self.dbpath = Pathier(path)
278                elif not path:
279                    print("Searching recursively...")
280                    dbs = list(cwd.rglob("*.db"))
281                    if len(dbs) == 1:
282                        self.dbpath = dbs[0]
283                        print(f"Using database {self.dbpath}.")
284                    elif dbs:
285                        self.dbpath = self._choose_db(dbs)
286                    else:
287                        print("Could not find a .db file.")
288                        self.dbpath = Pathier(input("Enter path to a .db file: "))
289        if not self.dbpath.exists():
290            raise FileNotFoundError(f"{self.dbpath} does not exist.")
291        if not self.dbpath.is_file():
292            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
default
completedefault
completenames
complete
get_names
complete_help
print_topics
columnize
argshell.argshell.ArgShell
do_quit
do_sys
do_help
cmdloop
emptyline
def main():
295def main():
296    DBShell().cmdloop()