My First App - Printable Version +- Python Forum (https://python-forum.io) +-- Forum: General (https://python-forum.io/forum-1.html) +--- Forum: Code Review (https://python-forum.io/forum-46.html) +--- Thread: My First App (/thread-39406.html) |
My First App - BCopeland64 - Feb-13-2023 Hello Everyone, I have created a habit tracking app that I would love to get some feedback on. This is my first real OOP project in Python. If anyone would be willing to review my app and give me some feedback, I would be extremely grateful. Thanks in advance! Github URL: https://github.com/Bcopeland64/IU-Habit-Tracker-App Apologies for not posting the code here. Please find the app in question below: main.py file: import click from habit import HabitTracker, Habit import questionary from analytics import * from db2 import HabitDB, HabitTracker tracker = HabitTracker() @click.group() def cli(): pass @cli.command() def create(): name = questionary.text("Enter the habit name: ").ask() period = questionary.select("Enter the habit period:", choices=["daily", "weekly", "monthly"]).ask() tracker.create_habit(name, period) click.echo(f'Habit "{name}" with period "{period}" created successfully!') @cli.command() def delete(): name = questionary.text("Enter the habit name: ").ask() tracker.delete_habit(name) click.echo(f'Habit "{name}" deleted successfully!') @cli.command() def list_habit_groups(): habits = tracker.get_habits() click.echo('Current habits:') for habit in habits: click.echo(f'- {habit.name} ({habit.period})') @cli.command() def list_habit_groups_period(): period = questionary.select("Enter the habit period:", choices=["daily", "weekly", "monthly"]).ask() habits = tracker.get_habits_by_period(period) click.echo(f'Current {period} habits:') for habit in habits: click.echo(f'- {habit.name}') @cli.command() def longest_streak(): longest_streak_habit = tracker.get_longest_streak() if longest_streak_habit: click.echo(f'Habit with longest streak: {longest_streak_habit.name} ({longest_streak_habit.get_streak()})') else: click.echo('No habits with a streak.') @cli.command() def longest_streak_habit(): name = questionary.text("Enter the habit name: ").ask() streak = tracker.get_longest_streak_by_habit(name) if streak: click.echo(f'Longest streak for habit "{name}": {streak}') else: click.echo(f'Habit "{name}" not found.') @cli.command() def mark(): name = questionary.text("Enter the habit name: ").ask() tracker.mark_complete(name) click.echo(f'Habit "{name}" marked successfully!') @cli.command() def unmark(): name = questionary.text("Enter the habit name: ").ask() tracker.mark_incomplete(name) click.echo(f'Habit "{name}" unmarked successfully!') def main(): while True: command = input('Enter a command (create, delete, list, list-period, longest-streak, longest-streak-habit, or exit): ') if command == 'create': name = input('Enter the habit name: ') period = input('Enter the habit period (daily or weekly): ') tracker.create_habit(name, period) click.echo(f'Habit "{name}" with period "{period}" created successfully!') elif command == 'delete': name = input('Enter the habit name: ') tracker.delete_habit(name) click.echo(f'Habit "{name}" deleted successfully!') elif command == 'habit_groups': habits = tracker.get_habits() click.echo('Current habits:') for habit in habits: click.echo(f'- {habit.name} ({habit.period})') elif command == 'habit_groups_period': period = input('Enter the habit period (daily or weekly): ') habits = tracker.get_habits_by_period(period) click.echo(f'Current {period} habits:') for habit in habits: click.echo(f'- {habit.name}') elif command == 'longest-streak': longest_streak_habit = tracker.get_longest_streak() if longest_streak_habit: click.echo(f'Habit with longest streak: {longest_streak_habit.name} ({longest_streak_habit.get_streak()})') else: click.echo('No habits with a streak.') elif command == 'longest-streak-habit': name = input('Enter the habit name: ') streak = tracker.get_longest_streak_by_habit(name) if streak: click.echo(f'Longest streak for habit "{name}": {streak}') else: click.echo(f'Habit "{name}" not found.') elif command == 'mark': name = input('Enter the habit name: ') tracker.mark_complete(name) click.echo(f'Habit "{name}" marked successfully!') elif command == 'unmark': name = input('Enter the habit name: ') tracker.mark_incomplete(name) click.echo(f'Habit "{name}" unmarked successfully!') elif command == 'exit': break if __name__ == '__main__': cli() main()habit.py file: from datetime import datetime class Habit: def __init__(self, name: str, period: str): self.name = name self.period = period self.created_at = datetime.now() self.completed_at = [] def mark_complete(self): self.completed_at.append(datetime.now()) def mark_incomplete(self): self.completed_at.pop() def get_streak(self): if not self.completed_at: return 0 current_streak = 1 for i in range(1, len(self.completed_at)): if self.completed_at[i] - self.completed_at[i-1] == self.period: current_streak += 1 else: break return current_streak class HabitTracker: def __init__(self): self.habits = [] def create_habit(self, name: str, period: str): new_habit = Habit(name, period) self.habits.append(new_habit) def delete_habit(self, name: str): for i, habit in enumerate(self.habits): if habit.name == name: del self.habits[i] break def get_habits(self): return self.habits def get_habits_by_period(self, period: str): return [habit for habit in self.habits if habit.period == period] def get_longest_streak(self): longest_streak = 0 longest_streak_habit = None for habit in self.habits: streak = habit.get_streak() if streak > longest_streak: longest_streak = streak longest_streak_habit = habit return longest_streak_habit def get_longest_streak_by_habit(self, name: str): for habit in self.habits: if habit.name == name: return habit.get_streak() return 0 def mark_complete(self, name: str): for habit in self.habits: if habit.name == name: habit.mark_complete() break def mark_incomplete(self, name: str): for i, completed_at in enumerate(self.completed_at): if self.name == name: del self.completed_at[i] def relationships(self): for habit in self.habits: print(habit.name, habit.get_streak())db.py file: import sqlite3 import datetime from habit import Habit, HabitTracker class HabitDB: def establish_a_connection(self): self.conn = sqlite3.connect('habits.db') self.cursor = self.conn.cursor() # Create habits table if it does not exist self.cursor.execute(''' CREATE TABLE IF NOT EXISTS habits ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, period TEXT NOT NULL, created_at DATETIME NOT NULL )''') # Create completions table if it does not exist self.cursor.execute(''' CREATE TABLE IF NOT EXISTS completions ( habit_id INTEGER NOT NULL, completed_at DATETIME NOT NULL, FOREIGN KEY(habit_id) REFERENCES habits(id) ON DELETE CASCADE //added ON DELETE CASCADE to delete the related entries in completions when a row in habits is deleted )''') self.conn.commit() def create_habit(self, name: str, period: str): self.cursor.execute( 'INSERT INTO habits (name, period, created_at) VALUES (?, ?, ?)', (name, period, datetime.now()) ) self.conn.commit() def delete_habit(self, name: str): self.cursor.execute( 'DELETE FROM habits WHERE name=?', (name,) ) self.conn.commit() def mark_complete(self, name: str): self.cursor.execute( 'SELECT id FROM habits WHERE name=?', (name,) ) habit_id = self.cursor.fetchone()[0] self.cursor.execute( 'INSERT INTO completions (habit_id, completed_at) VALUES (?, ?)', (habit_id, datetime.now()) ) self.conn.commit() def mark_incomplete(self, name: str): self.cursor.execute( 'SELECT id FROM habits WHERE name=?', (name,) ) habit_id = self.cursor.fetchone()[0] self.cursor.execute( 'DELETE FROM completions WHERE habit_id=? ORDER BY completed_at DESC LIMIT 1', (habit_id,) ) self.conn.commit() def get_habits(self): self.cursor.execute('SELECT * FROM habits') rows = self.cursor.fetchall() habits = [] for row in rows: id, name, period, createdat = row self.cursor.execute( 'SELECT completedat FROM completions WHERE habitid=?', (id,)) completedatrows = self.cursor.fetchall() completedat = [row[0] for row in completedatrows] habits.append(Habit(id, name, period, createdat, completedat)) return habits def get_habits_by_period(self, period: str): self.cursor.execute('SELECT * FROM habits WHERE period=?', (period,)) rows = self.cursor.fetchall() habits = [] for row in rows: id, name, _, created_at = row self.cursor.execute( 'SELECT completed_at FROM completions WHERE habit_id=?', (id,) ) completed_at_rows = self.cursor.fetchall() class HabitTracker: def __init__(self): self.habits = [] self.db = HabitDB() self.db.establish_a_connection() def create_habit(self, name: str, period: str): self.db.create_habit(name, period) def delete_habit(self, name: str): self.db.delete_habit(name) def get_habits(self): return self.db.get_habits() def get_habits_by_period(self, period: str): return self.db.get_habits_by_period(period) def get_longest_streak(self): longest_streak = 0 longest_streak_habit = None for habit in self.habits: streak = habit.get_streak() if streak > longest_streak: longest_streak = streak longest_streak_habit = habit return longest_streak_habit def get_longest_streak_by_habit(self, name: str): for habit in self.habits: if habit.name == name: return habit.get_streak() return 0 def mark_complete(self, name: str): self.db.mark_complete(name) def mark_incomplete(self, name: str): self.db.mark_incomplete(name) def get_longest_streak_by_habit(self, name: str): for habit in self.habits: if habit.name == name: return habit.get_streak() return 0 def mark_complete(self, name: str): self.db.mark_complete(name) def mark_incomplete(self, name: str): self.db.mark_incomplete(name) RE: My First App - buran - Feb-15-2023 Couple of things, after a brief flight over the code https://github.com/Bcopeland64/IU-Habit-Tracker-App/blob/cd9218143cfbf500c99e2fdc24b29489c6881e83/main.py#L70 What is the purpose of main() ? You never call it and also it repeats the different cli commands - you can always organise it better, e.g. using dict, instead of this huge if/elif/elsehttps://github.com/Bcopeland64/IU-Habit-Tracker-App/blob/cd9218143cfbf500c99e2fdc24b29489c6881e83/habit.py#L70 don't change list while iterating over it. Iterate over copy of the list instead: for i, completed_at in enumerate(self.completed_at[::]):In all SELECT queries - you will get IndexError if no result is returned (i.e. empty result set), e.g. self.cursor.execute( 'SELECT id FROM habits WHERE name=?', (name,) ) habit_id = self.cursor.fetchone()[0]if name is not present and fetchone() returns empty tuple, there is no index 0 RE: My First App - BCopeland64 - Feb-15-2023 With the main(), I thought that I needed this to run my interface, however it seems that it is completely unneeded. Also, can you suggest a better way to iterate over the copy of the list rather than changing it as you mentioned? I am also glad that you mentioned the select statements because it is returning empty lists. I have struggled with the db element for a while now and why it isn't returning populated lists (i.e. adding them to the db like it should). The maddening thing is that when I run my app, I can create habits etc, but when I run the function to list them, it comes up empty and with no traceback or error messages (at least in my IDE: VSCode). What am I doing wrong there? Thanks for your assistance. I am a real noob! RE: My First App - buran - Feb-15-2023 (Feb-15-2023, 05:46 PM)BCopeland64 Wrote: Also, can you suggest a better way to iterate over the copy of the list rather than changing it as you mentioned? Compare spam = [1, 2, 2, 3] for item in spam: if item == 2: spam.remove(item) print(spam) and spam = [1, 2, 2, 3] for item in spam[::]: if item == 2: spam.remove(item) print(spam) It may be irrelevant in your case, but it's not clear if you want to allow duplicate names. In mark_complete you break immediatelly when name is found (it implies no duplicate names by design) but in mark_incomplete you iterate over whole list and this imply duplicate names.Also, note that I use list.remove , not del list[i] But my preference will be spam = [1, 2, 2, 3] spam = [item for item in spam if item != 2] print(spam)Check https://stackoverflow.com/q/1207406/4046632 With regards to DB part - there are plenty of trivial errors in the db.py that do not allow the code to run, I mean not run at all RE: My First App - BCopeland64 - Feb-16-2023 (Feb-15-2023, 07:21 PM)buran Wrote:(Feb-15-2023, 05:46 PM)BCopeland64 Wrote: Also, can you suggest a better way to iterate over the copy of the list rather than changing it as you mentioned? Thanks again for the assistance. I think I fixed my habit and main.py files. I don't know where to begin on the db.py file since those errors escape me. Are we talking about indentation, the SQL, variables? I wrote a tester.db file that works just fine using the same logic, but in my original db.py file when I put the same logic behind classes and functions, it all goes out the window. RE: My First App - buran - Feb-16-2023 https://github.com/Bcopeland64/IU-Habit-Tracker-App/blob/cd9218143cfbf500c99e2fdc24b29489c6881e83/db.py#L26 DELETE CASCADE //added ON DELETE CASCADE to delete the related entries in completions when a row in habits is deletedThe "comment" is not valid SQL syntax for comment and this raise error. After removing the comment the error is fixed. Then when you try to create habit there is problem in create_habit with datetime.now() https://github.com/Bcopeland64/IU-Habit-Tracker-App/blob/cd9218143cfbf500c99e2fdc24b29489c6881e83/db.py#L32 You import datetime , however datetime module does not has now() method. You need to from datetime import datetime in order for datetime.now() to work.After fixing the import, there is problem with get_habit https://github.com/Bcopeland64/IU-Habit-Tracker-App/blob/cd9218143cfbf500c99e2fdc24b29489c6881e83/db.py#L78 when you instantiate Habit() you pass more arguments than expected.At that point I gave up And something I didn't realise at first glance You actually never use db.HabitDB . You only work with habit.HabitTracker . i.e. you never actually use the database part. The instance of HabitTracker class is in-memory and all data is lost when program finish
RE: My First App - BCopeland64 - Feb-16-2023 (Feb-16-2023, 11:20 AM)buran Wrote: https://github.com/Bcopeland64/IU-Habit-Tracker-App/blob/cd9218143cfbf500c99e2fdc24b29489c6881e83/db.py#L26 As always, thanks for all of your help. I will go back to the drawing board. I really appreciate your patience. RE: My First App - BCopeland64 - Feb-16-2023 This is the newest app. I think this one will hopefully work a little better. main.py: import datetime import questionary from db import HabitDB from habit import Habit def main(): while True: choices = [ {'name': 'Create a habit', 'value': create_habit}, {'name': 'List habits', 'value': list_habits}, {'name': 'Mark habit complete', 'value': mark_complete}, {'name': 'Mark habit incomplete', 'value': mark_incomplete}, {'name': 'Delete a habit', 'value': delete_habit}, {'name': 'Exit', 'value': exit} ] choice = questionary.select('What would you like to do?', choices=choices).ask() if choice: choice() def create_habit(): name = questionary.text('Enter the name of the habit:').ask() frequency = questionary.select('How often should this habit be completed?', choices=['daily', 'weekly', 'monthly']).ask() completed = False habit = HabitDB(name, frequency) habit.save() print(f'Habit "{name}" with frequency "{frequency}" created successfully!') def list_habits(): habits = Habit.list_all() for habit in habits: print(f'{habit.name} ({habit.frequency} days) - {"Complete" if habit.completed else "Incomplete"}') def mark_complete(): habits = Habit.list_all() habit_choices = [{'name': habit.name, 'value': habit} for habit in habits] habit = questionary.select('Which habit would you like to mark as complete?', choices=habit_choices).ask() habit.mark_complete() print(f'Habit "{habit.name}" marked as complete!') def mark_incomplete(): habits = Habit.list_all() habit_choices = [{'name': habit.name, 'value': habit} for habit in habits] habit = questionary.select('Which habit would you like to mark as incomplete?', choices=habit_choices).ask() habit.mark_incomplete() print(f'Habit "{habit.name}" marked as incomplete!') def delete_habit(): habits = Habit.list_all() habit_choices = [{'name': habit.name, 'value': habit} for habit in habits] habit = questionary.select('Which habit would you like to delete?', choices=habit_choices).ask() habit.delete() print(f'Habit "{habit.name}" deleted successfully!') if __name__ == '__main__': main()db.py: import sqlite3 from datetime import datetime from habit import Habit class HabitDB: def __init__(self, name, frequency): self.name = name self.frequency = frequency self.completed = [] def mark_complete(self): today = datetime.date.today() self.completed.append(today) def is_complete(self): if self.frequency == 'daily': return datetime.date.today() in self.completed elif self.frequency == 'weekly': start_of_week = (datetime.date.today() - datetime.timedelta(days=datetime.date.today().weekday())) end_of_week = start_of_week + datetime.timedelta(days=6) for day in range((end_of_week - start_of_week).days + 1): date = start_of_week + datetime.timedelta(days=day) if date in self.completed: return True return False elif self.frequency == 'monthly': today = datetime.date.today() if today.day >= 28: end_of_month = today.replace(day=28) + datetime.timedelta(days=4) for day in range((end_of_month - today).days + 1): date = today + datetime.timedelta(days=day) if date in self.completed: return True return False else: return today.replace(day=1) in self.completed def delete(self): with sqlite3.connect('habits.db') as conn: cursor = conn.cursor() cursor.execute('DELETE FROM habits WHERE name = ?', (self.name,)) @staticmethod def list_by_frequency(frequency): with sqlite3.connect('habits.db') as conn: cursor = conn.cursor() cursor.execute('SELECT name FROM habits WHERE frequency = ?', (frequency,)) return [row[0] for row in cursor.fetchall()] def save(self): with sqlite3.connect('habits.db') as conn: cursor = conn.cursor() cursor.execute('INSERT INTO habits (name, frequency, completed) VALUES (?, ?, ?)', (self.name, str(self.frequency), self.completed)) def update(self): with sqlite3.connect('habits.db') as conn: cursor = conn.cursor() cursor.execute('UPDATE habits SET completed = ? WHERE name = ?', (self.completed, self.name)) @staticmethod def get_all_habits(): with sqlite3.connect('habits.db') as conn: cursor = conn.cursor() cursor.execute('SELECT name, frequency, completed FROM habits') rows = cursor.fetchall() habits = [] for row in rows: habit = Habit(row[0], row[1]) habit.completed = [datetime.date.fromisoformat(date_str) for date_str in row[2].split(',')] habits.append(habit) return habits def create_table(): with sqlite3.connect('habits.db') as conn: cursor = conn.cursor() cursor.execute('''CREATE TABLE IF NOT EXISTS habits (name TEXT PRIMARY KEY, frequency TEXT, completed TEXT)''')habit.py: from datetime import datetime, date class Habit: def __init__(self, name, frequency): self.name = name self.frequency = frequency self.completed = [] def mark_complete(self): today = datetime.now() self.completed.append(today) def mark_incomplete(self): today = datetime.now() self.completed.remove(today) def is_complete(self): if self.frequency == 'daily': return datetime.now() in self.completed elif self.frequency == 'weekly': start_of_week = (datetime.date.now() - datetime.timedelta(days=datetime.now().weekday())) end_of_week = start_of_week + datetime.timedelta(days=6) for day in range((end_of_week - start_of_week).days + 1): date = start_of_week + datetime.timedelta(days=day) if date in self.completed: return True return False elif self.frequency == 'monthly': today = datetime.now() if today.day >= 28: end_of_month = today.replace(day=28) + datetime.timedelta(days=4) for day in range((end_of_month - today).days + 1): date = today + datetime.timedelta(days=day) if date in self.completed: return True return False else: return today.replace(day=1) in self.completed def delete(self): del self @staticmethod def list_by_frequency(habits, frequency): result = [] for habit in habits: if habit.frequency == frequency: result.append(habit.name) return result def list_all(habits): result = [] for habit in habits: result.append(habit.name) return result |