From c60a97d469655d4f1e5021bc6651973b37256011 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Tue, 3 May 2022 16:22:47 +0530 Subject: [PATCH 01/92] clean branch --- .gitignore | 3 +- data/schema.sql | 38 --- npbc_cli.py | 591 -------------------------------------------- npbc_core.py | 633 ------------------------------------------------ test_core.py | 564 ------------------------------------------ 5 files changed, 2 insertions(+), 1827 deletions(-) delete mode 100644 data/schema.sql delete mode 100644 npbc_cli.py delete mode 100644 npbc_core.py delete mode 100644 test_core.py diff --git a/.gitignore b/.gitignore index 1a796ae..e4a9c8b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ exp npbc.db pyenv-install bin -.vscode \ No newline at end of file +.vscode +.pytest_cache diff --git a/data/schema.sql b/data/schema.sql deleted file mode 100644 index 3d59e04..0000000 --- a/data/schema.sql +++ /dev/null @@ -1,38 +0,0 @@ -CREATE TABLE IF NOT EXISTS papers ( - paper_id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - CONSTRAINT unique_paper_name UNIQUE (name) -); -CREATE TABLE IF NOT EXISTS papers_days_delivered ( - paper_id INTEGER NOT NULL, - day_id INTEGER NOT NULL, - delivered INTEGER NOT NULL, - FOREIGN KEY(paper_id) REFERENCES papers(paper_id), - CONSTRAINT unique_paper_day UNIQUE (paper_id, day_id) -); -CREATE TABLE IF NOT EXISTS papers_days_cost( - paper_id INTEGER NOT NULL, - day_id INTEGER NOT NULL, - cost INTEGER, - FOREIGN KEY(paper_id) REFERENCES papers(paper_id), - CONSTRAINT unique_paper_day UNIQUE (paper_id, day_id) -); -CREATE TABLE IF NOT EXISTS undelivered_strings ( - entry_id INTEGER PRIMARY KEY AUTOINCREMENT, - year INTEGER NOT NULL, - month INTEGER NOT NULL, - paper_id INTEGER NOT NULL, - string TEXT NOT NULL, - FOREIGN KEY (paper_id) REFERENCES papers(paper_id) -); -CREATE TABLE IF NOT EXISTS undelivered_dates ( - entry_id INTEGER PRIMARY KEY AUTOINCREMENT, - timestamp TEXT NOT NULL, - year INTEGER NOT NULL, - month INTEGER NOT NULL, - paper_id INTEGER NOT NULL, - dates TEXT NOT NULL, - FOREIGN KEY (paper_id) REFERENCES papers(paper_id) -); -CREATE INDEX IF NOT EXISTS search_strings ON undelivered_strings(year, month); -CREATE INDEX IF NOT EXISTS paper_names ON papers(name); \ No newline at end of file diff --git a/npbc_cli.py b/npbc_cli.py deleted file mode 100644 index a0853e2..0000000 --- a/npbc_cli.py +++ /dev/null @@ -1,591 +0,0 @@ -from argparse import ArgumentParser -from argparse import Namespace as arg_namespace -from datetime import datetime - -from colorama import Fore, Style -from pyperclip import copy as copy_to_clipboard - -from npbc_core import (VALIDATE_REGEX, WEEKDAY_NAMES, add_new_paper, - add_undelivered_string, calculate_cost_of_all_papers, - delete_existing_paper, delete_undelivered_string, - edit_existing_paper, extract_days_and_costs, - format_output, generate_sql_query, get_previous_month, - query_database, save_results, setup_and_connect_DB, - validate_month_and_year, validate_undelivered_string) - - -## setup parsers -def define_and_read_args() -> arg_namespace: - - # main parser for all commands - main_parser = ArgumentParser( - prog="npbc", - description="Calculates your monthly newspaper bill." - ) - functions = main_parser.add_subparsers(required=True) - - - # calculate subparser - calculate_parser = functions.add_parser( - 'calculate', - help="Calculate the bill for one month. Previous month will be used if month or year flags are not set." - ) - - calculate_parser.set_defaults(func=calculate) - calculate_parser.add_argument('-m', '--month', type=int, help="Month to calculate bill for. Must be between 1 and 12.") - calculate_parser.add_argument('-y', '--year', type=int, help="Year to calculate bill for. Must be between 1 and 9999.") - calculate_parser.add_argument('-c', '--nocopy', help="Don't copy the result of the calculation to the clipboard.", action='store_true') - calculate_parser.add_argument('-l', '--nolog', help="Don't log the result of the calculation.", action='store_true') - - # add undelivered string subparser - addudl_parser = functions.add_parser( - 'addudl', - help="Store a date when paper(s) were not delivered. Previous month will be used if month or year flags are not set." - ) - - addudl_parser.set_defaults(func=addudl) - addudl_parser.add_argument('-m', '--month', type=int, help="Month to register undelivered incident(s) for. Must be between 1 and 12.") - addudl_parser.add_argument('-y', '--year', type=int, help="Year to register undelivered incident(s) for. Must be between 1 and 9999.") - addudl_parser.add_argument('-k', '--key', type=str, help="Key of paper to register undelivered incident(s) for.", required=True) - addudl_parser.add_argument('-u', '--undelivered', type=str, help="Dates when you did not receive any papers.", required=True) - - # delete undelivered string subparser - deludl_parser = functions.add_parser( - 'deludl', - help="Delete a stored date when paper(s) were not delivered. Previous month will be used if month or year flags are not set." - ) - - deludl_parser.set_defaults(func=deludl) - deludl_parser.add_argument('-k', '--key', type=str, help="Key of paper to unregister undelivered incident(s) for.", required=True) - deludl_parser.add_argument('-m', '--month', type=int, help="Month to unregister undelivered incident(s) for. Must be between 1 and 12.", required=True) - deludl_parser.add_argument('-y', '--year', type=int, help="Year to unregister undelivered incident(s) for. Must be between 1 and 9999.", required=True) - - # get undelivered string subparser - getudl_parser = functions.add_parser( - 'getudl', - help="Get a list of all stored date strings when paper(s) were not delivered." - ) - - getudl_parser.set_defaults(func=getudl) - getudl_parser.add_argument('-k', '--key', type=str, help="Key for paper.") - getudl_parser.add_argument('-m', '--month', type=int, help="Month. Must be between 1 and 12.") - getudl_parser.add_argument('-y', '--year', type=int, help="Year. Must be between 1 and 9999.") - getudl_parser.add_argument('-u', '--undelivered', type=str, help="Dates when you did not receive any papers.") - - # edit paper subparser - editpaper_parser = functions.add_parser( - 'editpaper', - help="Edit a newspaper\'s name, days delivered, and/or price." - ) - - editpaper_parser.set_defaults(func=editpaper) - editpaper_parser.add_argument('-n', '--name', type=str, help="Name for paper to be edited.") - editpaper_parser.add_argument('-d', '--days', type=str, help="Number of days the paper to be edited is delivered. Monday is the first day, and all seven weekdays are required. A 'Y' means it is delivered, and an 'N' means it isn't. No separator required.") - editpaper_parser.add_argument('-p', '--price', type=str, help="Daywise prices of paper to be edited. Monday is the first day. Values must be separated by semicolons, and 0s are ignored.") - editpaper_parser.add_argument('-k', '--key', type=str, help="Key for paper to be edited.", required=True) - - # add paper subparser - addpaper_parser = functions.add_parser( - 'addpaper', - help="Add a new newspaper to the list of newspapers." - ) - - addpaper_parser.set_defaults(func=addpaper) - addpaper_parser.add_argument('-n', '--name', type=str, help="Name for paper to be added.", required=True) - addpaper_parser.add_argument('-d', '--days', type=str, help="Number of days the paper to be added is delivered. Monday is the first day, and all seven weekdays are required. A 'Y' means it is delivered, and an 'N' means it isn't. No separator required.", required=True) - addpaper_parser.add_argument('-p', '--price', type=str, help="Daywise prices of paper to be added. Monday is the first day. Values must be separated by semicolons, and 0s are ignored.", required=True) - - # delete paper subparser - delpaper_parser = functions.add_parser( - 'delpaper', - help="Delete a newspaper from the list of newspapers." - ) - - delpaper_parser.set_defaults(func=delpaper) - delpaper_parser.add_argument('-k', '--key', type=str, help="Key for paper to be deleted.", required=True) - - # get paper subparser - getpapers_parser = functions.add_parser( - 'getpapers', - help="Get all newspapers." - ) - - getpapers_parser.set_defaults(func=getpapers) - getpapers_parser.add_argument('-n', '--names', help="Get the names of the newspapers.", action='store_true') - getpapers_parser.add_argument('-d', '--days', help="Get the days the newspapers are delivered. Monday is the first day, and all seven weekdays are required. A 'Y' means it is delivered, and an 'N' means it isn't.", action='store_true') - getpapers_parser.add_argument('-p', '--prices', help="Get the daywise prices of the newspapers. Monday is the first day. Values must be separated by semicolons.", action='store_true') - - # get undelivered logs subparser - getlogs_parser = functions.add_parser( - 'getlogs', - help="Get the log of all undelivered dates." - ) - - getlogs_parser.set_defaults(func=getlogs) - getlogs_parser.add_argument('-m', '--month', type=int, help="Month. Must be between 1 and 12.") - getlogs_parser.add_argument('-y', '--year', type=int, help="Year. Must be between 1 and 9999.") - getlogs_parser.add_argument('-k', '--key', type=str, help="Key for paper.", required=True) - - # update application subparser - update_parser = functions.add_parser( - 'update', - help="Update the application." - ) - - update_parser.set_defaults(func=update) - - - return main_parser.parse_args() - - -## print out a coloured status message using Colorama -def status_print(status: bool, message: str) -> None: - if status: - print(f"{Fore.GREEN}{Style.BRIGHT}{message}{Style.RESET_ALL}\n") - else: - print(f"{Fore.RED}{Style.BRIGHT}{message}{Style.RESET_ALL}\n") - -## calculate the cost for a given month and year - # default to the previous month if no month and no year is given - # default to the current month if no month is given and year is given - # default to the current year if no year is given and month is given -def calculate(args: arg_namespace) -> None: - - # deal with month and year - if args.month or args.year: - - feedback = validate_month_and_year(args.month, args.year) - - if not feedback[0]: - status_print(*feedback) - return - - if args.month: - month = args.month - - else: - month = datetime.now().month - - if args.year: - year = args.year - - else: - year = datetime.now().year - - else: - previous_month = get_previous_month() - month = previous_month.month - year = previous_month.year - - # look for undelivered strings in the database - existing_strings = query_database( - generate_sql_query( - 'undelivered_strings', - columns=['paper_id', 'string'], - conditions={ - 'month': month, - 'year': year - } - ) - ) - - # associate undelivered strings with their paper_id - undelivered_strings: dict[int, str] = { - paper_id: undelivered_string - for paper_id, undelivered_string in existing_strings - } - - # calculate the cost for each paper, as well as the total cost - costs, total, undelivered_dates = calculate_cost_of_all_papers( - undelivered_strings, - month, - year - ) - - # format the results - formatted = format_output(costs, total, month, year) - - # unless the user specifies so, copy the results to the clipboard - if not args.nocopy: - copy_to_clipboard(formatted) - - formatted += '\nSummary copied to clipboard.' - - # unless the user specifies so, log the results to the database - if not args.nolog: - save_results(undelivered_dates, month, year) - - formatted += '\nLog saved to file.' - - # print the results - status_print(True, "Success!") - print(f"SUMMARY:\n{formatted}") - - -## add undelivered strings to the database - # default to the current month if no month and/or no year is given -def addudl(args: arg_namespace): - - # validate the month and year - feedback = validate_month_and_year(args.month, args.year) - - if feedback[0]: - - # if no month is given, default to the current month - if args.month: - month = args.month - - else: - month = datetime.now().month - - # if no year is given, default to the current year - if args.year: - year = args.year - - else: - year = datetime.now().year - - # add the undelivered strings to the database - feedback = add_undelivered_string( - args.key, - str(args.undelivered).lower().strip(), - month, - year - ) - - status_print(*feedback) - - -## delete undelivered strings from the database -def deludl(args: arg_namespace) -> None: - - # validate the month and year - feedback = validate_month_and_year(args.month, args.year) - - # delete the undelivered strings from the database - if feedback[0]: - - feedback = delete_undelivered_string( - args.key, - args.month, - args.year - ) - - status_print(*feedback) - - -## get undelivered strings from the database - # filter by whichever parameter the user provides. they as many as they want. - # available parameters: month, year, key, string -def getudl(args: arg_namespace) -> None: - - # validate the month and year - feedback = validate_month_and_year(args.month, args.year) - - if not feedback[0]: - status_print(*feedback) - return - - conditions = {} - - if args.key: - conditions['paper_id'] = args.key - - if args.month: - conditions['month'] = args.month - - if args.year: - conditions['year'] = args.year - - if args.undelivered: - conditions['strings'] = str(args.undelivered).lower().strip() - - if not validate_undelivered_string(conditions['strings']): - status_print(False, "Invalid undelivered string.") - return - - # if the undelivered strings exist, fetch them - undelivered_strings = query_database( - generate_sql_query( - 'undelivered_strings', - conditions=conditions - ) - ) - - # if there were undelivered strings, print them - if undelivered_strings: - status_print(True, 'Found undelivered strings.') - - print(f"{Fore.YELLOW}entry_id{Style.RESET_ALL} | {Fore.YELLOW}year{Style.RESET_ALL} | {Fore.YELLOW}month{Style.RESET_ALL} | {Fore.YELLOW}paper_id{Style.RESET_ALL} | {Fore.YELLOW}string{Style.RESET_ALL}") - - for string in undelivered_strings: - print('|'.join([str(item) for item in string])) - - # otherwise, print that there were no undelivered strings - else: - status_print(False, 'No undelivered strings found.') - - -## edit the data for one paper -def editpaper(args: arg_namespace) -> None: - feedback = True, "" - days, costs = "", "" - - # validate the string for delivery days - if args.days: - days = str(args.days).lower().strip() - - if not VALIDATE_REGEX['delivery'].match(days): - feedback = False, "Invalid delivery days." - - # validate the string for costs - if args.costs: - costs = str(args.costs).lower().strip() - - if not VALIDATE_REGEX['prices'].match(costs): - feedback = False, "Invalid prices." - - # if the string for delivery days and costs are valid, edit the paper - if feedback[0]: - - feedback = edit_existing_paper( - args.key, - args.name, - *extract_days_and_costs(days, costs) - ) - - status_print(*feedback) - - -## add a new paper to the database -def addpaper(args: arg_namespace) -> None: - feedback = True, "" - days, costs = "", "" - - - # validate the string for delivery days - if args.days: - days = str(args.days).lower().strip() - - if not VALIDATE_REGEX['delivery'].match(days): - feedback = False, "Invalid delivery days." - - # validate the string for costs - if args.costs: - costs = str(args.costs).lower().strip() - - if not VALIDATE_REGEX['prices'].match(costs): - feedback = False, "Invalid prices." - - # if the string for delivery days and costs are valid, add the paper - if feedback[0]: - - feedback = add_new_paper( - args.name, - *extract_days_and_costs(days, costs) - ) - - status_print(*feedback) - - -## delete a paper from the database -def delpaper(args: arg_namespace) -> None: - - # attempt to delete the paper - feedback = delete_existing_paper( - args.key - ) - - status_print(*feedback) - - -## get a list of all papers in the database - # filter by whichever parameter the user provides. they may use as many as they want (but keys are always printed) - # available parameters: name, days, costs - # the output is provided as a formatted table, printed to the standard output -def getpapers(args: arg_namespace) -> None: - headers = ['paper_id'] - - # fetch a list of all papers' IDs - papers_id_list = [ - paper_id - for paper_id, in query_database( - generate_sql_query( - 'papers', - columns=['paper_id'] - ) - ) - ] - - # initialize lists for the data - paper_name_list, paper_days_list, paper_costs_list = [], [], [] - - # sort the papers' IDs (for the sake of consistency) - papers_id_list.sort() - - # if the user wants names, fetch that data and add it to the list - if args.names: - - # first get a dictionary of {paper_id: paper_name} - papers_names = { - paper_id: paper_name - for paper_id, paper_name in query_database( - generate_sql_query( - 'papers', - columns=['paper_id', 'name'] - ) - ) - } - - # then use the sorted IDs list to create a sorted names list - paper_name_list = [ - papers_names[paper_id] - for paper_id in papers_id_list - ] - - headers.append('name') - - # if the user wants delivery days, fetch that data and add it to the list - if args.days: - - # initialize a dictionary of {paper_id: {day_id: delivery}} - papers_days = { - paper_id: {} - for paper_id in papers_id_list - } - - # then get the data for each paper - for paper_id, day_id, delivered in query_database( - generate_sql_query( - 'papers_days_delivered', - columns=['paper_id', 'day_id', 'delivered'] - ) - ): - papers_days[paper_id][day_id] = delivered - - # format the data so that it matches the regex pattern /^[YN]{7}$/, the same way the user must input this data - paper_days_list = [ - ''.join([ - 'Y' if int(papers_days[paper_id][day_id]) == 1 else 'N' - for day_id, _ in enumerate(WEEKDAY_NAMES) - ]) - for paper_id in papers_id_list - ] - - headers.append('days') - - # if the user wants costs, fetch that data and add it to the list - if args.prices: - - # initialize a dictionary of {paper_id: {day_id: price}} - papers_costs = { - paper_id: {} - for paper_id in papers_id_list - } - - # then get the data for each paper - for paper_id, day_id, cost in query_database( - generate_sql_query( - 'papers_days_cost', - columns=['paper_id', 'day_id', 'cost'] - ) - ): - papers_costs[paper_id][day_id] = cost - - # format the data so that it matches the regex pattern /^[x](;[x]){6}$/, where /x/ is a number that may be either a floating point or an integer, the same way the user must input this data. - paper_costs_list = [ - ';'.join([ - str(papers_costs[paper_id][day_id]) - for day_id, _ in enumerate(WEEKDAY_NAMES) - ]) - for paper_id in papers_id_list - ] - - headers.append('costs') - - # print the headers - print(' | '.join([ - f"{Fore.YELLOW}{header}{Style.RESET_ALL}" - for header in headers - ])) - - # print the data - for index, paper_id in enumerate(papers_id_list): - print(f"{paper_id}: ", end='') - - values = [] - - if args.names: - values.append(paper_name_list[index]) - - if args.days: - values.append(paper_days_list[index]) - - if args.prices: - values.append(paper_costs_list[index]) - - print(', '.join(values)) - - -## get a log of all deliveries for a paper - # the user may specify parameters to filter the output by. they may use as many as they want, or none - # available parameters: paper_id, month, year -def getlogs(args: arg_namespace) -> None: - - # validate the month and year - feedback = validate_month_and_year(args.month, args.year) - - if not feedback[0]: - status_print(*feedback) - return - - conditions = {} - - # if the user specified a particular paper, add it to the conditions - if args.key: - conditions['paper_id'] = args.key - - if args.month: - conditions['month'] = args.month - - if args.year: - conditions['year'] = args.year - - # fetch the data - undelivered_dates = query_database( - generate_sql_query( - 'undelivered_dates', - conditions=conditions - ) - ) - - # if data was found, print it - if undelivered_dates: - status_print(True, 'Success!') - - print(f"{Fore.YELLOW}entry_id{Style.RESET_ALL} | {Fore.YELLOW}year{Style.RESET_ALL} | {Fore.YELLOW}month{Style.RESET_ALL} | {Fore.YELLOW}paper_id{Style.RESET_ALL} | {Fore.YELLOW}dates{Style.RESET_ALL}") - - for date in undelivered_dates: - print(', '.join(date)) - - # if no data was found, print an error message - else: - status_print(False, 'No results found.') - - -## update the application - # under normal operation, this function should never run - # if the update CLI argument is provided, this script will never run and the updater will be run instead -def update(args: arg_namespace) -> None: - status_print(False, "Update failed.") - - -## run the application -def main() -> None: - setup_and_connect_DB() - args = define_and_read_args() - args.func(args) - - -if __name__ == '__main__': - main() diff --git a/npbc_core.py b/npbc_core.py deleted file mode 100644 index b6c1537..0000000 --- a/npbc_core.py +++ /dev/null @@ -1,633 +0,0 @@ -from sqlite3 import connect -from calendar import day_name as weekday_names_iterable -from calendar import monthrange, monthcalendar -from datetime import date as date_type, datetime, timedelta -from pathlib import Path -from re import compile as compile_regex - -## paths for the folder containing schema and database files - # during normal use, the DB will be in ~/.npbc (where ~ is the user's home directory) and the schema will be bundled with the executable - # during development, the DB and schema will both be in "data" - -DATABASE_DIR = Path().home() / '.npbc' # normal use path -# DATABASE_DIR = Path('data') # development path - -DATABASE_PATH = DATABASE_DIR / 'npbc.db' - -SCHEMA_PATH = Path(__file__).parent / 'schema.sql' # normal use path -# SCHEMA_PATH = DATABASE_DIR / 'schema.sql' # development path - - -## list constant for names of weekdays -WEEKDAY_NAMES = list(weekday_names_iterable) - - -## regex for validating user input -VALIDATE_REGEX = { - # match for a list of comma separated values. each value must be/contain digits, or letters, or hyphens. spaces are allowed between values and commas. any number of values are allowed, but at least one must be present. - 'CSVs': compile_regex(r'^[-\w]+( *, *[-\w]+)*( *,)?$'), - - # match for a single number. must be one or two digits - 'number': compile_regex(r'^[\d]{1,2}?$'), - - # match for a range of numbers. each number must be one or two digits. numbers are separated by a hyphen. spaces are allowed between numbers and the hyphen. - 'range': compile_regex(r'^\d{1,2} *- *\d{1,2}$'), - - # match for weekday name. day must appear as "daynames" (example: "mondays"). all lowercase. - 'days': compile_regex(f"^{'|'.join([day_name.lower() + 's' for day_name in WEEKDAY_NAMES])}$"), - - # match for nth weekday name. day must appear as "n-dayname" (example: "1-monday"). all lowercase. must be one digit. - 'n-day': compile_regex(f"^\\d *- *({'|'.join([day_name.lower() for day_name in WEEKDAY_NAMES])})$"), - - # match for real values, delimited by semicolons. each value must be either an integer or a float with a decimal point. spaces are allowed between values and semicolons, and up to 7 (but at least 1) values are allowed. - 'costs': compile_regex(r'^\d+(\.\d+)?( *; *\d+(\.\d+)?){0,6} *;?$'), - - # match for seven values, each of which must be a 'Y' or an 'N'. there are no delimiters. - 'delivery': compile_regex(r'^[YN]{7}$') -} - -## regex for splitting strings -SPLIT_REGEX = { - # split on hyphens. spaces are allowed between hyphens and values. - 'hyphen': compile_regex(r' *- *'), - - # split on semicolons. spaces are allowed between hyphens and values. - 'semicolon': compile_regex(r' *; *'), - - # split on commas. spaces are allowed between commas and values. - 'comma': compile_regex(r' *, *') -} - - -## ensure DB exists and it's set up with the schema -def setup_and_connect_DB() -> None: - DATABASE_DIR.mkdir(parents=True, exist_ok=True) - DATABASE_PATH.touch(exist_ok=True) - - with connect(DATABASE_PATH) as connection: - connection.executescript(SCHEMA_PATH.read_text()) - connection.commit() - - -## generate a "SELECT" SQL query - # use params to specify columns to select, and "WHERE" conditions -def generate_sql_query(table_name: str, conditions: dict[str, int | str] | None = None, columns: list[str] | None = None) -> str: - sql_query = f"SELECT" - - if columns: - sql_query += f" {', '.join(columns)}" - - else: - sql_query += f" *" - - sql_query += f" FROM {table_name}" - - if conditions: - conditions_segment = ' AND '.join([ - f"{parameter_name} = {parameter_value}" - for parameter_name, parameter_value in conditions.items() - ]) - - sql_query += f" WHERE {conditions_segment}" - - return f"{sql_query};" - - -## execute a "SELECT" SQL query and return the results -def query_database(query: str) -> list[tuple]: - with connect(DATABASE_PATH) as connection: - return connection.execute(query).fetchall() - - return [] - - -## generate a list of number of times each weekday occurs in a given month - # the list will be in the same order as WEEKDAY_NAMES (so the first day should be Monday) -def get_number_of_days_per_week(month: int, year: int) -> list[int]: - main_calendar = monthcalendar(year, month) - number_of_weeks = len(main_calendar) - number_of_weekdays = [] - - for i, _ in enumerate(WEEKDAY_NAMES): - number_of_weekday = number_of_weeks - - if main_calendar[0][i] == 0: - number_of_weekday -= 1 - - if main_calendar[-1][i] == 0: - number_of_weekday -= 1 - - number_of_weekdays.append(number_of_weekday) - - return number_of_weekdays - - -## validate a string that specifies when a given paper was not delivered - # first check to see that it meets the comma-separated requirements - # then check against each of the other acceptable patterns in the regex dictionary -def validate_undelivered_string(string: str) -> bool: - if VALIDATE_REGEX['CSVs'].match(string): - - for section in SPLIT_REGEX['comma'].split(string.rstrip(',')): - section_validity = False - - for pattern, regex in VALIDATE_REGEX.items(): - if (not section_validity) and (pattern not in ["CSVs", "costs", "delivery"]) and (regex.match(section)): - section_validity = True - - if not section_validity: - return False - - return True - - return False - - -## parse a string that specifies when a given paper was not delivered - # each CSV section states some set of dates - # this function will return a set of dates that uniquely identifies each date mentioned across all the CSVs -def parse_undelivered_string(string: str, month: int, year: int) -> set[date_type]: - dates = set() - - for section in SPLIT_REGEX['comma'].split(string.rstrip(',')): - - # if the date is simply a number, it's a single day. so we just identify that date - if VALIDATE_REGEX['number'].match(section): - date = int(section) - - if date > 0 and date <= monthrange(year, month)[1]: - dates.add(date_type(year, month, date)) - - # if the date is a range of numbers, it's a range of days. we identify all the dates in that range, bounds inclusive - elif VALIDATE_REGEX['range'].match(section): - start, end = [int(date) for date in SPLIT_REGEX['hyphen'].split(section)] - - if (0 < start) and (start <= end) and (end <= monthrange(year, month)[1]): - dates.update( - date_type(year, month, day) - for day in range(start, end + 1) - ) - - # if the date is the plural of a weekday name, we identify all dates in that month which are the given weekday - elif VALIDATE_REGEX['days'].match(section): - weekday = WEEKDAY_NAMES.index(section.capitalize().rstrip('s')) - - dates.update( - date_type(year, month, day) - for day in range(1, monthrange(year, month)[1] + 1) - if date_type(year, month, day).weekday() == weekday - ) - - # if the date is a number and a weekday name (singular), we identify the date that is the nth occurrence of the given weekday in the month - elif VALIDATE_REGEX['n-day'].match(section): - n, weekday = SPLIT_REGEX['hyphen'].split(section) - - n = int(n) - - if n > 0 and n <= get_number_of_days_per_week(month, year)[WEEKDAY_NAMES.index(weekday.capitalize())]: - weekday = WEEKDAY_NAMES.index(weekday.capitalize()) - - valid_dates = [ - date_type(year, month, day) - for day in range(1, monthrange(year, month)[1] + 1) - if date_type(year, month, day).weekday() == weekday - ] - - dates.add(valid_dates[n - 1]) - - # bug report :) - else: - print("Congratulations! You broke the program!") - print("You managed to write a string that the program considers valid, but isn't actually.") - print("Please report it to the developer.") - print(f"\nThe string you wrote was: {string}") - print("This data has not been counted.") - - return dates - - -## get the cost and delivery data for a given paper from the DB - # each of them are converted to a dictionary, whose index is the day_id - # the two dictionaries are then returned as a tuple -def get_cost_and_delivery_data(paper_id: int) -> tuple[dict[int, float], dict[int, bool]]: - cost_query = generate_sql_query( - 'papers_days_cost', - columns=['day_id', 'cost'], - conditions={'paper_id': paper_id} - ) - - delivery_query = generate_sql_query( - 'papers_days_delivered', - columns=['day_id', 'delivered'], - conditions={'paper_id': paper_id} - ) - - with connect(DATABASE_PATH) as connection: - cost_tuple = connection.execute(cost_query).fetchall() - delivery_tuple = connection.execute(delivery_query).fetchall() - - cost_dict = { - day_id: cost - for day_id, cost in cost_tuple # type: ignore - } - - delivery_dict = { - day_id: delivery - for day_id, delivery in delivery_tuple # type: ignore - } - - return cost_dict, delivery_dict - - -## calculate the cost of one paper for the full month - # any dates when it was not delivered will be removed -def calculate_cost_of_one_paper(number_of_days_per_week: list[int], undelivered_dates: set[date_type], cost_and_delivered_data: tuple[dict[int, float], dict[int, bool]]) -> float: - cost_data, delivered_data = cost_and_delivered_data - - # initialize counters corresponding to each weekday when the paper was not delivered - number_of_days_per_week_not_received = [0] * len(number_of_days_per_week) - - # for each date that the paper was not delivered, we increment the counter for the corresponding weekday - for date in undelivered_dates: - number_of_days_per_week_not_received[date.weekday()] += 1 - - # calculate the total number of each weekday the paper was delivered (if it is supposed to be delivered) - number_of_days_delivered = [ - number_of_days_per_week[day_id] - number_of_days_per_week_not_received[day_id] if delivered else 0 - for day_id, delivered in delivered_data.items() - ] - - # calculate the total cost of the paper for the month - return sum( - cost * number_of_days_delivered[day_id] - for day_id, cost in cost_data.items() - ) - - -## calculate the cost of all papers for the full month - # return data about the cost of each paper, the total cost, and dates when each paper was not delivered -def calculate_cost_of_all_papers(undelivered_strings: dict[int, str], month: int, year: int) -> tuple[dict[int, float], float, dict[int, set[date_type]]]: - NUMBER_OF_DAYS_PER_WEEK = get_number_of_days_per_week(month, year) - - # get the IDs of papers that exist - with connect(DATABASE_PATH) as connection: - papers = connection.execute( - generate_sql_query( - 'papers', - columns=['paper_id'] - ) - ).fetchall() - - # get the data about cost and delivery for each paper - cost_and_delivery_data = [ - get_cost_and_delivery_data(paper_id) - for paper_id, in papers # type: ignore - ] - - # initialize a "blank" dictionary that will eventually contain any dates when a paper was not delivered - undelivered_dates: dict[int, set[date_type]] = { - paper_id: {} - for paper_id, in papers # type: ignore - } - - # calculate the undelivered dates for each paper - for paper_id, undelivered_string in undelivered_strings.items(): # type: ignore - undelivered_dates[paper_id] = parse_undelivered_string(undelivered_string, month, year) - - # calculate the cost of each paper - costs = { - paper_id: calculate_cost_of_one_paper( - NUMBER_OF_DAYS_PER_WEEK, - undelivered_dates[paper_id], - cost_and_delivery_data[index] - ) - for index, (paper_id,) in enumerate(papers) # type: ignore - } - - # calculate the total cost of all papers - total = sum(costs.values()) - - return costs, total, undelivered_dates - - -## save the results of undelivered dates to the DB - # save the dates any paper was not delivered -def save_results(undelivered_dates: dict[int, set[date_type]], month: int, year: int) -> None: - TIMESTAMP = datetime.now().strftime(r'%d/%m/%Y %I:%M:%S %p') - - with connect(DATABASE_PATH) as connection: - for paper_id, undelivered_date_instances in undelivered_dates.items(): - connection.execute( - "INSERT INTO undelivered_dates (timestamp, month, year, paper_id, dates) VALUES (?, ?, ?, ?, ?);", - ( - TIMESTAMP, - month, - year, - paper_id, - ','.join([ - undelivered_date_instance.strftime(r'%d') - for undelivered_date_instance in undelivered_date_instances - ]) - ) - ) - - -## format the output of calculating the cost of all papers -def format_output(costs: dict[int, float], total: float, month: int, year: int) -> str: - papers = { - paper_id: name - for paper_id, name in query_database( - generate_sql_query('papers') - ) - } - - - format_string = f"For {date_type(year=year, month=month, day=1).strftime(r'%B %Y')}\n\n" - format_string += f"*TOTAL*: {total}\n" - - format_string += '\n'.join([ - f"{papers[paper_id]}: {cost}" # type: ignore - for paper_id, cost in costs.items() - ]) - - return f"{format_string}\n" - - -## add a new paper - # do not allow if the paper already exists -def add_new_paper(name: str, days_delivered: list[bool], days_cost: list[float]) -> tuple[bool, str]: - with connect(DATABASE_PATH) as connection: - - # get the names of all papers that already exist - paper = connection.execute( - generate_sql_query('papers', columns=['name'], conditions={'name': f"\"{name}\""}) - ).fetchall() - - # if the proposed paper already exists, return an error message - if paper: - return False, "Paper already exists. Please try editing the paper instead." - - # otherwise, add the paper name to the database - connection.execute( - "INSERT INTO papers (name) VALUES (?);", - (name, ) - ) - - # get the ID of the paper that was just added - paper_id = connection.execute( - "SELECT paper_id FROM papers WHERE name = ?;", - (name, ) - ).fetchone()[0] - - # add the cost and delivery data for the paper - for day_id, (cost, delivered) in enumerate(zip(days_cost, days_delivered)): - connection.execute( - "INSERT INTO papers_days_cost (paper_id, day_id, cost) VALUES (?, ?, ?);", - (paper_id, day_id, cost) - ) - connection.execute( - "INSERT INTO papers_days_delivered (paper_id, day_id, delivered) VALUES (?, ?, ?);", - (paper_id, day_id, delivered) - ) - - connection.commit() - - return True, f"Paper {name} added." - - return False, "Something went wrong." - - -## edit an existing paper - # do not allow if the paper does not exist -def edit_existing_paper(paper_id: int, name: str | None = None, days_delivered: list[bool] | None = None, days_cost: list[float] | None = None) -> tuple[bool, str]: - with connect(DATABASE_PATH) as connection: - - # get the IDs of all papers that already exist - paper = connection.execute( - generate_sql_query('papers', columns=['paper_id'], conditions={'paper_id': paper_id}) - ).fetchone() - - # if the proposed paper does not exist, return an error message - if not paper: - return False, f"Paper {paper_id} does not exist. Please try adding it instead." - - # if a name is proposed, update the name of the paper - if name is not None: - connection.execute( - "UPDATE papers SET name = ? WHERE paper_id = ?;", - (name, paper_id) - ) - - # if delivery data is proposed, update the delivery data of the paper - if days_delivered is not None: - for day_id, delivered in enumerate(days_delivered): - connection.execute( - "UPDATE papers_days_delivered SET delivered = ? WHERE paper_id = ? AND day_id = ?;", - (delivered, paper_id, day_id) - ) - - # if cost data is proposed, update the cost data of the paper - if days_cost is not None: - for day_id, cost in enumerate(days_cost): - connection.execute( - "UPDATE papers_days_cost SET cost = ? WHERE paper_id = ? AND day_id = ?;", - (cost, paper_id, day_id) - ) - - connection.commit() - - return True, f"Paper {paper_id} edited." - - return False, "Something went wrong." - - -## delete an existing paper - # do not allow if the paper does not exist -def delete_existing_paper(paper_id: int) -> tuple[bool, str]: - with connect(DATABASE_PATH) as connection: - - # get the IDs of all papers that already exist - paper = connection.execute( - generate_sql_query('papers', columns=['paper_id'], conditions={'paper_id': paper_id}) - ).fetchone() - - # if the proposed paper does not exist, return an error message - if not paper: - return False, f"Paper {paper_id} does not exist. Please try adding it instead." - - # delete the paper from the names table - connection.execute( - "DELETE FROM papers WHERE paper_id = ?;", - (paper_id, ) - ) - - # delete the paper from the delivery data table - connection.execute( - "DELETE FROM papers_days_delivered WHERE paper_id = ?;", - (paper_id, ) - ) - - # delete the paper from the cost data table - connection.execute( - "DELETE FROM papers_days_cost WHERE paper_id = ?;", - (paper_id, ) - ) - - connection.commit() - - return True, f"Paper {paper_id} deleted." - - return False, "Something went wrong." - - -## record strings for date(s) paper(s) were not delivered -def add_undelivered_string(paper_id: int, undelivered_string: str, month: int, year: int) -> tuple[bool, str]: - - # if the string is not valid, return an error message - if not validate_undelivered_string(undelivered_string): - return False, f"Invalid undelivered string." - - with connect(DATABASE_PATH) as connection: - # check if given paper exists - paper = connection.execute( - generate_sql_query( - 'papers', - columns=['paper_id'], - conditions={'paper_id': paper_id} - ) - ).fetchone() - - # if the paper does not exist, return an error message - if not paper: - return False, f"Paper {paper_id} does not exist. Please try adding it instead." - - # check if a string with the same month and year, for the same paper, already exists - existing_string = connection.execute( - generate_sql_query( - 'undelivered_strings', - columns=['string'], - conditions={ - 'paper_id': paper_id, - 'month': month, - 'year': year - } - ) - ).fetchone() - - # if a string with the same month and year, for the same paper, already exists, concatenate the new string to it - if existing_string: - new_string = f"{existing_string[0]},{undelivered_string}" - - connection.execute( - "UPDATE undelivered_strings SET string = ? WHERE paper_id = ? AND month = ? AND year = ?;", - (new_string, paper_id, month, year) - ) - - # otherwise, add the new string to the database - else: - connection.execute( - "INSERT INTO undelivered_strings (string, paper_id, month, year) VALUES (?, ?, ?, ?);", - (undelivered_string, paper_id, month, year) - ) - - connection.commit() - - return True, f"Undelivered string added." - - -## delete an existing undelivered string - # do not allow if the string does not exist -def delete_undelivered_string(paper_id: int, month: int, year: int) -> tuple[bool, str]: - with connect(DATABASE_PATH) as connection: - - # check if a string with the same month and year, for the same paper, exists - existing_string = connection.execute( - generate_sql_query( - 'undelivered_strings', - columns=['string'], - conditions={ - 'paper_id': paper_id, - 'month': month, - 'year': year - } - ) - ).fetchone() - - # if it does, delete it - if existing_string: - connection.execute( - "DELETE FROM undelivered_strings WHERE paper_id = ? AND month = ? AND year = ?;", - (paper_id, month, year) - ) - - connection.commit() - - return True, f"Undelivered string deleted." - - # if the string does not exist, return an error message - return False, f"Undelivered string does not exist." - - return False, "Something went wrong." - - -## get the previous month, by looking at 1 day before the first day of the current month (duh) -def get_previous_month() -> date_type: - return (datetime.today().replace(day=1) - timedelta(days=1)).replace(day=1) - - -## extract delivery days and costs from user input -def extract_days_and_costs(days_delivered: str | None, prices: str | None, paper_id: int | None = None) -> tuple[list[bool], list[float]]: - days = [] - costs = [] - - # if the user has provided delivery days, extract them - if days_delivered is not None: - days = [ - bool(int(day == 'Y')) for day in str(days_delivered).upper() - ] - - # if the user has not provided delivery days, fetch them from the database - else: - if isinstance(paper_id, int): - days = [ - (int(day_id), bool(delivered)) - for day_id, delivered in query_database( - generate_sql_query( - 'papers_days_delivered', - columns=['day_id', 'delivered'], - conditions={ - 'paper_id': paper_id - } - ) - ) - ] - - days.sort(key=lambda x: x[0]) - - days = [delivered for _, delivered in days] - - # if the user has provided prices, extract them - if prices is not None: - - costs = [] - encoded_prices = [float(price) for price in SPLIT_REGEX['semicolon'].split(prices.rstrip(';')) if float(price) > 0] - - day_count = -1 - for day in days: - if day: - day_count += 1 - cost = encoded_prices[day_count] - - else: - cost = 0 - - costs.append(cost) - - return days, costs - -## validate month and year -def validate_month_and_year(month: int | None = None, year: int | None = None) -> tuple[bool, str]: - if ((month is None) or (isinstance(month, int) and (0 < month) and (month <= 12))) and ((year is None) or (isinstance(year, int) and (year >= 0))): - return True, "" - - return False, "Invalid month and/or year." \ No newline at end of file diff --git a/test_core.py b/test_core.py deleted file mode 100644 index 53a29b7..0000000 --- a/test_core.py +++ /dev/null @@ -1,564 +0,0 @@ -from datetime import date as date_type - -from npbc_core import (SPLIT_REGEX, VALIDATE_REGEX, - calculate_cost_of_one_paper, extract_days_and_costs, - generate_sql_query, get_number_of_days_per_week, - parse_undelivered_string, validate_month_and_year) - - -def test_regex_number(): - assert VALIDATE_REGEX['number'].match('') is None - assert VALIDATE_REGEX['number'].match('1') is not None - assert VALIDATE_REGEX['number'].match('1 2') is None - assert VALIDATE_REGEX['number'].match('1-2') is None - assert VALIDATE_REGEX['number'].match('11') is not None - assert VALIDATE_REGEX['number'].match('11-12') is None - assert VALIDATE_REGEX['number'].match('11-12,13') is None - assert VALIDATE_REGEX['number'].match('11-12,13-14') is None - assert VALIDATE_REGEX['number'].match('111') is None - assert VALIDATE_REGEX['number'].match('a') is None - assert VALIDATE_REGEX['number'].match('1a') is None - assert VALIDATE_REGEX['number'].match('1a2') is None - assert VALIDATE_REGEX['number'].match('12b') is None - -def test_regex_range(): - assert VALIDATE_REGEX['range'].match('') is None - assert VALIDATE_REGEX['range'].match('1') is None - assert VALIDATE_REGEX['range'].match('1 2') is None - assert VALIDATE_REGEX['range'].match('1-2') is not None - assert VALIDATE_REGEX['range'].match('11') is None - assert VALIDATE_REGEX['range'].match('11-') is None - assert VALIDATE_REGEX['range'].match('11-12') is not None - assert VALIDATE_REGEX['range'].match('11-12-1') is None - assert VALIDATE_REGEX['range'].match('11 -12') is not None - assert VALIDATE_REGEX['range'].match('11 - 12') is not None - assert VALIDATE_REGEX['range'].match('11- 12') is not None - assert VALIDATE_REGEX['range'].match('11-2') is not None - assert VALIDATE_REGEX['range'].match('11-12,13') is None - assert VALIDATE_REGEX['range'].match('11-12,13-14') is None - assert VALIDATE_REGEX['range'].match('111') is None - assert VALIDATE_REGEX['range'].match('a') is None - assert VALIDATE_REGEX['range'].match('1a') is None - assert VALIDATE_REGEX['range'].match('1a2') is None - assert VALIDATE_REGEX['range'].match('12b') is None - assert VALIDATE_REGEX['range'].match('11-a') is None - assert VALIDATE_REGEX['range'].match('11-12a') is None - -def test_regex_CSVs(): - assert VALIDATE_REGEX['CSVs'].match('') is None - assert VALIDATE_REGEX['CSVs'].match('1') is not None - assert VALIDATE_REGEX['CSVs'].match('a') is not None - assert VALIDATE_REGEX['CSVs'].match('adcef') is not None - assert VALIDATE_REGEX['CSVs'].match('-') is not None - assert VALIDATE_REGEX['CSVs'].match(' ') is None - assert VALIDATE_REGEX['CSVs'].match('1,2') is not None - assert VALIDATE_REGEX['CSVs'].match('1-3') is not None - assert VALIDATE_REGEX['CSVs'].match('monday') is not None - assert VALIDATE_REGEX['CSVs'].match('monday,tuesday') is not None - assert VALIDATE_REGEX['CSVs'].match('mondays') is not None - assert VALIDATE_REGEX['CSVs'].match('tuesdays') is not None - assert VALIDATE_REGEX['CSVs'].match('1,2,3') is not None - assert VALIDATE_REGEX['CSVs'].match('1-3') is not None - assert VALIDATE_REGEX['CSVs'].match('monday,tuesday') is not None - assert VALIDATE_REGEX['CSVs'].match('mondays,tuesdays') is not None - assert VALIDATE_REGEX['CSVs'].match(';') is None - assert VALIDATE_REGEX['CSVs'].match(':') is None - assert VALIDATE_REGEX['CSVs'].match(':') is None - assert VALIDATE_REGEX['CSVs'].match('!') is None - assert VALIDATE_REGEX['CSVs'].match('1,2,3,4') is not None - -def test_regex_days(): - assert VALIDATE_REGEX['days'].match('') is None - assert VALIDATE_REGEX['days'].match('1') is None - assert VALIDATE_REGEX['days'].match('1,2') is None - assert VALIDATE_REGEX['days'].match('1-3') is None - assert VALIDATE_REGEX['days'].match('monday') is None - assert VALIDATE_REGEX['days'].match('monday,tuesday') is None - assert VALIDATE_REGEX['days'].match('mondays') is not None - assert VALIDATE_REGEX['days'].match('tuesdays') is not None - -def test_regex_n_days(): - assert VALIDATE_REGEX['n-day'].match('') is None - assert VALIDATE_REGEX['n-day'].match('1') is None - assert VALIDATE_REGEX['n-day'].match('1-') is None - assert VALIDATE_REGEX['n-day'].match('1,2') is None - assert VALIDATE_REGEX['n-day'].match('1-3') is None - assert VALIDATE_REGEX['n-day'].match('monday') is None - assert VALIDATE_REGEX['n-day'].match('monday,tuesday') is None - assert VALIDATE_REGEX['n-day'].match('mondays') is None - assert VALIDATE_REGEX['n-day'].match('1-tuesday') is not None - assert VALIDATE_REGEX['n-day'].match('11-tuesday') is None - assert VALIDATE_REGEX['n-day'].match('111-tuesday') is None - assert VALIDATE_REGEX['n-day'].match('11-tuesdays') is None - assert VALIDATE_REGEX['n-day'].match('1 -tuesday') is not None - assert VALIDATE_REGEX['n-day'].match('1- tuesday') is not None - assert VALIDATE_REGEX['n-day'].match('1 - tuesday') is not None - -def test_regex_costs(): - assert VALIDATE_REGEX['costs'].match('') is None - assert VALIDATE_REGEX['costs'].match('a') is None - assert VALIDATE_REGEX['costs'].match('1') is not None - assert VALIDATE_REGEX['costs'].match('1.') is None - assert VALIDATE_REGEX['costs'].match('1.5') is not None - assert VALIDATE_REGEX['costs'].match('1.0') is not None - assert VALIDATE_REGEX['costs'].match('16.0') is not None - assert VALIDATE_REGEX['costs'].match('16.06') is not None - assert VALIDATE_REGEX['costs'].match('1;2') is not None - assert VALIDATE_REGEX['costs'].match('1 ;2') is not None - assert VALIDATE_REGEX['costs'].match('1; 2') is not None - assert VALIDATE_REGEX['costs'].match('1 ; 2') is not None - assert VALIDATE_REGEX['costs'].match('1;2;') is not None - assert VALIDATE_REGEX['costs'].match('1;2 ;') is not None - assert VALIDATE_REGEX['costs'].match('1:2') is None - assert VALIDATE_REGEX['costs'].match('1,2') is None - assert VALIDATE_REGEX['costs'].match('1-2') is None - assert VALIDATE_REGEX['costs'].match('1;2;3') is not None - assert VALIDATE_REGEX['costs'].match('1;2;3;4') is not None - assert VALIDATE_REGEX['costs'].match('1;2;3;4;5') is not None - assert VALIDATE_REGEX['costs'].match('1;2;3;4;5;6') is not None - assert VALIDATE_REGEX['costs'].match('1;2;3;4;5;6;7;') is not None - assert VALIDATE_REGEX['costs'].match('1;2;3;4;5;6;7') is not None - assert VALIDATE_REGEX['costs'].match('1;2;3;4;5;6;7;8') is None - -def test_delivery_regex(): - assert VALIDATE_REGEX['delivery'].match('') is None - assert VALIDATE_REGEX['delivery'].match('a') is None - assert VALIDATE_REGEX['delivery'].match('1') is None - assert VALIDATE_REGEX['delivery'].match('1.') is None - assert VALIDATE_REGEX['delivery'].match('1.5') is None - assert VALIDATE_REGEX['delivery'].match('1,2') is None - assert VALIDATE_REGEX['delivery'].match('1-2') is None - assert VALIDATE_REGEX['delivery'].match('1;2') is None - assert VALIDATE_REGEX['delivery'].match('1:2') is None - assert VALIDATE_REGEX['delivery'].match('1,2,3') is None - assert VALIDATE_REGEX['delivery'].match('Y') is None - assert VALIDATE_REGEX['delivery'].match('N') is None - assert VALIDATE_REGEX['delivery'].match('YY') is None - assert VALIDATE_REGEX['delivery'].match('YYY') is None - assert VALIDATE_REGEX['delivery'].match('YYYY') is None - assert VALIDATE_REGEX['delivery'].match('YYYYY') is None - assert VALIDATE_REGEX['delivery'].match('YYYYYY') is None - assert VALIDATE_REGEX['delivery'].match('YYYYYYY') is not None - assert VALIDATE_REGEX['delivery'].match('YYYYYYYY') is None - assert VALIDATE_REGEX['delivery'].match('NNNNNNN') is not None - assert VALIDATE_REGEX['delivery'].match('NYNNNNN') is not None - assert VALIDATE_REGEX['delivery'].match('NYYYYNN') is not None - assert VALIDATE_REGEX['delivery'].match('NYYYYYY') is not None - assert VALIDATE_REGEX['delivery'].match('NYYYYYYY') is None - assert VALIDATE_REGEX['delivery'].match('N,N,N,N,N,N,N') is None - assert VALIDATE_REGEX['delivery'].match('N;N;N;N;N;N;N') is None - assert VALIDATE_REGEX['delivery'].match('N-N-N-N-N-N-N') is None - assert VALIDATE_REGEX['delivery'].match('N N N N N N N') is None - assert VALIDATE_REGEX['delivery'].match('YYYYYYy') is None - assert VALIDATE_REGEX['delivery'].match('YYYYYYn') is None - - - -def test_regex_hyphen(): - assert SPLIT_REGEX['hyphen'].split('1-2') == ['1', '2'] - assert SPLIT_REGEX['hyphen'].split('1-2-3') == ['1', '2', '3'] - assert SPLIT_REGEX['hyphen'].split('1 -2-3') == ['1', '2', '3'] - assert SPLIT_REGEX['hyphen'].split('1 - 2-3') == ['1', '2', '3'] - assert SPLIT_REGEX['hyphen'].split('1- 2-3') == ['1', '2', '3'] - assert SPLIT_REGEX['hyphen'].split('1') == ['1'] - assert SPLIT_REGEX['hyphen'].split('1-') == ['1', ''] - assert SPLIT_REGEX['hyphen'].split('1-2-') == ['1', '2', ''] - assert SPLIT_REGEX['hyphen'].split('1-2-3-') == ['1', '2', '3', ''] - assert SPLIT_REGEX['hyphen'].split('1,2-3') == ['1,2', '3'] - assert SPLIT_REGEX['hyphen'].split('1,2-3-') == ['1,2', '3', ''] - assert SPLIT_REGEX['hyphen'].split('1,2, 3,') == ['1,2, 3,'] - assert SPLIT_REGEX['hyphen'].split('') == [''] - -def test_regex_comma(): - assert SPLIT_REGEX['comma'].split('1,2') == ['1', '2'] - assert SPLIT_REGEX['comma'].split('1,2,3') == ['1', '2', '3'] - assert SPLIT_REGEX['comma'].split('1 ,2,3') == ['1', '2', '3'] - assert SPLIT_REGEX['comma'].split('1 , 2,3') == ['1', '2', '3'] - assert SPLIT_REGEX['comma'].split('1, 2,3') == ['1', '2', '3'] - assert SPLIT_REGEX['comma'].split('1') == ['1'] - assert SPLIT_REGEX['comma'].split('1,') == ['1', ''] - assert SPLIT_REGEX['comma'].split('1, ') == ['1', ''] - assert SPLIT_REGEX['comma'].split('1,2,') == ['1', '2', ''] - assert SPLIT_REGEX['comma'].split('1,2,3,') == ['1', '2', '3', ''] - assert SPLIT_REGEX['comma'].split('1-2,3') == ['1-2', '3'] - assert SPLIT_REGEX['comma'].split('1-2,3,') == ['1-2', '3', ''] - assert SPLIT_REGEX['comma'].split('1-2-3') == ['1-2-3'] - assert SPLIT_REGEX['comma'].split('1-2- 3') == ['1-2- 3'] - assert SPLIT_REGEX['comma'].split('') == [''] - -def test_regex_semicolon(): - assert SPLIT_REGEX['semicolon'].split('1;2') == ['1', '2'] - assert SPLIT_REGEX['semicolon'].split('1;2;3') == ['1', '2', '3'] - assert SPLIT_REGEX['semicolon'].split('1 ;2;3') == ['1', '2', '3'] - assert SPLIT_REGEX['semicolon'].split('1 ; 2;3') == ['1', '2', '3'] - assert SPLIT_REGEX['semicolon'].split('1; 2;3') == ['1', '2', '3'] - assert SPLIT_REGEX['semicolon'].split('1') == ['1'] - assert SPLIT_REGEX['semicolon'].split('1;') == ['1', ''] - assert SPLIT_REGEX['semicolon'].split('1; ') == ['1', ''] - assert SPLIT_REGEX['semicolon'].split('1;2;') == ['1', '2', ''] - assert SPLIT_REGEX['semicolon'].split('1;2;3;') == ['1', '2', '3', ''] - assert SPLIT_REGEX['semicolon'].split('1-2;3') == ['1-2', '3'] - assert SPLIT_REGEX['semicolon'].split('1-2;3;') == ['1-2', '3', ''] - assert SPLIT_REGEX['semicolon'].split('1-2-3') == ['1-2-3'] - assert SPLIT_REGEX['semicolon'].split('1-2- 3') == ['1-2- 3'] - assert SPLIT_REGEX['semicolon'].split('') == [''] - - -def test_undelivered_string_parsing(): - MONTH = 5 - YEAR = 2017 - - - assert parse_undelivered_string('', MONTH, YEAR) == set([]) - - assert parse_undelivered_string('1', MONTH, YEAR) == set([ - date_type(year=YEAR, month=MONTH, day=1) - ]) - - assert parse_undelivered_string('1-2', MONTH, YEAR) == set([ - date_type(year=YEAR, month=MONTH, day=1), - date_type(year=YEAR, month=MONTH, day=2) - ]) - - assert parse_undelivered_string('5-17', MONTH, YEAR) == set([ - date_type(year=YEAR, month=MONTH, day=5), - date_type(year=YEAR, month=MONTH, day=6), - date_type(year=YEAR, month=MONTH, day=7), - date_type(year=YEAR, month=MONTH, day=8), - date_type(year=YEAR, month=MONTH, day=9), - date_type(year=YEAR, month=MONTH, day=10), - date_type(year=YEAR, month=MONTH, day=11), - date_type(year=YEAR, month=MONTH, day=12), - date_type(year=YEAR, month=MONTH, day=13), - date_type(year=YEAR, month=MONTH, day=14), - date_type(year=YEAR, month=MONTH, day=15), - date_type(year=YEAR, month=MONTH, day=16), - date_type(year=YEAR, month=MONTH, day=17) - ]) - - assert parse_undelivered_string('5-17,19', MONTH, YEAR) == set([ - date_type(year=YEAR, month=MONTH, day=5), - date_type(year=YEAR, month=MONTH, day=6), - date_type(year=YEAR, month=MONTH, day=7), - date_type(year=YEAR, month=MONTH, day=8), - date_type(year=YEAR, month=MONTH, day=9), - date_type(year=YEAR, month=MONTH, day=10), - date_type(year=YEAR, month=MONTH, day=11), - date_type(year=YEAR, month=MONTH, day=12), - date_type(year=YEAR, month=MONTH, day=13), - date_type(year=YEAR, month=MONTH, day=14), - date_type(year=YEAR, month=MONTH, day=15), - date_type(year=YEAR, month=MONTH, day=16), - date_type(year=YEAR, month=MONTH, day=17), - date_type(year=YEAR, month=MONTH, day=19) - ]) - - assert parse_undelivered_string('5-17,19-21', MONTH, YEAR) == set([ - date_type(year=YEAR, month=MONTH, day=5), - date_type(year=YEAR, month=MONTH, day=6), - date_type(year=YEAR, month=MONTH, day=7), - date_type(year=YEAR, month=MONTH, day=8), - date_type(year=YEAR, month=MONTH, day=9), - date_type(year=YEAR, month=MONTH, day=10), - date_type(year=YEAR, month=MONTH, day=11), - date_type(year=YEAR, month=MONTH, day=12), - date_type(year=YEAR, month=MONTH, day=13), - date_type(year=YEAR, month=MONTH, day=14), - date_type(year=YEAR, month=MONTH, day=15), - date_type(year=YEAR, month=MONTH, day=16), - date_type(year=YEAR, month=MONTH, day=17), - date_type(year=YEAR, month=MONTH, day=19), - date_type(year=YEAR, month=MONTH, day=20), - date_type(year=YEAR, month=MONTH, day=21) - ]) - - assert parse_undelivered_string('5-17,19-21,23', MONTH, YEAR) == set([ - date_type(year=YEAR, month=MONTH, day=5), - date_type(year=YEAR, month=MONTH, day=6), - date_type(year=YEAR, month=MONTH, day=7), - date_type(year=YEAR, month=MONTH, day=8), - date_type(year=YEAR, month=MONTH, day=9), - date_type(year=YEAR, month=MONTH, day=10), - date_type(year=YEAR, month=MONTH, day=11), - date_type(year=YEAR, month=MONTH, day=12), - date_type(year=YEAR, month=MONTH, day=13), - date_type(year=YEAR, month=MONTH, day=14), - date_type(year=YEAR, month=MONTH, day=15), - date_type(year=YEAR, month=MONTH, day=16), - date_type(year=YEAR, month=MONTH, day=17), - date_type(year=YEAR, month=MONTH, day=19), - date_type(year=YEAR, month=MONTH, day=20), - date_type(year=YEAR, month=MONTH, day=21), - date_type(year=YEAR, month=MONTH, day=23) - ]) - - assert parse_undelivered_string('mondays', MONTH, YEAR) == set([ - date_type(year=YEAR, month=MONTH, day=1), - date_type(year=YEAR, month=MONTH, day=8), - date_type(year=YEAR, month=MONTH, day=15), - date_type(year=YEAR, month=MONTH, day=22), - date_type(year=YEAR, month=MONTH, day=29) - ]) - - assert parse_undelivered_string('mondays, wednesdays', MONTH, YEAR) == set([ - date_type(year=YEAR, month=MONTH, day=1), - date_type(year=YEAR, month=MONTH, day=8), - date_type(year=YEAR, month=MONTH, day=15), - date_type(year=YEAR, month=MONTH, day=22), - date_type(year=YEAR, month=MONTH, day=29), - date_type(year=YEAR, month=MONTH, day=3), - date_type(year=YEAR, month=MONTH, day=10), - date_type(year=YEAR, month=MONTH, day=17), - date_type(year=YEAR, month=MONTH, day=24), - date_type(year=YEAR, month=MONTH, day=31) - ]) - - assert parse_undelivered_string('2-monday', MONTH, YEAR) == set([ - date_type(year=YEAR, month=MONTH, day=8) - ]) - - assert parse_undelivered_string('2-monday, 3-wednesday', MONTH, YEAR) == set([ - date_type(year=YEAR, month=MONTH, day=8), - date_type(year=YEAR, month=MONTH, day=17) - ]) - - -def test_sql_query(): - assert generate_sql_query( - 'test' - ) == "SELECT * FROM test;" - - assert generate_sql_query( - 'test', - columns=['a'] - ) == "SELECT a FROM test;" - - assert generate_sql_query( - 'test', - columns=['a', 'b'] - ) == "SELECT a, b FROM test;" - - assert generate_sql_query( - 'test', - conditions={'a': '\"b\"'} - ) == "SELECT * FROM test WHERE a = \"b\";" - - assert generate_sql_query( - 'test', - conditions={ - 'a': '\"b\"', - 'c': '\"d\"' - } - ) == "SELECT * FROM test WHERE a = \"b\" AND c = \"d\";" - - assert generate_sql_query( - 'test', - conditions={ - 'a': '\"b\"', - 'c': '\"d\"' - }, - columns=['a', 'b'] - ) == "SELECT a, b FROM test WHERE a = \"b\" AND c = \"d\";" - - -def test_number_of_days_per_week(): - assert get_number_of_days_per_week(1, 2022) == [5, 4, 4, 4, 4, 5, 5] - assert get_number_of_days_per_week(2, 2022) == [4, 4, 4, 4, 4, 4, 4] - assert get_number_of_days_per_week(3, 2022) == [4, 5, 5 ,5, 4, 4, 4] - assert get_number_of_days_per_week(2, 2020) == [4, 4, 4, 4, 4, 5, 4] - assert get_number_of_days_per_week(12, 1954) == [4, 4, 5, 5, 5, 4, 4] - - -def test_calculating_cost_of_one_paper(): - DAYS_PER_WEEK = [5, 4, 4, 4, 4, 5, 5] - COST_PER_DAY: dict[int, float] = { - 0: 0, - 1: 0, - 2: 2, - 3: 2, - 4: 5, - 5: 0, - 6: 1 - } - DELIVERY_DATA: dict[int, bool] = { - 0: False, - 1: False, - 2: True, - 3: True, - 4: True, - 5: False, - 6: True - } - - assert calculate_cost_of_one_paper( - DAYS_PER_WEEK, - set([]), - ( - COST_PER_DAY, - DELIVERY_DATA - ) - ) == 41 - - assert calculate_cost_of_one_paper( - DAYS_PER_WEEK, - set([]), - ( - COST_PER_DAY, - { - 0: False, - 1: False, - 2: True, - 3: True, - 4: True, - 5: False, - 6: False - } - ) - ) == 36 - - assert calculate_cost_of_one_paper( - DAYS_PER_WEEK, - set([ - date_type(year=2022, month=1, day=8) - ]), - ( - COST_PER_DAY, - DELIVERY_DATA - ) - ) == 41 - - assert calculate_cost_of_one_paper( - DAYS_PER_WEEK, - set([ - date_type(year=2022, month=1, day=8), - date_type(year=2022, month=1, day=8) - ]), - ( - COST_PER_DAY, - DELIVERY_DATA - ) - ) == 41 - - assert calculate_cost_of_one_paper( - DAYS_PER_WEEK, - set([ - date_type(year=2022, month=1, day=8), - date_type(year=2022, month=1, day=17) - ]), - ( - COST_PER_DAY, - DELIVERY_DATA - ) - ) == 41 - - assert calculate_cost_of_one_paper( - DAYS_PER_WEEK, - set([ - date_type(year=2022, month=1, day=2) - ]), - ( - COST_PER_DAY, - DELIVERY_DATA - ) - ) == 40 - - assert calculate_cost_of_one_paper( - DAYS_PER_WEEK, - set([ - date_type(year=2022, month=1, day=2), - date_type(year=2022, month=1, day=2) - ]), - ( - COST_PER_DAY, - DELIVERY_DATA - ) - ) == 40 - - assert calculate_cost_of_one_paper( - DAYS_PER_WEEK, - set([ - date_type(year=2022, month=1, day=6), - date_type(year=2022, month=1, day=7) - ]), - ( - COST_PER_DAY, - DELIVERY_DATA - ) - ) == 34 - - assert calculate_cost_of_one_paper( - DAYS_PER_WEEK, - set([ - date_type(year=2022, month=1, day=6), - date_type(year=2022, month=1, day=7), - date_type(year=2022, month=1, day=8) - ]), - ( - COST_PER_DAY, - DELIVERY_DATA - ) - ) == 34 - - assert calculate_cost_of_one_paper( - DAYS_PER_WEEK, - set([ - date_type(year=2022, month=1, day=6), - date_type(year=2022, month=1, day=7), - date_type(year=2022, month=1, day=7), - date_type(year=2022, month=1, day=7), - date_type(year=2022, month=1, day=8), - date_type(year=2022, month=1, day=8), - date_type(year=2022, month=1, day=8) - ]), - ( - COST_PER_DAY, - DELIVERY_DATA - ) - ) == 34 - - -def test_extracting_days_and_costs(): - assert extract_days_and_costs(None, None) == ([], []) - assert extract_days_and_costs('NNNNNNN', None) == ( - [False, False, False, False, False, False, False], - [] - ) - - assert extract_days_and_costs('NNNYNNN', '7') == ( - [False, False, False, True, False, False, False], - [0, 0, 0, 7, 0, 0, 0] - ) - - assert extract_days_and_costs('NNNYNNN', '7;7') == ( - [False, False, False, True, False, False, False], - [0, 0, 0, 7, 0, 0, 0] - ) - - assert extract_days_and_costs('NNNYNNY', '7;4') == ( - [False, False, False, True, False, False, True], - [0, 0, 0, 7, 0, 0, 4] - ) - - assert extract_days_and_costs('NNNYNNY', '7;4.7') == ( - [False, False, False, True, False, False, True], - [0, 0, 0, 7, 0, 0, 4.7] - ) - - -def test_validate_month_and_year(): - assert validate_month_and_year(1, 2020)[0] - assert validate_month_and_year(12, 2020)[0] - assert validate_month_and_year(1, 2021)[0] - assert validate_month_and_year(12, 2021)[0] - assert validate_month_and_year(1, 2022)[0] - assert validate_month_and_year(12, 2022)[0] - assert not validate_month_and_year(-54, 2020)[0] - assert not validate_month_and_year(0, 2020)[0] - assert not validate_month_and_year(13, 2020)[0] - assert not validate_month_and_year(45, 2020)[0] - assert not validate_month_and_year(1, -5)[0] - assert not validate_month_and_year(12, -5)[0] - assert not validate_month_and_year(1.6, 10)[0] # type: ignore - assert not validate_month_and_year(12.6, 10)[0] # type: ignore - assert not validate_month_and_year(1, '10')[0] # type: ignore - assert not validate_month_and_year(12, '10')[0] # type: ignore From cbfbf92e1cc9519e9a5a27e7360475a395330e8f Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Tue, 3 May 2022 16:54:41 +0530 Subject: [PATCH 02/92] begin making schema --- data/schema.sql | 59 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 data/schema.sql diff --git a/data/schema.sql b/data/schema.sql new file mode 100644 index 0000000..cb9222d --- /dev/null +++ b/data/schema.sql @@ -0,0 +1,59 @@ +CREATE TABLE IF NOT EXISTS papers ( + paper_id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + CONSTRAINT unique_paper_name UNIQUE (name) +); + +CREATE TABLE IF NOT EXISTS papers_days ( + paper_day_id INTEGER PRIMARY KEY AUTOINCREMENT, + paper_id INTEGER NOT NULL, + day_id INTEGER NOT NULL, + FOREIGN KEY (paper_id) REFERENCES papers(paper_id), + CONSTRAINT unique_paper_day UNIQUE (paper_id, day_id) +); + +CREATE TABLE IF NOT EXISTS papers_days_delivered ( + papers_days_delivered_id INTEGER PRIMARY KEY AUTOINCREMENT, + paper_day_id INTEGER NOT NULL, + delivered INTEGER NOT NULL CHECK (delivered IN (0, 1)), + FOREIGN KEY (paper_day_id) REFERENCES papers_days(paper_day_id) +); + +CREATE TABLE IF NOT EXISTS papers_days_cost ( + papers_days_cost_id INTEGER PRIMARY KEY AUTOINCREMENT, + paper_day_id INTEGER NOT NULL, + cost INTEGER, + FOREIGN KEY (paper_day_id) REFERENCES papers_days(paper_day_id) +); + +CREATE TABLE IF NOT EXISTS undelivered_strings ( + string_id INTEGER PRIMARY KEY AUTOINCREMENT, + year INTEGER NOT NULL CHECK (year >= 0), + month INTEGER NOT NULL CHECK (month >= 0 AND month <= 12), + paper_id INTEGER NOT NULL, + string TEXT NOT NULL, + FOREIGN KEY (paper_id) REFERENCES papers(paper_id) +); + +CREATE TABLE IF NOT EXISTS logs ( + log_id INTEGER PRIMARY KEY AUTOINCREMENT, + paper_id INTEGER NOT NULL, + entry_timestamp TEXT NOT NULL, + month INTEGER NOT NULL CHECK (month >= 0 AND month <= 12), + year INTEGER NOT NULL CHECK (year >= 0), + FOREIGN KEY (paper_id) REFERENCES papers(paper_id) +); + +CREATE TABLE IF NOT EXISTS undelivered_dates_logs ( + undelivered_dates_log_id INTEGER PRIMARY KEY AUTOINCREMENT, + log_id INTEGER NOT NULL, + date_not_delivered TEXT NOT NULL, + FOREIGN KEY (log_id) REFERENCES logs(log_id) +); + +CREATE TABLE IF NOT EXISTS cost_logs ( + cost_log_id INTEGER PRIMARY KEY AUTOINCREMENT, + log_id INTEGER NOT NULL, + cost INTEGER NOT NULL, + FOREIGN KEY (log_id) REFERENCES logs(log_id) +); \ No newline at end of file From 60c409714c20e570db6ab78feb515d3b1b2b1ea3 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Tue, 3 May 2022 16:59:35 +0530 Subject: [PATCH 03/92] inline foriegn keys --- data/schema.sql | 36 ++++++++++++++---------------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/data/schema.sql b/data/schema.sql index cb9222d..0fab4ae 100644 --- a/data/schema.sql +++ b/data/schema.sql @@ -6,54 +6,46 @@ CREATE TABLE IF NOT EXISTS papers ( CREATE TABLE IF NOT EXISTS papers_days ( paper_day_id INTEGER PRIMARY KEY AUTOINCREMENT, - paper_id INTEGER NOT NULL, - day_id INTEGER NOT NULL, - FOREIGN KEY (paper_id) REFERENCES papers(paper_id), - CONSTRAINT unique_paper_day UNIQUE (paper_id, day_id) + paper_id INTEGER NOT NULL REFERENCES papers(paper_id), + day_id INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS papers_days_delivered ( papers_days_delivered_id INTEGER PRIMARY KEY AUTOINCREMENT, - paper_day_id INTEGER NOT NULL, - delivered INTEGER NOT NULL CHECK (delivered IN (0, 1)), - FOREIGN KEY (paper_day_id) REFERENCES papers_days(paper_day_id) + paper_day_id INTEGER NOT NULL REFERENCES papers_days(paper_day_id), + delivered INTEGER NOT NULL CHECK (delivered IN (0, 1)) ); CREATE TABLE IF NOT EXISTS papers_days_cost ( papers_days_cost_id INTEGER PRIMARY KEY AUTOINCREMENT, - paper_day_id INTEGER NOT NULL, - cost INTEGER, - FOREIGN KEY (paper_day_id) REFERENCES papers_days(paper_day_id) + paper_day_id INTEGER NOT NULL REFERENCES papers_days(paper_day_id), + cost INTEGER ); CREATE TABLE IF NOT EXISTS undelivered_strings ( string_id INTEGER PRIMARY KEY AUTOINCREMENT, year INTEGER NOT NULL CHECK (year >= 0), month INTEGER NOT NULL CHECK (month >= 0 AND month <= 12), - paper_id INTEGER NOT NULL, - string TEXT NOT NULL, - FOREIGN KEY (paper_id) REFERENCES papers(paper_id) + paper_id INTEGER NOT NULL REFERENCES papers(paper_id), + string TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS logs ( log_id INTEGER PRIMARY KEY AUTOINCREMENT, - paper_id INTEGER NOT NULL, + paper_id INTEGER NOT NULL REFERENCES papers(paper_id), entry_timestamp TEXT NOT NULL, month INTEGER NOT NULL CHECK (month >= 0 AND month <= 12), - year INTEGER NOT NULL CHECK (year >= 0), - FOREIGN KEY (paper_id) REFERENCES papers(paper_id) + year INTEGER NOT NULL CHECK (year >= 0) ); CREATE TABLE IF NOT EXISTS undelivered_dates_logs ( undelivered_dates_log_id INTEGER PRIMARY KEY AUTOINCREMENT, - log_id INTEGER NOT NULL, - date_not_delivered TEXT NOT NULL, - FOREIGN KEY (log_id) REFERENCES logs(log_id) + log_id INTEGER NOT NULL REFERENCES logs(log_id), + date_not_delivered TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS cost_logs ( cost_log_id INTEGER PRIMARY KEY AUTOINCREMENT, - log_id INTEGER NOT NULL, - cost INTEGER NOT NULL, - FOREIGN KEY (log_id) REFERENCES logs(log_id) + log_id INTEGER NOT NULL REFERENCES logs(log_id), + cost INTEGER NOT NULL ); \ No newline at end of file From 2180207bba5ece0e0493241b5b0afdba79b54dbc Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Tue, 3 May 2022 17:29:25 +0530 Subject: [PATCH 04/92] add constraints --- data/schema.sql | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/data/schema.sql b/data/schema.sql index 0fab4ae..c1af73d 100644 --- a/data/schema.sql +++ b/data/schema.sql @@ -7,7 +7,8 @@ CREATE TABLE IF NOT EXISTS papers ( CREATE TABLE IF NOT EXISTS papers_days ( paper_day_id INTEGER PRIMARY KEY AUTOINCREMENT, paper_id INTEGER NOT NULL REFERENCES papers(paper_id), - day_id INTEGER NOT NULL + day_id INTEGER NOT NULL, + CONSTRAINT unique_paper_day UNIQUE (paper_id, day_id) ); CREATE TABLE IF NOT EXISTS papers_days_delivered ( @@ -35,7 +36,8 @@ CREATE TABLE IF NOT EXISTS logs ( paper_id INTEGER NOT NULL REFERENCES papers(paper_id), entry_timestamp TEXT NOT NULL, month INTEGER NOT NULL CHECK (month >= 0 AND month <= 12), - year INTEGER NOT NULL CHECK (year >= 0) + year INTEGER NOT NULL CHECK (year >= 0), + CONSTRAINT unique_log UNIQUE (entry_timestamp, paper_id, month, year) ); CREATE TABLE IF NOT EXISTS undelivered_dates_logs ( From 889b015a68f087a90d957cc325b16f75af432136 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Wed, 4 May 2022 20:22:25 +0530 Subject: [PATCH 05/92] create regex file --- npbc_regex.py | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 npbc_regex.py diff --git a/npbc_regex.py b/npbc_regex.py new file mode 100644 index 0000000..5a74765 --- /dev/null +++ b/npbc_regex.py @@ -0,0 +1,41 @@ +from calendar import day_name as WEEKDAY_NAMES_ITERABLE +from re import compile as compile_regex, match + + +## regex used to match against strings + +# match for a list of comma separated values. each value must be/contain digits, or letters, or hyphens. spaces are allowed between values and commas. any number of values are allowed, but at least one must be present. +CSV_MATCH_REGEX = compile_regex(r'^[-\w]+( *, *[-\w]+)*( *,)?$') + +# match for a single number. must be one or two digits +NUMBER_MATCH_REGEX = compile_regex(r'^[\d]{1,2}?$') + +# match for a range of numbers. each number must be one or two digits. numbers are separated by a hyphen. spaces are allowed between numbers and the hyphen. +RANGE_MATCH_REGEX = compile_regex(r'^\d{1,2} *- *\d{1,2}$') + +# match for weekday name. day must appear as "daynames" (example = "mondays"). all lowercase. +DAYS_MATCH_REGEX = compile_regex(f"^{'|'.join([day_name.lower() + 's' for day_name in WEEKDAY_NAMES_ITERABLE])}$") + +# match for nth weekday name. day must appear as "n-dayname" (example = "1-monday"). all lowercase. must be one digit. +N_DAY_MATCH_REGEX = compile_regex(f"^\\d *- *({'|'.join([day_name.lower() for day_name in WEEKDAY_NAMES_ITERABLE])})$") + +# match for the text "all" in any case. +ALL_MATCH_REGEX = compile_regex(r'^[aA][lL]{2}$') + +# match for real values, delimited by semicolons. each value must be either an integer or a float with a decimal point. spaces are allowed between values and semicolons, and up to 7 (but at least 1) values are allowed. +COSTS_MATCH_REGEX = compile_regex(r'^\d+(\.\d+)?( *; *\d+(\.\d+)?){0,6} *;?$') + +# match for seven values, each of which must be a 'Y' or an 'N'. there are no delimiters. +DELIVERY_MATCH_REGEX = compile_regex(r'^[YN]{7}$') + + +## regex used to split strings + +# split on hyphens. spaces are allowed between hyphens and values. +HYPHEN_SPLIT_REGEX = compile_regex(r' *- *') + +# split on semicolons. spaces are allowed between hyphens and values. +SEMICOLON_SPLIT_REGEX = compile_regex(r' *; *') + +# split on commas. spaces are allowed between commas and values. +COMMA_SPLIT_REGEX = compile_regex(r' *, *') \ No newline at end of file From 462f203cd822f2f968815a7b5fecd60c3f0e8eef Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Wed, 4 May 2022 20:38:53 +0530 Subject: [PATCH 06/92] create regex tests --- test_npbc_regex.py | 223 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 test_npbc_regex.py diff --git a/test_npbc_regex.py b/test_npbc_regex.py new file mode 100644 index 0000000..28acc21 --- /dev/null +++ b/test_npbc_regex.py @@ -0,0 +1,223 @@ +import npbc_regex + +def test_regex_number(): + assert npbc_regex.NUMBER_MATCH_REGEX.match('') is None + assert npbc_regex.NUMBER_MATCH_REGEX.match('1') is not None + assert npbc_regex.NUMBER_MATCH_REGEX.match('1 2') is None + assert npbc_regex.NUMBER_MATCH_REGEX.match('1-2') is None + assert npbc_regex.NUMBER_MATCH_REGEX.match('11') is not None + assert npbc_regex.NUMBER_MATCH_REGEX.match('11-12') is None + assert npbc_regex.NUMBER_MATCH_REGEX.match('11-12,13') is None + assert npbc_regex.NUMBER_MATCH_REGEX.match('11-12,13-14') is None + assert npbc_regex.NUMBER_MATCH_REGEX.match('111') is None + assert npbc_regex.NUMBER_MATCH_REGEX.match('a') is None + assert npbc_regex.NUMBER_MATCH_REGEX.match('1a') is None + assert npbc_regex.NUMBER_MATCH_REGEX.match('1a2') is None + assert npbc_regex.NUMBER_MATCH_REGEX.match('12b') is None + +def test_regex_range(): + assert npbc_regex.RANGE_MATCH_REGEX.match('') is None + assert npbc_regex.RANGE_MATCH_REGEX.match('1') is None + assert npbc_regex.RANGE_MATCH_REGEX.match('1 2') is None + assert npbc_regex.RANGE_MATCH_REGEX.match('1-2') is not None + assert npbc_regex.RANGE_MATCH_REGEX.match('11') is None + assert npbc_regex.RANGE_MATCH_REGEX.match('11-') is None + assert npbc_regex.RANGE_MATCH_REGEX.match('11-12') is not None + assert npbc_regex.RANGE_MATCH_REGEX.match('11-12-1') is None + assert npbc_regex.RANGE_MATCH_REGEX.match('11 -12') is not None + assert npbc_regex.RANGE_MATCH_REGEX.match('11 - 12') is not None + assert npbc_regex.RANGE_MATCH_REGEX.match('11- 12') is not None + assert npbc_regex.RANGE_MATCH_REGEX.match('11-2') is not None + assert npbc_regex.RANGE_MATCH_REGEX.match('11-12,13') is None + assert npbc_regex.RANGE_MATCH_REGEX.match('11-12,13-14') is None + assert npbc_regex.RANGE_MATCH_REGEX.match('111') is None + assert npbc_regex.RANGE_MATCH_REGEX.match('a') is None + assert npbc_regex.RANGE_MATCH_REGEX.match('1a') is None + assert npbc_regex.RANGE_MATCH_REGEX.match('1a2') is None + assert npbc_regex.RANGE_MATCH_REGEX.match('12b') is None + assert npbc_regex.RANGE_MATCH_REGEX.match('11-a') is None + assert npbc_regex.RANGE_MATCH_REGEX.match('11-12a') is None + +def test_regex_CSVs(): + assert npbc_regex.CSV_MATCH_REGEX.match('') is None + assert npbc_regex.CSV_MATCH_REGEX.match('1') is not None + assert npbc_regex.CSV_MATCH_REGEX.match('a') is not None + assert npbc_regex.CSV_MATCH_REGEX.match('adcef') is not None + assert npbc_regex.CSV_MATCH_REGEX.match('-') is not None + assert npbc_regex.CSV_MATCH_REGEX.match(' ') is None + assert npbc_regex.CSV_MATCH_REGEX.match('1,2') is not None + assert npbc_regex.CSV_MATCH_REGEX.match('1-3') is not None + assert npbc_regex.CSV_MATCH_REGEX.match('monday') is not None + assert npbc_regex.CSV_MATCH_REGEX.match('monday,tuesday') is not None + assert npbc_regex.CSV_MATCH_REGEX.match('mondays') is not None + assert npbc_regex.CSV_MATCH_REGEX.match('tuesdays') is not None + assert npbc_regex.CSV_MATCH_REGEX.match('1,2,3') is not None + assert npbc_regex.CSV_MATCH_REGEX.match('1-3') is not None + assert npbc_regex.CSV_MATCH_REGEX.match('monday,tuesday') is not None + assert npbc_regex.CSV_MATCH_REGEX.match('mondays,tuesdays') is not None + assert npbc_regex.CSV_MATCH_REGEX.match(';') is None + assert npbc_regex.CSV_MATCH_REGEX.match(':') is None + assert npbc_regex.CSV_MATCH_REGEX.match(':') is None + assert npbc_regex.CSV_MATCH_REGEX.match('!') is None + assert npbc_regex.CSV_MATCH_REGEX.match('1,2,3,4') is not None + +def test_regex_days(): + assert npbc_regex.DAYS_MATCH_REGEX.match('') is None + assert npbc_regex.DAYS_MATCH_REGEX.match('1') is None + assert npbc_regex.DAYS_MATCH_REGEX.match('1,2') is None + assert npbc_regex.DAYS_MATCH_REGEX.match('1-3') is None + assert npbc_regex.DAYS_MATCH_REGEX.match('monday') is None + assert npbc_regex.DAYS_MATCH_REGEX.match('monday,tuesday') is None + assert npbc_regex.DAYS_MATCH_REGEX.match('mondays') is not None + assert npbc_regex.DAYS_MATCH_REGEX.match('tuesdays') is not None + +def test_regex_n_days(): + assert npbc_regex.N_DAY_MATCH_REGEX.match('') is None + assert npbc_regex.N_DAY_MATCH_REGEX.match('1') is None + assert npbc_regex.N_DAY_MATCH_REGEX.match('1-') is None + assert npbc_regex.N_DAY_MATCH_REGEX.match('1,2') is None + assert npbc_regex.N_DAY_MATCH_REGEX.match('1-3') is None + assert npbc_regex.N_DAY_MATCH_REGEX.match('monday') is None + assert npbc_regex.N_DAY_MATCH_REGEX.match('monday,tuesday') is None + assert npbc_regex.N_DAY_MATCH_REGEX.match('mondays') is None + assert npbc_regex.N_DAY_MATCH_REGEX.match('1-tuesday') is not None + assert npbc_regex.N_DAY_MATCH_REGEX.match('11-tuesday') is None + assert npbc_regex.N_DAY_MATCH_REGEX.match('111-tuesday') is None + assert npbc_regex.N_DAY_MATCH_REGEX.match('11-tuesdays') is None + assert npbc_regex.N_DAY_MATCH_REGEX.match('1 -tuesday') is not None + assert npbc_regex.N_DAY_MATCH_REGEX.match('1- tuesday') is not None + assert npbc_regex.N_DAY_MATCH_REGEX.match('1 - tuesday') is not None + +def test_regex_all_text(): + assert npbc_regex.ALL_MATCH_REGEX.match('') is None + assert npbc_regex.ALL_MATCH_REGEX.match('1') is None + assert npbc_regex.ALL_MATCH_REGEX.match('1-') is None + assert npbc_regex.ALL_MATCH_REGEX.match('1,2') is None + assert npbc_regex.ALL_MATCH_REGEX.match('1-3') is None + assert npbc_regex.ALL_MATCH_REGEX.match('monday') is None + assert npbc_regex.ALL_MATCH_REGEX.match('monday,tuesday') is None + assert npbc_regex.ALL_MATCH_REGEX.match('mondays') is None + assert npbc_regex.ALL_MATCH_REGEX.match('tuesdays') is None + assert npbc_regex.ALL_MATCH_REGEX.match('1-tuesday') is None + assert npbc_regex.ALL_MATCH_REGEX.match('11-tuesday') is None + assert npbc_regex.ALL_MATCH_REGEX.match('111-tuesday') is None + assert npbc_regex.ALL_MATCH_REGEX.match('11-tuesdays') is None + assert npbc_regex.ALL_MATCH_REGEX.match('1 -tuesday') is None + assert npbc_regex.ALL_MATCH_REGEX.match('1- tuesday') is None + assert npbc_regex.ALL_MATCH_REGEX.match('1 - tuesday') is None + assert npbc_regex.ALL_MATCH_REGEX.match('all') is not None + assert npbc_regex.ALL_MATCH_REGEX.match('all,tuesday') is None + assert npbc_regex.ALL_MATCH_REGEX.match('all,tuesdays') is None + assert npbc_regex.ALL_MATCH_REGEX.match('All') is not None + assert npbc_regex.ALL_MATCH_REGEX.match('AlL') is not None + assert npbc_regex.ALL_MATCH_REGEX.match('ALL') is not None + + +def test_regex_costs(): + assert npbc_regex.COSTS_MATCH_REGEX.match('') is None + assert npbc_regex.COSTS_MATCH_REGEX.match('a') is None + assert npbc_regex.COSTS_MATCH_REGEX.match('1') is not None + assert npbc_regex.COSTS_MATCH_REGEX.match('1.') is None + assert npbc_regex.COSTS_MATCH_REGEX.match('1.5') is not None + assert npbc_regex.COSTS_MATCH_REGEX.match('1.0') is not None + assert npbc_regex.COSTS_MATCH_REGEX.match('16.0') is not None + assert npbc_regex.COSTS_MATCH_REGEX.match('16.06') is not None + assert npbc_regex.COSTS_MATCH_REGEX.match('1;2') is not None + assert npbc_regex.COSTS_MATCH_REGEX.match('1 ;2') is not None + assert npbc_regex.COSTS_MATCH_REGEX.match('1; 2') is not None + assert npbc_regex.COSTS_MATCH_REGEX.match('1 ; 2') is not None + assert npbc_regex.COSTS_MATCH_REGEX.match('1;2;') is not None + assert npbc_regex.COSTS_MATCH_REGEX.match('1;2 ;') is not None + assert npbc_regex.COSTS_MATCH_REGEX.match('1:2') is None + assert npbc_regex.COSTS_MATCH_REGEX.match('1,2') is None + assert npbc_regex.COSTS_MATCH_REGEX.match('1-2') is None + assert npbc_regex.COSTS_MATCH_REGEX.match('1;2;3') is not None + assert npbc_regex.COSTS_MATCH_REGEX.match('1;2;3;4') is not None + assert npbc_regex.COSTS_MATCH_REGEX.match('1;2;3;4;5') is not None + assert npbc_regex.COSTS_MATCH_REGEX.match('1;2;3;4;5;6') is not None + assert npbc_regex.COSTS_MATCH_REGEX.match('1;2;3;4;5;6;7;') is not None + assert npbc_regex.COSTS_MATCH_REGEX.match('1;2;3;4;5;6;7') is not None + assert npbc_regex.COSTS_MATCH_REGEX.match('1;2;3;4;5;6;7;8') is None + +def test_delivery_regex(): + assert npbc_regex.DELIVERY_MATCH_REGEX.match('') is None + assert npbc_regex.DELIVERY_MATCH_REGEX.match('a') is None + assert npbc_regex.DELIVERY_MATCH_REGEX.match('1') is None + assert npbc_regex.DELIVERY_MATCH_REGEX.match('1.') is None + assert npbc_regex.DELIVERY_MATCH_REGEX.match('1.5') is None + assert npbc_regex.DELIVERY_MATCH_REGEX.match('1,2') is None + assert npbc_regex.DELIVERY_MATCH_REGEX.match('1-2') is None + assert npbc_regex.DELIVERY_MATCH_REGEX.match('1;2') is None + assert npbc_regex.DELIVERY_MATCH_REGEX.match('1:2') is None + assert npbc_regex.DELIVERY_MATCH_REGEX.match('1,2,3') is None + assert npbc_regex.DELIVERY_MATCH_REGEX.match('Y') is None + assert npbc_regex.DELIVERY_MATCH_REGEX.match('N') is None + assert npbc_regex.DELIVERY_MATCH_REGEX.match('YY') is None + assert npbc_regex.DELIVERY_MATCH_REGEX.match('YYY') is None + assert npbc_regex.DELIVERY_MATCH_REGEX.match('YYYY') is None + assert npbc_regex.DELIVERY_MATCH_REGEX.match('YYYYY') is None + assert npbc_regex.DELIVERY_MATCH_REGEX.match('YYYYYY') is None + assert npbc_regex.DELIVERY_MATCH_REGEX.match('YYYYYYY') is not None + assert npbc_regex.DELIVERY_MATCH_REGEX.match('YYYYYYYY') is None + assert npbc_regex.DELIVERY_MATCH_REGEX.match('NNNNNNN') is not None + assert npbc_regex.DELIVERY_MATCH_REGEX.match('NYNNNNN') is not None + assert npbc_regex.DELIVERY_MATCH_REGEX.match('NYYYYNN') is not None + assert npbc_regex.DELIVERY_MATCH_REGEX.match('NYYYYYY') is not None + assert npbc_regex.DELIVERY_MATCH_REGEX.match('NYYYYYYY') is None + assert npbc_regex.DELIVERY_MATCH_REGEX.match('N,N,N,N,N,N,N') is None + assert npbc_regex.DELIVERY_MATCH_REGEX.match('N;N;N;N;N;N;N') is None + assert npbc_regex.DELIVERY_MATCH_REGEX.match('N-N-N-N-N-N-N') is None + assert npbc_regex.DELIVERY_MATCH_REGEX.match('N N N N N N N') is None + assert npbc_regex.DELIVERY_MATCH_REGEX.match('YYYYYYy') is None + assert npbc_regex.DELIVERY_MATCH_REGEX.match('YYYYYYn') is None + + + +def test_regex_hyphen(): + assert npbc_regex.HYPHEN_SPLIT_REGEX.split('1-2') == ['1', '2'] + assert npbc_regex.HYPHEN_SPLIT_REGEX.split('1-2-3') == ['1', '2', '3'] + assert npbc_regex.HYPHEN_SPLIT_REGEX.split('1 -2-3') == ['1', '2', '3'] + assert npbc_regex.HYPHEN_SPLIT_REGEX.split('1 - 2-3') == ['1', '2', '3'] + assert npbc_regex.HYPHEN_SPLIT_REGEX.split('1- 2-3') == ['1', '2', '3'] + assert npbc_regex.HYPHEN_SPLIT_REGEX.split('1') == ['1'] + assert npbc_regex.HYPHEN_SPLIT_REGEX.split('1-') == ['1', ''] + assert npbc_regex.HYPHEN_SPLIT_REGEX.split('1-2-') == ['1', '2', ''] + assert npbc_regex.HYPHEN_SPLIT_REGEX.split('1-2-3-') == ['1', '2', '3', ''] + assert npbc_regex.HYPHEN_SPLIT_REGEX.split('1,2-3') == ['1,2', '3'] + assert npbc_regex.HYPHEN_SPLIT_REGEX.split('1,2-3-') == ['1,2', '3', ''] + assert npbc_regex.HYPHEN_SPLIT_REGEX.split('1,2, 3,') == ['1,2, 3,'] + assert npbc_regex.HYPHEN_SPLIT_REGEX.split('') == [''] + +def test_regex_comma(): + assert npbc_regex.COMMA_SPLIT_REGEX.split('1,2') == ['1', '2'] + assert npbc_regex.COMMA_SPLIT_REGEX.split('1,2,3') == ['1', '2', '3'] + assert npbc_regex.COMMA_SPLIT_REGEX.split('1 ,2,3') == ['1', '2', '3'] + assert npbc_regex.COMMA_SPLIT_REGEX.split('1 , 2,3') == ['1', '2', '3'] + assert npbc_regex.COMMA_SPLIT_REGEX.split('1, 2,3') == ['1', '2', '3'] + assert npbc_regex.COMMA_SPLIT_REGEX.split('1') == ['1'] + assert npbc_regex.COMMA_SPLIT_REGEX.split('1,') == ['1', ''] + assert npbc_regex.COMMA_SPLIT_REGEX.split('1, ') == ['1', ''] + assert npbc_regex.COMMA_SPLIT_REGEX.split('1,2,') == ['1', '2', ''] + assert npbc_regex.COMMA_SPLIT_REGEX.split('1,2,3,') == ['1', '2', '3', ''] + assert npbc_regex.COMMA_SPLIT_REGEX.split('1-2,3') == ['1-2', '3'] + assert npbc_regex.COMMA_SPLIT_REGEX.split('1-2,3,') == ['1-2', '3', ''] + assert npbc_regex.COMMA_SPLIT_REGEX.split('1-2-3') == ['1-2-3'] + assert npbc_regex.COMMA_SPLIT_REGEX.split('1-2- 3') == ['1-2- 3'] + assert npbc_regex.COMMA_SPLIT_REGEX.split('') == [''] + +def test_regex_semicolon(): + assert npbc_regex.SEMICOLON_SPLIT_REGEX.split('1;2') == ['1', '2'] + assert npbc_regex.SEMICOLON_SPLIT_REGEX.split('1;2;3') == ['1', '2', '3'] + assert npbc_regex.SEMICOLON_SPLIT_REGEX.split('1 ;2;3') == ['1', '2', '3'] + assert npbc_regex.SEMICOLON_SPLIT_REGEX.split('1 ; 2;3') == ['1', '2', '3'] + assert npbc_regex.SEMICOLON_SPLIT_REGEX.split('1; 2;3') == ['1', '2', '3'] + assert npbc_regex.SEMICOLON_SPLIT_REGEX.split('1') == ['1'] + assert npbc_regex.SEMICOLON_SPLIT_REGEX.split('1;') == ['1', ''] + assert npbc_regex.SEMICOLON_SPLIT_REGEX.split('1; ') == ['1', ''] + assert npbc_regex.SEMICOLON_SPLIT_REGEX.split('1;2;') == ['1', '2', ''] + assert npbc_regex.SEMICOLON_SPLIT_REGEX.split('1;2;3;') == ['1', '2', '3', ''] + assert npbc_regex.SEMICOLON_SPLIT_REGEX.split('1-2;3') == ['1-2', '3'] + assert npbc_regex.SEMICOLON_SPLIT_REGEX.split('1-2;3;') == ['1-2', '3', ''] + assert npbc_regex.SEMICOLON_SPLIT_REGEX.split('1-2-3') == ['1-2-3'] + assert npbc_regex.SEMICOLON_SPLIT_REGEX.split('1-2- 3') == ['1-2- 3'] + assert npbc_regex.SEMICOLON_SPLIT_REGEX.split('') == [''] \ No newline at end of file From e6cc22d409918fee5a8980fdd73c2a0a33afb1b7 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Thu, 5 May 2022 01:56:05 +0530 Subject: [PATCH 07/92] get weekday counts to a generator --- .gitignore | 2 +- npbc_core.py | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ test_core.py | 8 ++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 npbc_core.py create mode 100644 test_core.py diff --git a/.gitignore b/.gitignore index e4a9c8b..82dca71 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ __pycache__ *build *dist .vs -exp +exp* *legacy* npbc.db pyenv-install diff --git a/npbc_core.py b/npbc_core.py new file mode 100644 index 0000000..16f19b3 --- /dev/null +++ b/npbc_core.py @@ -0,0 +1,51 @@ +from pathlib import Path +from calendar import day_name as weekday_names_iterable, monthcalendar +from sqlite3 import connect +from typing import Generator + + +## paths for the folder containing schema and database files + # during normal use, the DB will be in ~/.npbc (where ~ is the user's home directory) and the schema will be bundled with the executable + # during development, the DB and schema will both be in "data" + +DATABASE_DIR = Path().home() / '.npbc' # normal use path +# DATABASE_DIR = Path('data') # development path + +DATABASE_PATH = DATABASE_DIR / 'npbc.db' + +SCHEMA_PATH = Path(__file__).parent / 'schema.sql' # normal use path +# SCHEMA_PATH = DATABASE_DIR / 'schema.sql' # development path + + +## list constant for names of weekdays +WEEKDAY_NAMES = list(weekday_names_iterable) + + +## ensure DB exists and it's set up with the schema +def setup_and_connect_DB() -> None: + DATABASE_DIR.mkdir(parents=True, exist_ok=True) + DATABASE_PATH.touch(exist_ok=True) + + with connect(DATABASE_PATH) as connection: + connection.executescript(SCHEMA_PATH.read_text()) + connection.commit() + + +## generate a list of number of times each weekday occurs in a given month (return a generator) + # the list will be in the same order as WEEKDAY_NAMES (so the first day should be Monday) +def get_number_of_each_weekday(month: int, year: int) -> Generator[int, None, None]: + main_calendar = monthcalendar(year, month) + number_of_weeks = len(main_calendar) + + for i, _ in enumerate(WEEKDAY_NAMES): + number_of_weekday: int = number_of_weeks + + if main_calendar[0][i] == 0: + number_of_weekday -= 1 + + if main_calendar[-1][i] == 0: + number_of_weekday -= 1 + + yield number_of_weekday + + diff --git a/test_core.py b/test_core.py new file mode 100644 index 0000000..8f83a67 --- /dev/null +++ b/test_core.py @@ -0,0 +1,8 @@ +from npbc_core import get_number_of_each_weekday + +def test_get_number_of_each_weekday(): + assert list(get_number_of_each_weekday(1, 2022)) == [5, 4, 4, 4, 4, 5, 5] + assert list(get_number_of_each_weekday(2, 2022)) == [4, 4, 4, 4, 4, 4, 4] + assert list(get_number_of_each_weekday(3, 2022)) == [4, 5, 5 ,5, 4, 4, 4] + assert list(get_number_of_each_weekday(2, 2020)) == [4, 4, 4, 4, 4, 5, 4] + assert list(get_number_of_each_weekday(12, 1954)) == [4, 4, 5, 5, 5, 4, 4] \ No newline at end of file From 2ffd7915b981d554cbed019b402c297493bcc2b2 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Thu, 5 May 2022 03:26:11 +0530 Subject: [PATCH 08/92] finish parse rewrite --- npbc_core.py | 134 ++++++++++++++++++++++++++++++++++++++- test_core.py | 175 +++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 300 insertions(+), 9 deletions(-) diff --git a/npbc_core.py b/npbc_core.py index 16f19b3..44d4b75 100644 --- a/npbc_core.py +++ b/npbc_core.py @@ -1,7 +1,9 @@ +from datetime import date as date_type, datetime, timedelta from pathlib import Path -from calendar import day_name as weekday_names_iterable, monthcalendar +from calendar import day_name as weekday_names_iterable, monthcalendar, monthrange from sqlite3 import connect from typing import Generator +import npbc_regex ## paths for the folder containing schema and database files @@ -21,6 +23,134 @@ WEEKDAY_NAMES = list(weekday_names_iterable) +## validate a string that specifies when a given paper was not delivered +# first check to see that it meets the comma-separated requirements +# then check against each of the other acceptable patterns in the regex dictionary +def validate_undelivered_string(*strings: str) -> bool: + # check that the string matches one of the acceptable patterns + for string in strings: + if string and not ( + npbc_regex.NUMBER_MATCH_REGEX.match(string) or + npbc_regex.RANGE_MATCH_REGEX.match(string) or + npbc_regex.DAYS_MATCH_REGEX.match(string) or + npbc_regex.N_DAY_MATCH_REGEX.match(string) or + npbc_regex.ALL_MATCH_REGEX.match(string) + ): + + return False + + # if we get here, all strings passed the regex check + return True + +## if the date is simply a number, it's a single day. so we just identify that date +def extract_number(string: str, month: int, year: int) -> date_type | None: + date = int(string) + + if date > 0 and date <= monthrange(year, month)[1]: + return date_type(year, month, date) + + +## if the date is a range of numbers, it's a range of days. we identify all the dates in that range, bounds inclusive +def extract_range(string: str, month: int, year: int) -> Generator[date_type, None, None]: + start, end = [int(date) for date in npbc_regex.HYPHEN_SPLIT_REGEX.split(string)] + + if 0 < start <= end <= monthrange(year, month)[1]: + for date in range(start, end + 1): + yield date_type(year, month, date) + + +## if the date is the plural of a weekday name, we identify all dates in that month which are the given weekday +def extract_weekday(string: str, month: int, year: int) -> Generator[date_type, None, None]: + weekday = WEEKDAY_NAMES.index(string.capitalize().rstrip('s')) + + for day in range(1, monthrange(year, month)[1] + 1): + if date_type(year, month, day).weekday() == weekday: + yield date_type(year, month, day) + + +## if the date is a number and a weekday name (singular), we identify the date that is the nth occurrence of the given weekday in the month +def extract_nth_weekday(string: str, month: int, year: int) -> date_type | None: + n, weekday_name = npbc_regex.HYPHEN_SPLIT_REGEX.split(string) + + n = int(n) + + if n > 0 and n <= list(get_number_of_each_weekday(month, year))[WEEKDAY_NAMES.index(weekday_name.capitalize())]: + weekday = WEEKDAY_NAMES.index(weekday_name.capitalize()) + + valid_dates = [ + date_type(year, month, day) + for day in range(1, monthrange(year, month)[1] + 1) + if date_type(year, month, day).weekday() == weekday + ] + + return valid_dates[n - 1] + + +## if the text is "all", we identify all the dates in the month +def extract_all(month: int, year: int) -> Generator[date_type, None, None]: + for day in range(1, monthrange(year, month)[1] + 1): + yield date_type(year, month, day) + + +## parse a section of the strings + # each section is a string that specifies a set of dates + # this function will return a set of dates that uniquely identifies each date mentioned across the string +def parse_undelivered_string(month: int, year: int, string: str) -> set[date_type]: + # initialize the set of dates + dates = set() + + # check for each of the patterns + if npbc_regex.NUMBER_MATCH_REGEX.match(string): + number_date = extract_number(string, month, year) + + if number_date: + dates.add(number_date) + + elif npbc_regex.RANGE_MATCH_REGEX.match(string): + dates.update(extract_range(string, month, year)) + + elif npbc_regex.DAYS_MATCH_REGEX.match(string): + dates.update(extract_weekday(string, month, year)) + + elif npbc_regex.N_DAY_MATCH_REGEX.match(string): + n_day_date = extract_nth_weekday(string, month, year) + + if n_day_date: + dates.add(n_day_date) + + elif npbc_regex.ALL_MATCH_REGEX.match(string): + dates.update(extract_all(month, year)) + + else: + raise ValueError + + return dates + + +## parse a string that specifies when a given paper was not delivered + # each section states some set of dates + # this function will return a set of dates that uniquely identifies each date mentioned across all the strings +def parse_undelivered_strings(month: int, year: int, *strings: str) -> set[date_type]: + # initialize the set of dates + dates = set() + + # check for each of the patterns + for string in strings: + try: + dates.update(parse_undelivered_string(month, year, string)) + + except ValueError: + print( + f"""Congratulations! You broke the program! + You managed to write a string that the program considers valid, but isn't actually. + Please report it to the developer. + \nThe string you wrote was: {string} + This data has not been counted.""" + ) + + return dates + + ## ensure DB exists and it's set up with the schema def setup_and_connect_DB() -> None: DATABASE_DIR.mkdir(parents=True, exist_ok=True) @@ -47,5 +177,3 @@ def get_number_of_each_weekday(month: int, year: int) -> Generator[int, None, No number_of_weekday -= 1 yield number_of_weekday - - diff --git a/test_core.py b/test_core.py index 8f83a67..b9f4b3d 100644 --- a/test_core.py +++ b/test_core.py @@ -1,8 +1,171 @@ -from npbc_core import get_number_of_each_weekday +from datetime import date as date_type +import npbc_core def test_get_number_of_each_weekday(): - assert list(get_number_of_each_weekday(1, 2022)) == [5, 4, 4, 4, 4, 5, 5] - assert list(get_number_of_each_weekday(2, 2022)) == [4, 4, 4, 4, 4, 4, 4] - assert list(get_number_of_each_weekday(3, 2022)) == [4, 5, 5 ,5, 4, 4, 4] - assert list(get_number_of_each_weekday(2, 2020)) == [4, 4, 4, 4, 4, 5, 4] - assert list(get_number_of_each_weekday(12, 1954)) == [4, 4, 5, 5, 5, 4, 4] \ No newline at end of file + test_function = npbc_core.get_number_of_each_weekday + + assert list(test_function(1, 2022)) == [5, 4, 4, 4, 4, 5, 5] + assert list(test_function(2, 2022)) == [4, 4, 4, 4, 4, 4, 4] + assert list(test_function(3, 2022)) == [4, 5, 5 ,5, 4, 4, 4] + assert list(test_function(2, 2020)) == [4, 4, 4, 4, 4, 5, 4] + assert list(test_function(12, 1954)) == [4, 4, 5, 5, 5, 4, 4] + + +def test_validate_undelivered_string(): + test_function = npbc_core.validate_undelivered_string + + assert test_function("") + assert not test_function("a") + assert not test_function("monday") + assert not test_function("1-mondays") + assert not test_function("1monday") + assert not test_function("1 monday") + assert not test_function("monday-1") + assert not test_function("monday-1") + assert test_function("1") + assert test_function("6") + assert test_function("31") + assert test_function("31","") + assert test_function("3","1") + assert test_function("3","1","") + assert test_function("3","1") + assert test_function("3","1") + assert test_function("3","1") + assert test_function("1","2","3-9") + assert test_function("1","2","3-9","11","12","13-19") + assert test_function("1","2","3-9","11","12","13-19","21","22","23-29") + assert test_function("1","2","3-9","11","12","13-19","21","22","23-29","31") + assert test_function("1","2","3","4","5","6","7","8","9") + assert test_function("mondays") + assert test_function("mondays,tuesdays") + assert test_function("mondays","tuesdays","wednesdays") + assert test_function("mondays","5-21") + assert test_function("mondays","5-21","tuesdays","5-21") + assert test_function("1-monday") + assert test_function("2-monday") + assert test_function("all") + assert test_function("All") + assert test_function("aLl") + assert test_function("alL") + assert test_function("aLL") + assert test_function("ALL") + + +def test_undelivered_string_parsing(): + MONTH = 5 + YEAR = 2017 + test_function = npbc_core.parse_undelivered_strings + + + assert test_function(MONTH, YEAR, '') == set([]) + + assert test_function(MONTH, YEAR, '1') == set([ + date_type(year=YEAR, month=MONTH, day=1) + ]) + + assert test_function(MONTH, YEAR, '1-2') == set([ + date_type(year=YEAR, month=MONTH, day=1), + date_type(year=YEAR, month=MONTH, day=2) + ]) + + assert test_function(MONTH, YEAR, '5-17') == set([ + date_type(year=YEAR, month=MONTH, day=5), + date_type(year=YEAR, month=MONTH, day=6), + date_type(year=YEAR, month=MONTH, day=7), + date_type(year=YEAR, month=MONTH, day=8), + date_type(year=YEAR, month=MONTH, day=9), + date_type(year=YEAR, month=MONTH, day=10), + date_type(year=YEAR, month=MONTH, day=11), + date_type(year=YEAR, month=MONTH, day=12), + date_type(year=YEAR, month=MONTH, day=13), + date_type(year=YEAR, month=MONTH, day=14), + date_type(year=YEAR, month=MONTH, day=15), + date_type(year=YEAR, month=MONTH, day=16), + date_type(year=YEAR, month=MONTH, day=17) + ]) + + assert test_function(MONTH, YEAR, '5-17', '19') == set([ + date_type(year=YEAR, month=MONTH, day=5), + date_type(year=YEAR, month=MONTH, day=6), + date_type(year=YEAR, month=MONTH, day=7), + date_type(year=YEAR, month=MONTH, day=8), + date_type(year=YEAR, month=MONTH, day=9), + date_type(year=YEAR, month=MONTH, day=10), + date_type(year=YEAR, month=MONTH, day=11), + date_type(year=YEAR, month=MONTH, day=12), + date_type(year=YEAR, month=MONTH, day=13), + date_type(year=YEAR, month=MONTH, day=14), + date_type(year=YEAR, month=MONTH, day=15), + date_type(year=YEAR, month=MONTH, day=16), + date_type(year=YEAR, month=MONTH, day=17), + date_type(year=YEAR, month=MONTH, day=19) + ]) + + assert test_function(MONTH, YEAR, '5-17', '19-21') == set([ + date_type(year=YEAR, month=MONTH, day=5), + date_type(year=YEAR, month=MONTH, day=6), + date_type(year=YEAR, month=MONTH, day=7), + date_type(year=YEAR, month=MONTH, day=8), + date_type(year=YEAR, month=MONTH, day=9), + date_type(year=YEAR, month=MONTH, day=10), + date_type(year=YEAR, month=MONTH, day=11), + date_type(year=YEAR, month=MONTH, day=12), + date_type(year=YEAR, month=MONTH, day=13), + date_type(year=YEAR, month=MONTH, day=14), + date_type(year=YEAR, month=MONTH, day=15), + date_type(year=YEAR, month=MONTH, day=16), + date_type(year=YEAR, month=MONTH, day=17), + date_type(year=YEAR, month=MONTH, day=19), + date_type(year=YEAR, month=MONTH, day=20), + date_type(year=YEAR, month=MONTH, day=21) + ]) + + assert test_function(MONTH, YEAR, '5-17', '19-21', '23') == set([ + date_type(year=YEAR, month=MONTH, day=5), + date_type(year=YEAR, month=MONTH, day=6), + date_type(year=YEAR, month=MONTH, day=7), + date_type(year=YEAR, month=MONTH, day=8), + date_type(year=YEAR, month=MONTH, day=9), + date_type(year=YEAR, month=MONTH, day=10), + date_type(year=YEAR, month=MONTH, day=11), + date_type(year=YEAR, month=MONTH, day=12), + date_type(year=YEAR, month=MONTH, day=13), + date_type(year=YEAR, month=MONTH, day=14), + date_type(year=YEAR, month=MONTH, day=15), + date_type(year=YEAR, month=MONTH, day=16), + date_type(year=YEAR, month=MONTH, day=17), + date_type(year=YEAR, month=MONTH, day=19), + date_type(year=YEAR, month=MONTH, day=20), + date_type(year=YEAR, month=MONTH, day=21), + date_type(year=YEAR, month=MONTH, day=23) + ]) + + assert test_function(MONTH, YEAR, 'mondays') == set([ + date_type(year=YEAR, month=MONTH, day=1), + date_type(year=YEAR, month=MONTH, day=8), + date_type(year=YEAR, month=MONTH, day=15), + date_type(year=YEAR, month=MONTH, day=22), + date_type(year=YEAR, month=MONTH, day=29) + ]) + + assert test_function(MONTH, YEAR, 'mondays', 'wednesdays') == set([ + date_type(year=YEAR, month=MONTH, day=1), + date_type(year=YEAR, month=MONTH, day=8), + date_type(year=YEAR, month=MONTH, day=15), + date_type(year=YEAR, month=MONTH, day=22), + date_type(year=YEAR, month=MONTH, day=29), + date_type(year=YEAR, month=MONTH, day=3), + date_type(year=YEAR, month=MONTH, day=10), + date_type(year=YEAR, month=MONTH, day=17), + date_type(year=YEAR, month=MONTH, day=24), + date_type(year=YEAR, month=MONTH, day=31) + ]) + + assert test_function(MONTH, YEAR, '2-monday') == set([ + date_type(year=YEAR, month=MONTH, day=8) + ]) + + assert test_function(MONTH, YEAR, '2-monday', '3-wednesday') == set([ + date_type(year=YEAR, month=MONTH, day=8), + date_type(year=YEAR, month=MONTH, day=17) + ]) \ No newline at end of file From 95c57327bc17d103626ec86e6feeb24983749b07 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Thu, 5 May 2022 03:30:17 +0530 Subject: [PATCH 09/92] rearrange --- npbc_core.py | 62 ++++++++++++++++++++++++++-------------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/npbc_core.py b/npbc_core.py index 44d4b75..1961b32 100644 --- a/npbc_core.py +++ b/npbc_core.py @@ -23,6 +23,34 @@ WEEKDAY_NAMES = list(weekday_names_iterable) +## ensure DB exists and it's set up with the schema +def setup_and_connect_DB() -> None: + DATABASE_DIR.mkdir(parents=True, exist_ok=True) + DATABASE_PATH.touch(exist_ok=True) + + with connect(DATABASE_PATH) as connection: + connection.executescript(SCHEMA_PATH.read_text()) + connection.commit() + + +## generate a list of number of times each weekday occurs in a given month (return a generator) + # the list will be in the same order as WEEKDAY_NAMES (so the first day should be Monday) +def get_number_of_each_weekday(month: int, year: int) -> Generator[int, None, None]: + main_calendar = monthcalendar(year, month) + number_of_weeks = len(main_calendar) + + for i, _ in enumerate(WEEKDAY_NAMES): + number_of_weekday: int = number_of_weeks + + if main_calendar[0][i] == 0: + number_of_weekday -= 1 + + if main_calendar[-1][i] == 0: + number_of_weekday -= 1 + + yield number_of_weekday + + ## validate a string that specifies when a given paper was not delivered # first check to see that it meets the comma-separated requirements # then check against each of the other acceptable patterns in the regex dictionary @@ -93,8 +121,8 @@ def extract_all(month: int, year: int) -> Generator[date_type, None, None]: ## parse a section of the strings - # each section is a string that specifies a set of dates - # this function will return a set of dates that uniquely identifies each date mentioned across the string + # each section is a string that specifies a set of dates + # this function will return a set of dates that uniquely identifies each date mentioned across the string def parse_undelivered_string(month: int, year: int, string: str) -> set[date_type]: # initialize the set of dates dates = set() @@ -148,32 +176,4 @@ def parse_undelivered_strings(month: int, year: int, *strings: str) -> set[date_ This data has not been counted.""" ) - return dates - - -## ensure DB exists and it's set up with the schema -def setup_and_connect_DB() -> None: - DATABASE_DIR.mkdir(parents=True, exist_ok=True) - DATABASE_PATH.touch(exist_ok=True) - - with connect(DATABASE_PATH) as connection: - connection.executescript(SCHEMA_PATH.read_text()) - connection.commit() - - -## generate a list of number of times each weekday occurs in a given month (return a generator) - # the list will be in the same order as WEEKDAY_NAMES (so the first day should be Monday) -def get_number_of_each_weekday(month: int, year: int) -> Generator[int, None, None]: - main_calendar = monthcalendar(year, month) - number_of_weeks = len(main_calendar) - - for i, _ in enumerate(WEEKDAY_NAMES): - number_of_weekday: int = number_of_weeks - - if main_calendar[0][i] == 0: - number_of_weekday -= 1 - - if main_calendar[-1][i] == 0: - number_of_weekday -= 1 - - yield number_of_weekday + return dates \ No newline at end of file From 6bf32b374c67155bb4d8f26a10515c838d39a023 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Thu, 5 May 2022 16:50:33 +0530 Subject: [PATCH 10/92] update main calculation algo --- .gitignore | 3 +- npbc_core.py | 108 ++++++++++++++++++++++++++++++++++-- test_core.py | 152 ++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 256 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 82dca71..692e90d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,8 +6,7 @@ __pycache__ .vs exp* *legacy* -npbc.db -pyenv-install bin .vscode .pytest_cache +data/*.db diff --git a/npbc_core.py b/npbc_core.py index 1961b32..9ef104e 100644 --- a/npbc_core.py +++ b/npbc_core.py @@ -1,9 +1,11 @@ from datetime import date as date_type, datetime, timedelta from pathlib import Path from calendar import day_name as weekday_names_iterable, monthcalendar, monthrange -from sqlite3 import connect +from sqlite3 import Connection, connect +from textwrap import indent from typing import Generator import npbc_regex +from json import dumps ## paths for the folder containing schema and database files @@ -11,12 +13,12 @@ # during development, the DB and schema will both be in "data" DATABASE_DIR = Path().home() / '.npbc' # normal use path -# DATABASE_DIR = Path('data') # development path +DATABASE_DIR = Path('data') # development path DATABASE_PATH = DATABASE_DIR / 'npbc.db' SCHEMA_PATH = Path(__file__).parent / 'schema.sql' # normal use path -# SCHEMA_PATH = DATABASE_DIR / 'schema.sql' # development path +SCHEMA_PATH = DATABASE_DIR / 'schema.sql' # development path ## list constant for names of weekdays @@ -176,4 +178,102 @@ def parse_undelivered_strings(month: int, year: int, *strings: str) -> set[date_ This data has not been counted.""" ) - return dates \ No newline at end of file + return dates + + +## get the cost and delivery data for a given paper from the DB + # each of them are converted to a dictionary, whose index is the day_id + # the two dictionaries are then returned as a tuple +def get_cost_and_delivery_data(paper_id: int, connection: Connection) -> tuple[dict[int, float], dict[int, bool]]: + query = """ + SELECT papers_days.day_id, papers_days_delivered.delivered, papers_days_cost.cost + FROM papers_days + LEFT JOIN papers_days_delivered + ON papers_days.paper_day_id = papers_days_delivered.paper_day_id + LEFT JOIN papers_days_cost + ON papers_days.paper_day_id = papers_days_cost.paper_day_id + WHERE papers_days.paper_id = ? + """ + + retrieved_data = connection.execute(query, (paper_id, )).fetchall() + + cost_dict = { + row[0]: row[2] + for row in retrieved_data + } + + delivered_dict = { + row[0]: bool(row[1]) + for row in retrieved_data + } + + return cost_dict, delivered_dict + + +## calculate the cost of one paper for the full month + # any dates when it was not delivered will be removed +def calculate_cost_of_one_paper(number_of_each_weekday: list[int], undelivered_dates: set[date_type], cost_and_delivered_data: tuple[dict[int, float], dict[int, bool]]) -> float: + cost_data, delivered_data = cost_and_delivered_data + + # initialize counters corresponding to each weekday when the paper was not delivered + number_of_days_per_weekday_not_received = [0] * len(number_of_each_weekday) + + # for each date that the paper was not delivered, we increment the counter for the corresponding weekday + for date in undelivered_dates: + number_of_days_per_weekday_not_received[date.weekday()] += 1 + + # calculate the total number of each weekday the paper was delivered (if it is supposed to be delivered) + number_of_days_delivered = [ + number_of_each_weekday[day_id] - number_of_days_per_weekday_not_received[day_id] if delivered else 0 + for day_id, delivered in delivered_data.items() + ] + + # calculate the total cost of the paper for the month + return sum( + cost * number_of_days_delivered[day_id] + for day_id, cost in cost_data.items() + ) + + +## calculate the cost of all papers for the full month + # return data about the cost of each paper, the total cost, and dates when each paper was not delivered +def calculate_cost_of_all_papers(undelivered_strings: dict[int, list[str]], month: int, year: int) -> tuple[dict[int, float], float, dict[int, set[date_type]]]: + NUMBER_OF_EACH_WEEKDAY = list(get_number_of_each_weekday(month, year)) + cost_and_delivery_data = [] + + # get the IDs of papers that exist + with connect(DATABASE_PATH) as connection: + papers = connection.execute("SELECT paper_id FROM papers;").fetchall() + + # get the data about cost and delivery for each paper + cost_and_delivery_data = [ + get_cost_and_delivery_data(paper_id, connection) + for paper_id, in papers # type: ignore + ] + + # initialize a "blank" dictionary that will eventually contain any dates when a paper was not delivered + undelivered_dates: dict[int, set[date_type]] = { + paper_id: set() + for paper_id, in papers # type: ignore + } + + # calculate the undelivered dates for each paper + for paper_id, strings in undelivered_strings.items(): + undelivered_dates[paper_id].update( + parse_undelivered_strings(month, year, *strings) + ) + + # calculate the cost of each paper + costs = { + paper_id: calculate_cost_of_one_paper( + NUMBER_OF_EACH_WEEKDAY, + undelivered_dates[paper_id], + cost_and_delivery_data[index] + ) + for index, (paper_id,) in enumerate(papers) # type: ignore + } + + # calculate the total cost of all papers + total = sum(costs.values()) + + return costs, total, undelivered_dates \ No newline at end of file diff --git a/test_core.py b/test_core.py index b9f4b3d..f569b8a 100644 --- a/test_core.py +++ b/test_core.py @@ -168,4 +168,154 @@ def test_undelivered_string_parsing(): assert test_function(MONTH, YEAR, '2-monday', '3-wednesday') == set([ date_type(year=YEAR, month=MONTH, day=8), date_type(year=YEAR, month=MONTH, day=17) - ]) \ No newline at end of file + ]) + + +def test_calculating_cost_of_one_paper(): + DAYS_PER_WEEK = [5, 4, 4, 4, 4, 5, 5] + COST_PER_DAY: dict[int, float] = { + 0: 0, + 1: 0, + 2: 2, + 3: 2, + 4: 5, + 5: 0, + 6: 1 + } + DELIVERY_DATA: dict[int, bool] = { + 0: False, + 1: False, + 2: True, + 3: True, + 4: True, + 5: False, + 6: True + } + + test_function = npbc_core.calculate_cost_of_one_paper + + assert test_function( + DAYS_PER_WEEK, + set([]), + ( + COST_PER_DAY, + DELIVERY_DATA + ) + ) == 41 + + assert test_function( + DAYS_PER_WEEK, + set([]), + ( + COST_PER_DAY, + { + 0: False, + 1: False, + 2: True, + 3: True, + 4: True, + 5: False, + 6: False + } + ) + ) == 36 + + assert test_function( + DAYS_PER_WEEK, + set([ + date_type(year=2022, month=1, day=8) + ]), + ( + COST_PER_DAY, + DELIVERY_DATA + ) + ) == 41 + + assert test_function( + DAYS_PER_WEEK, + set([ + date_type(year=2022, month=1, day=8), + date_type(year=2022, month=1, day=8) + ]), + ( + COST_PER_DAY, + DELIVERY_DATA + ) + ) == 41 + + assert test_function( + DAYS_PER_WEEK, + set([ + date_type(year=2022, month=1, day=8), + date_type(year=2022, month=1, day=17) + ]), + ( + COST_PER_DAY, + DELIVERY_DATA + ) + ) == 41 + + assert test_function( + DAYS_PER_WEEK, + set([ + date_type(year=2022, month=1, day=2) + ]), + ( + COST_PER_DAY, + DELIVERY_DATA + ) + ) == 40 + + assert test_function( + DAYS_PER_WEEK, + set([ + date_type(year=2022, month=1, day=2), + date_type(year=2022, month=1, day=2) + ]), + ( + COST_PER_DAY, + DELIVERY_DATA + ) + ) == 40 + + assert test_function( + DAYS_PER_WEEK, + set([ + date_type(year=2022, month=1, day=6), + date_type(year=2022, month=1, day=7) + ]), + ( + COST_PER_DAY, + DELIVERY_DATA + ) + ) == 34 + + assert test_function( + DAYS_PER_WEEK, + set([ + date_type(year=2022, month=1, day=6), + date_type(year=2022, month=1, day=7), + date_type(year=2022, month=1, day=8) + ]), + ( + COST_PER_DAY, + DELIVERY_DATA + ) + ) == 34 + + assert test_function( + DAYS_PER_WEEK, + set([ + date_type(year=2022, month=1, day=6), + date_type(year=2022, month=1, day=7), + date_type(year=2022, month=1, day=7), + date_type(year=2022, month=1, day=7), + date_type(year=2022, month=1, day=8), + date_type(year=2022, month=1, day=8), + date_type(year=2022, month=1, day=8) + ]), + ( + COST_PER_DAY, + DELIVERY_DATA + ) + ) == 34 \ No newline at end of file From 27b1e6d9e708e3ef382ca679913eb2bf6d4d1006 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Thu, 5 May 2022 17:09:29 +0530 Subject: [PATCH 11/92] finish saving function --- npbc_core.py | 46 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/npbc_core.py b/npbc_core.py index 9ef104e..2def743 100644 --- a/npbc_core.py +++ b/npbc_core.py @@ -276,4 +276,48 @@ def calculate_cost_of_all_papers(undelivered_strings: dict[int, list[str]], mont # calculate the total cost of all papers total = sum(costs.values()) - return costs, total, undelivered_dates \ No newline at end of file + return costs, total, undelivered_dates + + +## save the results of undelivered dates to the DB + # save the dates any paper was not delivered +def save_results(costs: dict[int, float], undelivered_dates: dict[int, set[date_type]], month: int, year: int) -> None: + timestamp = datetime.now().strftime(r'%d/%m/%Y %I:%M:%S %p') + + with connect(DATABASE_PATH) as connection: + + # create log entries for each paper + log_ids = { + paper_id: connection.execute( + """ + INSERT INTO logs (paper_id, month, year, timestamp) + VALUES (?, ?, ?, ?) + RETURNING log_id; + """, + (paper_id, month, year, timestamp) + ).fetchone() + for paper_id in costs.keys() + } + + # create cost entries for each paper + for paper_id, log_id in log_ids.items(): + connection.execute( + """ + INSERT INTO cost_logs (log_id, cost) + VALUES (?, ?); + """, + (log_id, costs[paper_id]) + ) + + # create undelivered entries for each paper + for paper_id, dates in undelivered_dates.items(): + for date in dates: + connection.execute( + """ + INSERT INTO undelivered_logs (log_id, day_id) + VALUES (?, ?); + """, + (log_ids[paper_id], date.strftime("%Y-%m-%d")) + ) + + From d4beda7bad417d711216de97a1e6cc2d0af6d167 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Thu, 5 May 2022 17:14:18 +0530 Subject: [PATCH 12/92] format function yield --- npbc_core.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/npbc_core.py b/npbc_core.py index 2def743..7925010 100644 --- a/npbc_core.py +++ b/npbc_core.py @@ -321,3 +321,18 @@ def save_results(costs: dict[int, float], undelivered_dates: dict[int, set[date_ ) +## format the output of calculating the cost of all papers +def format_output(costs: dict[int, float], total: float, month: int, year: int) -> Generator[str, None, None]: + yield f"For {date_type(year=year, month=month, day=1).strftime(r'%B %Y')}\n" + yield f"*TOTAL*: {total}\n" + + with connect(DATABASE_PATH) as connection: + papers = { + row[0]: row[1] + for row in connection.execute("SELECT paper_id, name FROM papers;").fetchall() + } + + for paper_id, cost in costs.items(): + yield f"{papers[paper_id]}: {cost}" + + From 935ebb757db7d36ec087d0d3015550fc13cee7ba Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Thu, 5 May 2022 17:42:35 +0530 Subject: [PATCH 13/92] code rto add, edit, or delete papers --- npbc_core.py | 131 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 130 insertions(+), 1 deletion(-) diff --git a/npbc_core.py b/npbc_core.py index 7925010..1ce5190 100644 --- a/npbc_core.py +++ b/npbc_core.py @@ -32,7 +32,6 @@ def setup_and_connect_DB() -> None: with connect(DATABASE_PATH) as connection: connection.executescript(SCHEMA_PATH.read_text()) - connection.commit() ## generate a list of number of times each weekday occurs in a given month (return a generator) @@ -336,3 +335,133 @@ def format_output(costs: dict[int, float], total: float, month: int, year: int) yield f"{papers[paper_id]}: {cost}" +## add a new paper + # do not allow if the paper already exists +def add_new_paper(name: str, days_delivered: list[bool], days_cost: list[float]) -> None: + with connect(DATABASE_PATH) as connection: + + # check if the paper already exists + if connection.execute( + "SELECT EXISTS (SELECT 1 FROM papers WHERE name = ?);", + (name,)).fetchone()[0]: + raise ValueError(f"Paper \"{name}\" already exists.") + + # insert the paper + paper_id = connection.execute( + "INSERT INTO papers (name) VALUES (?) RETURNING paper_id;", + (name,) + ).fetchone()[0] + + # create days for the paper + paper_days = { + day_id: connection.execute( + "INSERT INTO days (paper_id, day_id) VALUES (?, ?) RETURNING paper_day_id;", + (paper_id, day_id) + ).fetchone()[0] + for day_id, _ in enumerate(days_delivered) + } + + # create cost entries for each day + for day_id, cost in enumerate(days_cost): + connection.execute( + "INSERT INTO papers_days_cost (paper_day_id, cost) VALUES (?, ?);", + (paper_days[day_id], cost) + ) + + # create delivered entries for each day + for day_id, delivered in enumerate(days_delivered): + connection.execute( + "INSERT INTO papers_days_delivered (paper_day_id, delivered) VALUES (?, ?);", + (paper_days[day_id], delivered) + ) + + +## edit an existing paper + # do not allow if the paper does not exist +def edit_existing_paper(paper_id: int, name: str | None = None, days_delivered: list[bool] | None = None, days_cost: list[float] | None = None) -> None: + with connect(DATABASE_PATH) as connection: + + # check if the paper exists + if not connection.execute( + "SELECT EXISTS (SELECT 1 FROM papers WHERE paper_id = ?);", + (paper_id,)).fetchone()[0]: + raise ValueError(f"Paper with ID {paper_id} does not exist.") + + # update the paper name + if name is not None: + connection.execute( + "UPDATE papers SET name = ? WHERE paper_id = ?;", + (name, paper_id) + ) + + # get the days for the paper + paper_days = { + row[0]: row[1] + for row in connection.execute( + "SELECT paper_day_id, day_id FROM papers_days WHERE paper_id = ?;", + (paper_id,) + ) + } + + # update the delivered data for the paper + if days_delivered is not None: + for day_id, delivered in enumerate(days_delivered): + connection.execute( + "UPDATE papers_days_delivered SET delivered = ? WHERE paper_day_id = ?;", + (delivered, paper_days[day_id]) + ) + + # update the days for the paper + if days_cost is not None: + for day_id, cost in enumerate(days_cost): + connection.execute( + "UPDATE papers_days_cost SET cost = ? WHERE paper_day_id = ?;", + (cost, paper_days[day_id]) + ) + + +## delete an existing paper + # do not allow if the paper does not exist +def delete_existing_paper(paper_id: int) -> None: + with connect(DATABASE_PATH) as connection: + + # check if the paper exists + if not connection.execute( + "SELECT EXISTS (SELECT 1 FROM papers WHERE paper_id = ?);", + (paper_id,)).fetchone()[0]: + raise ValueError(f"Paper with ID {paper_id} does not exist.") + + # delete the paper + connection.execute( + "DELETE FROM papers WHERE paper_id = ?;", + (paper_id,) + ) + + # get the days for the paper + paper_days = { + row[0]: row[1] + for row in connection.execute( + "SELECT paper_day_id, day_id FROM papers_days WHERE paper_id = ?;", + (paper_id,) + ) + } + + # delete the costs and delivery data for the paper + for paper_day_id in paper_days.values(): + connection.execute( + "DELETE FROM papers_days_cost WHERE paper_day_id = ?;", + (paper_day_id,) + ) + + connection.execute( + "DELETE FROM papers_days_delivered WHERE paper_day_id = ?;", + (paper_day_id,) + ) + + # delete the days for the paper + connection.execute( + "DELETE FROM days WHERE paper_id = ?;", + (paper_id,) + ) + + \ No newline at end of file From 7d391971b1ee4e4428bed884a4fab7ac89dc5125 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Thu, 5 May 2022 19:07:03 +0530 Subject: [PATCH 14/92] finish add/remove udl strings --- npbc_core.py | 136 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 114 insertions(+), 22 deletions(-) diff --git a/npbc_core.py b/npbc_core.py index 1ce5190..af1cb49 100644 --- a/npbc_core.py +++ b/npbc_core.py @@ -395,29 +395,30 @@ def edit_existing_paper(paper_id: int, name: str | None = None, days_delivered: ) # get the days for the paper - paper_days = { - row[0]: row[1] - for row in connection.execute( - "SELECT paper_day_id, day_id FROM papers_days WHERE paper_id = ?;", - (paper_id,) - ) - } - - # update the delivered data for the paper - if days_delivered is not None: - for day_id, delivered in enumerate(days_delivered): - connection.execute( - "UPDATE papers_days_delivered SET delivered = ? WHERE paper_day_id = ?;", - (delivered, paper_days[day_id]) + if (days_delivered is not None) or (days_cost is not None): + paper_days = { + row[0]: row[1] + for row in connection.execute( + "SELECT paper_day_id, day_id FROM papers_days WHERE paper_id = ?;", + (paper_id,) ) + } - # update the days for the paper - if days_cost is not None: - for day_id, cost in enumerate(days_cost): - connection.execute( - "UPDATE papers_days_cost SET cost = ? WHERE paper_day_id = ?;", - (cost, paper_days[day_id]) - ) + # update the delivered data for the paper + if days_delivered is not None: + for day_id, delivered in enumerate(days_delivered): + connection.execute( + "UPDATE papers_days_delivered SET delivered = ? WHERE paper_day_id = ?;", + (delivered, paper_days[day_id]) + ) + + # update the days for the paper + if days_cost is not None: + for day_id, cost in enumerate(days_cost): + connection.execute( + "UPDATE papers_days_cost SET cost = ? WHERE paper_day_id = ?;", + (cost, paper_days[day_id]) + ) ## delete an existing paper @@ -464,4 +465,95 @@ def delete_existing_paper(paper_id: int) -> None: (paper_id,) ) - \ No newline at end of file + +## record strings for date(s) paper(s) were not delivered + # if no paper ID is specified, all papers are assumed +def add_undelivered_string(month: int, year: int, paper_id: int | None = None, *undelivered_strings: str): + + # validate the strings + if not validate_undelivered_string(*undelivered_strings): + raise ValueError("Invalid string(s).") + + # if a paper ID is given + if paper_id: + + # check that specified paper exists in the database + with connect(DATABASE_PATH) as connection: + if not connection.execute( + "SELECT EXISTS (SELECT 1 FROM papers WHERE paper_id = ?);", + (paper_id,)).fetchone()[0]: + raise ValueError(f"Paper with ID {paper_id} does not exist.") + + # add the string(s) + params = [ + (month, year, paper_id, string) + for string in undelivered_strings + ] + + connection.executemany("INSERT INTO undelivered_strings (month, year, paper_id, string) VALUES (?, ?, ?, ?);", params) + + # if no paper ID is given + else: + + # get the IDs of all papers + with connect(DATABASE_PATH) as connection: + paper_ids = [ + row[0] + for row in connection.execute( + "SELECT paper_id FROM papers;" + ) + ] + + # add the string(s) + params = [ + (month, year, paper_id, string) + for paper_id in paper_ids + for string in undelivered_strings + ] + + connection.executemany("INSERT INTO undelivered_strings (month, year, paper_id, string) VALUES (?, ?, ?, ?);", params) + + +## delete an existing undelivered string + # do not allow if the string does not exist +def delete_undelivered_string(string_id: int | None = None, string: str | None = None, paper_id: int | None = None, month: int | None = None, year: int | None = None) -> None: + parameters = [] + values = () + + if month: + parameters.append("month") + values += (month,) + + if year: + parameters.append("year") + values += (year,) + + if paper_id: + parameters.append("paper_id") + values += (paper_id,) + + if string: + parameters.append("string") + values += (string,) + + if string_id: + parameters.append("string_id") + values += (string_id,) + + if not parameters: + raise ValueError("No parameters given.") + + with connect(DATABASE_PATH) as connection: + check_query = "SELECT EXISTS (SELECT 1 FROM undelivered_strings WHERE " + + conditions = ' AND '.join( + f"{parameter} = \"?\"" + for parameter in parameters + ) + ");" + + if not connection.execute(check_query + conditions, values).fetchall()[0]: + raise ValueError("String with given parameters does not exist.") + + delete_query = "DELETE FROM undelivered_strings WHERE " + + connection.execute(delete_query + conditions, values) \ No newline at end of file From 082d516b5342833b39bce18b0f1359f7d2a83b43 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Fri, 6 May 2022 01:03:23 +0530 Subject: [PATCH 15/92] add connection.close for DB --- npbc_core.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/npbc_core.py b/npbc_core.py index af1cb49..04d3b8c 100644 --- a/npbc_core.py +++ b/npbc_core.py @@ -2,10 +2,8 @@ from pathlib import Path from calendar import day_name as weekday_names_iterable, monthcalendar, monthrange from sqlite3 import Connection, connect -from textwrap import indent from typing import Generator import npbc_regex -from json import dumps ## paths for the folder containing schema and database files @@ -33,6 +31,8 @@ def setup_and_connect_DB() -> None: with connect(DATABASE_PATH) as connection: connection.executescript(SCHEMA_PATH.read_text()) + connection.close() + ## generate a list of number of times each weekday occurs in a given month (return a generator) # the list will be in the same order as WEEKDAY_NAMES (so the first day should be Monday) @@ -250,6 +250,8 @@ def calculate_cost_of_all_papers(undelivered_strings: dict[int, list[str]], mont for paper_id, in papers # type: ignore ] + connection.close() + # initialize a "blank" dictionary that will eventually contain any dates when a paper was not delivered undelivered_dates: dict[int, set[date_type]] = { paper_id: set() @@ -319,6 +321,8 @@ def save_results(costs: dict[int, float], undelivered_dates: dict[int, set[date_ (log_ids[paper_id], date.strftime("%Y-%m-%d")) ) + connection.close() + ## format the output of calculating the cost of all papers def format_output(costs: dict[int, float], total: float, month: int, year: int) -> Generator[str, None, None]: @@ -334,6 +338,8 @@ def format_output(costs: dict[int, float], total: float, month: int, year: int) for paper_id, cost in costs.items(): yield f"{papers[paper_id]}: {cost}" + connection.close() + ## add a new paper # do not allow if the paper already exists @@ -375,6 +381,8 @@ def add_new_paper(name: str, days_delivered: list[bool], days_cost: list[float]) (paper_days[day_id], delivered) ) + connection.close() + ## edit an existing paper # do not allow if the paper does not exist @@ -420,6 +428,8 @@ def edit_existing_paper(paper_id: int, name: str | None = None, days_delivered: (cost, paper_days[day_id]) ) + connection.close() + ## delete an existing paper # do not allow if the paper does not exist @@ -465,6 +475,8 @@ def delete_existing_paper(paper_id: int) -> None: (paper_id,) ) + connection.close() + ## record strings for date(s) paper(s) were not delivered # if no paper ID is specified, all papers are assumed @@ -492,6 +504,8 @@ def add_undelivered_string(month: int, year: int, paper_id: int | None = None, * connection.executemany("INSERT INTO undelivered_strings (month, year, paper_id, string) VALUES (?, ?, ?, ?);", params) + connection.close() + # if no paper ID is given else: @@ -513,6 +527,8 @@ def add_undelivered_string(month: int, year: int, paper_id: int | None = None, * connection.executemany("INSERT INTO undelivered_strings (month, year, paper_id, string) VALUES (?, ?, ?, ?);", params) + connection.close() + ## delete an existing undelivered string # do not allow if the string does not exist @@ -556,4 +572,6 @@ def delete_undelivered_string(string_id: int | None = None, string: str | None = delete_query = "DELETE FROM undelivered_strings WHERE " - connection.execute(delete_query + conditions, values) \ No newline at end of file + connection.execute(delete_query + conditions, values) + + connection.close() From 2459d2c113cb23dba6f785cd8bb62e7b12510183 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Fri, 6 May 2022 01:13:37 +0530 Subject: [PATCH 16/92] validation using exceptions --- npbc_core.py | 22 +++++++++--- test_core.py | 98 +++++++++++++++++++++++++++++++++------------------- 2 files changed, 79 insertions(+), 41 deletions(-) diff --git a/npbc_core.py b/npbc_core.py index 04d3b8c..6527aca 100644 --- a/npbc_core.py +++ b/npbc_core.py @@ -55,7 +55,7 @@ def get_number_of_each_weekday(month: int, year: int) -> Generator[int, None, No ## validate a string that specifies when a given paper was not delivered # first check to see that it meets the comma-separated requirements # then check against each of the other acceptable patterns in the regex dictionary -def validate_undelivered_string(*strings: str) -> bool: +def validate_undelivered_string(*strings: str) -> None: # check that the string matches one of the acceptable patterns for string in strings: if string and not ( @@ -66,10 +66,9 @@ def validate_undelivered_string(*strings: str) -> bool: npbc_regex.ALL_MATCH_REGEX.match(string) ): - return False + raise ValueError(f'{string} is not a valid undelivered string.') # if we get here, all strings passed the regex check - return True ## if the date is simply a number, it's a single day. so we just identify that date def extract_number(string: str, month: int, year: int) -> date_type | None: @@ -483,8 +482,7 @@ def delete_existing_paper(paper_id: int) -> None: def add_undelivered_string(month: int, year: int, paper_id: int | None = None, *undelivered_strings: str): # validate the strings - if not validate_undelivered_string(*undelivered_strings): - raise ValueError("Invalid string(s).") + validate_undelivered_string(*undelivered_strings) # if a paper ID is given if paper_id: @@ -575,3 +573,17 @@ def delete_undelivered_string(string_id: int | None = None, string: str | None = connection.execute(delete_query + conditions, values) connection.close() + + +## get the previous month, by looking at 1 day before the first day of the current month (duh) +def get_previous_month() -> date_type: + return (datetime.today().replace(day=1) - timedelta(days=1)).replace(day=1) + + +## validate month and year +def validate_month_and_year(month: int | None = None, year: int | None = None): + if (month is not None) and not (1 <= month <= 12): + raise ValueError("Month must be between 1 and 12.") + + if (year is not None) and (year <= 0): + raise ValueError("Year must be greater than 0.") \ No newline at end of file diff --git a/test_core.py b/test_core.py index f569b8a..17ef6cb 100644 --- a/test_core.py +++ b/test_core.py @@ -1,5 +1,6 @@ from datetime import date as date_type import npbc_core +from pytest import raises def test_get_number_of_each_weekday(): test_function = npbc_core.get_number_of_each_weekday @@ -14,41 +15,43 @@ def test_get_number_of_each_weekday(): def test_validate_undelivered_string(): test_function = npbc_core.validate_undelivered_string - assert test_function("") - assert not test_function("a") - assert not test_function("monday") - assert not test_function("1-mondays") - assert not test_function("1monday") - assert not test_function("1 monday") - assert not test_function("monday-1") - assert not test_function("monday-1") - assert test_function("1") - assert test_function("6") - assert test_function("31") - assert test_function("31","") - assert test_function("3","1") - assert test_function("3","1","") - assert test_function("3","1") - assert test_function("3","1") - assert test_function("3","1") - assert test_function("1","2","3-9") - assert test_function("1","2","3-9","11","12","13-19") - assert test_function("1","2","3-9","11","12","13-19","21","22","23-29") - assert test_function("1","2","3-9","11","12","13-19","21","22","23-29","31") - assert test_function("1","2","3","4","5","6","7","8","9") - assert test_function("mondays") - assert test_function("mondays,tuesdays") - assert test_function("mondays","tuesdays","wednesdays") - assert test_function("mondays","5-21") - assert test_function("mondays","5-21","tuesdays","5-21") - assert test_function("1-monday") - assert test_function("2-monday") - assert test_function("all") - assert test_function("All") - assert test_function("aLl") - assert test_function("alL") - assert test_function("aLL") - assert test_function("ALL") + with raises(ValueError): + test_function("a") + test_function("monday") + test_function("1-mondays") + test_function("1monday") + test_function("1 monday") + test_function("monday-1") + test_function("monday-1") + + test_function("") + test_function("1") + test_function("6") + test_function("31") + test_function("31","") + test_function("3","1") + test_function("3","1","") + test_function("3","1") + test_function("3","1") + test_function("3","1") + test_function("1","2","3-9") + test_function("1","2","3-9","11","12","13-19") + test_function("1","2","3-9","11","12","13-19","21","22","23-29") + test_function("1","2","3-9","11","12","13-19","21","22","23-29","31") + test_function("1","2","3","4","5","6","7","8","9") + test_function("mondays") + test_function("mondays,tuesdays") + test_function("mondays","tuesdays","wednesdays") + test_function("mondays","5-21") + test_function("mondays","5-21","tuesdays","5-21") + test_function("1-monday") + test_function("2-monday") + test_function("all") + test_function("All") + test_function("aLl") + test_function("alL") + test_function("aLL") + test_function("ALL") def test_undelivered_string_parsing(): @@ -318,4 +321,27 @@ def test_calculating_cost_of_one_paper(): COST_PER_DAY, DELIVERY_DATA ) - ) == 34 \ No newline at end of file + ) == 34 + + +def test_validate_month_and_year(): + test_function = npbc_core.validate_month_and_year + + test_function(1, 2020) + test_function(12, 2020) + test_function(1, 2021) + test_function(12, 2021) + test_function(1, 2022) + test_function(12, 2022) + + with raises(ValueError): + test_function(-54, 2020) + test_function(0, 2020) + test_function(13, 2020) + test_function(45, 2020) + test_function(1, -5) + test_function(12, -5) + test_function(1.6, 10) # type: ignore + test_function(12.6, 10) # type: ignore + test_function(1, '10') # type: ignore + test_function(12, '10') # type: ignore From ba8be96082be11e7900afa4e3a19e79a26101045 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Fri, 6 May 2022 01:39:49 +0530 Subject: [PATCH 17/92] add data retreival funcs --- npbc_core.py | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/npbc_core.py b/npbc_core.py index 6527aca..8f02db7 100644 --- a/npbc_core.py +++ b/npbc_core.py @@ -575,6 +575,80 @@ def delete_undelivered_string(string_id: int | None = None, string: str | None = connection.close() +## get all papers + # the user may specify parameters, either none or as many as they want + # available parameters: name, days_delivered, days_cost + # paper_id is always returned +def get_papers(name = None, days_delivered = None, days_cost = None) -> list[tuple[int, str]]: + parameters = [] + data = [] + + if name: + parameters.append("name") + + if days_delivered: + parameters.append("days_delivered") + + if days_cost: + parameters.append("days_cost") + + with connect(DATABASE_PATH) as connection: + query = f"SELECT paper_id, {', '.join(parameters)} FROM papers;" + + data = connection.execute(query).fetchall() + + connection.close() + + return data + + +## get undelivered strings + # the user may specify as many as they want parameters, but at least one + # available parameters: string_id, month, year, paper_id, string + # if no parameters are given, an error is raised +def get_undelivered_strings(string_id: int | None = None, month: int | None = None, year: int | None = None, paper_id: int | None = None, string: str | None = None) -> list[tuple[int, int, int, str]]: + parameters = [] + values = () + data = [] + + if string_id: + parameters.append("string_id") + values += (string_id,) + + if month: + parameters.append("month") + values += (month,) + + if year: + parameters.append("year") + values += (year,) + + if paper_id: + parameters.append("paper_id") + values += (paper_id,) + + if string: + parameters.append("string") + values += (string,) + + if not parameters: + raise ValueError("No parameters given.") + + with connect(DATABASE_PATH) as connection: + query = f"SELECT {', '.join(parameters)} FROM undelivered_strings WHERE " + + conditions = ' AND '.join( + f"{parameter} = \"?\"" + for parameter in parameters + ) + ";" + + data = connection.execute(query + conditions, values).fetchall() + + connection.close() + + return data + + ## get the previous month, by looking at 1 day before the first day of the current month (duh) def get_previous_month() -> date_type: return (datetime.today().replace(day=1) - timedelta(days=1)).replace(day=1) From 1cc4a3387a5ffbd75aafa41407b157a1bfa56aa2 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Fri, 6 May 2022 01:43:56 +0530 Subject: [PATCH 18/92] custom exceptions --- npbc_core.py | 25 +++++++++++++------------ npbc_exceptions.py | 9 +++++++++ 2 files changed, 22 insertions(+), 12 deletions(-) create mode 100644 npbc_exceptions.py diff --git a/npbc_core.py b/npbc_core.py index 8f02db7..6fc65e4 100644 --- a/npbc_core.py +++ b/npbc_core.py @@ -4,6 +4,7 @@ from sqlite3 import Connection, connect from typing import Generator import npbc_regex +import npbc_exceptions ## paths for the folder containing schema and database files @@ -66,7 +67,7 @@ def validate_undelivered_string(*strings: str) -> None: npbc_regex.ALL_MATCH_REGEX.match(string) ): - raise ValueError(f'{string} is not a valid undelivered string.') + raise npbc_exceptions.InvalidUndeliveredString(f'{string} is not a valid undelivered string.') # if we get here, all strings passed the regex check @@ -150,7 +151,7 @@ def parse_undelivered_string(month: int, year: int, string: str) -> set[date_typ dates.update(extract_all(month, year)) else: - raise ValueError + raise npbc_exceptions.InvalidUndeliveredString(f'{string} is not a valid undelivered string.') return dates @@ -167,7 +168,7 @@ def parse_undelivered_strings(month: int, year: int, *strings: str) -> set[date_ try: dates.update(parse_undelivered_string(month, year, string)) - except ValueError: + except npbc_exceptions.InvalidUndeliveredString: print( f"""Congratulations! You broke the program! You managed to write a string that the program considers valid, but isn't actually. @@ -349,7 +350,7 @@ def add_new_paper(name: str, days_delivered: list[bool], days_cost: list[float]) if connection.execute( "SELECT EXISTS (SELECT 1 FROM papers WHERE name = ?);", (name,)).fetchone()[0]: - raise ValueError(f"Paper \"{name}\" already exists.") + raise npbc_exceptions.PaperAlreadyExists(f"Paper \"{name}\" already exists.") # insert the paper paper_id = connection.execute( @@ -392,7 +393,7 @@ def edit_existing_paper(paper_id: int, name: str | None = None, days_delivered: if not connection.execute( "SELECT EXISTS (SELECT 1 FROM papers WHERE paper_id = ?);", (paper_id,)).fetchone()[0]: - raise ValueError(f"Paper with ID {paper_id} does not exist.") + raise npbc_exceptions.PaperNotExists(f"Paper with ID {paper_id} does not exist.") # update the paper name if name is not None: @@ -439,7 +440,7 @@ def delete_existing_paper(paper_id: int) -> None: if not connection.execute( "SELECT EXISTS (SELECT 1 FROM papers WHERE paper_id = ?);", (paper_id,)).fetchone()[0]: - raise ValueError(f"Paper with ID {paper_id} does not exist.") + raise npbc_exceptions.PaperNotExists(f"Paper with ID {paper_id} does not exist.") # delete the paper connection.execute( @@ -492,7 +493,7 @@ def add_undelivered_string(month: int, year: int, paper_id: int | None = None, * if not connection.execute( "SELECT EXISTS (SELECT 1 FROM papers WHERE paper_id = ?);", (paper_id,)).fetchone()[0]: - raise ValueError(f"Paper with ID {paper_id} does not exist.") + raise npbc_exceptions.PaperNotExists(f"Paper with ID {paper_id} does not exist.") # add the string(s) params = [ @@ -555,7 +556,7 @@ def delete_undelivered_string(string_id: int | None = None, string: str | None = values += (string_id,) if not parameters: - raise ValueError("No parameters given.") + raise npbc_exceptions.NoParameters("No parameters given.") with connect(DATABASE_PATH) as connection: check_query = "SELECT EXISTS (SELECT 1 FROM undelivered_strings WHERE " @@ -566,7 +567,7 @@ def delete_undelivered_string(string_id: int | None = None, string: str | None = ) + ");" if not connection.execute(check_query + conditions, values).fetchall()[0]: - raise ValueError("String with given parameters does not exist.") + raise npbc_exceptions.StringNotExists("String with given parameters does not exist.") delete_query = "DELETE FROM undelivered_strings WHERE " @@ -632,7 +633,7 @@ def get_undelivered_strings(string_id: int | None = None, month: int | None = No values += (string,) if not parameters: - raise ValueError("No parameters given.") + raise npbc_exceptions.NoParameters("No parameters given.") with connect(DATABASE_PATH) as connection: query = f"SELECT {', '.join(parameters)} FROM undelivered_strings WHERE " @@ -657,7 +658,7 @@ def get_previous_month() -> date_type: ## validate month and year def validate_month_and_year(month: int | None = None, year: int | None = None): if (month is not None) and not (1 <= month <= 12): - raise ValueError("Month must be between 1 and 12.") + raise npbc_exceptions.InvalidMonthYear("Month must be between 1 and 12.") if (year is not None) and (year <= 0): - raise ValueError("Year must be greater than 0.") \ No newline at end of file + raise npbc_exceptions.InvalidMonthYear("Year must be greater than 0.") \ No newline at end of file diff --git a/npbc_exceptions.py b/npbc_exceptions.py new file mode 100644 index 0000000..1bebafd --- /dev/null +++ b/npbc_exceptions.py @@ -0,0 +1,9 @@ +from sqlite3 import DatabaseError, OperationalError + +class InvalidUndeliveredString(ValueError): ... +class PaperAlreadyExists(OperationalError): ... +class PaperNotExists(OperationalError): ... +class StringNotExists(OperationalError): ... +class DatabaseFileError(DatabaseError): ... +class InvalidMonthYear(ValueError): ... +class NoParameters(ValueError): ... \ No newline at end of file From f380269104331cedc1aacc37969038df04d2340a Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Fri, 6 May 2022 01:45:17 +0530 Subject: [PATCH 19/92] exceptions in testing --- test_core.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test_core.py b/test_core.py index 17ef6cb..2d917b9 100644 --- a/test_core.py +++ b/test_core.py @@ -1,6 +1,7 @@ from datetime import date as date_type import npbc_core from pytest import raises +from npbc_exceptions import InvalidMonthYear, InvalidUndeliveredString def test_get_number_of_each_weekday(): test_function = npbc_core.get_number_of_each_weekday @@ -15,7 +16,7 @@ def test_get_number_of_each_weekday(): def test_validate_undelivered_string(): test_function = npbc_core.validate_undelivered_string - with raises(ValueError): + with raises(InvalidUndeliveredString): test_function("a") test_function("monday") test_function("1-mondays") @@ -334,7 +335,7 @@ def test_validate_month_and_year(): test_function(1, 2022) test_function(12, 2022) - with raises(ValueError): + with raises(InvalidMonthYear): test_function(-54, 2020) test_function(0, 2020) test_function(13, 2020) From 49c0c673757d698c01806fe5a804d03491c5ab47 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Fri, 6 May 2022 02:02:10 +0530 Subject: [PATCH 20/92] docstrings, formatting --- npbc_core.py | 167 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 112 insertions(+), 55 deletions(-) diff --git a/npbc_core.py b/npbc_core.py index 6fc65e4..de41f58 100644 --- a/npbc_core.py +++ b/npbc_core.py @@ -24,8 +24,10 @@ WEEKDAY_NAMES = list(weekday_names_iterable) -## ensure DB exists and it's set up with the schema + def setup_and_connect_DB() -> None: + """ensure DB exists and it's set up with the schema""" + DATABASE_DIR.mkdir(parents=True, exist_ok=True) DATABASE_PATH.touch(exist_ok=True) @@ -35,9 +37,10 @@ def setup_and_connect_DB() -> None: connection.close() -## generate a list of number of times each weekday occurs in a given month (return a generator) - # the list will be in the same order as WEEKDAY_NAMES (so the first day should be Monday) def get_number_of_each_weekday(month: int, year: int) -> Generator[int, None, None]: + """generate a list of number of times each weekday occurs in a given month (return a generator) + - the list will be in the same order as WEEKDAY_NAMES (so the first day should be Monday)""" + main_calendar = monthcalendar(year, month) number_of_weeks = len(main_calendar) @@ -53,10 +56,11 @@ def get_number_of_each_weekday(month: int, year: int) -> Generator[int, None, No yield number_of_weekday -## validate a string that specifies when a given paper was not delivered -# first check to see that it meets the comma-separated requirements -# then check against each of the other acceptable patterns in the regex dictionary def validate_undelivered_string(*strings: str) -> None: + """validate a string that specifies when a given paper was not delivered + - first check to see that it meets the comma-separated requirements + - then check against each of the other acceptable patterns in the regex dictionary""" + # check that the string matches one of the acceptable patterns for string in strings: if string and not ( @@ -71,16 +75,18 @@ def validate_undelivered_string(*strings: str) -> None: # if we get here, all strings passed the regex check -## if the date is simply a number, it's a single day. so we just identify that date def extract_number(string: str, month: int, year: int) -> date_type | None: + """if the date is simply a number, it's a single day. so we just identify that date""" + date = int(string) if date > 0 and date <= monthrange(year, month)[1]: return date_type(year, month, date) -## if the date is a range of numbers, it's a range of days. we identify all the dates in that range, bounds inclusive def extract_range(string: str, month: int, year: int) -> Generator[date_type, None, None]: + """if the date is a range of numbers, it's a range of days. we identify all the dates in that range, bounds inclusive""" + start, end = [int(date) for date in npbc_regex.HYPHEN_SPLIT_REGEX.split(string)] if 0 < start <= end <= monthrange(year, month)[1]: @@ -88,8 +94,9 @@ def extract_range(string: str, month: int, year: int) -> Generator[date_type, No yield date_type(year, month, date) -## if the date is the plural of a weekday name, we identify all dates in that month which are the given weekday def extract_weekday(string: str, month: int, year: int) -> Generator[date_type, None, None]: + """if the date is the plural of a weekday name, we identify all dates in that month which are the given weekday""" + weekday = WEEKDAY_NAMES.index(string.capitalize().rstrip('s')) for day in range(1, monthrange(year, month)[1] + 1): @@ -97,8 +104,9 @@ def extract_weekday(string: str, month: int, year: int) -> Generator[date_type, yield date_type(year, month, day) -## if the date is a number and a weekday name (singular), we identify the date that is the nth occurrence of the given weekday in the month def extract_nth_weekday(string: str, month: int, year: int) -> date_type | None: + """if the date is a number and a weekday name (singular), we identify the date that is the nth occurrence of the given weekday in the month""" + n, weekday_name = npbc_regex.HYPHEN_SPLIT_REGEX.split(string) n = int(n) @@ -115,16 +123,18 @@ def extract_nth_weekday(string: str, month: int, year: int) -> date_type | None: return valid_dates[n - 1] -## if the text is "all", we identify all the dates in the month def extract_all(month: int, year: int) -> Generator[date_type, None, None]: + """if the text is "all", we identify all the dates in the month""" + for day in range(1, monthrange(year, month)[1] + 1): yield date_type(year, month, day) -## parse a section of the strings - # each section is a string that specifies a set of dates - # this function will return a set of dates that uniquely identifies each date mentioned across the string def parse_undelivered_string(month: int, year: int, string: str) -> set[date_type]: + """parse a section of the strings + - each section is a string that specifies a set of dates + - this function will return a set of dates that uniquely identifies each date mentioned across the string""" + # initialize the set of dates dates = set() @@ -156,10 +166,11 @@ def parse_undelivered_string(month: int, year: int, string: str) -> set[date_typ return dates -## parse a string that specifies when a given paper was not delivered - # each section states some set of dates - # this function will return a set of dates that uniquely identifies each date mentioned across all the strings def parse_undelivered_strings(month: int, year: int, *strings: str) -> set[date_type]: + """parse a string that specifies when a given paper was not delivered + - each section states some set of dates + - this function will return a set of dates that uniquely identifies each date mentioned across all the strings""" + # initialize the set of dates dates = set() @@ -180,10 +191,11 @@ def parse_undelivered_strings(month: int, year: int, *strings: str) -> set[date_ return dates -## get the cost and delivery data for a given paper from the DB - # each of them are converted to a dictionary, whose index is the day_id - # the two dictionaries are then returned as a tuple def get_cost_and_delivery_data(paper_id: int, connection: Connection) -> tuple[dict[int, float], dict[int, bool]]: + """get the cost and delivery data for a given paper from the DB + - each of them are converted to a dictionary, whose index is the day_id + - the two dictionaries are then returned as a tuple""" + query = """ SELECT papers_days.day_id, papers_days_delivered.delivered, papers_days_cost.cost FROM papers_days @@ -209,9 +221,14 @@ def get_cost_and_delivery_data(paper_id: int, connection: Connection) -> tuple[d return cost_dict, delivered_dict -## calculate the cost of one paper for the full month - # any dates when it was not delivered will be removed -def calculate_cost_of_one_paper(number_of_each_weekday: list[int], undelivered_dates: set[date_type], cost_and_delivered_data: tuple[dict[int, float], dict[int, bool]]) -> float: +def calculate_cost_of_one_paper( + number_of_each_weekday: list[int], + undelivered_dates: set[date_type], + cost_and_delivered_data: tuple[dict[int, float], dict[int, bool]] + ) -> float: + """calculate the cost of one paper for the full month + - any dates when it was not delivered will be removed""" + cost_data, delivered_data = cost_and_delivered_data # initialize counters corresponding to each weekday when the paper was not delivered @@ -234,9 +251,14 @@ def calculate_cost_of_one_paper(number_of_each_weekday: list[int], undelivered_d ) -## calculate the cost of all papers for the full month - # return data about the cost of each paper, the total cost, and dates when each paper was not delivered -def calculate_cost_of_all_papers(undelivered_strings: dict[int, list[str]], month: int, year: int) -> tuple[dict[int, float], float, dict[int, set[date_type]]]: +def calculate_cost_of_all_papers(undelivered_strings: dict[int, list[str]], month: int, year: int) -> tuple[ + dict[int, float], + float, + dict[int, set[date_type]] +]: + """calculate the cost of all papers for the full month + - return data about the cost of each paper, the total cost, and dates when each paper was not delivered""" + NUMBER_OF_EACH_WEEKDAY = list(get_number_of_each_weekday(month, year)) cost_and_delivery_data = [] @@ -280,9 +302,16 @@ def calculate_cost_of_all_papers(undelivered_strings: dict[int, list[str]], mont return costs, total, undelivered_dates -## save the results of undelivered dates to the DB - # save the dates any paper was not delivered -def save_results(costs: dict[int, float], undelivered_dates: dict[int, set[date_type]], month: int, year: int) -> None: +def save_results( + costs: dict[int, float], + undelivered_dates: dict[int, set[date_type]], + month: int, + year: int +) -> None: + """save the results of undelivered dates to the DB + - save the dates any paper was not delivered + - save the final cost of each paper""" + timestamp = datetime.now().strftime(r'%d/%m/%Y %I:%M:%S %p') with connect(DATABASE_PATH) as connection: @@ -324,8 +353,9 @@ def save_results(costs: dict[int, float], undelivered_dates: dict[int, set[date_ connection.close() -## format the output of calculating the cost of all papers def format_output(costs: dict[int, float], total: float, month: int, year: int) -> Generator[str, None, None]: + """format the output of calculating the cost of all papers""" + yield f"For {date_type(year=year, month=month, day=1).strftime(r'%B %Y')}\n" yield f"*TOTAL*: {total}\n" @@ -341,9 +371,10 @@ def format_output(costs: dict[int, float], total: float, month: int, year: int) connection.close() -## add a new paper - # do not allow if the paper already exists def add_new_paper(name: str, days_delivered: list[bool], days_cost: list[float]) -> None: + """add a new paper + - do not allow if the paper already exists""" + with connect(DATABASE_PATH) as connection: # check if the paper already exists @@ -384,9 +415,15 @@ def add_new_paper(name: str, days_delivered: list[bool], days_cost: list[float]) connection.close() -## edit an existing paper - # do not allow if the paper does not exist -def edit_existing_paper(paper_id: int, name: str | None = None, days_delivered: list[bool] | None = None, days_cost: list[float] | None = None) -> None: +def edit_existing_paper( + paper_id: int, + name: str | None = None, + days_delivered: list[bool] | None = None, + days_cost: list[float] | None = None +) -> None: + """edit an existing paper + do not allow if the paper does not exist""" + with connect(DATABASE_PATH) as connection: # check if the paper exists @@ -431,9 +468,10 @@ def edit_existing_paper(paper_id: int, name: str | None = None, days_delivered: connection.close() -## delete an existing paper - # do not allow if the paper does not exist def delete_existing_paper(paper_id: int) -> None: + """delete an existing paper + - do not allow if the paper does not exist""" + with connect(DATABASE_PATH) as connection: # check if the paper exists @@ -478,9 +516,9 @@ def delete_existing_paper(paper_id: int) -> None: connection.close() -## record strings for date(s) paper(s) were not delivered - # if no paper ID is specified, all papers are assumed def add_undelivered_string(month: int, year: int, paper_id: int | None = None, *undelivered_strings: str): + """record strings for date(s) paper(s) were not delivered + - if no paper ID is specified, all papers are assumed""" # validate the strings validate_undelivered_string(*undelivered_strings) @@ -529,9 +567,16 @@ def add_undelivered_string(month: int, year: int, paper_id: int | None = None, * connection.close() -## delete an existing undelivered string - # do not allow if the string does not exist -def delete_undelivered_string(string_id: int | None = None, string: str | None = None, paper_id: int | None = None, month: int | None = None, year: int | None = None) -> None: +def delete_undelivered_string( + string_id: int | None = None, + string: str | None = None, + paper_id: int | None = None, + month: int | None = None, + year: int | None = None +) -> None: + """delete an existing undelivered string + - do not allow if the string does not exist""" + parameters = [] values = () @@ -576,11 +621,12 @@ def delete_undelivered_string(string_id: int | None = None, string: str | None = connection.close() -## get all papers - # the user may specify parameters, either none or as many as they want - # available parameters: name, days_delivered, days_cost - # paper_id is always returned def get_papers(name = None, days_delivered = None, days_cost = None) -> list[tuple[int, str]]: + """get all papers + - the user may specify parameters, either none or as many as they want + - available parameters: name, days_delivered, days_cost + - paper_id is always returned""" + parameters = [] data = [] @@ -603,11 +649,18 @@ def get_papers(name = None, days_delivered = None, days_cost = None) -> list[tup return data -## get undelivered strings - # the user may specify as many as they want parameters, but at least one - # available parameters: string_id, month, year, paper_id, string - # if no parameters are given, an error is raised -def get_undelivered_strings(string_id: int | None = None, month: int | None = None, year: int | None = None, paper_id: int | None = None, string: str | None = None) -> list[tuple[int, int, int, str]]: +def get_undelivered_strings( + string_id: int | None = None, + month: int | None = None, + year: int | None = None, + paper_id: int | None = None, + string: str | None = None +) -> list[tuple[int, int, int, str]]: + """get undelivered strings + - the user may specify as many as they want parameters, but at least one + - available parameters: string_id, month, year, paper_id, string + - if no parameters are given, an error is raised""" + parameters = [] values = () data = [] @@ -650,15 +703,19 @@ def get_undelivered_strings(string_id: int | None = None, month: int | None = No return data -## get the previous month, by looking at 1 day before the first day of the current month (duh) def get_previous_month() -> date_type: + """get the previous month, by looking at 1 day before the first day of the current month (duh)""" + return (datetime.today().replace(day=1) - timedelta(days=1)).replace(day=1) -## validate month and year def validate_month_and_year(month: int | None = None, year: int | None = None): - if (month is not None) and not (1 <= month <= 12): + """validate month and year + - month must be an integer between 1 and 12 inclusive + - year must be an integer greater than 0""" + + if isinstance(month, int) and not (1 <= month <= 12): raise npbc_exceptions.InvalidMonthYear("Month must be between 1 and 12.") - if (year is not None) and (year <= 0): + if isinstance(year, int) and (year <= 0): raise npbc_exceptions.InvalidMonthYear("Year must be greater than 0.") \ No newline at end of file From 238a186bde12f6e59fe8ee81209e750d499ae668 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Fri, 6 May 2022 02:46:07 +0530 Subject: [PATCH 21/92] create parsers --- npbc_cli.py | 139 +++++++++++++++++++++++++++++++++++++++++++++++++++ npbc_core.py | 4 +- 2 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 npbc_cli.py diff --git a/npbc_cli.py b/npbc_cli.py new file mode 100644 index 0000000..6235453 --- /dev/null +++ b/npbc_cli.py @@ -0,0 +1,139 @@ +from argparse import ArgumentParser, Namespace as ArgNamespace +from datetime import datetime +from colorama import Fore, Style +import npbc_core + + +def define_and_read_args() -> ArgNamespace: + """configure parsers + - define the main parser for the application executable + - define subparsers (one for each functionality) + - parse the arguments""" + + # main parser for all commands + main_parser = ArgumentParser( + prog="npbc", + description="Calculates your monthly newspaper bill." + ) + functions = main_parser.add_subparsers(required=True) + + + # calculate subparser + calculate_parser = functions.add_parser( + 'calculate', + help="Calculate the bill for one month. Previous month will be used if month or year flags are not set." + ) + + calculate_parser.set_defaults(func=calculate) + calculate_parser.add_argument('-m', '--month', type=int, help="Month to calculate bill for. Must be between 1 and 12.") + calculate_parser.add_argument('-y', '--year', type=int, help="Year to calculate bill for. Must be greater than 0.") + calculate_parser.add_argument('-c', '--nocopy', help="Don't copy the result of the calculation to the clipboard.", action='store_true') + calculate_parser.add_argument('-l', '--nolog', help="Don't log the result of the calculation.", action='store_true') + + + # add undelivered string subparser + addudl_parser = functions.add_parser( + 'addudl', + help="Store a date when paper(s) were not delivered. Current month will be used if month or year flags are not set." + ) + + addudl_parser.set_defaults(func=addudl) + addudl_parser.add_argument('-m', '--month', type=int, help="Month to register undelivered incident(s) for. Must be between 1 and 12.") + addudl_parser.add_argument('-y', '--year', type=int, help="Year to register undelivered incident(s) for. Must be greater than 0.") + addudl_parser.add_argument('-i', '--id', type=str, help="ID of paper to register undelivered incident(s) for.", required=True) + addudl_parser.add_argument('-u', '--undelivered', type=str, help="Dates when you did not receive any papers.", required=True, nargs='+') + + + # delete undelivered string subparser + deludl_parser = functions.add_parser( + 'deludl', + help="Delete a stored date when paper(s) were not delivered. If no parameters are provided, the function will not default; it will throw an error instead." + ) + + deludl_parser.set_defaults(func=deludl) + deludl_parser.add_argument('-i', '--id', type=str, help="ID of paper to unregister undelivered incident(s) for.") + deludl_parser.add_argument('-m', '--month', type=int, help="Month to unregister undelivered incident(s) for. Must be between 1 and 12.") + deludl_parser.add_argument('-y', '--year', type=int, help="Year to unregister undelivered incident(s) for. Must be greater than 0.") + + + # get undelivered string subparser + getudl_parser = functions.add_parser( + 'getudl', + help="Get a list of all stored date strings when paper(s) were not delivered. All parameters are optional and act as filters." + ) + + getudl_parser.set_defaults(func=getudl) + getudl_parser.add_argument('-i', '--id', type=str, help="ID for paper.") + getudl_parser.add_argument('-m', '--month', type=int, help="Month. Must be between 1 and 12.") + getudl_parser.add_argument('-y', '--year', type=int, help="Year. Must be greater than 0.") + getudl_parser.add_argument('-u', '--undelivered', type=str, help="Dates when you did not receive any papers.") + + + # edit paper subparser + editpaper_parser = functions.add_parser( + 'editpaper', + help="Edit a newspaper's name, days delivered, and/or price." + ) + + editpaper_parser.set_defaults(func=editpaper) + editpaper_parser.add_argument('-n', '--name', type=str, help="Name for paper to be edited.") + editpaper_parser.add_argument('-d', '--days', type=str, help="Number of days the paper to be edited is delivered. All seven weekdays are required. A 'Y' means it is delivered, and an 'N' means it isn't. No separator required.") + editpaper_parser.add_argument('-p', '--price', type=str, help="Daywise prices of paper to be edited. Values must be separated by semicolons, and 0s are ignored.") + editpaper_parser.add_argument('-i', '--id', type=str, help="ID for paper to be edited.", required=True) + + + # add paper subparser + addpaper_parser = functions.add_parser( + 'addpaper', + help="Add a new newspaper to the list of newspapers." + ) + + addpaper_parser.set_defaults(func=addpaper) + addpaper_parser.add_argument('-n', '--name', type=str, help="Name for paper to be added.", required=True) + addpaper_parser.add_argument('-d', '--days', type=str, help="Number of days the paper to be added is delivered. All seven weekdays are required. A 'Y' means it is delivered, and an 'N' means it isn't. No separator required.", required=True) + addpaper_parser.add_argument('-p', '--price', type=str, help="Daywise prices of paper to be added. Values must be separated by semicolons, and 0s are ignored.", required=True) + + + # delete paper subparser + delpaper_parser = functions.add_parser( + 'delpaper', + help="Delete a newspaper from the list of newspapers." + ) + + delpaper_parser.set_defaults(func=delpaper) + delpaper_parser.add_argument('-i', '--id', type=str, help="ID for paper to be deleted.", required=True) + + # get paper subparser + getpapers_parser = functions.add_parser( + 'getpapers', + help="Get all newspapers." + ) + + getpapers_parser.set_defaults(func=getpapers) + getpapers_parser.add_argument('-n', '--names', help="Get the names of the newspapers.", action='store_true') + getpapers_parser.add_argument('-d', '--days', help="Get the days the newspapers are delivered. All seven weekdays are required. A 'Y' means it is delivered, and an 'N' means it isn't.", action='store_true') + getpapers_parser.add_argument('-p', '--price', help="Get the daywise prices of the newspapers. Values must be separated by semicolons.", action='store_true') + + # get undelivered logs subparser + getlogs_parser = functions.add_parser( + 'getlogs', + help="Get the log of all undelivered dates." + ) + + getlogs_parser.set_defaults(func=getlogs) + getlogs_parser.add_argument('-m', '--month', type=int, help="Month. Must be between 1 and 12.") + getlogs_parser.add_argument('-y', '--year', type=int, help="Year. Must be greater than 0.") + getlogs_parser.add_argument('-i', '--id', type=str, help="ID for paper.", required=True) + getlogs_parser.add_argument('-u', '--undelivered', action='store_true', help="Get the undelivered dates.") + getlogs_parser.add_argument('-p', '--price', action='store_true', help="Get the daywise prices.") + + # update application subparser + update_parser = functions.add_parser( + 'update', + help="Update the application." + ) + + update_parser.set_defaults(func=update) + + + return main_parser.parse_args() \ No newline at end of file diff --git a/npbc_core.py b/npbc_core.py index de41f58..e5b25c4 100644 --- a/npbc_core.py +++ b/npbc_core.py @@ -516,7 +516,7 @@ def delete_existing_paper(paper_id: int) -> None: connection.close() -def add_undelivered_string(month: int, year: int, paper_id: int | None = None, *undelivered_strings: str): +def add_undelivered_string(month: int, year: int, paper_id: int | None = None, *undelivered_strings: str) -> None: """record strings for date(s) paper(s) were not delivered - if no paper ID is specified, all papers are assumed""" @@ -709,7 +709,7 @@ def get_previous_month() -> date_type: return (datetime.today().replace(day=1) - timedelta(days=1)).replace(day=1) -def validate_month_and_year(month: int | None = None, year: int | None = None): +def validate_month_and_year(month: int | None = None, year: int | None = None) -> None: """validate month and year - month must be an integer between 1 and 12 inclusive - year must be an integer greater than 0""" From b49f22b7b272c0571662b789de2c2725cdf3dfa5 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Fri, 6 May 2022 02:48:23 +0530 Subject: [PATCH 22/92] colorama --- npbc_cli.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/npbc_cli.py b/npbc_cli.py index 6235453..6f1866f 100644 --- a/npbc_cli.py +++ b/npbc_cli.py @@ -136,4 +136,15 @@ def define_and_read_args() -> ArgNamespace: update_parser.set_defaults(func=update) - return main_parser.parse_args() \ No newline at end of file + return main_parser.parse_args() + + +def status_print(status: bool, message: str) -> None: + """print out a coloured status message using Colorama""" + + if status: + print(f"{Fore.GREEN}", end="") + else: + print(f"{Fore.RED}", end="") + + print(f"{Style.BRIGHT}{message}{Style.RESET_ALL}\n") \ No newline at end of file From 7ff8632ebe2788a06097c8d546d061cb3d63ffc5 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Fri, 6 May 2022 03:34:06 +0530 Subject: [PATCH 23/92] make some progress on CLI --- npbc_cli.py | 170 ++++++++++++++++++++++++++++++++++++++++++++++++++- npbc_core.py | 17 ++++-- 2 files changed, 179 insertions(+), 8 deletions(-) diff --git a/npbc_cli.py b/npbc_cli.py index 6f1866f..218b764 100644 --- a/npbc_cli.py +++ b/npbc_cli.py @@ -2,6 +2,7 @@ from datetime import datetime from colorama import Fore, Style import npbc_core +import npbc_exceptions def define_and_read_args() -> ArgNamespace: @@ -34,13 +35,14 @@ def define_and_read_args() -> ArgNamespace: # add undelivered string subparser addudl_parser = functions.add_parser( 'addudl', - help="Store a date when paper(s) were not delivered. Current month will be used if month or year flags are not set." + help="Store a date when paper(s) were not delivered. Current month will be used if month or year flags are not set. Either paper ID must be provided, or the all flag must be set." ) addudl_parser.set_defaults(func=addudl) addudl_parser.add_argument('-m', '--month', type=int, help="Month to register undelivered incident(s) for. Must be between 1 and 12.") addudl_parser.add_argument('-y', '--year', type=int, help="Year to register undelivered incident(s) for. Must be greater than 0.") - addudl_parser.add_argument('-i', '--id', type=str, help="ID of paper to register undelivered incident(s) for.", required=True) + addudl_parser.add_argument('-i', '--id', type=str, help="ID of paper to register undelivered incident(s) for.") + addudl_parser.add_argument('-a', '--all', help="Register undelivered incidents for all papers.", action='store_true') addudl_parser.add_argument('-u', '--undelivered', type=str, help="Dates when you did not receive any papers.", required=True, nargs='+') @@ -52,8 +54,10 @@ def define_and_read_args() -> ArgNamespace: deludl_parser.set_defaults(func=deludl) deludl_parser.add_argument('-i', '--id', type=str, help="ID of paper to unregister undelivered incident(s) for.") + deludl_parser.add_argument('-s', '--strid', type=str, help="String ID of paper to unregister undelivered incident(s) for.") deludl_parser.add_argument('-m', '--month', type=int, help="Month to unregister undelivered incident(s) for. Must be between 1 and 12.") deludl_parser.add_argument('-y', '--year', type=int, help="Year to unregister undelivered incident(s) for. Must be greater than 0.") + deludl_parser.add_argument('-u', '--undelivered', type=str, help="Dates when you did not receive any papers.") # get undelivered string subparser @@ -64,6 +68,7 @@ def define_and_read_args() -> ArgNamespace: getudl_parser.set_defaults(func=getudl) getudl_parser.add_argument('-i', '--id', type=str, help="ID for paper.") + deludl_parser.add_argument('-s', '--strid', type=str, help="String ID of paper to unregister undelivered incident(s) for.") getudl_parser.add_argument('-m', '--month', type=int, help="Month. Must be between 1 and 12.") getudl_parser.add_argument('-y', '--year', type=int, help="Year. Must be greater than 0.") getudl_parser.add_argument('-u', '--undelivered', type=str, help="Dates when you did not receive any papers.") @@ -147,4 +152,163 @@ def status_print(status: bool, message: str) -> None: else: print(f"{Fore.RED}", end="") - print(f"{Style.BRIGHT}{message}{Style.RESET_ALL}\n") \ No newline at end of file + print(f"{Style.BRIGHT}{message}{Style.RESET_ALL}\n") + + +def calculate(args: ArgNamespace) -> None: + """calculate the cost for a given month and year + - default to the previous month if no month and no year is given + - default to the current month if no month is given and year is given + - default to the current year if no year is given and month is given""" + + # deal with month and year + if args.month or args.year: + + try: + npbc_core.validate_month_and_year(args.month, args.year) + + except npbc_exceptions.InvalidMonthYear: + status_print(False, "Invalid month and/or year.") + return + + month = args.month or datetime.now().month + year = args.year or datetime.now().year + + else: + previous_month = npbc_core.get_previous_month() + month = previous_month.month + year = previous_month.year + + undelivered_strings = { + int(paper_id[0]): [] + for paper_id in npbc_core.get_papers() + } + + try: + raw_undelivered_strings = npbc_core.get_undelivered_strings(month=month, year=year) + + for _, paper_id, _, _, string in raw_undelivered_strings: + undelivered_strings[paper_id].append(string) + + except npbc_exceptions.StringNotExists: + pass + + costs, total, undelivered_dates = npbc_core.calculate_cost_of_all_papers( + undelivered_strings, + month, + year + ) + + # format the results + formatted = '\n'.join(npbc_core.format_output(costs, total, month, year)) + + # unless the user specifies so, log the results to the database + if not args.nolog: + npbc_core.save_results(costs, undelivered_dates, month, year) + + formatted += '\nLog saved to file.' + + # print the results + status_print(True, "Success!") + print(f"SUMMARY:\n{formatted}") + + +def addudl(args: ArgNamespace) -> None: + """add undelivered strings to the database + - default to the current month if no month and/or no year is given""" + + try: + npbc_core.validate_month_and_year(args.month, args.year) + + except npbc_exceptions.InvalidMonthYear: + status_print(False, "Invalid month and/or year.") + return + + month = args.month or datetime.now().month + year = args.year or datetime.now().year + + if args.id or args.all: + + try: + npbc_core.add_undelivered_string(month, year, paper_id=args.id, *args.undelivered) + + except npbc_exceptions.PaperNotExists: + status_print(False, f"Paper with ID {args.id} does not exist.") + return + + except npbc_exceptions.InvalidUndeliveredString: + status_print(False, "Invalid undelivered string(s).") + return + + else: + status_print(False, "No paper(s) specified.") + + status_print(True, "Success!") + + +def deludl(args: ArgNamespace) -> None: + """delete undelivered strings from the database""" + + try: + npbc_core.validate_month_and_year(args.month, args.year) + + except npbc_exceptions.InvalidMonthYear: + status_print(False, "Invalid month and/or year.") + return + + try: + npbc_core.delete_undelivered_string( + month=args.month, + year=args.year, + paper_id=args.id, + string=args.string + string_id=args.string_id + ) + + except npbc_exceptions.NoParameters: + status_print(False, "No parameters specified.") + return + + except npbc_exceptions.StringNotExists: + status_print(False, "String does not exist.") + return + + status_print(True, "Success!") + + +def getudl(args: ArgNamespace) -> None: + """get undelivered strings from the database + filter by whichever parameter the user provides. they as many as they want. + available parameters: month, year, paper_id, string_id, string""" + + try: + npbc_core.validate_month_and_year(args.month, args.year) + + except npbc_exceptions.InvalidMonthYear: + status_print(False, "Invalid month and/or year.") + return + + try: + undelivered_strings = npbc_core.get_undelivered_strings( + month=args.month, + year=args.year, + paper_id=args.id, + string_id=args.strid, + string=args.string + ) + + except npbc_exceptions.NoParameters: + status_print(False, "No parameters specified.") + return + + except npbc_exceptions.StringNotExists: + status_print(False, "No strings found for the given parameters.") + return + + # format the results + status_print(True, "Success!") + + print(f"{Fore.YELLOW}string_id{Style.RESET_ALL} | {Fore.YELLOW}paper_id{Style.RESET_ALL} | {Fore.YELLOW}year{Style.RESET_ALL} | {Fore.YELLOW}month{Style.RESET_ALL} | {Fore.YELLOW}string{Style.RESET_ALL}") + + for items in undelivered_strings: + print('|'.join([str(item) for item in items])) diff --git a/npbc_core.py b/npbc_core.py index e5b25c4..ee01f1c 100644 --- a/npbc_core.py +++ b/npbc_core.py @@ -621,7 +621,7 @@ def delete_undelivered_string( connection.close() -def get_papers(name = None, days_delivered = None, days_cost = None) -> list[tuple[int, str]]: +def get_papers(name = None, days_delivered = None, days_cost = None) -> list[tuple[int | str]]: """get all papers - the user may specify parameters, either none or as many as they want - available parameters: name, days_delivered, days_cost @@ -640,7 +640,7 @@ def get_papers(name = None, days_delivered = None, days_cost = None) -> list[tup parameters.append("days_cost") with connect(DATABASE_PATH) as connection: - query = f"SELECT paper_id, {', '.join(parameters)} FROM papers;" + query = f"SELECT paper_id{',' if parameters else ''} {', '.join(parameters)} FROM papers;" data = connection.execute(query).fetchall() @@ -655,7 +655,7 @@ def get_undelivered_strings( year: int | None = None, paper_id: int | None = None, string: str | None = None -) -> list[tuple[int, int, int, str]]: +) -> list[tuple[int, int, int, int, str]]: """get undelivered strings - the user may specify as many as they want parameters, but at least one - available parameters: string_id, month, year, paper_id, string @@ -689,7 +689,7 @@ def get_undelivered_strings( raise npbc_exceptions.NoParameters("No parameters given.") with connect(DATABASE_PATH) as connection: - query = f"SELECT {', '.join(parameters)} FROM undelivered_strings WHERE " + query = f"SELECT string_id, paper_id, year, month, string FROM undelivered_strings WHERE " conditions = ' AND '.join( f"{parameter} = \"?\"" @@ -700,6 +700,9 @@ def get_undelivered_strings( connection.close() + if not data: + raise npbc_exceptions.StringNotExists("String with given parameters does not exist.") + return data @@ -718,4 +721,8 @@ def validate_month_and_year(month: int | None = None, year: int | None = None) - raise npbc_exceptions.InvalidMonthYear("Month must be between 1 and 12.") if isinstance(year, int) and (year <= 0): - raise npbc_exceptions.InvalidMonthYear("Year must be greater than 0.") \ No newline at end of file + raise npbc_exceptions.InvalidMonthYear("Year must be greater than 0.") + + +if __name__ == "__main__": + print(get_papers()) \ No newline at end of file From 5fff007e6758f15e8db1e52fbbdda39057e38a11 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Fri, 6 May 2022 17:27:21 +0530 Subject: [PATCH 24/92] some more progress --- npbc_cli.py | 116 +++++++++++++++++++++++++++++++++++++++++++-- npbc_core.py | 37 ++++++--------- npbc_exceptions.py | 5 +- 3 files changed, 129 insertions(+), 29 deletions(-) diff --git a/npbc_cli.py b/npbc_cli.py index 218b764..d05be5b 100644 --- a/npbc_cli.py +++ b/npbc_cli.py @@ -3,6 +3,7 @@ from colorama import Fore, Style import npbc_core import npbc_exceptions +from npbc_regex import DAYS_MATCH_REGEX def define_and_read_args() -> ArgNamespace: @@ -83,7 +84,7 @@ def define_and_read_args() -> ArgNamespace: editpaper_parser.set_defaults(func=editpaper) editpaper_parser.add_argument('-n', '--name', type=str, help="Name for paper to be edited.") editpaper_parser.add_argument('-d', '--days', type=str, help="Number of days the paper to be edited is delivered. All seven weekdays are required. A 'Y' means it is delivered, and an 'N' means it isn't. No separator required.") - editpaper_parser.add_argument('-p', '--price', type=str, help="Daywise prices of paper to be edited. Values must be separated by semicolons, and 0s are ignored.") + editpaper_parser.add_argument('-p', '--price', type=str, help="Daywise prices of paper to be edited. 0s are ignored.", nargs='*') editpaper_parser.add_argument('-i', '--id', type=str, help="ID for paper to be edited.", required=True) @@ -96,7 +97,7 @@ def define_and_read_args() -> ArgNamespace: addpaper_parser.set_defaults(func=addpaper) addpaper_parser.add_argument('-n', '--name', type=str, help="Name for paper to be added.", required=True) addpaper_parser.add_argument('-d', '--days', type=str, help="Number of days the paper to be added is delivered. All seven weekdays are required. A 'Y' means it is delivered, and an 'N' means it isn't. No separator required.", required=True) - addpaper_parser.add_argument('-p', '--price', type=str, help="Daywise prices of paper to be added. Values must be separated by semicolons, and 0s are ignored.", required=True) + addpaper_parser.add_argument('-p', '--price', type=str, help="Daywise prices of paper to be added. 0s are ignored.", required=True, nargs=7) # delete paper subparser @@ -180,8 +181,8 @@ def calculate(args: ArgNamespace) -> None: year = previous_month.year undelivered_strings = { - int(paper_id[0]): [] - for paper_id in npbc_core.get_papers() + int(paper_id): [] + for paper_id, _, _, _, _ in npbc_core.get_papers() } try: @@ -312,3 +313,110 @@ def getudl(args: ArgNamespace) -> None: for items in undelivered_strings: print('|'.join([str(item) for item in items])) + + +def extract_delivery_from_user_input(input_delivery: str) -> list[bool]: + """convert the /[YN]{7}/ user input to a Boolean list""" + + if not DAYS_MATCH_REGEX.match(input_delivery): + raise npbc_exceptions.InvalidInput + + return [ + day == 'Y' + for day in input_delivery + ] + + +def extract_costs_from_user_input(*input_costs: float) -> list[float]: + """convert the user input to a float list""" + + return [ + cost + for cost in input_costs + if cost != 0 + ] + + +def editpaper(args: ArgNamespace) -> None: + """edit a paper's information""" + try: + npbc_core.edit_existing_paper( + paper_id=args.id, + name=args.name, + days_delivered=extract_delivery_from_user_input(args.days_delivered), + days_cost=extract_costs_from_user_input(args.days_cost) + ) + + except npbc_exceptions.PaperNotExists: + status_print(False, "Paper does not exist.") + return + + except npbc_exceptions.InvalidInput as e: + status_print(False, f"Invalid input: {e}") + return + + status_print(True, "Success!") + + +def addpaper(args: ArgNamespace) -> None: + """add a new paper to the database""" + + try: + npbc_core.add_new_paper( + name=args.name, + days_delivered=extract_delivery_from_user_input(args.days_delivered), + days_cost=extract_costs_from_user_input(args.days_cost) + ) + + except npbc_exceptions.InvalidInput as e: + status_print(False, f"Invalid input: {e}") + return + + except npbc_exceptions.PaperAlreadyExists: + status_print(False, "Paper already exists.") + return + + status_print(True, "Success!") + + +def delpaper(args: ArgNamespace) -> None: + """delete a paper from the database""" + + try: + npbc_core.delete_existing_paper(args.id) + + except npbc_exceptions.PaperNotExists: + status_print(False, "Paper does not exist.") + return + + status_print(True, "Success!") + + +def getpapers(args: ArgNamespace) -> None: + """get a list of all papers in the database + - filter by whichever parameter the user provides. they may use as many as they want (but keys are always printed) + - available parameters: name, days, costs + - the output is provided as a formatted table, printed to the standard output""" + + try: + papers = npbc_core.get_papers() + + except npbc_exceptions.DatabaseError as e: + status_print(False, f"Database error: {e}\nPlease report this to the developer.") + return + + headers = ['paper_id'] + + # format the results + status_print(True, "Success!") + + if args.name: + headers.append('name') + + if args.days: + headers.append('days') + + if args.price: + headers.append('price') + + \ No newline at end of file diff --git a/npbc_core.py b/npbc_core.py index ee01f1c..d4cc0cb 100644 --- a/npbc_core.py +++ b/npbc_core.py @@ -621,32 +621,27 @@ def delete_undelivered_string( connection.close() -def get_papers(name = None, days_delivered = None, days_cost = None) -> list[tuple[int | str]]: +def get_papers() -> list[tuple[int, str, int, int, int]]: """get all papers - - the user may specify parameters, either none or as many as they want - - available parameters: name, days_delivered, days_cost - - paper_id is always returned""" + - returns a list of tuples containing the following fields: + paper_id, paper_name, day_id, paper_delivered, paper_cost""" - parameters = [] - data = [] - - if name: - parameters.append("name") + raw_data = [] - if days_delivered: - parameters.append("days_delivered") - - if days_cost: - parameters.append("days_cost") + query = """ + SELECT papers.paper_id, papers.name, papers_days.day_id, papers_days_delivered.delivered, papers_days_cost.cost + FROM papers + LEFT JOIN papers_days ON papers.paper_id = papers_days.paper_id + LEFT JOIN papers_days_delivered ON papers_days.paper_day_id = papers_days_delivered.paper_day_id + LEFT JOIN papers_days_cost ON papers_days.paper_day_id = papers_days_cost.paper_day_id; + """ with connect(DATABASE_PATH) as connection: - query = f"SELECT paper_id{',' if parameters else ''} {', '.join(parameters)} FROM papers;" - - data = connection.execute(query).fetchall() + raw_data = connection.execute(query).fetchall() connection.close() - return data + return raw_data def get_undelivered_strings( @@ -721,8 +716,4 @@ def validate_month_and_year(month: int | None = None, year: int | None = None) - raise npbc_exceptions.InvalidMonthYear("Month must be between 1 and 12.") if isinstance(year, int) and (year <= 0): - raise npbc_exceptions.InvalidMonthYear("Year must be greater than 0.") - - -if __name__ == "__main__": - print(get_papers()) \ No newline at end of file + raise npbc_exceptions.InvalidMonthYear("Year must be greater than 0.") \ No newline at end of file diff --git a/npbc_exceptions.py b/npbc_exceptions.py index 1bebafd..8561310 100644 --- a/npbc_exceptions.py +++ b/npbc_exceptions.py @@ -1,9 +1,10 @@ from sqlite3 import DatabaseError, OperationalError -class InvalidUndeliveredString(ValueError): ... +class InvalidInput(ValueError): ... +class InvalidUndeliveredString(InvalidInput): ... class PaperAlreadyExists(OperationalError): ... class PaperNotExists(OperationalError): ... class StringNotExists(OperationalError): ... class DatabaseFileError(DatabaseError): ... -class InvalidMonthYear(ValueError): ... +class InvalidMonthYear(InvalidInput): ... class NoParameters(ValueError): ... \ No newline at end of file From 18ab30bbf247087e2be4551ab88d6e817307facf Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Sat, 7 May 2022 16:04:34 +0530 Subject: [PATCH 25/92] completed cli? --- npbc_cli.py | 133 ++++++++++++++++++++++++++++++++++++++++----- npbc_core.py | 60 +++++++++++++++++++- npbc_exceptions.py | 3 +- npbc_regex.py | 3 - 4 files changed, 178 insertions(+), 21 deletions(-) diff --git a/npbc_cli.py b/npbc_cli.py index d05be5b..c0fffc7 100644 --- a/npbc_cli.py +++ b/npbc_cli.py @@ -1,5 +1,8 @@ from argparse import ArgumentParser, Namespace as ArgNamespace -from datetime import datetime +from datetime import datetime, date as date_type +from json import dumps +import sqlite3 +from unicodedata import name from colorama import Fore, Style import npbc_core import npbc_exceptions @@ -127,11 +130,11 @@ def define_and_read_args() -> ArgNamespace: ) getlogs_parser.set_defaults(func=getlogs) + getlogs_parser.add_argument('-i', '--id', type=str, help="ID for paper.", required=True) getlogs_parser.add_argument('-m', '--month', type=int, help="Month. Must be between 1 and 12.") getlogs_parser.add_argument('-y', '--year', type=int, help="Year. Must be greater than 0.") - getlogs_parser.add_argument('-i', '--id', type=str, help="ID for paper.", required=True) - getlogs_parser.add_argument('-u', '--undelivered', action='store_true', help="Get the undelivered dates.") - getlogs_parser.add_argument('-p', '--price', action='store_true', help="Get the daywise prices.") + getlogs_parser.add_argument('-t' , '--timestamp', type=str, help="Timestamp. Must be in the format dd/mm/yyyy hh:mm:ss AM/PM.") + # update application subparser update_parser = functions.add_parser( @@ -262,7 +265,7 @@ def deludl(args: ArgNamespace) -> None: month=args.month, year=args.year, paper_id=args.id, - string=args.string + string=args.string, string_id=args.string_id ) @@ -392,31 +395,131 @@ def delpaper(args: ArgNamespace) -> None: status_print(True, "Success!") -def getpapers(args: ArgNamespace) -> None: +def getpapers(args: ArgNamespace): """get a list of all papers in the database - filter by whichever parameter the user provides. they may use as many as they want (but keys are always printed) - available parameters: name, days, costs - the output is provided as a formatted table, printed to the standard output""" try: - papers = npbc_core.get_papers() + raw_data = npbc_core.get_papers() - except npbc_exceptions.DatabaseError as e: + except sqlite3.DatabaseError as e: status_print(False, f"Database error: {e}\nPlease report this to the developer.") return headers = ['paper_id'] + ids = [] + delivery = {} + costs = {} + names = {} - # format the results - status_print(True, "Success!") + ids = list(set(paper[0] for paper in raw_data)) + ids.sort() if args.name: headers.append('name') - if args.days: - headers.append('days') + names = [ + (paper_id, name) + for paper_id, name, _, _, _ in raw_data + ] + + names.sort(key=lambda item: item[0]) + names = [name for _, name in names] + + if args.days or args.price: + days = { + paper_id: {} + for paper_id in ids + } + + for paper_id, _, day_id, _, _ in raw_data: + days[paper_id][day_id] = {} + + for paper_id, _, day_id, day_delivery, day_cost in raw_data: + days[paper_id][day_id]['delivery'] = day_delivery + days[paper_id][day_id]['cost'] = day_cost + + if args.days: + headers.append('days') + + delivery = [ + ''.join([ + 'Y' if days[paper_id][day_id]['delivery'] else 'N' + for day_id in enumerate(npbc_core.WEEKDAY_NAMES) + ]) + for paper_id in ids + ] + + if args.price: + headers.append('costs') + + costs = [ + ';'.join([ + str(days[paper_id][day_id]['cost']) + for day_id, cost in enumerate(npbc_core.WEEKDAY_NAMES) + if days[paper_id][day_id]['cost'] != 0 + ]) + for paper_id in ids + ] + + print(' | '.join([ + f"{Fore.YELLOW}{header}{Style.RESET_ALL}" + for header in headers + ])) + + # print the data + for paper_id, name, delivered, cost in zip(ids, names, delivery, costs): + print(paper_id) + + if names: + print(f", {name}", end='') + + if delivery: + print(f", {delivered}", end='') + + if costs: + print(f", {cost}", end='') + + print() + + +def getlogs(args: ArgNamespace): + """get a list of all logs in the database + - filter by whichever parameter the user provides. they may use as many as they want (but log IDs are always printed) + - available parameters: paper_id, month, year, timestamp + - will return both date logs and cost logs""" + + try: + data = npbc_core.get_logged_data( + paper_id=args.paper_id, + month=args.month, + year=args.year, + timestamp=datetime.strptime(args.timestamp, r'%d/%m/%Y %I:%M:%S %p') + ) + + except sqlite3.DatabaseError: + status_print(False, "Database error. Please report this to the developer.") + return + + except ValueError: + status_print(False, "Invalid date format. Please use the following format: dd/mm/yyyy hh:mm:ss AM/PM") + return + + print(' | '.join( + f"{Fore.YELLOW}{header}{Style.RESET_ALL}" + for header in ['log_id', 'paper_id', 'month', 'year', 'timestamp', 'date', 'cost'] + )) + + # print the data + for row in data: + print(', '.join(str(item) for item in row)) + - if args.price: - headers.append('price') +def update(args: ArgNamespace): + """update the application + - under normal operation, this function should never run + - if the update CLI argument is provided, this script will never run and the updater will be run instead""" - \ No newline at end of file + status_print(False, "Update failed.") \ No newline at end of file diff --git a/npbc_core.py b/npbc_core.py index d4cc0cb..99f0e8a 100644 --- a/npbc_core.py +++ b/npbc_core.py @@ -621,7 +621,7 @@ def delete_undelivered_string( connection.close() -def get_papers() -> list[tuple[int, str, int, int, int]]: +def get_papers() -> list[tuple[int, str, int, int, float]]: """get all papers - returns a list of tuples containing the following fields: paper_id, paper_name, day_id, paper_delivered, paper_cost""" @@ -701,6 +701,64 @@ def get_undelivered_strings( return data +def get_logged_data( + paper_id: int | None = None, + log_id: int | None = None, + month: int | None = None, + year: int | None = None, + timestamp: date_type | None = None +): + """get logged data + - the user may specify as parameters many as they want + - available parameters: paper_id, log_id, month, year, timestamp + - returns: a list of tuples containing the following fields: + log_id, paper_id, month, year, timestamp, date, cost.""" + + data = [] + parameters = [] + values = () + + if paper_id: + parameters.append("paper_id") + values += (paper_id,) + + if log_id: + parameters.append("log_id") + values += (log_id,) + + if month: + parameters.append("month") + values += (month,) + + if year: + parameters.append("year") + values += (year,) + + if timestamp: + parameters.append("timestamp") + values += (timestamp.strftime(r'%d/%m/%Y %I:%M:%S %p'),) + + query = """ + SELECT logs.log_id, logs.paper_id, logs.month, logs.year, logs.timestamp, undelivered_dates_logs.date_not_delivered, cost_logs.cost + FROM logs + LEFT JOIN undelivered_dates_logs ON logs.log_id = undelivered_dates_logs.log_id + LEFT JOIN cost_logs ON logs.log_id = cost_logs.log_id + WHERE """ + + conditions = ' AND '.join( + f"{parameter} = \"?\"" + for parameter in parameters + ) + ";" + + with connect(DATABASE_PATH) as connection: + data = connection.execute(query + conditions, values).fetchall() + + connection.close() + + return data + + + def get_previous_month() -> date_type: """get the previous month, by looking at 1 day before the first day of the current month (duh)""" diff --git a/npbc_exceptions.py b/npbc_exceptions.py index 8561310..f1eaa2b 100644 --- a/npbc_exceptions.py +++ b/npbc_exceptions.py @@ -1,10 +1,9 @@ -from sqlite3 import DatabaseError, OperationalError +from sqlite3 import OperationalError class InvalidInput(ValueError): ... class InvalidUndeliveredString(InvalidInput): ... class PaperAlreadyExists(OperationalError): ... class PaperNotExists(OperationalError): ... class StringNotExists(OperationalError): ... -class DatabaseFileError(DatabaseError): ... class InvalidMonthYear(InvalidInput): ... class NoParameters(ValueError): ... \ No newline at end of file diff --git a/npbc_regex.py b/npbc_regex.py index 5a74765..7861fb0 100644 --- a/npbc_regex.py +++ b/npbc_regex.py @@ -22,9 +22,6 @@ # match for the text "all" in any case. ALL_MATCH_REGEX = compile_regex(r'^[aA][lL]{2}$') -# match for real values, delimited by semicolons. each value must be either an integer or a float with a decimal point. spaces are allowed between values and semicolons, and up to 7 (but at least 1) values are allowed. -COSTS_MATCH_REGEX = compile_regex(r'^\d+(\.\d+)?( *; *\d+(\.\d+)?){0,6} *;?$') - # match for seven values, each of which must be a 'Y' or an 'N'. there are no delimiters. DELIVERY_MATCH_REGEX = compile_regex(r'^[YN]{7}$') From ea51afbd385e5c4f1287c51a4d679d5ae4a24054 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Sat, 7 May 2022 20:51:37 +0530 Subject: [PATCH 26/92] fin up to testing del udl --- .gitignore | 1 + npbc_core.py | 143 +++++++++------- test.sql | 31 ++++ test_db.py | 245 ++++++++++++++++++++++++++++ test_npbc_regex.py => test_regex.py | 27 --- 5 files changed, 361 insertions(+), 86 deletions(-) create mode 100644 test.sql create mode 100644 test_db.py rename test_npbc_regex.py => test_regex.py (87%) diff --git a/.gitignore b/.gitignore index 692e90d..1c29774 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ bin .vscode .pytest_cache data/*.db +.env diff --git a/npbc_core.py b/npbc_core.py index 99f0e8a..a3f0e5b 100644 --- a/npbc_core.py +++ b/npbc_core.py @@ -5,29 +5,31 @@ from typing import Generator import npbc_regex import npbc_exceptions - +from os import environ ## paths for the folder containing schema and database files - # during normal use, the DB will be in ~/.npbc (where ~ is the user's home directory) and the schema will be bundled with the executable - # during development, the DB and schema will both be in "data" - -DATABASE_DIR = Path().home() / '.npbc' # normal use path -DATABASE_DIR = Path('data') # development path +# during normal use, the DB will be in ~/.npbc (where ~ is the user's home directory) and the schema will be bundled with the executable +# during development, the DB and schema will both be in "data" -DATABASE_PATH = DATABASE_DIR / 'npbc.db' +# default to PRODUCTION +DATABASE_DIR = Path.home() / '.npbc' +SCHEMA_PATH = Path(__file__).parent / 'schema.sql' -SCHEMA_PATH = Path(__file__).parent / 'schema.sql' # normal use path -SCHEMA_PATH = DATABASE_DIR / 'schema.sql' # development path +# if in a development environment, set the paths to the data folder +if str(environ.get('NPBC_DEVELOPMENT')) == "1" or str(environ.get('CI')) == "true": + DATABASE_DIR = Path(__file__).parent / 'data' + SCHEMA_PATH = Path('data') / 'schema.sql' +DATABASE_PATH = DATABASE_DIR / 'npbc.db' ## list constant for names of weekdays WEEKDAY_NAMES = list(weekday_names_iterable) - - def setup_and_connect_DB() -> None: """ensure DB exists and it's set up with the schema""" + global DATABASE_DIR, DATABASE_PATH, SCHEMA_PATH + DATABASE_DIR.mkdir(parents=True, exist_ok=True) DATABASE_PATH.touch(exist_ok=True) @@ -259,6 +261,8 @@ def calculate_cost_of_all_papers(undelivered_strings: dict[int, list[str]], mont """calculate the cost of all papers for the full month - return data about the cost of each paper, the total cost, and dates when each paper was not delivered""" + global DATABASE_PATH + NUMBER_OF_EACH_WEEKDAY = list(get_number_of_each_weekday(month, year)) cost_and_delivery_data = [] @@ -312,6 +316,8 @@ def save_results( - save the dates any paper was not delivered - save the final cost of each paper""" + global DATABASE_PATH + timestamp = datetime.now().strftime(r'%d/%m/%Y %I:%M:%S %p') with connect(DATABASE_PATH) as connection: @@ -356,6 +362,8 @@ def save_results( def format_output(costs: dict[int, float], total: float, month: int, year: int) -> Generator[str, None, None]: """format the output of calculating the cost of all papers""" + global DATABASE_PATH + yield f"For {date_type(year=year, month=month, day=1).strftime(r'%B %Y')}\n" yield f"*TOTAL*: {total}\n" @@ -375,6 +383,8 @@ def add_new_paper(name: str, days_delivered: list[bool], days_cost: list[float]) """add a new paper - do not allow if the paper already exists""" + global DATABASE_PATH + with connect(DATABASE_PATH) as connection: # check if the paper already exists @@ -392,7 +402,7 @@ def add_new_paper(name: str, days_delivered: list[bool], days_cost: list[float]) # create days for the paper paper_days = { day_id: connection.execute( - "INSERT INTO days (paper_id, day_id) VALUES (?, ?) RETURNING paper_day_id;", + "INSERT INTO papers_days (paper_id, day_id) VALUES (?, ?) RETURNING paper_day_id;", (paper_id, day_id) ).fetchone()[0] for day_id, _ in enumerate(days_delivered) @@ -424,6 +434,8 @@ def edit_existing_paper( """edit an existing paper do not allow if the paper does not exist""" + global DATABASE_PATH + with connect(DATABASE_PATH) as connection: # check if the paper exists @@ -444,7 +456,7 @@ def edit_existing_paper( paper_days = { row[0]: row[1] for row in connection.execute( - "SELECT paper_day_id, day_id FROM papers_days WHERE paper_id = ?;", + "SELECT day_id, paper_day_id FROM papers_days WHERE paper_id = ?;", (paper_id,) ) } @@ -472,6 +484,8 @@ def delete_existing_paper(paper_id: int) -> None: """delete an existing paper - do not allow if the paper does not exist""" + global DATABASE_PATH + with connect(DATABASE_PATH) as connection: # check if the paper exists @@ -487,16 +501,13 @@ def delete_existing_paper(paper_id: int) -> None: ) # get the days for the paper - paper_days = { - row[0]: row[1] - for row in connection.execute( - "SELECT paper_day_id, day_id FROM papers_days WHERE paper_id = ?;", - (paper_id,) - ) - } + paper_days = [ + row[0] + for row in connection.execute("SELECT paper_day_id FROM papers_days WHERE paper_id = ?;", (paper_id,)) + ] # delete the costs and delivery data for the paper - for paper_day_id in paper_days.values(): + for paper_day_id in paper_days: connection.execute( "DELETE FROM papers_days_cost WHERE paper_day_id = ?;", (paper_day_id,) @@ -509,7 +520,7 @@ def delete_existing_paper(paper_id: int) -> None: # delete the days for the paper connection.execute( - "DELETE FROM days WHERE paper_id = ?;", + "DELETE FROM papers_days WHERE paper_id = ?;", (paper_id,) ) @@ -520,6 +531,8 @@ def add_undelivered_string(month: int, year: int, paper_id: int | None = None, * """record strings for date(s) paper(s) were not delivered - if no paper ID is specified, all papers are assumed""" + global DATABASE_PATH + # validate the strings validate_undelivered_string(*undelivered_strings) @@ -577,46 +590,48 @@ def delete_undelivered_string( """delete an existing undelivered string - do not allow if the string does not exist""" + global DATABASE_PATH + parameters = [] - values = () + values = [] - if month: - parameters.append("month") - values += (month,) + if string_id: + parameters.append("string_id") + values.append(string_id) - if year: - parameters.append("year") - values += (year,) + if string: + parameters.append("string") + values.append(string) if paper_id: parameters.append("paper_id") - values += (paper_id,) + values.append(paper_id) - if string: - parameters.append("string") - values += (string,) + if month: + parameters.append("month") + values.append(month) - if string_id: - parameters.append("string_id") - values += (string_id,) + if year: + parameters.append("year") + values.append(year) if not parameters: raise npbc_exceptions.NoParameters("No parameters given.") with connect(DATABASE_PATH) as connection: - check_query = "SELECT EXISTS (SELECT 1 FROM undelivered_strings WHERE " + check_query = "SELECT EXISTS (SELECT 1 FROM undelivered_strings" conditions = ' AND '.join( - f"{parameter} = \"?\"" + f"{parameter} = ?" for parameter in parameters - ) + ");" + ) - if not connection.execute(check_query + conditions, values).fetchall()[0]: + if (1,) not in connection.execute(f"{check_query} WHERE {conditions});", values).fetchall(): raise npbc_exceptions.StringNotExists("String with given parameters does not exist.") - delete_query = "DELETE FROM undelivered_strings WHERE " + delete_query = "DELETE FROM undelivered_strings" - connection.execute(delete_query + conditions, values) + connection.execute(f"{delete_query} WHERE {conditions};", values) connection.close() @@ -626,6 +641,8 @@ def get_papers() -> list[tuple[int, str, int, int, float]]: - returns a list of tuples containing the following fields: paper_id, paper_name, day_id, paper_delivered, paper_cost""" + global DATABASE_PATH + raw_data = [] query = """ @@ -652,47 +669,53 @@ def get_undelivered_strings( string: str | None = None ) -> list[tuple[int, int, int, int, str]]: """get undelivered strings - - the user may specify as many as they want parameters, but at least one + - the user may specify as many as they want parameters - available parameters: string_id, month, year, paper_id, string - - if no parameters are given, an error is raised""" + - returns a list of tuples containing the following fields: + string_id, paper_id, year, month, string""" + + global DATABASE_PATH parameters = [] - values = () + values = [] data = [] if string_id: parameters.append("string_id") - values += (string_id,) + values.append(string_id) if month: parameters.append("month") - values += (month,) + values.append(month) if year: parameters.append("year") - values += (year,) + values.append(year) if paper_id: parameters.append("paper_id") - values += (paper_id,) + values.append(paper_id) if string: parameters.append("string") - values += (string,) + values.append(string) - if not parameters: - raise npbc_exceptions.NoParameters("No parameters given.") with connect(DATABASE_PATH) as connection: - query = f"SELECT string_id, paper_id, year, month, string FROM undelivered_strings WHERE " + main_query = "SELECT string_id, paper_id, year, month, string FROM undelivered_strings" + + if not parameters: + query = f"{main_query};" - conditions = ' AND '.join( - f"{parameter} = \"?\"" - for parameter in parameters - ) + ";" + else: + conditions = ' AND '.join( + f"{parameter} = ?" + for parameter in parameters + ) - data = connection.execute(query + conditions, values).fetchall() + query = f"{main_query} WHERE {conditions};" + data = connection.execute(query, values).fetchall() connection.close() if not data: @@ -714,6 +737,8 @@ def get_logged_data( - returns: a list of tuples containing the following fields: log_id, paper_id, month, year, timestamp, date, cost.""" + global DATABASE_PATH + data = [] parameters = [] values = () @@ -746,7 +771,7 @@ def get_logged_data( WHERE """ conditions = ' AND '.join( - f"{parameter} = \"?\"" + f"{parameter} = ?" for parameter in parameters ) + ";" diff --git a/test.sql b/test.sql new file mode 100644 index 0000000..0f7e419 --- /dev/null +++ b/test.sql @@ -0,0 +1,31 @@ +INSERT INTO papers (name) +VALUES + ('paper1'), + ('paper2'), + ('paper3'); + +INSERT INTO papers_days (paper_id, day_id) +VALUES + (1, 0), (1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), + (2, 0), (2, 1), (2, 2), (2, 3), (2, 4), (2, 5), (2, 6), + (3, 0), (3, 1), (3, 2), (3, 3), (3, 4), (3, 5), (3, 6); + +INSERT INTO papers_days_delivered (paper_day_id, delivered) +VALUES + (01, 0), (02, 1), (03, 0), (04, 0), (05, 0), (06, 1), (07, 1), + (08, 0), (09, 0), (10, 0), (11, 0), (12, 1), (13, 0), (14, 1), + (15, 1), (16, 1), (17, 0), (18, 0), (19, 1), (20, 1), (21, 1); + +INSERT INTO papers_days_cost (paper_day_id, cost) +VALUES + (01, 0.0), (02, 6.4), (03, 0.0), (04, 0.0), (05, 0.0), (06, 7.9), (07, 4.0), + (08, 0.0), (09, 0.0), (10, 0.0), (11, 0.0), (12, 3.4), (13, 0.0), (14, 8.4), + (15, 2.4), (16, 4.6), (17, 0.0), (18, 0.0), (19, 3.4), (20, 4.6), (21, 6.0); + +INSERT INTO undelivered_strings (year, month, paper_id, string) +VALUES + (2020, 11, 1, '5'), + (2020, 11, 1, '6-12'), + (2020, 11, 2, 'sundays'), + (2020, 11, 3, '2-tuesday'), + (2020, 10, 3, 'all'); \ No newline at end of file diff --git a/test_db.py b/test_db.py new file mode 100644 index 0000000..f2fa4cf --- /dev/null +++ b/test_db.py @@ -0,0 +1,245 @@ +from sqlite3 import connect +from pathlib import Path +from typing import Counter +import npbc_core, npbc_exceptions +from pytest import raises + +DATABASE_PATH = Path("data") / "npbc.db" +SCHEMA_PATH = Path("data") / "schema.sql" +TEST_SQL = Path("test.sql") + + +def setup_db(database_path: Path, schema_path: Path, test_sql: Path): + DATABASE_PATH.unlink(missing_ok=True) + + with connect(database_path) as connection: + connection.executescript(schema_path.read_text()) + connection.commit() + connection.executescript(test_sql.read_text()) + + connection.close() + + +def test_get_papers(): + setup_db(DATABASE_PATH, SCHEMA_PATH, TEST_SQL) + + known_data = [ + (1, 'paper1', 0, 0, 0), + (1, 'paper1', 1, 1, 6.4), + (1, 'paper1', 2, 0, 0), + (1, 'paper1', 3, 0, 0), + (1, 'paper1', 4, 0, 0), + (1, 'paper1', 5, 1, 7.9), + (1, 'paper1', 6, 1, 4), + (2, 'paper2', 0, 0, 0), + (2, 'paper2', 1, 0, 0), + (2, 'paper2', 2, 0, 0), + (2, 'paper2', 3, 0, 0), + (2, 'paper2', 4, 1, 3.4), + (2, 'paper2', 5, 0, 0), + (2, 'paper2', 6, 1, 8.4), + (3, 'paper3', 0, 1, 2.4), + (3, 'paper3', 1, 1, 4.6), + (3, 'paper3', 2, 0, 0), + (3, 'paper3', 3, 0, 0), + (3, 'paper3', 4, 1, 3.4), + (3, 'paper3', 5, 1, 4.6), + (3, 'paper3', 6, 1, 6) + ] + + assert Counter(npbc_core.get_papers()) == Counter(known_data) + + +def test_get_undelivered_strings(): + setup_db(DATABASE_PATH, SCHEMA_PATH, TEST_SQL) + + known_data = [ + (1, 1, 2020, 11, '5'), + (2, 1, 2020, 11, '6-12'), + (3, 2, 2020, 11, 'sundays'), + (4, 3, 2020, 11, '2-tuesday'), + (5, 3, 2020, 10, 'all') + ] + + assert Counter(npbc_core.get_undelivered_strings()) == Counter(known_data) + assert Counter(npbc_core.get_undelivered_strings(string_id=3)) == Counter([known_data[2]]) + assert Counter(npbc_core.get_undelivered_strings(month=11)) == Counter(known_data[:4]) + assert Counter(npbc_core.get_undelivered_strings(paper_id=1)) == Counter(known_data[:2]) + assert Counter(npbc_core.get_undelivered_strings(paper_id=1, string='6-12')) == Counter([known_data[1]]) + + with raises(npbc_exceptions.StringNotExists): + npbc_core.get_undelivered_strings(year=1986) + + +def test_delete_paper(): + setup_db(DATABASE_PATH, SCHEMA_PATH, TEST_SQL) + + npbc_core.delete_existing_paper(2) + + known_data = [ + (1, 'paper1', 0, 0, 0), + (1, 'paper1', 1, 1, 6.4), + (1, 'paper1', 2, 0, 0), + (1, 'paper1', 3, 0, 0), + (1, 'paper1', 4, 0, 0), + (1, 'paper1', 5, 1, 7.9), + (1, 'paper1', 6, 1, 4), + (3, 'paper3', 0, 1, 2.4), + (3, 'paper3', 1, 1, 4.6), + (3, 'paper3', 2, 0, 0), + (3, 'paper3', 3, 0, 0), + (3, 'paper3', 4, 1, 3.4), + (3, 'paper3', 5, 1, 4.6), + (3, 'paper3', 6, 1, 6) + ] + + assert Counter(npbc_core.get_papers()) == Counter(known_data) + + with raises(npbc_exceptions.PaperNotExists): + npbc_core.delete_existing_paper(7) + npbc_core.delete_existing_paper(2) + + +def test_add_paper(): + setup_db(DATABASE_PATH, SCHEMA_PATH, TEST_SQL) + + known_data = [ + (1, 'paper1', 0, 0, 0), + (1, 'paper1', 1, 1, 6.4), + (1, 'paper1', 2, 0, 0), + (1, 'paper1', 3, 0, 0), + (1, 'paper1', 4, 0, 0), + (1, 'paper1', 5, 1, 7.9), + (1, 'paper1', 6, 1, 4), + (2, 'paper2', 0, 0, 0), + (2, 'paper2', 1, 0, 0), + (2, 'paper2', 2, 0, 0), + (2, 'paper2', 3, 0, 0), + (2, 'paper2', 4, 1, 3.4), + (2, 'paper2', 5, 0, 0), + (2, 'paper2', 6, 1, 8.4), + (3, 'paper3', 0, 1, 2.4), + (3, 'paper3', 1, 1, 4.6), + (3, 'paper3', 2, 0, 0), + (3, 'paper3', 3, 0, 0), + (3, 'paper3', 4, 1, 3.4), + (3, 'paper3', 5, 1, 4.6), + (3, 'paper3', 6, 1, 6), + (4, 'paper4', 0, 1, 4), + (4, 'paper4', 1, 0, 0), + (4, 'paper4', 2, 1, 2.6), + (4, 'paper4', 3, 0, 0), + (4, 'paper4', 4, 0, 0), + (4, 'paper4', 5, 1, 1), + (4, 'paper4', 6, 1, 7) + ] + + npbc_core.add_new_paper( + 'paper4', + [True, False, True, False, False, True, True], + [4, 0, 2.6, 0, 0, 1, 7] + ) + + assert Counter(npbc_core.get_papers()) == Counter(known_data) + + with raises(npbc_exceptions.PaperAlreadyExists): + npbc_core.add_new_paper( + 'paper4', + [True, False, True, False, False, True, True], + [4, 0, 2.6, 0, 0, 1, 7] + ) + + +def test_edit_paper(): + setup_db(DATABASE_PATH, SCHEMA_PATH, TEST_SQL) + + known_data = [ + (1, 'paper1', 0, 0, 0), + (1, 'paper1', 1, 1, 6.4), + (1, 'paper1', 2, 0, 0), + (1, 'paper1', 3, 0, 0), + (1, 'paper1', 4, 0, 0), + (1, 'paper1', 5, 1, 7.9), + (1, 'paper1', 6, 1, 4), + (2, 'paper2', 0, 0, 0), + (2, 'paper2', 1, 0, 0), + (2, 'paper2', 2, 0, 0), + (2, 'paper2', 3, 0, 0), + (2, 'paper2', 4, 1, 3.4), + (2, 'paper2', 5, 0, 0), + (2, 'paper2', 6, 1, 8.4), + (3, 'paper3', 0, 1, 2.4), + (3, 'paper3', 1, 1, 4.6), + (3, 'paper3', 2, 0, 0), + (3, 'paper3', 3, 0, 0), + (3, 'paper3', 4, 1, 3.4), + (3, 'paper3', 5, 1, 4.6), + (3, 'paper3', 6, 1, 6) + ] + + npbc_core.edit_existing_paper( + 1, + days_delivered=[True, False, True, False, False, True, True], + days_cost=[6.4, 0, 0, 0, 0, 7.9, 4] + ) + + known_data[0] = (1, 'paper1', 0, 1, 6.4) + known_data[1] = (1, 'paper1', 1, 0, 0) + known_data[2] = (1, 'paper1', 2, 1, 0) + known_data[3] = (1, 'paper1', 3, 0, 0) + known_data[4] = (1, 'paper1', 4, 0, 0) + known_data[5] = (1, 'paper1', 5, 1, 7.9) + known_data[6] = (1, 'paper1', 6, 1, 4) + + assert Counter(npbc_core.get_papers()) == Counter(known_data) + + npbc_core.edit_existing_paper( + 3, + name="New paper" + ) + + known_data[14] = (3, 'New paper', 0, 1, 2.4) + known_data[15] = (3, 'New paper', 1, 1, 4.6) + known_data[16] = (3, 'New paper', 2, 0, 0) + known_data[17] = (3, 'New paper', 3, 0, 0) + known_data[18] = (3, 'New paper', 4, 1, 3.4) + known_data[19] = (3, 'New paper', 5, 1, 4.6) + known_data[20] = (3, 'New paper', 6, 1, 6) + + assert Counter(npbc_core.get_papers()) == Counter(known_data) + + with raises(npbc_exceptions.PaperNotExists): + npbc_core.edit_existing_paper(7, name="New paper") + + +def test_delete_string(): + known_data = [ + (1, 1, 2020, 11, '5'), + (2, 1, 2020, 11, '6-12'), + (3, 2, 2020, 11, 'sundays'), + (4, 3, 2020, 11, '2-tuesday'), + (5, 3, 2020, 10, 'all') + ] + + setup_db(DATABASE_PATH, SCHEMA_PATH, TEST_SQL) + npbc_core.delete_undelivered_string(string='all') + assert Counter(npbc_core.get_undelivered_strings()) == Counter(known_data[:4]) + + setup_db(DATABASE_PATH, SCHEMA_PATH, TEST_SQL) + npbc_core.delete_undelivered_string(month=11) + assert Counter(npbc_core.get_undelivered_strings()) == Counter([known_data[4]]) + + setup_db(DATABASE_PATH, SCHEMA_PATH, TEST_SQL) + npbc_core.delete_undelivered_string(paper_id=1) + assert Counter(npbc_core.get_undelivered_strings()) == Counter(known_data[2:]) + + setup_db(DATABASE_PATH, SCHEMA_PATH, TEST_SQL) + + with raises(npbc_exceptions.StringNotExists): + npbc_core.delete_undelivered_string(string='not exists') + + with raises(npbc_exceptions.NoParameters): + npbc_core.delete_undelivered_string() + +if __name__ == '__main__': + test_delete_string() \ No newline at end of file diff --git a/test_npbc_regex.py b/test_regex.py similarity index 87% rename from test_npbc_regex.py rename to test_regex.py index 28acc21..f33ce08 100644 --- a/test_npbc_regex.py +++ b/test_regex.py @@ -112,33 +112,6 @@ def test_regex_all_text(): assert npbc_regex.ALL_MATCH_REGEX.match('AlL') is not None assert npbc_regex.ALL_MATCH_REGEX.match('ALL') is not None - -def test_regex_costs(): - assert npbc_regex.COSTS_MATCH_REGEX.match('') is None - assert npbc_regex.COSTS_MATCH_REGEX.match('a') is None - assert npbc_regex.COSTS_MATCH_REGEX.match('1') is not None - assert npbc_regex.COSTS_MATCH_REGEX.match('1.') is None - assert npbc_regex.COSTS_MATCH_REGEX.match('1.5') is not None - assert npbc_regex.COSTS_MATCH_REGEX.match('1.0') is not None - assert npbc_regex.COSTS_MATCH_REGEX.match('16.0') is not None - assert npbc_regex.COSTS_MATCH_REGEX.match('16.06') is not None - assert npbc_regex.COSTS_MATCH_REGEX.match('1;2') is not None - assert npbc_regex.COSTS_MATCH_REGEX.match('1 ;2') is not None - assert npbc_regex.COSTS_MATCH_REGEX.match('1; 2') is not None - assert npbc_regex.COSTS_MATCH_REGEX.match('1 ; 2') is not None - assert npbc_regex.COSTS_MATCH_REGEX.match('1;2;') is not None - assert npbc_regex.COSTS_MATCH_REGEX.match('1;2 ;') is not None - assert npbc_regex.COSTS_MATCH_REGEX.match('1:2') is None - assert npbc_regex.COSTS_MATCH_REGEX.match('1,2') is None - assert npbc_regex.COSTS_MATCH_REGEX.match('1-2') is None - assert npbc_regex.COSTS_MATCH_REGEX.match('1;2;3') is not None - assert npbc_regex.COSTS_MATCH_REGEX.match('1;2;3;4') is not None - assert npbc_regex.COSTS_MATCH_REGEX.match('1;2;3;4;5') is not None - assert npbc_regex.COSTS_MATCH_REGEX.match('1;2;3;4;5;6') is not None - assert npbc_regex.COSTS_MATCH_REGEX.match('1;2;3;4;5;6;7;') is not None - assert npbc_regex.COSTS_MATCH_REGEX.match('1;2;3;4;5;6;7') is not None - assert npbc_regex.COSTS_MATCH_REGEX.match('1;2;3;4;5;6;7;8') is None - def test_delivery_regex(): assert npbc_regex.DELIVERY_MATCH_REGEX.match('') is None assert npbc_regex.DELIVERY_MATCH_REGEX.match('a') is None From 82617ca5d01e0b03941c220ff1737bc386afdce7 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Sun, 8 May 2022 00:57:20 +0530 Subject: [PATCH 27/92] testing db complete --- test_db.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test_db.py b/test_db.py index f2fa4cf..e437c7c 100644 --- a/test_db.py +++ b/test_db.py @@ -241,5 +241,28 @@ def test_delete_string(): with raises(npbc_exceptions.NoParameters): npbc_core.delete_undelivered_string() + +def test_add_string(): + setup_db(DATABASE_PATH, SCHEMA_PATH, TEST_SQL) + + known_data = [ + (1, 1, 2020, 11, '5'), + (2, 1, 2020, 11, '6-12'), + (3, 2, 2020, 11, 'sundays'), + (4, 3, 2020, 11, '2-tuesday'), + (5, 3, 2020, 10, 'all') + ] + + npbc_core.add_undelivered_string(4, 2017, 3, 'sundays') + known_data.append((6, 3, 2017, 4, 'sundays')) + assert Counter(npbc_core.get_undelivered_strings()) == Counter(known_data) + + npbc_core.add_undelivered_string(9, 2017, None, '11') + known_data.append((7, 1, 2017, 9, '11')) + known_data.append((8, 2, 2017, 9, '11')) + known_data.append((9, 3, 2017, 9, '11')) + assert Counter(npbc_core.get_undelivered_strings()) == Counter(known_data) + + if __name__ == '__main__': test_delete_string() \ No newline at end of file From aad3d29894ae88e9d7e3075f71958f283b0cf25d Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Sun, 8 May 2022 02:46:21 +0530 Subject: [PATCH 28/92] finished lots of testing --- README.md | 2 +- data/schema.sql | 8 +-- test.sql => data/test.sql | 0 npbc_cli.py | 126 +++++++++++++++++++++++--------------- npbc_core.py | 48 +++++++++------ requirements.txt | 1 - test_db.py | 29 ++++++++- 7 files changed, 136 insertions(+), 78 deletions(-) rename test.sql => data/test.sql (100%) diff --git a/README.md b/README.md index 7cc7e48..7eec8a7 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ This app calculates your monthly newspaper bill. 2. Each newspaper may or may not be delivered on a given day 3. Each newspaper has a name, and a number called a key 4. You may register any dates when you didn't receive a paper in advance using the `addudl` command -5. Once you calculate, the results are displayed and copied to your clipboard +5. Once you calculate, the results are displayed and logged. ## Installation 1. From [the latest release](https://github.com/eccentricOrange/npbc/releases/latest), download the "updater" file for your operating system in any folder, and make it executable. diff --git a/data/schema.sql b/data/schema.sql index c1af73d..65ead90 100644 --- a/data/schema.sql +++ b/data/schema.sql @@ -20,7 +20,7 @@ CREATE TABLE IF NOT EXISTS papers_days_delivered ( CREATE TABLE IF NOT EXISTS papers_days_cost ( papers_days_cost_id INTEGER PRIMARY KEY AUTOINCREMENT, paper_day_id INTEGER NOT NULL REFERENCES papers_days(paper_day_id), - cost INTEGER + cost REAL ); CREATE TABLE IF NOT EXISTS undelivered_strings ( @@ -34,10 +34,10 @@ CREATE TABLE IF NOT EXISTS undelivered_strings ( CREATE TABLE IF NOT EXISTS logs ( log_id INTEGER PRIMARY KEY AUTOINCREMENT, paper_id INTEGER NOT NULL REFERENCES papers(paper_id), - entry_timestamp TEXT NOT NULL, + timestamp TEXT NOT NULL, month INTEGER NOT NULL CHECK (month >= 0 AND month <= 12), year INTEGER NOT NULL CHECK (year >= 0), - CONSTRAINT unique_log UNIQUE (entry_timestamp, paper_id, month, year) + CONSTRAINT unique_log UNIQUE (timestamp, paper_id, month, year) ); CREATE TABLE IF NOT EXISTS undelivered_dates_logs ( @@ -49,5 +49,5 @@ CREATE TABLE IF NOT EXISTS undelivered_dates_logs ( CREATE TABLE IF NOT EXISTS cost_logs ( cost_log_id INTEGER PRIMARY KEY AUTOINCREMENT, log_id INTEGER NOT NULL REFERENCES logs(log_id), - cost INTEGER NOT NULL + cost REAL NOT NULL ); \ No newline at end of file diff --git a/test.sql b/data/test.sql similarity index 100% rename from test.sql rename to data/test.sql diff --git a/npbc_cli.py b/npbc_cli.py index c0fffc7..26076bc 100644 --- a/npbc_cli.py +++ b/npbc_cli.py @@ -1,8 +1,6 @@ from argparse import ArgumentParser, Namespace as ArgNamespace -from datetime import datetime, date as date_type -from json import dumps +from datetime import datetime import sqlite3 -from unicodedata import name from colorama import Fore, Style import npbc_core import npbc_exceptions @@ -32,7 +30,6 @@ def define_and_read_args() -> ArgNamespace: calculate_parser.set_defaults(func=calculate) calculate_parser.add_argument('-m', '--month', type=int, help="Month to calculate bill for. Must be between 1 and 12.") calculate_parser.add_argument('-y', '--year', type=int, help="Year to calculate bill for. Must be greater than 0.") - calculate_parser.add_argument('-c', '--nocopy', help="Don't copy the result of the calculation to the clipboard.", action='store_true') calculate_parser.add_argument('-l', '--nolog', help="Don't log the result of the calculation.", action='store_true') @@ -45,9 +42,9 @@ def define_and_read_args() -> ArgNamespace: addudl_parser.set_defaults(func=addudl) addudl_parser.add_argument('-m', '--month', type=int, help="Month to register undelivered incident(s) for. Must be between 1 and 12.") addudl_parser.add_argument('-y', '--year', type=int, help="Year to register undelivered incident(s) for. Must be greater than 0.") - addudl_parser.add_argument('-i', '--id', type=str, help="ID of paper to register undelivered incident(s) for.") + addudl_parser.add_argument('-p', '--paperid', type=str, help="ID of paper to register undelivered incident(s) for.") addudl_parser.add_argument('-a', '--all', help="Register undelivered incidents for all papers.", action='store_true') - addudl_parser.add_argument('-u', '--undelivered', type=str, help="Dates when you did not receive any papers.", required=True, nargs='+') + addudl_parser.add_argument('-s', '--strings', type=str, help="Dates when you did not receive any papers.", required=True, nargs='+') # delete undelivered string subparser @@ -57,11 +54,11 @@ def define_and_read_args() -> ArgNamespace: ) deludl_parser.set_defaults(func=deludl) - deludl_parser.add_argument('-i', '--id', type=str, help="ID of paper to unregister undelivered incident(s) for.") - deludl_parser.add_argument('-s', '--strid', type=str, help="String ID of paper to unregister undelivered incident(s) for.") + deludl_parser.add_argument('-p', '--paperid', type=str, help="ID of paper to unregister undelivered incident(s) for.") + deludl_parser.add_argument('-i', '--stringid', type=str, help="String ID of paper to unregister undelivered incident(s) for.") deludl_parser.add_argument('-m', '--month', type=int, help="Month to unregister undelivered incident(s) for. Must be between 1 and 12.") deludl_parser.add_argument('-y', '--year', type=int, help="Year to unregister undelivered incident(s) for. Must be greater than 0.") - deludl_parser.add_argument('-u', '--undelivered', type=str, help="Dates when you did not receive any papers.") + deludl_parser.add_argument('-s', '--string', type=str, help="Dates when you did not receive any papers.") # get undelivered string subparser @@ -71,11 +68,11 @@ def define_and_read_args() -> ArgNamespace: ) getudl_parser.set_defaults(func=getudl) - getudl_parser.add_argument('-i', '--id', type=str, help="ID for paper.") - deludl_parser.add_argument('-s', '--strid', type=str, help="String ID of paper to unregister undelivered incident(s) for.") + getudl_parser.add_argument('-p', '--paperid', type=str, help="ID for paper.") + getudl_parser.add_argument('-i', '--stringid', type=str, help="String ID of paper to unregister undelivered incident(s) for.") getudl_parser.add_argument('-m', '--month', type=int, help="Month. Must be between 1 and 12.") getudl_parser.add_argument('-y', '--year', type=int, help="Year. Must be greater than 0.") - getudl_parser.add_argument('-u', '--undelivered', type=str, help="Dates when you did not receive any papers.") + getudl_parser.add_argument('-s', '--string', type=str, help="Dates when you did not receive any papers.") # edit paper subparser @@ -86,9 +83,9 @@ def define_and_read_args() -> ArgNamespace: editpaper_parser.set_defaults(func=editpaper) editpaper_parser.add_argument('-n', '--name', type=str, help="Name for paper to be edited.") - editpaper_parser.add_argument('-d', '--days', type=str, help="Number of days the paper to be edited is delivered. All seven weekdays are required. A 'Y' means it is delivered, and an 'N' means it isn't. No separator required.") - editpaper_parser.add_argument('-p', '--price', type=str, help="Daywise prices of paper to be edited. 0s are ignored.", nargs='*') - editpaper_parser.add_argument('-i', '--id', type=str, help="ID for paper to be edited.", required=True) + editpaper_parser.add_argument('-d', '--delivered', type=str, help="Number of days the paper to be edited is delivered. All seven weekdays are required. A 'Y' means it is delivered, and an 'N' means it isn't. No separator required.") + editpaper_parser.add_argument('-c', '--costs', type=str, help="Daywise prices of paper to be edited. 0s are ignored.", nargs='*') + editpaper_parser.add_argument('-p', '--paperid', type=str, help="ID for paper to be edited.", required=True) # add paper subparser @@ -99,8 +96,8 @@ def define_and_read_args() -> ArgNamespace: addpaper_parser.set_defaults(func=addpaper) addpaper_parser.add_argument('-n', '--name', type=str, help="Name for paper to be added.", required=True) - addpaper_parser.add_argument('-d', '--days', type=str, help="Number of days the paper to be added is delivered. All seven weekdays are required. A 'Y' means it is delivered, and an 'N' means it isn't. No separator required.", required=True) - addpaper_parser.add_argument('-p', '--price', type=str, help="Daywise prices of paper to be added. 0s are ignored.", required=True, nargs=7) + addpaper_parser.add_argument('-d', '--delivered', type=str, help="Number of days the paper to be added is delivered. All seven weekdays are required. A 'Y' means it is delivered, and an 'N' means it isn't. No separator required.", required=True) + addpaper_parser.add_argument('-c', '--costs', type=str, help="Daywise prices of paper to be added. 0s are ignored.", required=True, nargs=7) # delete paper subparser @@ -110,7 +107,7 @@ def define_and_read_args() -> ArgNamespace: ) delpaper_parser.set_defaults(func=delpaper) - delpaper_parser.add_argument('-i', '--id', type=str, help="ID for paper to be deleted.", required=True) + delpaper_parser.add_argument('-p', '--paperid', type=str, help="ID for paper to be deleted.", required=True) # get paper subparser getpapers_parser = functions.add_parser( @@ -120,8 +117,8 @@ def define_and_read_args() -> ArgNamespace: getpapers_parser.set_defaults(func=getpapers) getpapers_parser.add_argument('-n', '--names', help="Get the names of the newspapers.", action='store_true') - getpapers_parser.add_argument('-d', '--days', help="Get the days the newspapers are delivered. All seven weekdays are required. A 'Y' means it is delivered, and an 'N' means it isn't.", action='store_true') - getpapers_parser.add_argument('-p', '--price', help="Get the daywise prices of the newspapers. Values must be separated by semicolons.", action='store_true') + getpapers_parser.add_argument('-d', '--delivered', help="Get the days the newspapers are delivered. All seven weekdays are required. A 'Y' means it is delivered, and an 'N' means it isn't.", action='store_true') + getpapers_parser.add_argument('-c', '--cost', help="Get the daywise prices of the newspapers. Values must be separated by semicolons.", action='store_true') # get undelivered logs subparser getlogs_parser = functions.add_parser( @@ -130,7 +127,8 @@ def define_and_read_args() -> ArgNamespace: ) getlogs_parser.set_defaults(func=getlogs) - getlogs_parser.add_argument('-i', '--id', type=str, help="ID for paper.", required=True) + getlogs_parser.add_argument('-i', '--logid', type=int, help="ID for log to be retrieved.") + getlogs_parser.add_argument('-p', '--paperid', type=str, help="ID for paper.") getlogs_parser.add_argument('-m', '--month', type=int, help="Month. Must be between 1 and 12.") getlogs_parser.add_argument('-y', '--year', type=int, help="Year. Must be greater than 0.") getlogs_parser.add_argument('-t' , '--timestamp', type=str, help="Timestamp. Must be in the format dd/mm/yyyy hh:mm:ss AM/PM.") @@ -165,9 +163,12 @@ def calculate(args: ArgNamespace) -> None: - default to the current month if no month is given and year is given - default to the current year if no year is given and month is given""" - # deal with month and year + ## deal with month and year + + # if either of them are given if args.month or args.year: + # validate them try: npbc_core.validate_month_and_year(args.month, args.year) @@ -175,28 +176,37 @@ def calculate(args: ArgNamespace) -> None: status_print(False, "Invalid month and/or year.") return + # for each, if it is not given, set it to the current month and/or year month = args.month or datetime.now().month year = args.year or datetime.now().year + # if neither are given else: + + # set them to the previous month and year previous_month = npbc_core.get_previous_month() month = previous_month.month year = previous_month.year + # prepare a dictionary for undelivered strings undelivered_strings = { int(paper_id): [] for paper_id, _, _, _, _ in npbc_core.get_papers() } + # get the undelivered strings from the database try: raw_undelivered_strings = npbc_core.get_undelivered_strings(month=month, year=year) + # add them to the dictionary for _, paper_id, _, _, string in raw_undelivered_strings: undelivered_strings[paper_id].append(string) + # ignore if none exist except npbc_exceptions.StringNotExists: pass + # calculate the cost for each paper costs, total, undelivered_dates = npbc_core.calculate_cost_of_all_papers( undelivered_strings, month, @@ -210,11 +220,11 @@ def calculate(args: ArgNamespace) -> None: if not args.nolog: npbc_core.save_results(costs, undelivered_dates, month, year) - formatted += '\nLog saved to file.' + formatted += '\n\nLog saved to file.' # print the results status_print(True, "Success!") - print(f"SUMMARY:\n{formatted}") + print(f"SUMMARY:\n\n{formatted}") def addudl(args: ArgNamespace) -> None: @@ -231,13 +241,14 @@ def addudl(args: ArgNamespace) -> None: month = args.month or datetime.now().month year = args.year or datetime.now().year - if args.id or args.all: + if args.paperid or args.all: try: - npbc_core.add_undelivered_string(month, year, paper_id=args.id, *args.undelivered) + print(f"{month=} {year=} {args.paperid=} {args.strings=}") + npbc_core.add_undelivered_string(month, year, args.paperid, *args.strings) except npbc_exceptions.PaperNotExists: - status_print(False, f"Paper with ID {args.id} does not exist.") + status_print(False, f"Paper with ID {args.paperid} does not exist.") return except npbc_exceptions.InvalidUndeliveredString: @@ -246,6 +257,7 @@ def addudl(args: ArgNamespace) -> None: else: status_print(False, "No paper(s) specified.") + return status_print(True, "Success!") @@ -264,9 +276,9 @@ def deludl(args: ArgNamespace) -> None: npbc_core.delete_undelivered_string( month=args.month, year=args.year, - paper_id=args.id, + paper_id=args.paperid, string=args.string, - string_id=args.string_id + string_id=args.stringid ) except npbc_exceptions.NoParameters: @@ -296,8 +308,8 @@ def getudl(args: ArgNamespace) -> None: undelivered_strings = npbc_core.get_undelivered_strings( month=args.month, year=args.year, - paper_id=args.id, - string_id=args.strid, + paper_id=args.paperid, + string_id=args.stringid, string=args.string ) @@ -344,10 +356,10 @@ def editpaper(args: ArgNamespace) -> None: """edit a paper's information""" try: npbc_core.edit_existing_paper( - paper_id=args.id, + paper_id=args.paperid, name=args.name, - days_delivered=extract_delivery_from_user_input(args.days_delivered), - days_cost=extract_costs_from_user_input(args.days_cost) + days_delivered=extract_delivery_from_user_input(args.delivered), + days_cost=extract_costs_from_user_input(args.costs) ) except npbc_exceptions.PaperNotExists: @@ -367,8 +379,8 @@ def addpaper(args: ArgNamespace) -> None: try: npbc_core.add_new_paper( name=args.name, - days_delivered=extract_delivery_from_user_input(args.days_delivered), - days_cost=extract_costs_from_user_input(args.days_cost) + days_delivered=extract_delivery_from_user_input(args.delivered), + days_cost=extract_costs_from_user_input(args.costs) ) except npbc_exceptions.InvalidInput as e: @@ -386,7 +398,7 @@ def delpaper(args: ArgNamespace) -> None: """delete a paper from the database""" try: - npbc_core.delete_existing_paper(args.id) + npbc_core.delete_existing_paper(args.paperid) except npbc_exceptions.PaperNotExists: status_print(False, "Paper does not exist.") @@ -395,7 +407,7 @@ def delpaper(args: ArgNamespace) -> None: status_print(True, "Success!") -def getpapers(args: ArgNamespace): +def getpapers(args: ArgNamespace) -> None: """get a list of all papers in the database - filter by whichever parameter the user provides. they may use as many as they want (but keys are always printed) - available parameters: name, days, costs @@ -428,7 +440,7 @@ def getpapers(args: ArgNamespace): names.sort(key=lambda item: item[0]) names = [name for _, name in names] - if args.days or args.price: + if args.delivered or args.costs: days = { paper_id: {} for paper_id in ids @@ -441,7 +453,7 @@ def getpapers(args: ArgNamespace): days[paper_id][day_id]['delivery'] = day_delivery days[paper_id][day_id]['cost'] = day_cost - if args.days: + if args.delivered: headers.append('days') delivery = [ @@ -452,7 +464,7 @@ def getpapers(args: ArgNamespace): for paper_id in ids ] - if args.price: + if args.costs: headers.append('costs') costs = [ @@ -485,22 +497,23 @@ def getpapers(args: ArgNamespace): print() -def getlogs(args: ArgNamespace): +def getlogs(args: ArgNamespace) -> None: """get a list of all logs in the database - filter by whichever parameter the user provides. they may use as many as they want (but log IDs are always printed) - - available parameters: paper_id, month, year, timestamp + - available parameters: log_id, paper_id, month, year, timestamp - will return both date logs and cost logs""" try: data = npbc_core.get_logged_data( - paper_id=args.paper_id, + log_id = args.logid, + paper_id=args.paperid, month=args.month, year=args.year, - timestamp=datetime.strptime(args.timestamp, r'%d/%m/%Y %I:%M:%S %p') + timestamp= datetime.strptime(args.timestamp, r'%d/%m/%Y %I:%M:%S %p') if args.timestamp else None ) - except sqlite3.DatabaseError: - status_print(False, "Database error. Please report this to the developer.") + except sqlite3.DatabaseError as e: + status_print(False, f"Database error. Please report this to the developer.\n{e}") return except ValueError: @@ -517,9 +530,24 @@ def getlogs(args: ArgNamespace): print(', '.join(str(item) for item in row)) -def update(args: ArgNamespace): +def update(args: ArgNamespace) -> None: """update the application - under normal operation, this function should never run - if the update CLI argument is provided, this script will never run and the updater will be run instead""" - status_print(False, "Update failed.") \ No newline at end of file + status_print(False, "Update failed.") + + +def main() -> None: + """main function + - initialize the database + - parses the command line arguments + - calls the appropriate function based on the arguments""" + + npbc_core.setup_and_connect_DB() + parsed = define_and_read_args() + parsed.func(parsed) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/npbc_core.py b/npbc_core.py index a3f0e5b..04a6b27 100644 --- a/npbc_core.py +++ b/npbc_core.py @@ -201,9 +201,9 @@ def get_cost_and_delivery_data(paper_id: int, connection: Connection) -> tuple[d query = """ SELECT papers_days.day_id, papers_days_delivered.delivered, papers_days_cost.cost FROM papers_days - LEFT JOIN papers_days_delivered + INNER JOIN papers_days_delivered ON papers_days.paper_day_id = papers_days_delivered.paper_day_id - LEFT JOIN papers_days_cost + INNER JOIN papers_days_cost ON papers_days.paper_day_id = papers_days_cost.paper_day_id WHERE papers_days.paper_id = ? """ @@ -284,6 +284,7 @@ def calculate_cost_of_all_papers(undelivered_strings: dict[int, list[str]], mont for paper_id, in papers # type: ignore } + # calculate the undelivered dates for each paper for paper_id, strings in undelivered_strings.items(): undelivered_dates[paper_id].update( @@ -310,7 +311,8 @@ def save_results( costs: dict[int, float], undelivered_dates: dict[int, set[date_type]], month: int, - year: int + year: int, + custom_timestamp: datetime | None = None ) -> None: """save the results of undelivered dates to the DB - save the dates any paper was not delivered @@ -318,7 +320,7 @@ def save_results( global DATABASE_PATH - timestamp = datetime.now().strftime(r'%d/%m/%Y %I:%M:%S %p') + timestamp = (custom_timestamp if custom_timestamp else datetime.now()).strftime(r'%d/%m/%Y %I:%M:%S %p') with connect(DATABASE_PATH) as connection: @@ -331,7 +333,7 @@ def save_results( RETURNING log_id; """, (paper_id, month, year, timestamp) - ).fetchone() + ).fetchone()[0] for paper_id in costs.keys() } @@ -350,7 +352,7 @@ def save_results( for date in dates: connection.execute( """ - INSERT INTO undelivered_logs (log_id, day_id) + INSERT INTO undelivered_dates_logs (log_id, date_not_delivered) VALUES (?, ?); """, (log_ids[paper_id], date.strftime("%Y-%m-%d")) @@ -364,8 +366,8 @@ def format_output(costs: dict[int, float], total: float, month: int, year: int) global DATABASE_PATH - yield f"For {date_type(year=year, month=month, day=1).strftime(r'%B %Y')}\n" - yield f"*TOTAL*: {total}\n" + yield f"For {date_type(year=year, month=month, day=1).strftime(r'%B %Y')},\n" + yield f"*TOTAL*: {total}" with connect(DATABASE_PATH) as connection: papers = { @@ -648,9 +650,9 @@ def get_papers() -> list[tuple[int, str, int, int, float]]: query = """ SELECT papers.paper_id, papers.name, papers_days.day_id, papers_days_delivered.delivered, papers_days_cost.cost FROM papers - LEFT JOIN papers_days ON papers.paper_id = papers_days.paper_id - LEFT JOIN papers_days_delivered ON papers_days.paper_day_id = papers_days_delivered.paper_day_id - LEFT JOIN papers_days_cost ON papers_days.paper_day_id = papers_days_cost.paper_day_id; + INNER JOIN papers_days ON papers.paper_id = papers_days.paper_id + INNER JOIN papers_days_delivered ON papers_days.paper_day_id = papers_days_delivered.paper_day_id + INNER JOIN papers_days_cost ON papers_days.paper_day_id = papers_days_cost.paper_day_id; """ with connect(DATABASE_PATH) as connection: @@ -763,20 +765,26 @@ def get_logged_data( parameters.append("timestamp") values += (timestamp.strftime(r'%d/%m/%Y %I:%M:%S %p'),) - query = """ + columns_only_query = """ SELECT logs.log_id, logs.paper_id, logs.month, logs.year, logs.timestamp, undelivered_dates_logs.date_not_delivered, cost_logs.cost FROM logs - LEFT JOIN undelivered_dates_logs ON logs.log_id = undelivered_dates_logs.log_id - LEFT JOIN cost_logs ON logs.log_id = cost_logs.log_id - WHERE """ + INNER JOIN undelivered_dates_logs ON logs.log_id = undelivered_dates_logs.log_id + INNER JOIN cost_logs ON logs.log_id = cost_logs.log_id""" + + if parameters: + conditions = ' AND '.join( + f"logs.{parameter} = ?" + for parameter in parameters + ) + + final_query = f"{columns_only_query} WHERE {conditions};" + + else: + final_query = f"{columns_only_query};" - conditions = ' AND '.join( - f"{parameter} = ?" - for parameter in parameters - ) + ";" with connect(DATABASE_PATH) as connection: - data = connection.execute(query + conditions, values).fetchall() + data = connection.execute(final_query, values).fetchall() connection.close() diff --git a/requirements.txt b/requirements.txt index cced67c..72015e8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,5 @@ ## CLI colorama -pyperclip ## API # Flask diff --git a/test_db.py b/test_db.py index e437c7c..91a18ca 100644 --- a/test_db.py +++ b/test_db.py @@ -1,3 +1,4 @@ +from datetime import date, datetime from sqlite3 import connect from pathlib import Path from typing import Counter @@ -6,7 +7,7 @@ DATABASE_PATH = Path("data") / "npbc.db" SCHEMA_PATH = Path("data") / "schema.sql" -TEST_SQL = Path("test.sql") +TEST_SQL = Path("data") / "test.sql" def setup_db(database_path: Path, schema_path: Path, test_sql: Path): @@ -264,5 +265,27 @@ def test_add_string(): assert Counter(npbc_core.get_undelivered_strings()) == Counter(known_data) -if __name__ == '__main__': - test_delete_string() \ No newline at end of file +def test_save_results(): + setup_db(DATABASE_PATH, SCHEMA_PATH, TEST_SQL) + + known_data = [ + (1, 1, 1, 2020, '04/01/2022 01:05:42 AM', '2020-01-01', 105.0), + (1, 1, 1, 2020, '04/01/2022 01:05:42 AM', '2020-01-02', 105.0), + (2, 2, 1, 2020, '04/01/2022 01:05:42 AM', '2020-01-03', 51.0), + (2, 2, 1, 2020, '04/01/2022 01:05:42 AM', '2020-01-01', 51.0), + (2, 2, 1, 2020, '04/01/2022 01:05:42 AM', '2020-01-05', 51.0) + ] + + npbc_core.save_results( + {1: 105, 2: 51, 3: 647}, + { + 1: set([date(month=1, day=1, year=2020), date(month=1, day=2, year=2020)]), + 2: set([date(month=1, day=1, year=2020), date(month=1, day=5, year=2020), date(month=1, day=3, year=2020)]), + 3: set() + }, + 1, + 2020, + datetime(year=2022, month=1, day=4, hour=1, minute=5, second=42) + ) + + assert Counter(npbc_core.get_logged_data()) == Counter(known_data) \ No newline at end of file From 826ccdf4fb21aa3989daab35f2159ab51ae065b5 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Sun, 8 May 2022 03:06:30 +0530 Subject: [PATCH 29/92] docstrnings, rm some regex --- npbc_cli.py | 12 +++++++++++- npbc_core.py | 21 ++++++++++++++++----- npbc_exceptions.py | 7 +++++++ npbc_regex.py | 16 ++++++++-------- test_core.py | 5 +++++ test_db.py | 9 +++++++++ test_regex.py | 36 +----------------------------------- 7 files changed, 57 insertions(+), 49 deletions(-) diff --git a/npbc_cli.py b/npbc_cli.py index 26076bc..4ac8f78 100644 --- a/npbc_cli.py +++ b/npbc_cli.py @@ -1,3 +1,13 @@ +""" +wraps a CLI around the core functionality (argparse) +- inherits functionality from `npbc_core.py` +- inherits regex from `npbc_regex.py`, used for validation +- inherits exceptions from `npbc_exceptions.py`, used for error handling +- performs some additional validation +- formats data retrieved from the core for the user +""" + + from argparse import ArgumentParser, Namespace as ArgNamespace from datetime import datetime import sqlite3 @@ -550,4 +560,4 @@ def main() -> None: if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/npbc_core.py b/npbc_core.py index 04a6b27..0009dda 100644 --- a/npbc_core.py +++ b/npbc_core.py @@ -1,11 +1,22 @@ -from datetime import date as date_type, datetime, timedelta +""" +provides the core functionality +- sets up and communicates with the DB +- adds, deletes, edits, or retrieves data from the DB (such as undelivered strings, paper data, logs) +- performs the main calculations +- handles validation and parsing of many values (such as undelivered strings) +""" + +from calendar import day_name as weekday_names_iterable +from calendar import monthcalendar, monthrange +from datetime import date as date_type +from datetime import datetime, timedelta +from os import environ from pathlib import Path -from calendar import day_name as weekday_names_iterable, monthcalendar, monthrange from sqlite3 import Connection, connect from typing import Generator -import npbc_regex + import npbc_exceptions -from os import environ +import npbc_regex ## paths for the folder containing schema and database files # during normal use, the DB will be in ~/.npbc (where ~ is the user's home directory) and the schema will be bundled with the executable @@ -807,4 +818,4 @@ def validate_month_and_year(month: int | None = None, year: int | None = None) - raise npbc_exceptions.InvalidMonthYear("Month must be between 1 and 12.") if isinstance(year, int) and (year <= 0): - raise npbc_exceptions.InvalidMonthYear("Year must be greater than 0.") \ No newline at end of file + raise npbc_exceptions.InvalidMonthYear("Year must be greater than 0.") diff --git a/npbc_exceptions.py b/npbc_exceptions.py index f1eaa2b..85d9a1f 100644 --- a/npbc_exceptions.py +++ b/npbc_exceptions.py @@ -1,3 +1,10 @@ +""" +provide exceptions for other modules +- these are custom exceptions used to make error handling easier +- none of them inherit from BaseException +""" + + from sqlite3 import OperationalError class InvalidInput(ValueError): ... diff --git a/npbc_regex.py b/npbc_regex.py index 7861fb0..ee6e171 100644 --- a/npbc_regex.py +++ b/npbc_regex.py @@ -1,5 +1,11 @@ +""" +regex used by other files +- MATCH regex are used to validate (usually user input) +- SPLIT regex are used to split strings (usually user input) +""" + from calendar import day_name as WEEKDAY_NAMES_ITERABLE -from re import compile as compile_regex, match +from re import compile as compile_regex ## regex used to match against strings @@ -29,10 +35,4 @@ ## regex used to split strings # split on hyphens. spaces are allowed between hyphens and values. -HYPHEN_SPLIT_REGEX = compile_regex(r' *- *') - -# split on semicolons. spaces are allowed between hyphens and values. -SEMICOLON_SPLIT_REGEX = compile_regex(r' *; *') - -# split on commas. spaces are allowed between commas and values. -COMMA_SPLIT_REGEX = compile_regex(r' *, *') \ No newline at end of file +HYPHEN_SPLIT_REGEX = compile_regex(r' *- *') \ No newline at end of file diff --git a/test_core.py b/test_core.py index 2d917b9..089f930 100644 --- a/test_core.py +++ b/test_core.py @@ -1,3 +1,8 @@ +""" +test data-independent functions from the core +- none of these depend on data in the database +""" + from datetime import date as date_type import npbc_core from pytest import raises diff --git a/test_db.py b/test_db.py index 91a18ca..4d29249 100644 --- a/test_db.py +++ b/test_db.py @@ -1,3 +1,12 @@ +""" +test data-dependent functions from the core +- all of these depend on the DB +- for consistency, the DB will always be initialised with the same data +- the test data is contained in `data/test.sql` +- the schema is the same as the core (`data/schema.sql` during development) +""" + + from datetime import date, datetime from sqlite3 import connect from pathlib import Path diff --git a/test_regex.py b/test_regex.py index f33ce08..e0e4959 100644 --- a/test_regex.py +++ b/test_regex.py @@ -159,38 +159,4 @@ def test_regex_hyphen(): assert npbc_regex.HYPHEN_SPLIT_REGEX.split('1,2-3') == ['1,2', '3'] assert npbc_regex.HYPHEN_SPLIT_REGEX.split('1,2-3-') == ['1,2', '3', ''] assert npbc_regex.HYPHEN_SPLIT_REGEX.split('1,2, 3,') == ['1,2, 3,'] - assert npbc_regex.HYPHEN_SPLIT_REGEX.split('') == [''] - -def test_regex_comma(): - assert npbc_regex.COMMA_SPLIT_REGEX.split('1,2') == ['1', '2'] - assert npbc_regex.COMMA_SPLIT_REGEX.split('1,2,3') == ['1', '2', '3'] - assert npbc_regex.COMMA_SPLIT_REGEX.split('1 ,2,3') == ['1', '2', '3'] - assert npbc_regex.COMMA_SPLIT_REGEX.split('1 , 2,3') == ['1', '2', '3'] - assert npbc_regex.COMMA_SPLIT_REGEX.split('1, 2,3') == ['1', '2', '3'] - assert npbc_regex.COMMA_SPLIT_REGEX.split('1') == ['1'] - assert npbc_regex.COMMA_SPLIT_REGEX.split('1,') == ['1', ''] - assert npbc_regex.COMMA_SPLIT_REGEX.split('1, ') == ['1', ''] - assert npbc_regex.COMMA_SPLIT_REGEX.split('1,2,') == ['1', '2', ''] - assert npbc_regex.COMMA_SPLIT_REGEX.split('1,2,3,') == ['1', '2', '3', ''] - assert npbc_regex.COMMA_SPLIT_REGEX.split('1-2,3') == ['1-2', '3'] - assert npbc_regex.COMMA_SPLIT_REGEX.split('1-2,3,') == ['1-2', '3', ''] - assert npbc_regex.COMMA_SPLIT_REGEX.split('1-2-3') == ['1-2-3'] - assert npbc_regex.COMMA_SPLIT_REGEX.split('1-2- 3') == ['1-2- 3'] - assert npbc_regex.COMMA_SPLIT_REGEX.split('') == [''] - -def test_regex_semicolon(): - assert npbc_regex.SEMICOLON_SPLIT_REGEX.split('1;2') == ['1', '2'] - assert npbc_regex.SEMICOLON_SPLIT_REGEX.split('1;2;3') == ['1', '2', '3'] - assert npbc_regex.SEMICOLON_SPLIT_REGEX.split('1 ;2;3') == ['1', '2', '3'] - assert npbc_regex.SEMICOLON_SPLIT_REGEX.split('1 ; 2;3') == ['1', '2', '3'] - assert npbc_regex.SEMICOLON_SPLIT_REGEX.split('1; 2;3') == ['1', '2', '3'] - assert npbc_regex.SEMICOLON_SPLIT_REGEX.split('1') == ['1'] - assert npbc_regex.SEMICOLON_SPLIT_REGEX.split('1;') == ['1', ''] - assert npbc_regex.SEMICOLON_SPLIT_REGEX.split('1; ') == ['1', ''] - assert npbc_regex.SEMICOLON_SPLIT_REGEX.split('1;2;') == ['1', '2', ''] - assert npbc_regex.SEMICOLON_SPLIT_REGEX.split('1;2;3;') == ['1', '2', '3', ''] - assert npbc_regex.SEMICOLON_SPLIT_REGEX.split('1-2;3') == ['1-2', '3'] - assert npbc_regex.SEMICOLON_SPLIT_REGEX.split('1-2;3;') == ['1-2', '3', ''] - assert npbc_regex.SEMICOLON_SPLIT_REGEX.split('1-2-3') == ['1-2-3'] - assert npbc_regex.SEMICOLON_SPLIT_REGEX.split('1-2- 3') == ['1-2- 3'] - assert npbc_regex.SEMICOLON_SPLIT_REGEX.split('') == [''] \ No newline at end of file + assert npbc_regex.HYPHEN_SPLIT_REGEX.split('') == [''] \ No newline at end of file From c02531560b2d156546d97f91b3e0bc2aa8f5e744 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Sun, 8 May 2022 04:03:40 +0530 Subject: [PATCH 30/92] bugfixes --- npbc_cli.py | 79 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 55 insertions(+), 24 deletions(-) diff --git a/npbc_cli.py b/npbc_cli.py index 4ac8f78..9153c6b 100644 --- a/npbc_cli.py +++ b/npbc_cli.py @@ -10,11 +10,12 @@ from argparse import ArgumentParser, Namespace as ArgNamespace from datetime import datetime +from typing import Generator import sqlite3 from colorama import Fore, Style import npbc_core import npbc_exceptions -from npbc_regex import DAYS_MATCH_REGEX +from npbc_regex import DELIVERY_MATCH_REGEX def define_and_read_args() -> ArgNamespace: @@ -107,7 +108,7 @@ def define_and_read_args() -> ArgNamespace: addpaper_parser.set_defaults(func=addpaper) addpaper_parser.add_argument('-n', '--name', type=str, help="Name for paper to be added.", required=True) addpaper_parser.add_argument('-d', '--delivered', type=str, help="Number of days the paper to be added is delivered. All seven weekdays are required. A 'Y' means it is delivered, and an 'N' means it isn't. No separator required.", required=True) - addpaper_parser.add_argument('-c', '--costs', type=str, help="Daywise prices of paper to be added. 0s are ignored.", required=True, nargs=7) + addpaper_parser.add_argument('-c', '--costs', type=str, help="Daywise prices of paper to be added. 0s are ignored.", required=True, nargs='+') # delete paper subparser @@ -343,8 +344,8 @@ def getudl(args: ArgNamespace) -> None: def extract_delivery_from_user_input(input_delivery: str) -> list[bool]: """convert the /[YN]{7}/ user input to a Boolean list""" - if not DAYS_MATCH_REGEX.match(input_delivery): - raise npbc_exceptions.InvalidInput + if not DELIVERY_MATCH_REGEX.match(input_delivery): + raise npbc_exceptions.InvalidInput("Invalid delivery days.") return [ day == 'Y' @@ -352,24 +353,51 @@ def extract_delivery_from_user_input(input_delivery: str) -> list[bool]: ] -def extract_costs_from_user_input(*input_costs: float) -> list[float]: +def extract_costs_from_user_input(paper_id: int | None, delivery_data: list[bool] | None, *input_costs: float) -> Generator[float, None, None]: """convert the user input to a float list""" - return [ + suspected_data = [ cost for cost in input_costs if cost != 0 ] + suspected_data.reverse() + + if delivery_data: + if (len(suspected_data) != delivery_data.count(True)): + raise npbc_exceptions.InvalidInput("Number of costs don't match number of days delivered.") + + for day in delivery_data: + yield suspected_data.pop() if day else 0 + + elif paper_id: + raw_data = [paper for paper in npbc_core.get_papers() if paper[0] == int(paper_id)] + + delivered = [ + bool(delivered) + for _, _, day_id, delivered, _ in raw_data + ] + + if len(suspected_data) != delivered.count(True): + raise npbc_exceptions.InvalidInput("Number of costs don't match number of days delivered.") + + for day in delivered: + yield suspected_data.pop() if day else 0 + + else: + raise npbc_exceptions.InvalidInput("Something went wrong.") def editpaper(args: ArgNamespace) -> None: """edit a paper's information""" try: + delivery_data = extract_delivery_from_user_input(args.delivered) if args.delivered else None + npbc_core.edit_existing_paper( paper_id=args.paperid, name=args.name, - days_delivered=extract_delivery_from_user_input(args.delivered), - days_cost=extract_costs_from_user_input(args.costs) + days_delivered=delivery_data, + days_cost=list(extract_costs_from_user_input(args.paperid, delivery_data, *args.costs)) if args.costs else None ) except npbc_exceptions.PaperNotExists: @@ -387,10 +415,12 @@ def addpaper(args: ArgNamespace) -> None: """add a new paper to the database""" try: + delivery_data = extract_delivery_from_user_input(args.delivered) + npbc_core.add_new_paper( name=args.name, - days_delivered=extract_delivery_from_user_input(args.delivered), - days_cost=extract_costs_from_user_input(args.costs) + days_delivered=delivery_data, + days_cost=list(extract_costs_from_user_input(None, delivery_data, *args.costs)) ) except npbc_exceptions.InvalidInput as e: @@ -432,25 +462,26 @@ def getpapers(args: ArgNamespace) -> None: headers = ['paper_id'] ids = [] - delivery = {} - costs = {} - names = {} ids = list(set(paper[0] for paper in raw_data)) ids.sort() + + delivery = [None for _ in ids] + costs = [None for _ in ids] + names = [None for _ in ids] - if args.name: + if args.names: headers.append('name') - names = [ + names = list(set( (paper_id, name) for paper_id, name, _, _, _ in raw_data - ] + )) names.sort(key=lambda item: item[0]) names = [name for _, name in names] - if args.delivered or args.costs: + if args.delivered or args.cost: days = { paper_id: {} for paper_id in ids @@ -469,18 +500,18 @@ def getpapers(args: ArgNamespace) -> None: delivery = [ ''.join([ 'Y' if days[paper_id][day_id]['delivery'] else 'N' - for day_id in enumerate(npbc_core.WEEKDAY_NAMES) + for day_id, _ in enumerate(npbc_core.WEEKDAY_NAMES) ]) for paper_id in ids ] - if args.costs: + if args.cost: headers.append('costs') costs = [ ';'.join([ str(days[paper_id][day_id]['cost']) - for day_id, cost in enumerate(npbc_core.WEEKDAY_NAMES) + for day_id, _ in enumerate(npbc_core.WEEKDAY_NAMES) if days[paper_id][day_id]['cost'] != 0 ]) for paper_id in ids @@ -493,15 +524,15 @@ def getpapers(args: ArgNamespace) -> None: # print the data for paper_id, name, delivered, cost in zip(ids, names, delivery, costs): - print(paper_id) + print(paper_id, end='') - if names: + if args.names: print(f", {name}", end='') - if delivery: + if args.delivered: print(f", {delivered}", end='') - if costs: + if args.cost: print(f", {cost}", end='') print() From 38686e6a0ce9a770250fe048366163dc5141d202 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Sun, 8 May 2022 04:05:06 +0530 Subject: [PATCH 31/92] sort --- npbc_cli.py | 7 +++++-- test_core.py | 5 ++++- test_db.py | 9 ++++++--- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/npbc_cli.py b/npbc_cli.py index 9153c6b..8b8b250 100644 --- a/npbc_cli.py +++ b/npbc_cli.py @@ -8,11 +8,14 @@ """ -from argparse import ArgumentParser, Namespace as ArgNamespace +import sqlite3 +from argparse import ArgumentParser +from argparse import Namespace as ArgNamespace from datetime import datetime from typing import Generator -import sqlite3 + from colorama import Fore, Style + import npbc_core import npbc_exceptions from npbc_regex import DELIVERY_MATCH_REGEX diff --git a/test_core.py b/test_core.py index 089f930..af29656 100644 --- a/test_core.py +++ b/test_core.py @@ -4,10 +4,13 @@ """ from datetime import date as date_type -import npbc_core + from pytest import raises + +import npbc_core from npbc_exceptions import InvalidMonthYear, InvalidUndeliveredString + def test_get_number_of_each_weekday(): test_function = npbc_core.get_number_of_each_weekday diff --git a/test_db.py b/test_db.py index 4d29249..ba674a1 100644 --- a/test_db.py +++ b/test_db.py @@ -8,12 +8,15 @@ from datetime import date, datetime -from sqlite3 import connect from pathlib import Path +from sqlite3 import connect from typing import Counter -import npbc_core, npbc_exceptions + from pytest import raises +import npbc_core +import npbc_exceptions + DATABASE_PATH = Path("data") / "npbc.db" SCHEMA_PATH = Path("data") / "schema.sql" TEST_SQL = Path("data") / "test.sql" @@ -297,4 +300,4 @@ def test_save_results(): datetime(year=2022, month=1, day=4, hour=1, minute=5, second=42) ) - assert Counter(npbc_core.get_logged_data()) == Counter(known_data) \ No newline at end of file + assert Counter(npbc_core.get_logged_data()) == Counter(known_data) From de9736a2b1799974686ef415dbe0215f2d63ff46 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Sun, 8 May 2022 15:53:41 +0530 Subject: [PATCH 32/92] test_build --- .github/workflows/containerized_build.yml | 28 +++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/containerized_build.yml diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml new file mode 100644 index 0000000..87e2739 --- /dev/null +++ b/.github/workflows/containerized_build.yml @@ -0,0 +1,28 @@ +name: Test, build and release + +# whenever a branch or commit is pushed +on: push + + +jobs: + + # use pytest + test: + + # used to ensure testing is done right + env: + DEVELOPMENT: '1' + runs-on: ubuntu-latest + + # to avoid using old sqlite version + container: + image: ubuntu:latest + + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: 3.10 + - run: pip install -r requirements.txt pytest + - uses: cclauss/GitHub-Action-for-pytest@0.5.0 + - run: pytest \ No newline at end of file From 4e08599b8e1c57a8f053d1bbfdedf23664bed49f Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Sun, 8 May 2022 15:55:27 +0530 Subject: [PATCH 33/92] string/number error --- .github/workflows/build.yml | 142 +++++++++++----------- .github/workflows/containerized_build.yml | 2 +- 2 files changed, 72 insertions(+), 72 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 64829ee..4297227 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,75 +1,75 @@ -name: build and release +# name: build and release -on: [push] +# on: [push] -jobs: - test: - name: pytest - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - with: - python-version: '3.10' - - run: pip install -r requirements.txt pytest - - uses: cclauss/GitHub-Action-for-pytest@0.5.0 - - run: pytest +# jobs: +# test: +# name: pytest +# runs-on: ubuntu-latest +# steps: +# - uses: actions/checkout@v2 +# - uses: actions/setup-python@v2 +# with: +# python-version: '3.10' +# - run: pip install -r requirements.txt pytest +# - uses: cclauss/GitHub-Action-for-pytest@0.5.0 +# - run: pytest - build: - needs: - - test - strategy: - matrix: - os: - - ubuntu - - windows - - macos - architecture: ['x64'] - app: ['cli', updater] - include: - - os: windows - data-file: data/schema.sql;. - name: windows - - os: macos - data-file: data/schema.sql:. - name: macos - - os: ubuntu - data-file: data/schema.sql:. - name: linux - runs-on: ${{ matrix.os }}-latest - name: ${{ matrix.app }}-${{ matrix.name }}-${{ matrix.architecture }} - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - with: - python-version: '3.10' - architecture: ${{ matrix.architecture }} - - run: pip install -r requirements.txt pyinstaller - - run: mkdir build - - run: mkdir bin - - run: pyinstaller --distpath bin --clean --add-data "${{ matrix.data-file }}" --onefile --name npbc_${{ matrix.app }}-${{ matrix.name }}-${{ matrix.architecture }} npbc_${{ matrix.app }}.py - - uses: actions/upload-artifact@v2 - with: - path: bin - name: npbc_${{ matrix.app }}-${{ matrix.name }}-${{ matrix.architecture }} +# build: +# needs: +# - test +# strategy: +# matrix: +# os: +# - ubuntu +# - windows +# - macos +# architecture: ['x64'] +# app: ['cli', updater] +# include: +# - os: windows +# data-file: data/schema.sql;. +# name: windows +# - os: macos +# data-file: data/schema.sql:. +# name: macos +# - os: ubuntu +# data-file: data/schema.sql:. +# name: linux +# runs-on: ${{ matrix.os }}-latest +# name: ${{ matrix.app }}-${{ matrix.name }}-${{ matrix.architecture }} +# steps: +# - uses: actions/checkout@v2 +# - uses: actions/setup-python@v2 +# with: +# python-version: '3.10' +# architecture: ${{ matrix.architecture }} +# - run: pip install -r requirements.txt pyinstaller +# - run: mkdir build +# - run: mkdir bin +# - run: pyinstaller --distpath bin --clean --add-data "${{ matrix.data-file }}" --onefile --name npbc_${{ matrix.app }}-${{ matrix.name }}-${{ matrix.architecture }} npbc_${{ matrix.app }}.py +# - uses: actions/upload-artifact@v2 +# with: +# path: bin +# name: npbc_${{ matrix.app }}-${{ matrix.name }}-${{ matrix.architecture }} - release: - needs: - - build - if: startsWith(github.ref, 'refs/tags/v') - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v2 - - run: mkdir bin - - uses: actions/download-artifact@v2 - with: - path: bin - - uses: ncipollo/release-action@v1 - with: - artifacts: "bin/npbc*/*" - token: ${{ secrets.GITHUB_TOKEN }} - generateReleaseNotes: true - artifactErrorsFailBuild: true - prerelease: false +# release: +# needs: +# - build +# if: startsWith(github.ref, 'refs/tags/v') +# runs-on: ubuntu-latest +# permissions: +# contents: write +# steps: +# - uses: actions/checkout@v2 +# - run: mkdir bin +# - uses: actions/download-artifact@v2 +# with: +# path: bin +# - uses: ncipollo/release-action@v1 +# with: +# artifacts: "bin/npbc*/*" +# token: ${{ secrets.GITHUB_TOKEN }} +# generateReleaseNotes: true +# artifactErrorsFailBuild: true +# prerelease: false diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index 87e2739..40bfd80 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -22,7 +22,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 with: - python-version: 3.10 + python-version: '3.10' - run: pip install -r requirements.txt pytest - uses: cclauss/GitHub-Action-for-pytest@0.5.0 - run: pytest \ No newline at end of file From 9f39b041b0508c5ab5cc6e1be94f9c45edb0b79c Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Sun, 8 May 2022 15:56:39 +0530 Subject: [PATCH 34/92] pip? --- .github/workflows/containerized_build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index 40bfd80..7d74d27 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -23,6 +23,6 @@ jobs: - uses: actions/setup-python@v2 with: python-version: '3.10' - - run: pip install -r requirements.txt pytest + - run: python -m pip install -r requirements.txt pytest - uses: cclauss/GitHub-Action-for-pytest@0.5.0 - run: pytest \ No newline at end of file From ff8c6be593cb77630dc1c256408f3430efbef4b4 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Sun, 8 May 2022 15:57:15 +0530 Subject: [PATCH 35/92] on array --- .github/workflows/containerized_build.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index 7d74d27..a7491ad 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -1,8 +1,7 @@ name: Test, build and release # whenever a branch or commit is pushed -on: push - +on: [push] jobs: From 975dbb7775c799ccb217af01a7adfae72a8965bc Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Sun, 8 May 2022 16:01:40 +0530 Subject: [PATCH 36/92] ubuntu version --- .github/workflows/containerized_build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index a7491ad..7f3754a 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -15,7 +15,7 @@ jobs: # to avoid using old sqlite version container: - image: ubuntu:latest + image: ubuntu:20.04 steps: - uses: actions/checkout@v2 From 0273e87dd2c6b3b0b5184ff390714210273979a1 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Sun, 8 May 2022 16:09:35 +0530 Subject: [PATCH 37/92] try without setup python --- .github/workflows/containerized_build.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index 7f3754a..56b73fc 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -15,13 +15,9 @@ jobs: # to avoid using old sqlite version container: - image: ubuntu:20.04 + image: ubuntu:latest steps: - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - with: - python-version: '3.10' - run: python -m pip install -r requirements.txt pytest - - uses: cclauss/GitHub-Action-for-pytest@0.5.0 - run: pytest \ No newline at end of file From 36ace702d28ba18e2a38b317b9bcd6f81ffb5704 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Sun, 8 May 2022 16:12:12 +0530 Subject: [PATCH 38/92] different image --- .github/workflows/containerized_build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index 56b73fc..30cc031 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -15,7 +15,7 @@ jobs: # to avoid using old sqlite version container: - image: ubuntu:latest + image: python:3.10.4-bullseye steps: - uses: actions/checkout@v2 From 91639bd78e953eaf63a76d627f1ee5934d0fa7c8 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Sun, 8 May 2022 16:14:31 +0530 Subject: [PATCH 39/92] sqlite version --- .github/workflows/containerized_build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index 30cc031..e6b39fa 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -18,6 +18,7 @@ jobs: image: python:3.10.4-bullseye steps: + - run: python -c "import sqlite; print(sqlite3.sqlite_version)" - uses: actions/checkout@v2 - run: python -m pip install -r requirements.txt pytest - run: pytest \ No newline at end of file From 97cf7c6499cea11b9c1b44987ac2174362c53ee7 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Sun, 8 May 2022 16:24:47 +0530 Subject: [PATCH 40/92] manual python --- .github/workflows/containerized_build.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index e6b39fa..126223b 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -15,10 +15,16 @@ jobs: # to avoid using old sqlite version container: - image: python:3.10.4-bullseye + image: ubuntu-latest steps: - - run: python -c "import sqlite; print(sqlite3.sqlite_version)" - - uses: actions/checkout@v2 - - run: python -m pip install -r requirements.txt pytest + - run: sudo apt-get update + - run: sudo apt-get install make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev + - run: curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash + - run: exec $SHELL + - run: pyenv update + - run: pyenv install 3.10.4 + - run: pyenv virtualenv 3.10.4 npbc + - run: pyenv local 3.10.4/envs/npbc + - run: pip install -r requirements.txt pytest - run: pytest \ No newline at end of file From 19be09f6baf5d444b271bbd023fb43a0184763ea Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Sun, 8 May 2022 16:30:11 +0530 Subject: [PATCH 41/92] typo in OS --- .github/workflows/containerized_build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index 126223b..5742833 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -15,7 +15,7 @@ jobs: # to avoid using old sqlite version container: - image: ubuntu-latest + image: ubuntu:latest steps: - run: sudo apt-get update From 21f16e05b0d26583c2517cb348bd241437a9ac8e Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Sun, 8 May 2022 16:31:05 +0530 Subject: [PATCH 42/92] sudo? --- .github/workflows/containerized_build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index 5742833..de3ce88 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -18,8 +18,8 @@ jobs: image: ubuntu:latest steps: - - run: sudo apt-get update - - run: sudo apt-get install make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev + - run: apt-get update + - run: apt-get install make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev - run: curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash - run: exec $SHELL - run: pyenv update From 53cd2168903854b89a935aada76cdbee1dad5943 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Sun, 8 May 2022 16:32:00 +0530 Subject: [PATCH 43/92] -y in install --- .github/workflows/containerized_build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index de3ce88..33f708c 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -19,7 +19,7 @@ jobs: steps: - run: apt-get update - - run: apt-get install make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev + - run: apt-get install make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev -y - run: curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash - run: exec $SHELL - run: pyenv update From 31ba4fd511dd22074e86032b6b2211dc3369d695 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Sun, 8 May 2022 16:34:39 +0530 Subject: [PATCH 44/92] silent --- .github/workflows/containerized_build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index 33f708c..7f152a8 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -18,8 +18,8 @@ jobs: image: ubuntu:latest steps: - - run: apt-get update - - run: apt-get install make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev -y + - run: apt-get -qq update + - run: apt-get -qq install make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev -y - run: curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash - run: exec $SHELL - run: pyenv update From 588b97359c42e5f941d66cbe0f849c18ec551e16 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Sun, 8 May 2022 16:36:09 +0530 Subject: [PATCH 45/92] wrong apt --- .github/workflows/containerized_build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index 7f152a8..a0b9e3e 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -18,8 +18,8 @@ jobs: image: ubuntu:latest steps: - - run: apt-get -qq update - - run: apt-get -qq install make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev -y + - run: apt -q update + - run: apt -q install make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev -y - run: curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash - run: exec $SHELL - run: pyenv update From 943c9d0ba27d3208c7c44fb9a9e58ab777c5151f Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Sun, 8 May 2022 16:40:13 +0530 Subject: [PATCH 46/92] non_interactive? --- .github/workflows/containerized_build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index a0b9e3e..243ef6c 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -18,6 +18,7 @@ jobs: image: ubuntu:latest steps: + - run: export DEBIAN_FRONTEND=noninteractive - run: apt -q update - run: apt -q install make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev -y - run: curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash From 0c96e9612276cee45f2e2b2fab7995e108694a51 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Sun, 8 May 2022 16:42:58 +0530 Subject: [PATCH 47/92] debian? --- .github/workflows/containerized_build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index 243ef6c..b933f02 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -15,7 +15,7 @@ jobs: # to avoid using old sqlite version container: - image: ubuntu:latest + image: debian:latest steps: - run: export DEBIAN_FRONTEND=noninteractive From 987d5fd6262dc208c3e95fc18c302dfb2edfb450 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Sun, 8 May 2022 16:44:42 +0530 Subject: [PATCH 48/92] install git --- .github/workflows/containerized_build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index b933f02..337d0b4 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -20,7 +20,7 @@ jobs: steps: - run: export DEBIAN_FRONTEND=noninteractive - run: apt -q update - - run: apt -q install make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev -y + - run: apt -q install make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev git -y - run: curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash - run: exec $SHELL - run: pyenv update From 4042137255d0cf23fb36fd607001d78789fe8b30 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Sun, 8 May 2022 16:47:45 +0530 Subject: [PATCH 49/92] pyenv abs --- .github/workflows/containerized_build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index 337d0b4..3279ce1 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -23,9 +23,9 @@ jobs: - run: apt -q install make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev git -y - run: curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash - run: exec $SHELL - - run: pyenv update - - run: pyenv install 3.10.4 - - run: pyenv virtualenv 3.10.4 npbc - - run: pyenv local 3.10.4/envs/npbc - - run: pip install -r requirements.txt pytest - - run: pytest \ No newline at end of file + - run: ~/.pyenv/bin/pyenv update + - run: ~/.pyenv/bin/pyenv install 3.10.4 + - run: ~/.pyenv/bin/pyenv virtualenv 3.10.4 npbc + - run: ~/.pyenv/bin/pyenv local 3.10.4/envs/npbc + - run: ~/.pyenv/shims/pip install -r requirements.txt pytest + - run: ~/.pyenv/shims/pytest \ No newline at end of file From e8a40acad548e842fdcaa9ee26905e7116a8f80b Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Sun, 8 May 2022 16:51:45 +0530 Subject: [PATCH 50/92] checkout --- .github/workflows/containerized_build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index 3279ce1..072db0c 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -18,6 +18,7 @@ jobs: image: debian:latest steps: + - run: actions/checkout@v2 - run: export DEBIAN_FRONTEND=noninteractive - run: apt -q update - run: apt -q install make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev git -y From 0e0574b66662c61256064dd8bfbad29c94373e3f Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Sun, 8 May 2022 16:52:09 +0530 Subject: [PATCH 51/92] checkout later --- .github/workflows/containerized_build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index 072db0c..dcbb80a 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -18,7 +18,6 @@ jobs: image: debian:latest steps: - - run: actions/checkout@v2 - run: export DEBIAN_FRONTEND=noninteractive - run: apt -q update - run: apt -q install make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev git -y @@ -28,5 +27,6 @@ jobs: - run: ~/.pyenv/bin/pyenv install 3.10.4 - run: ~/.pyenv/bin/pyenv virtualenv 3.10.4 npbc - run: ~/.pyenv/bin/pyenv local 3.10.4/envs/npbc + - run: actions/checkout@v2 - run: ~/.pyenv/shims/pip install -r requirements.txt pytest - run: ~/.pyenv/shims/pytest \ No newline at end of file From 1890491c0bf93e70d1b6cce1cb74f3b2587720eb Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Sun, 8 May 2022 17:02:51 +0530 Subject: [PATCH 52/92] try as root --- .github/workflows/containerized_build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index dcbb80a..39953fe 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -16,6 +16,7 @@ jobs: # to avoid using old sqlite version container: image: debian:latest + options: --user root steps: - run: export DEBIAN_FRONTEND=noninteractive From 0a22f6b29e406df7e554adecbc876073ee4ddfe0 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Sun, 8 May 2022 17:08:14 +0530 Subject: [PATCH 53/92] run/uses action --- .github/workflows/containerized_build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index 39953fe..34a3497 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -19,6 +19,7 @@ jobs: options: --user root steps: + - uses: actions/checkout@v2 - run: export DEBIAN_FRONTEND=noninteractive - run: apt -q update - run: apt -q install make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev git -y @@ -28,6 +29,5 @@ jobs: - run: ~/.pyenv/bin/pyenv install 3.10.4 - run: ~/.pyenv/bin/pyenv virtualenv 3.10.4 npbc - run: ~/.pyenv/bin/pyenv local 3.10.4/envs/npbc - - run: actions/checkout@v2 - run: ~/.pyenv/shims/pip install -r requirements.txt pytest - run: ~/.pyenv/shims/pytest \ No newline at end of file From 9208b516152740ca0fb4e930a607e61075e77174 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Sun, 8 May 2022 17:13:52 +0530 Subject: [PATCH 54/92] try with upgrade --- .github/workflows/containerized_build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index 34a3497..024d531 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -23,6 +23,7 @@ jobs: - run: export DEBIAN_FRONTEND=noninteractive - run: apt -q update - run: apt -q install make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev git -y + - run: apt -q upgrade - run: curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash - run: exec $SHELL - run: ~/.pyenv/bin/pyenv update From 682c71bf8cf0f43fe7532e3e17501167c7885973 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Sun, 8 May 2022 17:16:17 +0530 Subject: [PATCH 55/92] -y in upgrade --- .github/workflows/containerized_build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index 024d531..46e25b7 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -23,7 +23,7 @@ jobs: - run: export DEBIAN_FRONTEND=noninteractive - run: apt -q update - run: apt -q install make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev git -y - - run: apt -q upgrade + - run: apt -q upgrade -y - run: curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash - run: exec $SHELL - run: ~/.pyenv/bin/pyenv update From ba6f8b2b763cb826f1f47667ba266a72fbe58dac Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Sun, 8 May 2022 17:27:28 +0530 Subject: [PATCH 56/92] get versions --- .github/workflows/containerized_build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index 46e25b7..d431ca3 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -30,5 +30,7 @@ jobs: - run: ~/.pyenv/bin/pyenv install 3.10.4 - run: ~/.pyenv/bin/pyenv virtualenv 3.10.4 npbc - run: ~/.pyenv/bin/pyenv local 3.10.4/envs/npbc + - run: ~/.pyenv/shims/python -V + - run: ~/.pyenv/shims/python -c "import sqlite3; print(sqlite3.version)" - run: ~/.pyenv/shims/pip install -r requirements.txt pytest - run: ~/.pyenv/shims/pytest \ No newline at end of file From 133a95d091dab8f5c04c8a6b1c112f64b73cd6dc Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Sun, 8 May 2022 17:34:23 +0530 Subject: [PATCH 57/92] get sqlite --- .github/workflows/containerized_build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index d431ca3..378b340 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -22,7 +22,7 @@ jobs: - uses: actions/checkout@v2 - run: export DEBIAN_FRONTEND=noninteractive - run: apt -q update - - run: apt -q install make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev git -y + - run: apt -q install make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev git sqlite3 -y - run: apt -q upgrade -y - run: curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash - run: exec $SHELL From 4ab28ce06b1b231f936f52bd220eeecce203dd0f Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Mon, 9 May 2022 03:21:36 +0530 Subject: [PATCH 58/92] get detail --- .github/workflows/containerized_build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index 378b340..147a8ff 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -33,4 +33,4 @@ jobs: - run: ~/.pyenv/shims/python -V - run: ~/.pyenv/shims/python -c "import sqlite3; print(sqlite3.version)" - run: ~/.pyenv/shims/pip install -r requirements.txt pytest - - run: ~/.pyenv/shims/pytest \ No newline at end of file + - run: ~/.pyenv/shims/pytest -vv \ No newline at end of file From 5de39acb6442cd449bb636591fd5cd0b45de9140 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Mon, 9 May 2022 03:28:02 +0530 Subject: [PATCH 59/92] different python --- .github/workflows/containerized_build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index 147a8ff..c18ab83 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -27,9 +27,9 @@ jobs: - run: curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash - run: exec $SHELL - run: ~/.pyenv/bin/pyenv update - - run: ~/.pyenv/bin/pyenv install 3.10.4 - - run: ~/.pyenv/bin/pyenv virtualenv 3.10.4 npbc - - run: ~/.pyenv/bin/pyenv local 3.10.4/envs/npbc + - run: ~/.pyenv/bin/pyenv install 3.10.2 + - run: ~/.pyenv/bin/pyenv virtualenv 3.10.2 npbc + - run: ~/.pyenv/bin/pyenv local 3.10.2/envs/npbc - run: ~/.pyenv/shims/python -V - run: ~/.pyenv/shims/python -c "import sqlite3; print(sqlite3.version)" - run: ~/.pyenv/shims/pip install -r requirements.txt pytest From 8ff3ae32e9d65879d16872e7d704d7c4d40ac0cf Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Mon, 9 May 2022 11:57:50 +0530 Subject: [PATCH 60/92] recent ubuntu --- .github/workflows/containerized_build.yml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index c18ab83..11b66ad 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -15,22 +15,37 @@ jobs: # to avoid using old sqlite version container: - image: debian:latest + image: ubuntu:22.04 options: --user root steps: + # check out repo - uses: actions/checkout@v2 + + # prevent from asking user for input - run: export DEBIAN_FRONTEND=noninteractive + + # install recommended tools for building Python - run: apt -q update - run: apt -q install make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev git sqlite3 -y - run: apt -q upgrade -y + + # install pyenv - run: curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash - run: exec $SHELL - run: ~/.pyenv/bin/pyenv update + + # install and set up required Python - run: ~/.pyenv/bin/pyenv install 3.10.2 - run: ~/.pyenv/bin/pyenv virtualenv 3.10.2 npbc - run: ~/.pyenv/bin/pyenv local 3.10.2/envs/npbc + + # print version info (debugging) - run: ~/.pyenv/shims/python -V - run: ~/.pyenv/shims/python -c "import sqlite3; print(sqlite3.version)" + + # install pip packages - run: ~/.pyenv/shims/pip install -r requirements.txt pytest + + # run test - run: ~/.pyenv/shims/pytest -vv \ No newline at end of file From c6d633d7ad3b6882486e70fb75ad933f5981a11e Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Mon, 9 May 2022 12:11:18 +0530 Subject: [PATCH 61/92] make and install sqlite --- .github/workflows/containerized_build.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index 11b66ad..5db45b9 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -15,7 +15,7 @@ jobs: # to avoid using old sqlite version container: - image: ubuntu:22.04 + image: debian:latest options: --user root steps: @@ -30,6 +30,16 @@ jobs: - run: apt -q install make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev git sqlite3 -y - run: apt -q upgrade -y + # make and install sqlite + - run: mkdir /tmp/sqlite -p + - run: wget https://sqlite.org/2022/sqlite-autoconf-3380500.tar.gz -P /tmp/sqlite + - run: tar -xvf /tmp/sqlite/sqlite-autoconf-sqlite-autoconf-3380500 && cd sqlite-autoconf-3340100 + - run: ./configure + - run: make + - run: make install + - run: export PATH="/usr/local/lib:$PATH" + - run: sqlite3 --version + # install pyenv - run: curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash - run: exec $SHELL From c447f20b9d2f49626174842ca612da482166acd0 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Mon, 9 May 2022 12:18:48 +0530 Subject: [PATCH 62/92] tar --- .github/workflows/containerized_build.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index 5db45b9..9ab80bb 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -21,6 +21,7 @@ jobs: steps: # check out repo - uses: actions/checkout@v2 + - run: pwd # prevent from asking user for input - run: export DEBIAN_FRONTEND=noninteractive @@ -33,7 +34,7 @@ jobs: # make and install sqlite - run: mkdir /tmp/sqlite -p - run: wget https://sqlite.org/2022/sqlite-autoconf-3380500.tar.gz -P /tmp/sqlite - - run: tar -xvf /tmp/sqlite/sqlite-autoconf-sqlite-autoconf-3380500 && cd sqlite-autoconf-3340100 + - run: tar -xvf /tmp/sqlite/sqlite-autoconf-sqlite-autoconf-3380500.tar.gz && cd sqlite-autoconf-3340100 - run: ./configure - run: make - run: make install From df690a745585c32621e0c0ec21e7fefe1031acb5 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Mon, 9 May 2022 14:08:09 +0530 Subject: [PATCH 63/92] retry --- .github/workflows/containerized_build.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index 9ab80bb..d865da9 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -21,7 +21,6 @@ jobs: steps: # check out repo - uses: actions/checkout@v2 - - run: pwd # prevent from asking user for input - run: export DEBIAN_FRONTEND=noninteractive @@ -32,9 +31,8 @@ jobs: - run: apt -q upgrade -y # make and install sqlite - - run: mkdir /tmp/sqlite -p - - run: wget https://sqlite.org/2022/sqlite-autoconf-3380500.tar.gz -P /tmp/sqlite - - run: tar -xvf /tmp/sqlite/sqlite-autoconf-sqlite-autoconf-3380500.tar.gz && cd sqlite-autoconf-3340100 + - run: wget https://sqlite.org/2022/sqlite-autoconf-3380500.tar.gz + - run: tar -xvf sqlite-autoconf-3380500.tar.gz && cd sqlite-autoconf-3380500 - run: ./configure - run: make - run: make install @@ -42,6 +40,7 @@ jobs: - run: sqlite3 --version # install pyenv + - run: cd /__w/npbc/npbc - run: curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash - run: exec $SHELL - run: ~/.pyenv/bin/pyenv update From 9e04ad85e853436b916fbd677069a40a421bcc1b Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Mon, 9 May 2022 14:15:56 +0530 Subject: [PATCH 64/92] dirs --- .github/workflows/build.yml | 142 +++++++++++----------- .github/workflows/containerized_build.yml | 5 +- 2 files changed, 75 insertions(+), 72 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4297227..64829ee 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,75 +1,75 @@ -# name: build and release +name: build and release -# on: [push] +on: [push] -# jobs: -# test: -# name: pytest -# runs-on: ubuntu-latest -# steps: -# - uses: actions/checkout@v2 -# - uses: actions/setup-python@v2 -# with: -# python-version: '3.10' -# - run: pip install -r requirements.txt pytest -# - uses: cclauss/GitHub-Action-for-pytest@0.5.0 -# - run: pytest +jobs: + test: + name: pytest + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: '3.10' + - run: pip install -r requirements.txt pytest + - uses: cclauss/GitHub-Action-for-pytest@0.5.0 + - run: pytest -# build: -# needs: -# - test -# strategy: -# matrix: -# os: -# - ubuntu -# - windows -# - macos -# architecture: ['x64'] -# app: ['cli', updater] -# include: -# - os: windows -# data-file: data/schema.sql;. -# name: windows -# - os: macos -# data-file: data/schema.sql:. -# name: macos -# - os: ubuntu -# data-file: data/schema.sql:. -# name: linux -# runs-on: ${{ matrix.os }}-latest -# name: ${{ matrix.app }}-${{ matrix.name }}-${{ matrix.architecture }} -# steps: -# - uses: actions/checkout@v2 -# - uses: actions/setup-python@v2 -# with: -# python-version: '3.10' -# architecture: ${{ matrix.architecture }} -# - run: pip install -r requirements.txt pyinstaller -# - run: mkdir build -# - run: mkdir bin -# - run: pyinstaller --distpath bin --clean --add-data "${{ matrix.data-file }}" --onefile --name npbc_${{ matrix.app }}-${{ matrix.name }}-${{ matrix.architecture }} npbc_${{ matrix.app }}.py -# - uses: actions/upload-artifact@v2 -# with: -# path: bin -# name: npbc_${{ matrix.app }}-${{ matrix.name }}-${{ matrix.architecture }} + build: + needs: + - test + strategy: + matrix: + os: + - ubuntu + - windows + - macos + architecture: ['x64'] + app: ['cli', updater] + include: + - os: windows + data-file: data/schema.sql;. + name: windows + - os: macos + data-file: data/schema.sql:. + name: macos + - os: ubuntu + data-file: data/schema.sql:. + name: linux + runs-on: ${{ matrix.os }}-latest + name: ${{ matrix.app }}-${{ matrix.name }}-${{ matrix.architecture }} + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: '3.10' + architecture: ${{ matrix.architecture }} + - run: pip install -r requirements.txt pyinstaller + - run: mkdir build + - run: mkdir bin + - run: pyinstaller --distpath bin --clean --add-data "${{ matrix.data-file }}" --onefile --name npbc_${{ matrix.app }}-${{ matrix.name }}-${{ matrix.architecture }} npbc_${{ matrix.app }}.py + - uses: actions/upload-artifact@v2 + with: + path: bin + name: npbc_${{ matrix.app }}-${{ matrix.name }}-${{ matrix.architecture }} -# release: -# needs: -# - build -# if: startsWith(github.ref, 'refs/tags/v') -# runs-on: ubuntu-latest -# permissions: -# contents: write -# steps: -# - uses: actions/checkout@v2 -# - run: mkdir bin -# - uses: actions/download-artifact@v2 -# with: -# path: bin -# - uses: ncipollo/release-action@v1 -# with: -# artifacts: "bin/npbc*/*" -# token: ${{ secrets.GITHUB_TOKEN }} -# generateReleaseNotes: true -# artifactErrorsFailBuild: true -# prerelease: false + release: + needs: + - build + if: startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v2 + - run: mkdir bin + - uses: actions/download-artifact@v2 + with: + path: bin + - uses: ncipollo/release-action@v1 + with: + artifacts: "bin/npbc*/*" + token: ${{ secrets.GITHUB_TOKEN }} + generateReleaseNotes: true + artifactErrorsFailBuild: true + prerelease: false diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index d865da9..728102f 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -32,7 +32,10 @@ jobs: # make and install sqlite - run: wget https://sqlite.org/2022/sqlite-autoconf-3380500.tar.gz - - run: tar -xvf sqlite-autoconf-3380500.tar.gz && cd sqlite-autoconf-3380500 + - run: tar -xvf sqlite-autoconf-3380500.tar.gz + - run: cd sqlite-autoconf-3380500 + - run: ls + - run: pwd - run: ./configure - run: make - run: make install From 852b86d5d355a6a5f80ca1ed0dd2fe596f21f2d1 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Mon, 9 May 2022 14:22:11 +0530 Subject: [PATCH 65/92] try abs --- .github/workflows/containerized_build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index 728102f..9267845 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -33,12 +33,12 @@ jobs: # make and install sqlite - run: wget https://sqlite.org/2022/sqlite-autoconf-3380500.tar.gz - run: tar -xvf sqlite-autoconf-3380500.tar.gz - - run: cd sqlite-autoconf-3380500 + # - run: cd sqlite-autoconf-3380500 - run: ls - run: pwd - - run: ./configure - - run: make - - run: make install + - run: sqlite-autoconf-3380500/configure + - run: make sqlite-autoconf-3380500 + - run: make sqlite-autoconf-3380500/install - run: export PATH="/usr/local/lib:$PATH" - run: sqlite3 --version From 37a14fce588827289e31ad9aaa915268851f7f65 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Mon, 9 May 2022 14:25:36 +0530 Subject: [PATCH 66/92] try cd again --- .github/workflows/containerized_build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index 9267845..8a42dc4 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -33,12 +33,12 @@ jobs: # make and install sqlite - run: wget https://sqlite.org/2022/sqlite-autoconf-3380500.tar.gz - run: tar -xvf sqlite-autoconf-3380500.tar.gz - # - run: cd sqlite-autoconf-3380500 + - run: cd cd /__w/npbc/npbc/sqlite-autoconf-3380500 - run: ls - run: pwd - - run: sqlite-autoconf-3380500/configure - - run: make sqlite-autoconf-3380500 - - run: make sqlite-autoconf-3380500/install + - run: ./configure + - run: make + - run: make install - run: export PATH="/usr/local/lib:$PATH" - run: sqlite3 --version From 445fada72f1ff295f631633280b2dcbd9af6b2d6 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Mon, 9 May 2022 14:25:45 +0530 Subject: [PATCH 67/92] cd typo --- .github/workflows/containerized_build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index 8a42dc4..fa572fe 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -33,7 +33,7 @@ jobs: # make and install sqlite - run: wget https://sqlite.org/2022/sqlite-autoconf-3380500.tar.gz - run: tar -xvf sqlite-autoconf-3380500.tar.gz - - run: cd cd /__w/npbc/npbc/sqlite-autoconf-3380500 + - run: cd /__w/npbc/npbc/sqlite-autoconf-3380500 - run: ls - run: pwd - run: ./configure From e8ad9fd9eb8978356eed6ecc1e5165ad0dff238d Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Mon, 9 May 2022 14:27:39 +0530 Subject: [PATCH 68/92] list all --- .github/workflows/containerized_build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index fa572fe..92a4fbb 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -34,7 +34,7 @@ jobs: - run: wget https://sqlite.org/2022/sqlite-autoconf-3380500.tar.gz - run: tar -xvf sqlite-autoconf-3380500.tar.gz - run: cd /__w/npbc/npbc/sqlite-autoconf-3380500 - - run: ls + - run: ls -al - run: pwd - run: ./configure - run: make From 091cbd2eb7c7944ddb08e11268ad3f51e20d6f66 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Mon, 9 May 2022 14:32:19 +0530 Subject: [PATCH 69/92] working dir --- .github/workflows/containerized_build.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index 92a4fbb..b685b80 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -33,12 +33,17 @@ jobs: # make and install sqlite - run: wget https://sqlite.org/2022/sqlite-autoconf-3380500.tar.gz - run: tar -xvf sqlite-autoconf-3380500.tar.gz - - run: cd /__w/npbc/npbc/sqlite-autoconf-3380500 + # - run: cd /__w/npbc/npbc/sqlite-autoconf-3380500 - run: ls -al + working-directory: sqlite-autoconf-3380500 - run: pwd + working-directory: sqlite-autoconf-3380500 - run: ./configure - - run: make + working-directory: sqlite-autoconf-3380500 + - run: make + working-directory: sqlite-autoconf-3380500 - run: make install + working-directory: sqlite-autoconf-3380500 - run: export PATH="/usr/local/lib:$PATH" - run: sqlite3 --version From cf0a14e3b633f6c933b36961e8dad43bfc94bf9f Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Mon, 9 May 2022 14:57:44 +0530 Subject: [PATCH 70/92] try new --- .github/workflows/containerized_build.yml | 1 - .github/workflows/non-container-test.yml | 32 +++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/non-container-test.yml diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index b685b80..e5798a4 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -33,7 +33,6 @@ jobs: # make and install sqlite - run: wget https://sqlite.org/2022/sqlite-autoconf-3380500.tar.gz - run: tar -xvf sqlite-autoconf-3380500.tar.gz - # - run: cd /__w/npbc/npbc/sqlite-autoconf-3380500 - run: ls -al working-directory: sqlite-autoconf-3380500 - run: pwd diff --git a/.github/workflows/non-container-test.yml b/.github/workflows/non-container-test.yml new file mode 100644 index 0000000..aa4f1a5 --- /dev/null +++ b/.github/workflows/non-container-test.yml @@ -0,0 +1,32 @@ +name: Test + +on: [push] + +jobs: + test: + name: pytest + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: '3.10' + + - run: wget https://sqlite.org/2022/sqlite-autoconf-3380500.tar.gz + - run: tar -xvf sqlite-autoconf-3380500.tar.gz + - run: ls -al + working-directory: sqlite-autoconf-3380500 + - run: pwd + working-directory: sqlite-autoconf-3380500 + - run: ./configure + working-directory: sqlite-autoconf-3380500 + - run: make + working-directory: sqlite-autoconf-3380500 + - run: make install + working-directory: sqlite-autoconf-3380500 + - run: export PATH="/usr/local/lib:$PATH" + - run: sqlite3 --version + + - run: pip install -r requirements.txt pytest + - uses: cclauss/GitHub-Action-for-pytest@0.5.0 + - run: pytest \ No newline at end of file From fa6f1163c7cddf154ec5a71cefde836122eb97bd Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Mon, 9 May 2022 15:04:55 +0530 Subject: [PATCH 71/92] sudo --- .github/workflows/non-container-test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/non-container-test.yml b/.github/workflows/non-container-test.yml index aa4f1a5..60ea9d4 100644 --- a/.github/workflows/non-container-test.yml +++ b/.github/workflows/non-container-test.yml @@ -6,6 +6,7 @@ jobs: test: name: pytest runs-on: ubuntu-latest + steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 @@ -22,7 +23,7 @@ jobs: working-directory: sqlite-autoconf-3380500 - run: make working-directory: sqlite-autoconf-3380500 - - run: make install + - run: sudo make install working-directory: sqlite-autoconf-3380500 - run: export PATH="/usr/local/lib:$PATH" - run: sqlite3 --version From 1f1f819e4c4b96999d5e2fe786f12ae2b165df03 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Mon, 9 May 2022 15:15:44 +0530 Subject: [PATCH 72/92] try change query to specify table --- npbc_core.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/npbc_core.py b/npbc_core.py index 0009dda..535aee5 100644 --- a/npbc_core.py +++ b/npbc_core.py @@ -341,7 +341,7 @@ def save_results( """ INSERT INTO logs (paper_id, month, year, timestamp) VALUES (?, ?, ?, ?) - RETURNING log_id; + RETURNING logs.log_id; """, (paper_id, month, year, timestamp) ).fetchone()[0] @@ -408,14 +408,14 @@ def add_new_paper(name: str, days_delivered: list[bool], days_cost: list[float]) # insert the paper paper_id = connection.execute( - "INSERT INTO papers (name) VALUES (?) RETURNING paper_id;", + "INSERT INTO papers (name) VALUES (?) RETURNING papers.paper_id;", (name,) ).fetchone()[0] # create days for the paper paper_days = { day_id: connection.execute( - "INSERT INTO papers_days (paper_id, day_id) VALUES (?, ?) RETURNING paper_day_id;", + "INSERT INTO papers_days (paper_id, day_id) VALUES (?, ?) RETURNING papers_days.paper_day_id;", (paper_id, day_id) ).fetchone()[0] for day_id, _ in enumerate(days_delivered) From cf47956bd0d34806f9b9a5e6bc6cf9fd529c604c Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Mon, 9 May 2022 15:30:52 +0530 Subject: [PATCH 73/92] linker path --- .github/workflows/containerized_build.yml | 1 + .github/workflows/non-container-test.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index e5798a4..21b268a 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -47,6 +47,7 @@ jobs: - run: sqlite3 --version # install pyenv + - run: LD_LIBRARY_PATH=/usr/local/lib - run: cd /__w/npbc/npbc - run: curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash - run: exec $SHELL diff --git a/.github/workflows/non-container-test.yml b/.github/workflows/non-container-test.yml index 60ea9d4..3e8723c 100644 --- a/.github/workflows/non-container-test.yml +++ b/.github/workflows/non-container-test.yml @@ -26,6 +26,7 @@ jobs: - run: sudo make install working-directory: sqlite-autoconf-3380500 - run: export PATH="/usr/local/lib:$PATH" + - run: LD_LIBRARY_PATH=/usr/local/lib - run: sqlite3 --version - run: pip install -r requirements.txt pytest From 2ededaca8a5323c730eae00b0f0b5c50e8dd56c9 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Mon, 9 May 2022 15:36:40 +0530 Subject: [PATCH 74/92] env --- .github/workflows/containerized_build.yml | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml index 21b268a..3e431c0 100644 --- a/.github/workflows/containerized_build.yml +++ b/.github/workflows/containerized_build.yml @@ -47,23 +47,42 @@ jobs: - run: sqlite3 --version # install pyenv - - run: LD_LIBRARY_PATH=/usr/local/lib - run: cd /__w/npbc/npbc - run: curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash + env: + LD_LIBRARY_PATH: /usr/local/lib - run: exec $SHELL + env: + LD_LIBRARY_PATH: /usr/local/lib - run: ~/.pyenv/bin/pyenv update + env: + LD_LIBRARY_PATH: /usr/local/lib # install and set up required Python - run: ~/.pyenv/bin/pyenv install 3.10.2 + env: + LD_LIBRARY_PATH: /usr/local/lib - run: ~/.pyenv/bin/pyenv virtualenv 3.10.2 npbc + env: + LD_LIBRARY_PATH: /usr/local/lib - run: ~/.pyenv/bin/pyenv local 3.10.2/envs/npbc + env: + LD_LIBRARY_PATH: /usr/local/lib # print version info (debugging) - run: ~/.pyenv/shims/python -V + env: + LD_LIBRARY_PATH: /usr/local/lib - run: ~/.pyenv/shims/python -c "import sqlite3; print(sqlite3.version)" + env: + LD_LIBRARY_PATH: /usr/local/lib # install pip packages - run: ~/.pyenv/shims/pip install -r requirements.txt pytest + env: + LD_LIBRARY_PATH: /usr/local/lib # run test - - run: ~/.pyenv/shims/pytest -vv \ No newline at end of file + - run: ~/.pyenv/shims/pytest -vv + env: + LD_LIBRARY_PATH: /usr/local/lib \ No newline at end of file From 00122281095a4387919b1348f52554f9e9c53c2e Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Mon, 9 May 2022 15:37:34 +0530 Subject: [PATCH 75/92] env --- .github/workflows/non-container-test.yml | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/non-container-test.yml b/.github/workflows/non-container-test.yml index 3e8723c..cb451d1 100644 --- a/.github/workflows/non-container-test.yml +++ b/.github/workflows/non-container-test.yml @@ -9,9 +9,6 @@ jobs: steps: - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - with: - python-version: '3.10' - run: wget https://sqlite.org/2022/sqlite-autoconf-3380500.tar.gz - run: tar -xvf sqlite-autoconf-3380500.tar.gz @@ -26,9 +23,19 @@ jobs: - run: sudo make install working-directory: sqlite-autoconf-3380500 - run: export PATH="/usr/local/lib:$PATH" - - run: LD_LIBRARY_PATH=/usr/local/lib - run: sqlite3 --version + env: + LD_LIBRARY_PATH: /usr/local/lib + - uses: actions/setup-python@v2 + with: + python-version: '3.10' - run: pip install -r requirements.txt pytest + env: + LD_LIBRARY_PATH: /usr/local/lib - uses: cclauss/GitHub-Action-for-pytest@0.5.0 - - run: pytest \ No newline at end of file + env: + LD_LIBRARY_PATH: /usr/local/lib + - run: pytest + env: + LD_LIBRARY_PATH: /usr/local/lib \ No newline at end of file From c2898b3b95c188742b91b8e9d07e674481104386 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Mon, 9 May 2022 15:41:13 +0530 Subject: [PATCH 76/92] rm unnecessary envs --- .github/workflows/non-container-test.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/non-container-test.yml b/.github/workflows/non-container-test.yml index cb451d1..8fe77a3 100644 --- a/.github/workflows/non-container-test.yml +++ b/.github/workflows/non-container-test.yml @@ -23,19 +23,12 @@ jobs: - run: sudo make install working-directory: sqlite-autoconf-3380500 - run: export PATH="/usr/local/lib:$PATH" - - run: sqlite3 --version - env: - LD_LIBRARY_PATH: /usr/local/lib - uses: actions/setup-python@v2 with: python-version: '3.10' - run: pip install -r requirements.txt pytest - env: - LD_LIBRARY_PATH: /usr/local/lib - uses: cclauss/GitHub-Action-for-pytest@0.5.0 - env: - LD_LIBRARY_PATH: /usr/local/lib - run: pytest env: LD_LIBRARY_PATH: /usr/local/lib \ No newline at end of file From 8c5cab45ec75502210cbcae4d5f20381224841d3 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Mon, 9 May 2022 16:00:32 +0530 Subject: [PATCH 77/92] build windows --- .github/workflows/non-container-test.yml | 4 -- .github/workflows/test-build-release.yml | 64 ++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/test-build-release.yml diff --git a/.github/workflows/non-container-test.yml b/.github/workflows/non-container-test.yml index 8fe77a3..d40c70e 100644 --- a/.github/workflows/non-container-test.yml +++ b/.github/workflows/non-container-test.yml @@ -12,10 +12,6 @@ jobs: - run: wget https://sqlite.org/2022/sqlite-autoconf-3380500.tar.gz - run: tar -xvf sqlite-autoconf-3380500.tar.gz - - run: ls -al - working-directory: sqlite-autoconf-3380500 - - run: pwd - working-directory: sqlite-autoconf-3380500 - run: ./configure working-directory: sqlite-autoconf-3380500 - run: make diff --git a/.github/workflows/test-build-release.yml b/.github/workflows/test-build-release.yml new file mode 100644 index 0000000..0279843 --- /dev/null +++ b/.github/workflows/test-build-release.yml @@ -0,0 +1,64 @@ +name: Test, build and release + +on: [push] + +jobs: + + # test with pytest + test: + name: pytest + runs-on: ubuntu-latest + + # used to ensure testing directories are used, not user directories + env: + DEVELOPMENT: '1' + + steps: + - uses: actions/checkout@v2 + + # build SQLite from source, because I need 3.35<= + - run: wget https://sqlite.org/2022/sqlite-autoconf-3380500.tar.gz + - run: tar -xvf sqlite-autoconf-3380500.tar.gz + - run: ./configure + working-directory: sqlite-autoconf-3380500 + - run: make + working-directory: sqlite-autoconf-3380500 + - run: sudo make install + working-directory: sqlite-autoconf-3380500 + - run: export PATH="/usr/local/lib:$PATH" + + # run pytest + - uses: actions/setup-python@v2 + with: + python-version: '3.10' + - run: pip install -r requirements.txt pytest + - run: pytest + env: + LD_LIBRARY_PATH: /usr/local/lib + + # build executable for windows + build-windows: + name: build for windows + runs-on: windows-latest + needs: test + + steps: + + # setup + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - run: pip install -r requirements.txt pyinstaller + - run: mkdir bin + - run: mkdir build + + # build + - run: pyinstaller --distpath bin --clean --add-data "data/schema.sql;" --onefile --name npbc_cli-windows-x64 npbc_cli.py + - run: pyinstaller --distpath bin --clean --onefile --name npbc_updater-windows-x64 npbc_updater.py + + # upload artifacts + - uses: actions/upload-artifact@v2 + with: + path: bin + name: | + npbc_cli-windows-x64 + npbc_updater-windows-x64 \ No newline at end of file From fb7e6afe2bb116c045be038521d223e43159eaa5 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Mon, 9 May 2022 16:05:48 +0530 Subject: [PATCH 78/92] . issue --- .github/workflows/test-build-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-build-release.yml b/.github/workflows/test-build-release.yml index 0279843..a1ee29f 100644 --- a/.github/workflows/test-build-release.yml +++ b/.github/workflows/test-build-release.yml @@ -52,7 +52,7 @@ jobs: - run: mkdir build # build - - run: pyinstaller --distpath bin --clean --add-data "data/schema.sql;" --onefile --name npbc_cli-windows-x64 npbc_cli.py + - run: pyinstaller --distpath bin --clean --add-data "data/schema.sql;." --onefile --name npbc_cli-windows-x64 npbc_cli.py - run: pyinstaller --distpath bin --clean --onefile --name npbc_updater-windows-x64 npbc_updater.py # upload artifacts From a5a32394604153f800647314bd8a272f0ab66817 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Mon, 9 May 2022 16:06:34 +0530 Subject: [PATCH 79/92] temp faster --- .github/workflows/test-build-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-build-release.yml b/.github/workflows/test-build-release.yml index a1ee29f..8daab9c 100644 --- a/.github/workflows/test-build-release.yml +++ b/.github/workflows/test-build-release.yml @@ -40,7 +40,7 @@ jobs: build-windows: name: build for windows runs-on: windows-latest - needs: test + # needs: test steps: From 408fd39c2d598187798ee8548446e3853cbc9929 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Mon, 9 May 2022 16:10:15 +0530 Subject: [PATCH 80/92] try .exe --- .github/workflows/test-build-release.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-build-release.yml b/.github/workflows/test-build-release.yml index 8daab9c..51e33f4 100644 --- a/.github/workflows/test-build-release.yml +++ b/.github/workflows/test-build-release.yml @@ -55,10 +55,12 @@ jobs: - run: pyinstaller --distpath bin --clean --add-data "data/schema.sql;." --onefile --name npbc_cli-windows-x64 npbc_cli.py - run: pyinstaller --distpath bin --clean --onefile --name npbc_updater-windows-x64 npbc_updater.py + - run: ls + # upload artifacts - uses: actions/upload-artifact@v2 with: path: bin name: | - npbc_cli-windows-x64 - npbc_updater-windows-x64 \ No newline at end of file + npbc_cli-windows-x64.exe + npbc_updater-windows-x64.exe From eeed929a7d49485c132425d4063b5ef7aaee78a4 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Mon, 9 May 2022 16:13:13 +0530 Subject: [PATCH 81/92] all? --- .github/workflows/test-build-release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-build-release.yml b/.github/workflows/test-build-release.yml index 51e33f4..883bc41 100644 --- a/.github/workflows/test-build-release.yml +++ b/.github/workflows/test-build-release.yml @@ -56,11 +56,11 @@ jobs: - run: pyinstaller --distpath bin --clean --onefile --name npbc_updater-windows-x64 npbc_updater.py - run: ls + - run: bin + working-directory: bin # upload artifacts - uses: actions/upload-artifact@v2 with: path: bin - name: | - npbc_cli-windows-x64.exe - npbc_updater-windows-x64.exe + name: ./* \ No newline at end of file From dbf63c5aa4d8378391fb18ed9524a240db8cb847 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Mon, 9 May 2022 16:20:15 +0530 Subject: [PATCH 82/92] separate --- .github/workflows/test-build-release.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test-build-release.yml b/.github/workflows/test-build-release.yml index 883bc41..95cb92a 100644 --- a/.github/workflows/test-build-release.yml +++ b/.github/workflows/test-build-release.yml @@ -55,12 +55,12 @@ jobs: - run: pyinstaller --distpath bin --clean --add-data "data/schema.sql;." --onefile --name npbc_cli-windows-x64 npbc_cli.py - run: pyinstaller --distpath bin --clean --onefile --name npbc_updater-windows-x64 npbc_updater.py - - run: ls - - run: bin - working-directory: bin - # upload artifacts - uses: actions/upload-artifact@v2 with: path: bin - name: ./* \ No newline at end of file + name: npbc_cli-windows-x64 + - uses: actions/upload-artifact@v2 + with: + path: bin + name: npbc_updater-windows-x64 \ No newline at end of file From 6baa31ff5645839510fc24215b2a29a0aa420b33 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Mon, 9 May 2022 16:27:00 +0530 Subject: [PATCH 83/92] introduce linux and macos --- .github/workflows/test-build-release.yml | 60 +++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-build-release.yml b/.github/workflows/test-build-release.yml index 95cb92a..ee48fe7 100644 --- a/.github/workflows/test-build-release.yml +++ b/.github/workflows/test-build-release.yml @@ -63,4 +63,62 @@ jobs: - uses: actions/upload-artifact@v2 with: path: bin - name: npbc_updater-windows-x64 \ No newline at end of file + name: npbc_updater-windows-x64 + + # build executable for linux + build-linux: + name: build for linux + runs-on: ubuntu-latest + # needs: test + + steps: + + # setup + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - run: pip install -r requirements.txt pyinstaller + - run: mkdir bin + - run: mkdir build + + # build + - run: pyinstaller --distpath bin --clean --add-data "data/schema.sql:." --onefile --name npbc_cli-linux-x64 npbc_cli.py + - run: pyinstaller --distpath bin --clean --onefile --name npbc_updater-linux-x64 npbc_updater.py + + # upload artifacts + - uses: actions/upload-artifact@v2 + with: + path: bin + name: npbc_cli-linux-x64 + - uses: actions/upload-artifact@v2 + with: + path: bin + name: npbc_updater-linux-x64 + + # build executable for windows + build-macos: + name: build for macos + runs-on: macos-latest + # needs: test + + steps: + + # setup + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - run: pip install -r requirements.txt pyinstaller + - run: mkdir bin + - run: mkdir build + + # build + - run: pyinstaller --distpath bin --clean --add-data "data/schema.sql:." --onefile --name npbc_cli-macos-x64 npbc_cli.py + - run: pyinstaller --distpath bin --clean --onefile --name npbc_updater-macos-x64 npbc_updater.py + + # upload artifacts + - uses: actions/upload-artifact@v2 + with: + path: bin + name: npbc_cli-macos-x64 + - uses: actions/upload-artifact@v2 + with: + path: bin + name: npbc_updater-macos-x64 \ No newline at end of file From 9b813cbd701d4c3612d69d01eaff1a998309d8d0 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Mon, 9 May 2022 16:31:11 +0530 Subject: [PATCH 84/92] cleanup --- .github/workflows/test-build-release.yml | 49 +++++++++++++----------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/.github/workflows/test-build-release.yml b/.github/workflows/test-build-release.yml index ee48fe7..f4bafd3 100644 --- a/.github/workflows/test-build-release.yml +++ b/.github/workflows/test-build-release.yml @@ -18,14 +18,13 @@ jobs: # build SQLite from source, because I need 3.35<= - run: wget https://sqlite.org/2022/sqlite-autoconf-3380500.tar.gz - - run: tar -xvf sqlite-autoconf-3380500.tar.gz - - run: ./configure + - run: | + tar -xvf sqlite-autoconf-3380500.tar.gz + ./configure + make + sudo make install + export PATH="/usr/local/lib:$PATH" working-directory: sqlite-autoconf-3380500 - - run: make - working-directory: sqlite-autoconf-3380500 - - run: sudo make install - working-directory: sqlite-autoconf-3380500 - - run: export PATH="/usr/local/lib:$PATH" # run pytest - uses: actions/setup-python@v2 @@ -47,13 +46,15 @@ jobs: # setup - uses: actions/checkout@v2 - uses: actions/setup-python@v2 - - run: pip install -r requirements.txt pyinstaller - - run: mkdir bin - - run: mkdir build + - run: | + pip install -r requirements.txt pyinstaller + mkdir bin + mkdir build # build - - run: pyinstaller --distpath bin --clean --add-data "data/schema.sql;." --onefile --name npbc_cli-windows-x64 npbc_cli.py - - run: pyinstaller --distpath bin --clean --onefile --name npbc_updater-windows-x64 npbc_updater.py + - run: | + pyinstaller --distpath bin --clean --add-data "data/schema.sql;." --onefile --name npbc_cli-windows-x64 npbc_cli.py + pyinstaller --distpath bin --clean --onefile --name npbc_updater-windows-x64 npbc_updater.py # upload artifacts - uses: actions/upload-artifact@v2 @@ -76,13 +77,15 @@ jobs: # setup - uses: actions/checkout@v2 - uses: actions/setup-python@v2 - - run: pip install -r requirements.txt pyinstaller - - run: mkdir bin - - run: mkdir build + - run: | + pip install -r requirements.txt pyinstaller + mkdir bin + mkdir build # build - - run: pyinstaller --distpath bin --clean --add-data "data/schema.sql:." --onefile --name npbc_cli-linux-x64 npbc_cli.py - - run: pyinstaller --distpath bin --clean --onefile --name npbc_updater-linux-x64 npbc_updater.py + - run: | + pyinstaller --distpath bin --clean --add-data "data/schema.sql:." --onefile --name npbc_cli-linux-x64 npbc_cli.py + pyinstaller --distpath bin --clean --onefile --name npbc_updater-linux-x64 npbc_updater.py # upload artifacts - uses: actions/upload-artifact@v2 @@ -105,13 +108,15 @@ jobs: # setup - uses: actions/checkout@v2 - uses: actions/setup-python@v2 - - run: pip install -r requirements.txt pyinstaller - - run: mkdir bin - - run: mkdir build + - run: | + pip install -r requirements.txt pyinstaller + mkdir bin + mkdir build # build - - run: pyinstaller --distpath bin --clean --add-data "data/schema.sql:." --onefile --name npbc_cli-macos-x64 npbc_cli.py - - run: pyinstaller --distpath bin --clean --onefile --name npbc_updater-macos-x64 npbc_updater.py + - run: | + pyinstaller --distpath bin --clean --add-data "data/schema.sql:." --onefile --name npbc_cli-macos-x64 npbc_cli.py + pyinstaller --distpath bin --clean --onefile --name npbc_updater-macos-x64 npbc_updater.py # upload artifacts - uses: actions/upload-artifact@v2 From a10631995bb300029d76e72aec72001ca184ac16 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Mon, 9 May 2022 16:32:20 +0530 Subject: [PATCH 85/92] step error --- .github/workflows/test-build-release.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-build-release.yml b/.github/workflows/test-build-release.yml index f4bafd3..be77c82 100644 --- a/.github/workflows/test-build-release.yml +++ b/.github/workflows/test-build-release.yml @@ -17,9 +17,10 @@ jobs: - uses: actions/checkout@v2 # build SQLite from source, because I need 3.35<= - - run: wget https://sqlite.org/2022/sqlite-autoconf-3380500.tar.gz - run: | + wget https://sqlite.org/2022/sqlite-autoconf-3380500.tar. gz tar -xvf sqlite-autoconf-3380500.tar.gz + - run: | ./configure make sudo make install From 15ed95cdb1c1ab258ba9a9f9324d7cf05c8b16f6 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Mon, 9 May 2022 16:34:29 +0530 Subject: [PATCH 86/92] gz name --- .github/workflows/test-build-release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-build-release.yml b/.github/workflows/test-build-release.yml index be77c82..58baba6 100644 --- a/.github/workflows/test-build-release.yml +++ b/.github/workflows/test-build-release.yml @@ -18,7 +18,7 @@ jobs: # build SQLite from source, because I need 3.35<= - run: | - wget https://sqlite.org/2022/sqlite-autoconf-3380500.tar. gz + wget https://sqlite.org/2022/sqlite-autoconf-3380500.tar.gz tar -xvf sqlite-autoconf-3380500.tar.gz - run: | ./configure @@ -127,4 +127,4 @@ jobs: - uses: actions/upload-artifact@v2 with: path: bin - name: npbc_updater-macos-x64 \ No newline at end of file + name: npbc_updater-macos-x64 \ No newline at end of file From f6f2f9714df8c852b6926dc67ab1e8bf13f932ff Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Mon, 9 May 2022 16:39:01 +0530 Subject: [PATCH 87/92] run release + needs --- .github/workflows/test-build-release.yml | 75 +++++++++++++++++------- 1 file changed, 54 insertions(+), 21 deletions(-) diff --git a/.github/workflows/test-build-release.yml b/.github/workflows/test-build-release.yml index 58baba6..c978d6b 100644 --- a/.github/workflows/test-build-release.yml +++ b/.github/workflows/test-build-release.yml @@ -36,11 +36,11 @@ jobs: env: LD_LIBRARY_PATH: /usr/local/lib - # build executable for windows - build-windows: - name: build for windows - runs-on: windows-latest - # needs: test + # build executable for linux + build-linux: + name: build for linux + runs-on: ubuntu-latest + needs: test steps: @@ -54,24 +54,24 @@ jobs: # build - run: | - pyinstaller --distpath bin --clean --add-data "data/schema.sql;." --onefile --name npbc_cli-windows-x64 npbc_cli.py - pyinstaller --distpath bin --clean --onefile --name npbc_updater-windows-x64 npbc_updater.py + pyinstaller --distpath bin --clean --add-data "data/schema.sql:." --onefile --name npbc_cli-linux-x64 npbc_cli.py + pyinstaller --distpath bin --clean --onefile --name npbc_updater-linux-x64 npbc_updater.py # upload artifacts - uses: actions/upload-artifact@v2 with: path: bin - name: npbc_cli-windows-x64 + name: npbc_cli-linux-x64 - uses: actions/upload-artifact@v2 with: path: bin - name: npbc_updater-windows-x64 + name: npbc_updater-linux-x64 - # build executable for linux - build-linux: - name: build for linux - runs-on: ubuntu-latest - # needs: test + # build executable for windows + build-windows: + name: build for windows + runs-on: windows-latest + needs: test steps: @@ -85,24 +85,24 @@ jobs: # build - run: | - pyinstaller --distpath bin --clean --add-data "data/schema.sql:." --onefile --name npbc_cli-linux-x64 npbc_cli.py - pyinstaller --distpath bin --clean --onefile --name npbc_updater-linux-x64 npbc_updater.py + pyinstaller --distpath bin --clean --add-data "data/schema.sql;." --onefile --name npbc_cli-windows-x64 npbc_cli.py + pyinstaller --distpath bin --clean --onefile --name npbc_updater-windows-x64 npbc_updater.py # upload artifacts - uses: actions/upload-artifact@v2 with: path: bin - name: npbc_cli-linux-x64 + name: npbc_cli-windows-x64 - uses: actions/upload-artifact@v2 with: path: bin - name: npbc_updater-linux-x64 + name: npbc_updater-windows-x64 - # build executable for windows + # build executable for macos build-macos: name: build for macos runs-on: macos-latest - # needs: test + needs: test steps: @@ -127,4 +127,37 @@ jobs: - uses: actions/upload-artifact@v2 with: path: bin - name: npbc_updater-macos-x64 \ No newline at end of file + name: npbc_updater-macos-x64 + + # create release from tag + release: + + # ensure that build is complete for all platforms + needs: + - build-linux + - build-macos + - build-windows + + # run only if we're on a tag beginning with 'v' ('v1.2.5', for example) + if: startsWith(github.ref, 'refs/tags/v') + + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + + # download the artifacts + - run: mkdir bin + - uses: actions/download-artifact@v2 + with: + path: bin + + # do the release + - uses: ncipollo/release-action@v1 + with: + artifacts: "bin/npbc*/*" + token: ${{ secrets.GITHUB_TOKEN }} + generateReleaseNotes: true + artifactErrorsFailBuild: true + prerelease: false \ No newline at end of file From 077d1e20362747e0818429df085602b65857118d Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Mon, 9 May 2022 16:46:50 +0530 Subject: [PATCH 88/92] reconfigure pyinstaller --- .github/workflows/test-build-release.yml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test-build-release.yml b/.github/workflows/test-build-release.yml index c978d6b..36d6ecf 100644 --- a/.github/workflows/test-build-release.yml +++ b/.github/workflows/test-build-release.yml @@ -48,14 +48,15 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 - run: | - pip install -r requirements.txt pyinstaller + pip install pyinstaller mkdir bin mkdir build # build - run: | - pyinstaller --distpath bin --clean --add-data "data/schema.sql:." --onefile --name npbc_cli-linux-x64 npbc_cli.py pyinstaller --distpath bin --clean --onefile --name npbc_updater-linux-x64 npbc_updater.py + pip install -r requirements.txt + pyinstaller --distpath bin --clean --add-data "data/schema.sql:." --onefile --name npbc_cli-linux-x64 npbc_cli.py # upload artifacts - uses: actions/upload-artifact@v2 @@ -79,14 +80,15 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 - run: | - pip install -r requirements.txt pyinstaller + pip install pyinstaller mkdir bin mkdir build # build - run: | - pyinstaller --distpath bin --clean --add-data "data/schema.sql;." --onefile --name npbc_cli-windows-x64 npbc_cli.py pyinstaller --distpath bin --clean --onefile --name npbc_updater-windows-x64 npbc_updater.py + pip install -r requirements.txt + pyinstaller --distpath bin --clean --add-data "data/schema.sql;." --onefile --name npbc_cli-windows-x64 npbc_cli.py # upload artifacts - uses: actions/upload-artifact@v2 @@ -110,14 +112,15 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 - run: | - pip install -r requirements.txt pyinstaller + pip install pyinstaller mkdir bin mkdir build # build - run: | - pyinstaller --distpath bin --clean --add-data "data/schema.sql:." --onefile --name npbc_cli-macos-x64 npbc_cli.py pyinstaller --distpath bin --clean --onefile --name npbc_updater-macos-x64 npbc_updater.py + pip install -r requirements.txt + pyinstaller --distpath bin --clean --add-data "data/schema.sql:." --onefile --name npbc_cli-macos-x64 npbc_cli.py # upload artifacts - uses: actions/upload-artifact@v2 From b41f2d7e60a8035f595ff6d0dd65a9af1bd3357f Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Mon, 9 May 2022 16:55:47 +0530 Subject: [PATCH 89/92] delete old workflows --- .github/workflows/build.yml | 75 ------------------- .github/workflows/containerized_build.yml | 88 ----------------------- .github/workflows/non-container-test.yml | 30 -------- requirements.txt | 3 + 4 files changed, 3 insertions(+), 193 deletions(-) delete mode 100644 .github/workflows/build.yml delete mode 100644 .github/workflows/containerized_build.yml delete mode 100644 .github/workflows/non-container-test.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 64829ee..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,75 +0,0 @@ -name: build and release - -on: [push] - -jobs: - test: - name: pytest - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - with: - python-version: '3.10' - - run: pip install -r requirements.txt pytest - - uses: cclauss/GitHub-Action-for-pytest@0.5.0 - - run: pytest - - build: - needs: - - test - strategy: - matrix: - os: - - ubuntu - - windows - - macos - architecture: ['x64'] - app: ['cli', updater] - include: - - os: windows - data-file: data/schema.sql;. - name: windows - - os: macos - data-file: data/schema.sql:. - name: macos - - os: ubuntu - data-file: data/schema.sql:. - name: linux - runs-on: ${{ matrix.os }}-latest - name: ${{ matrix.app }}-${{ matrix.name }}-${{ matrix.architecture }} - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - with: - python-version: '3.10' - architecture: ${{ matrix.architecture }} - - run: pip install -r requirements.txt pyinstaller - - run: mkdir build - - run: mkdir bin - - run: pyinstaller --distpath bin --clean --add-data "${{ matrix.data-file }}" --onefile --name npbc_${{ matrix.app }}-${{ matrix.name }}-${{ matrix.architecture }} npbc_${{ matrix.app }}.py - - uses: actions/upload-artifact@v2 - with: - path: bin - name: npbc_${{ matrix.app }}-${{ matrix.name }}-${{ matrix.architecture }} - - release: - needs: - - build - if: startsWith(github.ref, 'refs/tags/v') - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v2 - - run: mkdir bin - - uses: actions/download-artifact@v2 - with: - path: bin - - uses: ncipollo/release-action@v1 - with: - artifacts: "bin/npbc*/*" - token: ${{ secrets.GITHUB_TOKEN }} - generateReleaseNotes: true - artifactErrorsFailBuild: true - prerelease: false diff --git a/.github/workflows/containerized_build.yml b/.github/workflows/containerized_build.yml deleted file mode 100644 index 3e431c0..0000000 --- a/.github/workflows/containerized_build.yml +++ /dev/null @@ -1,88 +0,0 @@ -name: Test, build and release - -# whenever a branch or commit is pushed -on: [push] - -jobs: - - # use pytest - test: - - # used to ensure testing is done right - env: - DEVELOPMENT: '1' - runs-on: ubuntu-latest - - # to avoid using old sqlite version - container: - image: debian:latest - options: --user root - - steps: - # check out repo - - uses: actions/checkout@v2 - - # prevent from asking user for input - - run: export DEBIAN_FRONTEND=noninteractive - - # install recommended tools for building Python - - run: apt -q update - - run: apt -q install make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev git sqlite3 -y - - run: apt -q upgrade -y - - # make and install sqlite - - run: wget https://sqlite.org/2022/sqlite-autoconf-3380500.tar.gz - - run: tar -xvf sqlite-autoconf-3380500.tar.gz - - run: ls -al - working-directory: sqlite-autoconf-3380500 - - run: pwd - working-directory: sqlite-autoconf-3380500 - - run: ./configure - working-directory: sqlite-autoconf-3380500 - - run: make - working-directory: sqlite-autoconf-3380500 - - run: make install - working-directory: sqlite-autoconf-3380500 - - run: export PATH="/usr/local/lib:$PATH" - - run: sqlite3 --version - - # install pyenv - - run: cd /__w/npbc/npbc - - run: curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash - env: - LD_LIBRARY_PATH: /usr/local/lib - - run: exec $SHELL - env: - LD_LIBRARY_PATH: /usr/local/lib - - run: ~/.pyenv/bin/pyenv update - env: - LD_LIBRARY_PATH: /usr/local/lib - - # install and set up required Python - - run: ~/.pyenv/bin/pyenv install 3.10.2 - env: - LD_LIBRARY_PATH: /usr/local/lib - - run: ~/.pyenv/bin/pyenv virtualenv 3.10.2 npbc - env: - LD_LIBRARY_PATH: /usr/local/lib - - run: ~/.pyenv/bin/pyenv local 3.10.2/envs/npbc - env: - LD_LIBRARY_PATH: /usr/local/lib - - # print version info (debugging) - - run: ~/.pyenv/shims/python -V - env: - LD_LIBRARY_PATH: /usr/local/lib - - run: ~/.pyenv/shims/python -c "import sqlite3; print(sqlite3.version)" - env: - LD_LIBRARY_PATH: /usr/local/lib - - # install pip packages - - run: ~/.pyenv/shims/pip install -r requirements.txt pytest - env: - LD_LIBRARY_PATH: /usr/local/lib - - # run test - - run: ~/.pyenv/shims/pytest -vv - env: - LD_LIBRARY_PATH: /usr/local/lib \ No newline at end of file diff --git a/.github/workflows/non-container-test.yml b/.github/workflows/non-container-test.yml deleted file mode 100644 index d40c70e..0000000 --- a/.github/workflows/non-container-test.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Test - -on: [push] - -jobs: - test: - name: pytest - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - - run: wget https://sqlite.org/2022/sqlite-autoconf-3380500.tar.gz - - run: tar -xvf sqlite-autoconf-3380500.tar.gz - - run: ./configure - working-directory: sqlite-autoconf-3380500 - - run: make - working-directory: sqlite-autoconf-3380500 - - run: sudo make install - working-directory: sqlite-autoconf-3380500 - - run: export PATH="/usr/local/lib:$PATH" - - - uses: actions/setup-python@v2 - with: - python-version: '3.10' - - run: pip install -r requirements.txt pytest - - uses: cclauss/GitHub-Action-for-pytest@0.5.0 - - run: pytest - env: - LD_LIBRARY_PATH: /usr/local/lib \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 72015e8..61d2c72 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,6 @@ colorama ## Testing # pytest + +## Building +# pyinstaller \ No newline at end of file From 08be4f83c0df09b05fd0147394bad4f43f2dc7b2 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Mon, 9 May 2022 17:06:10 +0530 Subject: [PATCH 90/92] gitignore rules changed --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 1c29774..903704f 100644 --- a/.gitignore +++ b/.gitignore @@ -9,5 +9,7 @@ exp* bin .vscode .pytest_cache -data/*.db +data/* +!data/test.sql +!data/schema.sql .env From 0e6b2f917d2c6300b1bebda69632d314263d9669 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Mon, 9 May 2022 19:21:08 +0530 Subject: [PATCH 91/92] remove unnecessary env variable --- .github/workflows/test-build-release.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/test-build-release.yml b/.github/workflows/test-build-release.yml index 36d6ecf..60f3cff 100644 --- a/.github/workflows/test-build-release.yml +++ b/.github/workflows/test-build-release.yml @@ -9,10 +9,6 @@ jobs: name: pytest runs-on: ubuntu-latest - # used to ensure testing directories are used, not user directories - env: - DEVELOPMENT: '1' - steps: - uses: actions/checkout@v2 From 5fc32775a40853dd227c4fa1ba98d1640acc3e04 Mon Sep 17 00:00:00 2001 From: Anuj Verma Date: Mon, 9 May 2022 19:21:14 +0530 Subject: [PATCH 92/92] add comments --- npbc_cli.py | 173 ++++++++++++++++++++++++++++++++++++++++++++------- npbc_core.py | 35 +++++++++-- 2 files changed, 181 insertions(+), 27 deletions(-) diff --git a/npbc_cli.py b/npbc_cli.py index 8b8b250..2874d8f 100644 --- a/npbc_cli.py +++ b/npbc_cli.py @@ -1,5 +1,5 @@ """ -wraps a CLI around the core functionality (argparse) +wraps a CLI around the core functionality (using argparse) - inherits functionality from `npbc_core.py` - inherits regex from `npbc_regex.py`, used for validation - inherits exceptions from `npbc_exceptions.py`, used for error handling @@ -161,7 +161,11 @@ def define_and_read_args() -> ArgNamespace: def status_print(status: bool, message: str) -> None: - """print out a coloured status message using Colorama""" + """ + print out a coloured status message using Colorama + - if the status is True, print in green (success) + - if the status is False, print in red (failure) + """ if status: print(f"{Fore.GREEN}", end="") @@ -219,20 +223,37 @@ def calculate(args: ArgNamespace) -> None: # ignore if none exist except npbc_exceptions.StringNotExists: pass + + # if there is a database error, print an error message + except sqlite3.DatabaseError as e: + status_print(False, f"Database error: {e}\nPlease report this to the developer.") + return - # calculate the cost for each paper - costs, total, undelivered_dates = npbc_core.calculate_cost_of_all_papers( - undelivered_strings, - month, - year - ) + try: + # calculate the cost for each paper + costs, total, undelivered_dates = npbc_core.calculate_cost_of_all_papers( + undelivered_strings, + month, + year + ) + + # if there is a database error, print an error message + except sqlite3.DatabaseError as e: + status_print(False, f"Database error: {e}\nPlease report this to the developer.") + return # format the results formatted = '\n'.join(npbc_core.format_output(costs, total, month, year)) # unless the user specifies so, log the results to the database if not args.nolog: - npbc_core.save_results(costs, undelivered_dates, month, year) + try: + npbc_core.save_results(costs, undelivered_dates, month, year) + + # if there is a database error, print an error message + except sqlite3.DatabaseError as e: + status_print(False, f"Database error: {e}\nPlease report this to the developer.") + return formatted += '\n\nLog saved to file.' @@ -245,30 +266,44 @@ def addudl(args: ArgNamespace) -> None: """add undelivered strings to the database - default to the current month if no month and/or no year is given""" + # validate the month and year try: npbc_core.validate_month_and_year(args.month, args.year) + # if they are invalid, print an error message except npbc_exceptions.InvalidMonthYear: status_print(False, "Invalid month and/or year.") return + ## deal with month and year + # for any that are not given, set them to the current month and year month = args.month or datetime.now().month year = args.year or datetime.now().year + # if the user either specifies a specific paper or specifies all papers if args.paperid or args.all: + # attempt to add the strings to the database try: print(f"{month=} {year=} {args.paperid=} {args.strings=}") npbc_core.add_undelivered_string(month, year, args.paperid, *args.strings) + # if the paper doesn't exist, print an error message except npbc_exceptions.PaperNotExists: status_print(False, f"Paper with ID {args.paperid} does not exist.") return + # if the string was invalid, print an error message except npbc_exceptions.InvalidUndeliveredString: status_print(False, "Invalid undelivered string(s).") return + + # if there is a database error, print an error message + except sqlite3.DatabaseError as e: + status_print(False, f"Database error: {e}\nPlease report this to the developer.") + return + # if no paper is specified, print an error message else: status_print(False, "No paper(s) specified.") return @@ -279,13 +314,16 @@ def addudl(args: ArgNamespace) -> None: def deludl(args: ArgNamespace) -> None: """delete undelivered strings from the database""" + # validate the month and year try: npbc_core.validate_month_and_year(args.month, args.year) + # if they are invalid, print an error message except npbc_exceptions.InvalidMonthYear: status_print(False, "Invalid month and/or year.") return + # attempt to delete the strings from the database try: npbc_core.delete_undelivered_string( month=args.month, @@ -295,13 +333,20 @@ def deludl(args: ArgNamespace) -> None: string_id=args.stringid ) + # if no parameters are given, print an error message except npbc_exceptions.NoParameters: status_print(False, "No parameters specified.") return + # if the string doesn't exist, print an error message except npbc_exceptions.StringNotExists: status_print(False, "String does not exist.") return + + # if there is a database error, print an error message + except sqlite3.DatabaseError as e: + status_print(False, f"Database error: {e}\nPlease report this to the developer.") + return status_print(True, "Success!") @@ -311,13 +356,16 @@ def getudl(args: ArgNamespace) -> None: filter by whichever parameter the user provides. they as many as they want. available parameters: month, year, paper_id, string_id, string""" + # validate the month and year try: npbc_core.validate_month_and_year(args.month, args.year) + # if they are invalid, print an error message except npbc_exceptions.InvalidMonthYear: status_print(False, "Invalid month and/or year.") return + # attempt to get the strings from the database try: undelivered_strings = npbc_core.get_undelivered_strings( month=args.month, @@ -327,21 +375,21 @@ def getudl(args: ArgNamespace) -> None: string=args.string ) - except npbc_exceptions.NoParameters: - status_print(False, "No parameters specified.") - return - + # if the string doesn't exist, print an error message except npbc_exceptions.StringNotExists: status_print(False, "No strings found for the given parameters.") return - # format the results status_print(True, "Success!") + ## format the results + + # print the column headers print(f"{Fore.YELLOW}string_id{Style.RESET_ALL} | {Fore.YELLOW}paper_id{Style.RESET_ALL} | {Fore.YELLOW}year{Style.RESET_ALL} | {Fore.YELLOW}month{Style.RESET_ALL} | {Fore.YELLOW}string{Style.RESET_ALL}") + # print the strings for items in undelivered_strings: - print('|'.join([str(item) for item in items])) + print(', '.join([str(item) for item in items])) def extract_delivery_from_user_input(input_delivery: str) -> list[bool]: @@ -359,43 +407,69 @@ def extract_delivery_from_user_input(input_delivery: str) -> list[bool]: def extract_costs_from_user_input(paper_id: int | None, delivery_data: list[bool] | None, *input_costs: float) -> Generator[float, None, None]: """convert the user input to a float list""" + # filter the data to remove zeros suspected_data = [ cost for cost in input_costs if cost != 0 ] + + # reverse it so that we can pop to get the first element first (FILO to FIFO) suspected_data.reverse() + # if the delivery data is given, use it if delivery_data: + + # if the number of days the paper is delivered is not equal to the number of costs, raise an error if (len(suspected_data) != delivery_data.count(True)): raise npbc_exceptions.InvalidInput("Number of costs don't match number of days delivered.") + # for each day, yield the cost if it is delivered or 0 if it is not for day in delivery_data: yield suspected_data.pop() if day else 0 + # if the delivery data is not given, but the paper ID is given, get the delivery data from the database elif paper_id: - raw_data = [paper for paper in npbc_core.get_papers() if paper[0] == int(paper_id)] + + # get the delivery data from the database, and filter for the paper ID + try: + raw_data = [paper for paper in npbc_core.get_papers() if paper[0] == int(paper_id)] + + # if there is a database error, print an error message + except sqlite3.DatabaseError as e: + status_print(False, f"Database error: {e}\nPlease report this to the developer.") + return + + raw_data.sort(key=lambda paper: paper[2]) + # extract the data from the database delivered = [ bool(delivered) - for _, _, day_id, delivered, _ in raw_data + for _, _, _, delivered, _ in raw_data ] + # if the number of days the paper is delivered is not equal to the number of costs, raise an error if len(suspected_data) != delivered.count(True): raise npbc_exceptions.InvalidInput("Number of costs don't match number of days delivered.") + # for each day, yield the cost if it is delivered or 0 if it is not for day in delivered: yield suspected_data.pop() if day else 0 else: - raise npbc_exceptions.InvalidInput("Something went wrong.") + raise npbc_exceptions.InvalidInput("Neither delivery data nor paper ID given.") def editpaper(args: ArgNamespace) -> None: """edit a paper's information""" + + try: + + # attempt to get the delivery data. if it's not given, set it to None delivery_data = extract_delivery_from_user_input(args.delivered) if args.delivered else None + # attempt to edit the paper. if costs are given, use them, else use None npbc_core.edit_existing_paper( paper_id=args.paperid, name=args.name, @@ -403,13 +477,20 @@ def editpaper(args: ArgNamespace) -> None: days_cost=list(extract_costs_from_user_input(args.paperid, delivery_data, *args.costs)) if args.costs else None ) + # if the paper doesn't exist, print an error message except npbc_exceptions.PaperNotExists: status_print(False, "Paper does not exist.") return + # if some input is invalid, print an error message except npbc_exceptions.InvalidInput as e: status_print(False, f"Invalid input: {e}") return + + # if there is a database error, print an error message + except sqlite3.DatabaseError as e: + status_print(False, f"Database error: {e}\nPlease report this to the developer.") + return status_print(True, "Success!") @@ -418,20 +499,29 @@ def addpaper(args: ArgNamespace) -> None: """add a new paper to the database""" try: + # attempt to get the delivery data delivery_data = extract_delivery_from_user_input(args.delivered) + # attempt to add the paper. npbc_core.add_new_paper( name=args.name, days_delivered=delivery_data, days_cost=list(extract_costs_from_user_input(None, delivery_data, *args.costs)) ) + # if the paper already exists, print an error message + except npbc_exceptions.PaperAlreadyExists: + status_print(False, "Paper already exists.") + return + + # if some input is invalid, print an error message except npbc_exceptions.InvalidInput as e: status_print(False, f"Invalid input: {e}") return - - except npbc_exceptions.PaperAlreadyExists: - status_print(False, "Paper already exists.") + + # if there is a database error, print an error message + except sqlite3.DatabaseError as e: + status_print(False, f"Database error: {e}\nPlease report this to the developer.") return status_print(True, "Success!") @@ -440,12 +530,19 @@ def addpaper(args: ArgNamespace) -> None: def delpaper(args: ArgNamespace) -> None: """delete a paper from the database""" + # attempt to delete the paper try: npbc_core.delete_existing_paper(args.paperid) + # if the paper doesn't exist, print an error message except npbc_exceptions.PaperNotExists: status_print(False, "Paper does not exist.") return + + # if there is a database error, print an error message + except sqlite3.DatabaseError as e: + status_print(False, f"Database error: {e}\nPlease report this to the developer.") + return status_print(True, "Success!") @@ -456,23 +553,29 @@ def getpapers(args: ArgNamespace) -> None: - available parameters: name, days, costs - the output is provided as a formatted table, printed to the standard output""" + # get the papers from the database try: raw_data = npbc_core.get_papers() + # if there is a database error, print an error message except sqlite3.DatabaseError as e: status_print(False, f"Database error: {e}\nPlease report this to the developer.") return + # initialize lists for column headers and paper IDs headers = ['paper_id'] ids = [] + # extract paperr IDs ids = list(set(paper[0] for paper in raw_data)) ids.sort() + # initialize lists for the data, based on the number of paper IDs delivery = [None for _ in ids] costs = [None for _ in ids] names = [None for _ in ids] + # if the user wants the name, add it to the headers and the data to the list if args.names: headers.append('name') @@ -481,25 +584,33 @@ def getpapers(args: ArgNamespace) -> None: for paper_id, name, _, _, _ in raw_data )) + # sort the names by paper ID and extract the names only names.sort(key=lambda item: item[0]) names = [name for _, name in names] + # if the user wants the delivery data or the costs, get the data about days if args.delivered or args.cost: + + # initialize a dictionary for the days of each paper days = { paper_id: {} for paper_id in ids } + # for each paper, add the days to the dictionary for paper_id, _, day_id, _, _ in raw_data: days[paper_id][day_id] = {} + # for each paper, add the costs and delivery data to the dictionary for paper_id, _, day_id, day_delivery, day_cost in raw_data: days[paper_id][day_id]['delivery'] = day_delivery days[paper_id][day_id]['cost'] = day_cost + # if the user wants the delivery data, add it to the headers and the data to the list if args.delivered: headers.append('days') + # convert the data to the /[YN]{7}/ format the user is used to delivery = [ ''.join([ 'Y' if days[paper_id][day_id]['delivery'] else 'N' @@ -508,9 +619,11 @@ def getpapers(args: ArgNamespace) -> None: for paper_id in ids ] + # if the user wants the costs, add it to the headers and the data to the list if args.cost: headers.append('costs') + # convert the data to the /x(;x){0,6}/ where x is a floating point number format the user is used to costs = [ ';'.join([ str(days[paper_id][day_id]['cost']) @@ -520,6 +633,7 @@ def getpapers(args: ArgNamespace) -> None: for paper_id in ids ] + # print the headers print(' | '.join([ f"{Fore.YELLOW}{header}{Style.RESET_ALL}" for header in headers @@ -547,6 +661,7 @@ def getlogs(args: ArgNamespace) -> None: - available parameters: log_id, paper_id, month, year, timestamp - will return both date logs and cost logs""" + # attempt to get the logs from the database try: data = npbc_core.get_logged_data( log_id = args.logid, @@ -556,14 +671,17 @@ def getlogs(args: ArgNamespace) -> None: timestamp= datetime.strptime(args.timestamp, r'%d/%m/%Y %I:%M:%S %p') if args.timestamp else None ) + # if there is a database error, print an error message except sqlite3.DatabaseError as e: status_print(False, f"Database error. Please report this to the developer.\n{e}") return + # if there is a date format error, print an error message except ValueError: status_print(False, "Invalid date format. Please use the following format: dd/mm/yyyy hh:mm:ss AM/PM") return + # print column headers print(' | '.join( f"{Fore.YELLOW}{header}{Style.RESET_ALL}" for header in ['log_id', 'paper_id', 'month', 'year', 'timestamp', 'date', 'cost'] @@ -588,8 +706,19 @@ def main() -> None: - parses the command line arguments - calls the appropriate function based on the arguments""" - npbc_core.setup_and_connect_DB() + # attempt to initialize the database + try: + npbc_core.setup_and_connect_DB() + + # if there is a database error, print an error message + except sqlite3.DatabaseError as e: + status_print(False, f"Database error: {e}\nPlease report this to the developer.") + return + + # parse the command line arguments parsed = define_and_read_args() + + # execute the appropriate function parsed.func(parsed) diff --git a/npbc_core.py b/npbc_core.py index 535aee5..8e8be03 100644 --- a/npbc_core.py +++ b/npbc_core.py @@ -28,7 +28,7 @@ # if in a development environment, set the paths to the data folder if str(environ.get('NPBC_DEVELOPMENT')) == "1" or str(environ.get('CI')) == "true": - DATABASE_DIR = Path(__file__).parent / 'data' + DATABASE_DIR = Path('data') SCHEMA_PATH = Path('data') / 'schema.sql' DATABASE_PATH = DATABASE_DIR / 'npbc.db' @@ -93,6 +93,7 @@ def extract_number(string: str, month: int, year: int) -> date_type | None: date = int(string) + # if the date is valid for the given month if date > 0 and date <= monthrange(year, month)[1]: return date_type(year, month, date) @@ -102,6 +103,7 @@ def extract_range(string: str, month: int, year: int) -> Generator[date_type, No start, end = [int(date) for date in npbc_regex.HYPHEN_SPLIT_REGEX.split(string)] + # if the range is valid for the given month if 0 < start <= end <= monthrange(year, month)[1]: for date in range(start, end + 1): yield date_type(year, month, date) @@ -124,15 +126,20 @@ def extract_nth_weekday(string: str, month: int, year: int) -> date_type | None: n = int(n) + # if the day is valid for the given month if n > 0 and n <= list(get_number_of_each_weekday(month, year))[WEEKDAY_NAMES.index(weekday_name.capitalize())]: + + # record the "day_id" corresponding to the given weekday name weekday = WEEKDAY_NAMES.index(weekday_name.capitalize()) + # store all dates when the given weekday occurs in the given month valid_dates = [ date_type(year, month, day) for day in range(1, monthrange(year, month)[1] + 1) if date_type(year, month, day).weekday() == weekday ] + # return the date that is the nth occurrence of the given weekday in the month return valid_dates[n - 1] @@ -221,11 +228,13 @@ def get_cost_and_delivery_data(paper_id: int, connection: Connection) -> tuple[d retrieved_data = connection.execute(query, (paper_id, )).fetchall() + # extract the cost data for each day_id cost_dict = { row[0]: row[2] for row in retrieved_data } + # extract the delivery data for each day_id delivered_dict = { row[0]: bool(row[1]) for row in retrieved_data @@ -273,7 +282,6 @@ def calculate_cost_of_all_papers(undelivered_strings: dict[int, list[str]], mont - return data about the cost of each paper, the total cost, and dates when each paper was not delivered""" global DATABASE_PATH - NUMBER_OF_EACH_WEEKDAY = list(get_number_of_each_weekday(month, year)) cost_and_delivery_data = [] @@ -330,7 +338,6 @@ def save_results( - save the final cost of each paper""" global DATABASE_PATH - timestamp = (custom_timestamp if custom_timestamp else datetime.now()).strftime(r'%d/%m/%Y %I:%M:%S %p') with connect(DATABASE_PATH) as connection: @@ -358,7 +365,7 @@ def save_results( (log_id, costs[paper_id]) ) - # create undelivered entries for each paper + # create undelivered date entries for each paper for paper_id, dates in undelivered_dates.items(): for date in dates: connection.execute( @@ -377,9 +384,13 @@ def format_output(costs: dict[int, float], total: float, month: int, year: int) global DATABASE_PATH + # output the name of the month for which the total cost was calculated yield f"For {date_type(year=year, month=month, day=1).strftime(r'%B %Y')},\n" + + # output the total cost of all papers yield f"*TOTAL*: {total}" + # output the cost of each paper with its name with connect(DATABASE_PATH) as connection: papers = { row[0]: row[1] @@ -605,9 +616,11 @@ def delete_undelivered_string( global DATABASE_PATH + # initialize parameters for the WHERE clause of the SQL query parameters = [] values = [] + # check each parameter and add it to the WHERE clause if it is given if string_id: parameters.append("string_id") values.append(string_id) @@ -628,10 +641,13 @@ def delete_undelivered_string( parameters.append("year") values.append(year) + # if no parameters are given, raise an error if not parameters: raise npbc_exceptions.NoParameters("No parameters given.") with connect(DATABASE_PATH) as connection: + + # check if the string exists check_query = "SELECT EXISTS (SELECT 1 FROM undelivered_strings" conditions = ' AND '.join( @@ -642,6 +658,7 @@ def delete_undelivered_string( if (1,) not in connection.execute(f"{check_query} WHERE {conditions});", values).fetchall(): raise npbc_exceptions.StringNotExists("String with given parameters does not exist.") + # if the string did exist, delete it delete_query = "DELETE FROM undelivered_strings" connection.execute(f"{delete_query} WHERE {conditions};", values) @@ -689,10 +706,12 @@ def get_undelivered_strings( global DATABASE_PATH + # initialize parameters for the WHERE clause of the SQL query parameters = [] values = [] data = [] + # check each parameter and add it to the WHERE clause if it is given if string_id: parameters.append("string_id") values.append(string_id) @@ -715,6 +734,8 @@ def get_undelivered_strings( with connect(DATABASE_PATH) as connection: + + # generate the SQL query main_query = "SELECT string_id, paper_id, year, month, string FROM undelivered_strings" if not parameters: @@ -731,6 +752,7 @@ def get_undelivered_strings( data = connection.execute(query, values).fetchall() connection.close() + # if no data was found, raise an error if not data: raise npbc_exceptions.StringNotExists("String with given parameters does not exist.") @@ -752,10 +774,12 @@ def get_logged_data( global DATABASE_PATH + # initialize parameters for the WHERE clause of the SQL query data = [] parameters = [] values = () + # check each parameter and add it to the WHERE clause if it is given if paper_id: parameters.append("paper_id") values += (paper_id,) @@ -776,6 +800,7 @@ def get_logged_data( parameters.append("timestamp") values += (timestamp.strftime(r'%d/%m/%Y %I:%M:%S %p'),) + # generate the SQL query columns_only_query = """ SELECT logs.log_id, logs.paper_id, logs.month, logs.year, logs.timestamp, undelivered_dates_logs.date_not_delivered, cost_logs.cost FROM logs @@ -793,7 +818,7 @@ def get_logged_data( else: final_query = f"{columns_only_query};" - + # execute the query with connect(DATABASE_PATH) as connection: data = connection.execute(final_query, values).fetchall()