databased.dbmanager

  1import argparse
  2import shlex
  3import sys
  4from pathlib import Path
  5
  6from databased import DataBased, data_to_string
  7
  8""" A command line tool to interact with a database file.
  9Works like a standard argparse based cli
 10except it will ask you for arguments indefinitely in a loop
 11instead of having to invoke the script with arguments over and over.
 12I.e. instead of "python dbManager.py -db database.db -f someString -t someTable",
 13just invoke "python dbManager.py" then a repeating "Enter command: " prompt will appear
 14and "-db database.db -f someString -t someTable" can be entered.
 15Note: a -db arg only needs to be provided once (if there is no default set) unless
 16you wish to change databases. So a subsequent command to the above
 17can just be entered as "-f someOtherString -t someTable".
 18
 19This is just a quick template and can be customized by adding arguments and adding/overriding functions
 20for specific database projects."""
 21
 22# Subclassing to prevent program exit when the -h/--help arg is passed.
 23class ArgParser(argparse.ArgumentParser):
 24    def exit(self, status=0, message=None):
 25        if message:
 26            self._print_message(message, sys.stderr)
 27
 28
 29def get_args(command: str) -> argparse.Namespace:
 30    parser = ArgParser()
 31
 32    parser.add_argument(
 33        "-db",
 34        "--dbname",
 35        type=str,
 36        default="$dbname",
 37        help="""Name of database file to use.
 38        Required on the first loop if no default is set,
 39        but subsequent loops will resuse the same database
 40        unless a new one is provided through this arg.""",
 41    )
 42
 43    parser.add_argument(
 44        "-i",
 45        "--info",
 46        action="store_true",
 47        help=""" Display table names, their respective columns, and how many records they contain.
 48        If a -t/--tables arg is passed, just the columns and row count for those tables will be shown.""",
 49    )
 50
 51    parser.add_argument(
 52        "-t",
 53        "--tables",
 54        type=str,
 55        nargs="*",
 56        default=[],
 57        help="""Limits commands to a specific list of tables.
 58        Optional for some commands, required for others.
 59        If this is the only arg given (besides -db if not already set),
 60        the whole table will be printed to the terminal.""",
 61    )
 62
 63    parser.add_argument(
 64        "-c",
 65        "--columns",
 66        type=str,
 67        nargs="*",
 68        default=[],
 69        help=""" Limits commands to a specific list of columns.
 70        Optional for some commands, required for others.
 71        If this and -t are the only args given 
 72        (besides -db if not already set), the whole table will be printed
 73        to the terminal, but with only the columns provided with this arg.""",
 74    )
 75
 76    parser.add_argument(
 77        "-f",
 78        "--find",
 79        type=str,
 80        default=None,
 81        help=""" A substring to search the database for. 
 82        If a -c/--columns arg(s) is not given, the values will be matched against all columns.
 83        Similarly, if a -t/--tables arg(s) is not given, the values will be searched for in all tables.""",
 84    )
 85
 86    parser.add_argument(
 87        "-sco",
 88        "--show_count_only",
 89        action="store_true",
 90        help=""" Show the number of results returned by -f/--find,
 91        but don't print the results to the terminal.""",
 92    )
 93
 94    parser.add_argument(
 95        "-d",
 96        "--delete",
 97        type=str,
 98        nargs="*",
 99        default=[],
100        help=""" A list of values to be deleted from the database.
101        A -c/--columns arg must be supplied.
102        A -t/--tables arg must be supplied.""",
103    )
104
105    parser.add_argument(
106        "-u",
107        "--update",
108        type=str,
109        default=None,
110        nargs="*",
111        help=""" Update a record in the database.
112        Expects the first argument to be the new value and interprets
113        subsequent arguements as pairs of 'column' and 'value' to use
114        when selecting which rows to update. The -c/--columns arg will
115        be the column that is updated with the new value for matching rows.
116        A -c/--columns arg must be supplied.
117        A -t/--tables arg must be supplied.
118        e.g '-t birds -c last_seen -u today name sparrow migratory 0'
119        will update the 'last_seen' column of the 'birds' table to 'today'
120        for all rows that have either/both of their 'name' and 'migratory'
121        columns set to 'sparrow' and '0', respectively.""",
122    )
123
124    parser.add_argument(
125        "-sb", "--sort_by", type=str, default=None, help="Column to sort results by."
126    )
127
128    parser.add_argument(
129        "-q",
130        "--query",
131        type=str,
132        default=None,
133        help=""" Directly execute a query against the database. """,
134    )
135
136    args = parser.parse_args(command)
137
138    if args.dbname and not Path(args.dbname).exists():
139        raise Exception(f"{args.dbname} does not exist.")
140
141    return args
142
143
144def info():
145    print("Getting database info...")
146    print()
147    if not args.tables:
148        args.tables = db.get_table_names()
149    results = []
150    for table in args.tables:
151        count = db.count(table)
152        columns = db.get_column_names(table)
153        results.append(
154            {
155                "table name": table,
156                "columns": ", ".join(columns),
157                "number of rows": count,
158            }
159        )
160    if args.sort_by and args.sort_by in results[0]:
161        results = sorted(results, key=lambda x: x[args.sort_by])
162    print(data_to_string(results))
163
164
165def find():
166    print("Finding records... ")
167    print()
168    if not args.tables:
169        args.tables = db.get_table_names()
170    for table in args.tables:
171        results = db.find(table, args.find, args.columns)
172        if args.sort_by and args.sort_by in results[0]:
173            results = sorted(results, key=lambda x: x[args.sort_by])
174        if args.columns:
175            print(
176                f"{len(results)} results for '{args.find}' in '{', '.join(args.columns)}' column(s) of '{table}' table:"
177            )
178        else:
179            print(f"{len(results)} results for '{args.find}' in '{table}' table:")
180        if not args.show_count_only:
181            try:
182                print(data_to_string(results))
183            except Exception as e:
184                print("Couldn't fit data into a grid.")
185                print(*results, sep="\n")
186        print()
187
188
189def delete():
190    if not args.tables:
191        raise ValueError("Missing -t/--tables arg for -d/--delete function.")
192    if not args.columns:
193        raise ValueError("Missing -c/--columns arg for -d/--delete function.")
194    print("Deleting records... ")
195    print()
196    num_deleted_records = 0
197    failed_deletions = []
198    for item in args.delete:
199        success = db.delete(args.tables[0], [(args.columns[0], item)])
200        if success:
201            num_deleted_records += success
202        else:
203            failed_deletions.append(item)
204    print(f"Deleted {num_deleted_records} record(s) from '{args.tables[0]}' table.")
205    if len(failed_deletions) > 0:
206        print(
207            f"Failed to delete the following {len(failed_deletions)} record(s) from '{args.tables[0]}' table:"
208        )
209        for fail in failed_deletions:
210            print(f"  {fail}")
211
212
213def update():
214    if not args.tables:
215        raise ValueError("Missing -t/--tables arg for -u/--update function.")
216    if not args.columns:
217        raise ValueError("Missing -c/--columns arg for -u/--update function.")
218    print("Updating record... ")
219    print()
220    new_value = args.update[0]
221    if len(args.update) > 1:
222        args.update = args.update[1:]
223        match_criteria = [
224            (args.update[i], args.update[i + 1]) for i in range(0, len(args.update), 2)
225        ]
226    else:
227        match_criteria = None
228    if db.update(
229        args.tables[0],
230        args.columns[0],
231        new_value,
232        match_criteria,
233    ):
234        print(
235            f"Updated '{args.columns[0]}' column to '{new_value}' in '{args.tables[0]}' table for match_criteria {match_criteria}."
236        )
237    else:
238        print(
239            f"Failed to update '{args.columns[0]}' column to '{new_value}' in '{args.tables[0]}' table for match_criteria {match_criteria}."
240        )
241
242
243def print_table():
244    for table in args.tables:
245        rows = db.get_rows(
246            table, columns_to_return=args.columns, sort_by_column=args.sort_by
247        )
248        print(f"{table} table:")
249        print(data_to_string(rows))
250
251
252def query():
253    results = db.query(args.query)
254    try:
255        for result in results:
256            print(*result, sep=" * ")
257    except Exception as e:
258        print(f"Couldn't display results of '{args.query}'.")
259
260
261if __name__ == "__main__":
262    sys.tracebacklimit = 0
263    while True:
264        try:
265            command = shlex.split(input("Enter command: "))
266            args = get_args(command)
267            if args.dbname:
268                dbname = args.dbname
269            with DataBased(dbpath=dbname) as db:
270                if args.info:
271                    info()
272                elif args.find:
273                    find()
274                elif args.delete:
275                    delete()
276                elif args.update:
277                    update()
278                elif args.query:
279                    query()
280                else:
281                    print_table()
282        except KeyboardInterrupt:
283            break
284        except Exception as e:
285            print(e)
class ArgParser(argparse.ArgumentParser):
24class ArgParser(argparse.ArgumentParser):
25    def exit(self, status=0, message=None):
26        if message:
27            self._print_message(message, sys.stderr)

