Code is posted after explanation.
Due to the size of the project, this is being posted in three separate posts. This also ensures each post is more focused.
- Post 2 of 3, CLI: Newspaper Bill Calculator CLI with Python (2 of 3, CLI)
- Post 3 of 3, Database: Newspaper Bill Calculator CLI with Python (3 of 3, Database)
What is this?
This application helps you calculate monthly newspaper bills. The goal is to generate a message that I can paste into WhatsApp and send to my newspaper vendor. The end result here is a CLI tool that will be later used as a back-end to build GUIs (hence learn about: C#, HTML/CSS/JS, Flutter). In its current form, everything will be "compiled" by PyInstaller into one-file stand-alone executables for the end-user using GitHub Actions.
The other important goal was to be a testbed for learning a bunch of new tools: more Python libraries, SQL connectors, GitHub Actions (CI/CD, if I understand correctly), unit tests, CLI libraries, type-hinting, regex. I had earlier built this on a different platform, so I now have a solid idea of how this application is used.
Key concepts
- Each newspaper has a certain cost per day of the week
- Each newspaper may or may not be delivered on a given day
- Each newspaper has a name, and a number called a key
- You may register any dates when you didn't receive a paper in advance using the
addudl
command - Once you calculate, the results are displayed and copied to your clipboard
What files exist?
(ignoring conventional ones like README
and requirements.txt
)
File | Purpose/Description | Review |
---|---|---|
npbc_core.py |
Provide the core functionality: the calculation, parsing and validation of user input, interaction with the DB etc. Later on, some functionality from this will be extracted to create server-side code that can service more users, but I have to learn a lot more before getting there. | Please review this. |
npbc_cli.py |
Import functionality from npbc_core.py and wrap a CLI layer on it using argparse . Also provide some additional validation. |
Please review this. |
npbc_updater.py |
Provide a utility to update the application on the user's end. | Don't bother reviewing this (code not included). |
test_core.py |
Test the functionality of the core file (pytest). This isn't as exhaustive as I'd like, but it did a good job of capturing many of my mistakes. | Please review this. |
data/schema.sql |
Database schema. In my local environment, the data folder also has a test database file (but I don't want to upload this online). |
Please review this if you can (not high priority). |
Known problems
- Tests are not exhaustive (please suggest anything you think of).
- Tests are not well commented (working on this right now in a local branch).
- SQL injection is possible in some cases by
-k
/--key
CLI parameters, if you can figure out a way to insert a semicolon in an integer. I will remove this in a future version, once I find a way to improve or remove thegenerate_sql_query()
function. - A lot of documentation is tied up in the CLI UI and comments, and is not an explicit document.
npbc_core.py
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."
test_core.py
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
If you need it, here is a link to the GitHub repo for this project. It's at the same commit as the code above, and I won't edit this so that any discussion is consistent.
https://github.com/eccentricOrange/npbc/tree/6020a4f5db0bf40f54e35b725b305cfeafdd8f2b
1 Answer 1
Once you calculate, the results are displayed and copied to your clipboard
This is not a good idea for a CLI. It may be a reasonable (opt-in!) option for a GUI, but for a CLI the overwhelming expectation is that the results are printed to stdout, and the user can copy from there if they want. You say
I will be the sole user of this app until it's converted into a server thing as stated in the table. But you're not expected to know that, and it's probably a bad idea for me to learn non-standard practices.
Indeed. For us to get better as practitioners, even if our programs are only intended for us, best practice is to make it approachable for everyone.
Overall this code is pretty good! There are type hints, you're using pathlib
, you have reasonable data directories, you have tests, etc.
Don't represent VALIDATE_REGEX
as a dictionary where all of the keys are referenced statically. A saner option is to move this to a different file, which will still offer scope but not require the machinery of a dictionary. SPLIT_REGEX
is probably best as three separate regex variables.
Having a regex validator for CSV content is concerning. You shouldn't be parsing CSV content yourself. You say:
This refers to user input, not a "data" file
CSV is vaguely a machine-legible format and not expected to be a user-legible format. So if it doesn't come from a file, then it's probably not an appropriate choice for user input of your undelivered string. Your other post shows that you're accepting this as one argument. Instead, consider using argparse
and its nargs='+'
support to accumulate multiple arguments to a list, which will not require a comma separator.
generate_sql_query
is attempting to be too clever. Don't construct your queries like this; just write them out.
What's with query_database
returning []
? That statement will never be executed, so remove it.
get_number_of_days_per_week
is a good candidate for being converted from returning a list to yielding an iterator. You ask:
it's called only once and the value is saved to a constant. So, why does list vs generator matter?
It doesn't matter how many times it's called, and it doesn't matter whether it's saved to a constant. An iterator function offers "streamed" calculation instead of in-memory calculation to any callers that want it, and if a caller does actually want a list or tuple, they can cast to it there. Other than the memory representation flexibility, this is mostly a matter of syntactic sugar since no return
is necessary and no inner list variable is needed.
Your "bug report" here:
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.")
is well-intended, but would be better-represented by
- refactoring out the contents of the loop to its own function;
- in that function, throwing an exception at this scope - likely a custom exception subclass that can accept your invalid string as a member;
- back in the loop that calls that function, wrap it in a
try
/except
that catches your (specific) exception and prints something like what you've already shown.
Similar for return False, "Something went wrong."
which should be replaced with proper exception handling rather than a boolean return.
(0 < start) and (start <= end)
is better-represented as
0 < start <= end
. Similar for 0 < n <= get_number_of_days_per_week(month, year)[WEEKDAY_NAMES.index(weekday.capitalize())]
.
TIMESTAMP
should not be capitalised, since it's a local.
Your ','.join([
should not have an inner list and should pass the generator directly.
This formatting block:
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"
is... fine? Other than the last f-string, which - since you've already iteratively concatenated, should just have a + '\n'
. If you're concerned about the O(n^2) cost of successive string concatenation (which you probably shouldn't need to be), then either use StringIO
, or do '\n'.join()
. The latter option would be nice as an iterator function, something like
def format_total() -> Iterator[str]:
# ...
date_type_str = date_type(year=year, month=month, day=1).strftime(r'%B %Y')
yield f"For {date_type_str}"
yield f"*TOTAL*: {total}"
for paper_id, cost in costs.items():
yield f"{papers[paper_id]}: {cost}"
# ...
'\n'.join(format_total())
bool(int(day == 'Y'))
should just be day == 'Y'
which is already a boolean.
-
\$\begingroup\$ I agree with most of this, thank you very much, but have a few questions. (1 about clipboard) I will be the sole user of this app until it's converted into a server thing as stated in the table, and copying is the norm. But you're not expected to know that, and it's probably a bad idea for me to learn non-standard practices. (2 CSV validation) This refers to user input, not a "data" file, so is this okay? (3 yield
get_number_of_days_per_week
) Why? If you refer to where it's called, it's called only once and the value is saved to a constant. So, why does list vs generator matter? \$\endgroup\$eccentricOrange– eccentricOrange2022年05月02日 14:47:34 +00:00Commented May 2, 2022 at 14:47 -
\$\begingroup\$ Disregard point 3, sorry. I'd forgotten about the use in
parse_undelivered_string
and referred only tocalculate_cost_of_all_papers
. \$\endgroup\$eccentricOrange– eccentricOrange2022年05月02日 14:53:51 +00:00Commented May 2, 2022 at 14:53 -
1\$\begingroup\$ Edited for all points \$\endgroup\$Reinderien– Reinderien2022年05月02日 15:06:53 +00:00Commented May 2, 2022 at 15:06
-
\$\begingroup\$ Understood, thank you very much for your time :) No further clarifications are needed. \$\endgroup\$eccentricOrange– eccentricOrange2022年05月02日 15:26:18 +00:00Commented May 2, 2022 at 15:26
Explore related questions
See similar questions with these tags.