Object for parsing command line strings into Python objects.

Keyword Arguments: - prog -- The name of the program (default: os.path.basename(sys.argv[0])) - usage -- A usage message (default: auto-generated from arguments) - description -- A description of what the program does - epilog -- Text following the argument descriptions - parents -- Parsers whose arguments should be copied into this one - formatter_class -- HelpFormatter class for printing help messages - prefix_chars -- Characters that prefix optional arguments - fromfile_prefix_chars -- Characters that prefix files containing additional arguments - argument_default -- The default value for all arguments - conflict_handler -- String indicating how to handle conflicts - add_help -- Add a -h/-help option - allow_abbrev -- Allow long options to be abbreviated unambiguously - exit_on_error -- Determines whether or not ArgumentParser exits with error info when an error occurs

def exit(self, status=0, message=None):
25    def exit(self, status=0, message=None):
26        if message:
27            self._print_message(message, sys.stderr)
Inherited Members
argparse.ArgumentParser
ArgumentParser
add_subparsers
parse_args
parse_known_args
convert_arg_line_to_args
parse_intermixed_args
parse_known_intermixed_args
format_usage
format_help
print_usage
print_help
error
argparse._ActionsContainer
register
set_defaults
get_default
add_argument
add_argument_group
add_mutually_exclusive_group
def get_args(command: str) -> argparse.Namespace:
 30def get_args(command: str) -> argparse.Namespace:
 31    parser = ArgParser()
 32
 33    parser.add_argument(
 34        "-db",
 35        "--dbname",
 36        type=str,
 37        default="$dbname",
 38        help="""Name of database file to use.
 39        Required on the first loop if no default is set,
 40        but subsequent loops will resuse the same database
 41        unless a new one is provided through this arg.""",
 42    )
 43
 44    parser.add_argument(
 45        "-i",
 46        "--info",
 47        action="store_true",
 48        help=""" Display table names, their respective columns, and how many records they contain.
 49        If a -t/--tables arg is passed, just the columns and row count for those tables will be shown.""",
 50    )
 51
 52    parser.add_argument(
 53        "-t",
 54        "--tables",
 55        type=str,
 56        nargs="*",
 57        default=[],
 58        help="""Limits commands to a specific list of tables.
 59        Optional for some commands, required for others.
 60        If this is the only arg given (besides -db if not already set),
 61        the whole table will be printed to the terminal.""",
 62    )
 63
 64    parser.add_argument(
 65        "-c",
 66        "--columns",
 67        type=str,
 68        nargs="*",
 69        default=[],
 70        help=""" Limits commands to a specific list of columns.
 71        Optional for some commands, required for others.
 72        If this and -t are the only args given 
 73        (besides -db if not already set), the whole table will be printed
 74        to the terminal, but with only the columns provided with this arg.""",
 75    )
 76
 77    parser.add_argument(
 78        "-f",
 79        "--find",
 80        type=str,
 81        default=None,
 82        help=""" A substring to search the database for. 
 83        If a -c/--columns arg(s) is not given, the values will be matched against all columns.
 84        Similarly, if a -t/--tables arg(s) is not given, the values will be searched for in all tables.""",
 85    )
 86
 87    parser.add_argument(
 88        "-sco",
 89        "--show_count_only",
 90        action="store_true",
 91        help=""" Show the number of results returned by -f/--find,
 92        but don't print the results to the terminal.""",
 93    )
 94
 95    parser.add_argument(
 96        "-d",
 97        "--delete",
 98        type=str,
 99        nargs="*",
100        default=[],
101        help=""" A list of values to be deleted from the database.
102        A -c/--columns arg must be supplied.
103        A -t/--tables arg must be supplied.""",
104    )
105
106    parser.add_argument(
107        "-u",
108        "--update",
109        type=str,
110        default=None,
111        nargs="*",
112        help=""" Update a record in the database.
113        Expects the first argument to be the new value and interprets
114        subsequent arguements as pairs of 'column' and 'value' to use
115        when selecting which rows to update. The -c/--columns arg will
116        be the column that is updated with the new value for matching rows.
117        A -c/--columns arg must be supplied.
118        A -t/--tables arg must be supplied.
119        e.g '-t birds -c last_seen -u today name sparrow migratory 0'
120        will update the 'last_seen' column of the 'birds' table to 'today'
121        for all rows that have either/both of their 'name' and 'migratory'
122        columns set to 'sparrow' and '0', respectively.""",
123    )
124
125    parser.add_argument(
126        "-sb", "--sort_by", type=str, default=None, help="Column to sort results by."
127    )
128
129    parser.add_argument(
130        "-q",
131        "--query",
132        type=str,
133        default=None,
134        help=""" Directly execute a query against the database. """,
135    )
136
137    args = parser.parse_args(command)
138
139    if args.dbname and not Path(args.dbname).exists():
140        raise Exception(f"{args.dbname} does not exist.")
141
142    return args
def info():
145def info():
146    print("Getting database info...")
147    print()
148    if not args.tables:
149        args.tables = db.get_table_names()
150    results = []
151    for table in args.tables:
152        count = db.count(table)
153        columns = db.get_column_names(table)
154        results.append(
155            {
156                "table name": table,
157                "columns": ", ".join(columns),
158                "number of rows": count,
159            }
160        )
161    if args.sort_by and args.sort_by in results[0]:
162        results = sorted(results, key=lambda x: x[args.sort_by])
163    print(data_to_string(results))
def find():
166def find():
167    print("Finding records... ")
168    print()
169    if not args.tables:
170        args.tables = db.get_table_names()
171    for table in args.tables:
172        results = db.find(table, args.find, args.columns)
173        if args.sort_by and args.sort_by in results[0]:
174            results = sorted(results, key=lambda x: x[args.sort_by])
175        if args.columns:
176            print(
177                f"{len(results)} results for '{args.find}' in '{', '.join(args.columns)}' column(s) of '{table}' table:"
178            )
179        else:
180            print(f"{len(results)} results for '{args.find}' in '{table}' table:")
181        if not args.show_count_only:
182            try:
183                print(data_to_string(results))
184            except Exception as e:
185                print("Couldn't fit data into a grid.")
186                print(*results, sep="\n")
187        print()
def delete():
190def delete():
191    if not args.tables:
192        raise ValueError("Missing -t/--tables arg for -d/--delete function.")
193    if not args.columns:
194        raise ValueError("Missing -c/--columns arg for -d/--delete function.")
195    print("Deleting records... ")
196    print()
197    num_deleted_records = 0
198    failed_deletions = []
199    for item in args.delete:
200        success = db.delete(args.tables[0], [(args.columns[0], item)])
201        if success:
202            num_deleted_records += success
203        else:
204            failed_deletions.append(item)
205    print(f"Deleted {num_deleted_records} record(s) from '{args.tables[0]}' table.")
206    if len(failed_deletions) > 0:
207        print(
208            f"Failed to delete the following {len(failed_deletions)} record(s) from '{args.tables[0]}' table:"
209        )
210        for fail in failed_deletions:
211            print(f"  {fail}")
def update():
214def update():
215    if not args.tables:
216        raise ValueError("Missing -t/--tables arg for -u/--update function.")
217    if not args.columns:
218        raise ValueError("Missing -c/--columns arg for -u/--update function.")
219    print("Updating record... ")
220    print()
221    new_value = args.update[0]
222    if len(args.update) > 1:
223        args.update = args.update[1:]
224        match_criteria = [
225            (args.update[i], args.update[i + 1]) for i in range(0, len(args.update), 2)
226        ]
227    else:
228        match_criteria = None
229    if db.update(
230        args.tables[0],
231        args.columns[0],
232        new_value,
233        match_criteria,
234    ):
235        print(
236            f"Updated '{args.columns[0]}' column to '{new_value}' in '{args.tables[0]}' table for match_criteria {match_criteria}."
237        )
238    else:
239        print(
240            f"Failed to update '{args.columns[0]}' column to '{new_value}' in '{args.tables[0]}' table for match_criteria {match_criteria}."
241        )
def query():
253def query():
254    results = db.query(args.query)
255    try:
256        for result in results:
257            print(*result, sep=" * ")
258    except Exception as e:
259        print(f"Couldn't display results of '{args.query}'.")