diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 62d5357..9a9581d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,7 +1,7 @@ name: Pre-Commit and Cross-Platform Test Suite on: - pull_request: + pull_request: branches: - master types: @@ -12,7 +12,7 @@ on: # Run every Friday at 23:59 UTC - cron: '59 23 * * 5' workflow_dispatch: - + jobs: pre-commit: name: Pre-Commit Checks diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cb02b54..ce06987 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: name: Reorder Python Imports language_version: python3 - repo: https://github.com/psf/black - rev: 20.8b1 + rev: 22.12.0 hooks: - id: black name: Formate with Black diff --git a/docs/conf.py b/docs/conf.py index a880e32..b57f072 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -52,18 +52,18 @@ master_doc = "index" # General information about the project. -project = u"readit" -copyright = u"2018, projectreadit organization" -author = u"projectreadit organization" +project = "readit" +copyright = "2018, projectreadit organization" +author = "projectreadit organization" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = u"" +version = "" # The full version, including alpha/beta/rc tags. -release = u"v0.2" +release = "v0.2" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -145,7 +145,7 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, "readit.tex", u"readit Documentation", u"projectreadit organization", "manual"), + (master_doc, "readit.tex", "readit Documentation", "projectreadit organization", "manual"), ] @@ -153,7 +153,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [(master_doc, "readit", u"readit Documentation", [author], 1)] +man_pages = [(master_doc, "readit", "readit Documentation", [author], 1)] # -- Options for Texinfo output ------------------------------------------- @@ -165,7 +165,7 @@ ( master_doc, "readit", - u"readit Documentation", + "readit Documentation", author, "readit", "One line description of project.", diff --git a/readit/cli.py b/readit/cli.py index 4a456d5..53f9036 100644 --- a/readit/cli.py +++ b/readit/cli.py @@ -30,12 +30,27 @@ @click.option("--tag", "-t", help="Use to tag url --> readit -a -t ") @click.option("--delete", "-d", help="Remove a URL of particular ID --> readit -d ") @click.option("--clear", "-c", nargs=0, help="Clear bookmarks --> readit -c") -@click.option("--update", "-u", help="Update a URL for specific ID --> readit -u ") -@click.option("--search", "-s", help="Search for bookmarks using either a tag or a substring of the URL --> readit -s or ") +@click.option( + "--update", "-u", help="Update a URL for specific ID --> readit -u " +) +@click.option( + "--search", + "-s", + help=""" + Search for bookmarks using either a tag or a substring of the URL + --> readit -s or + """, +) @click.option("--view", "-v", multiple=True, nargs=0, help="Show bookmarks --> readit -v") -@click.option("--openurl", "-o", help="Open a URL in your browser by entering a part of the URL. --> readit -o ") +@click.option( + "--openurl", + "-o", + help="Open a URL in your browser by entering a part of the URL. --> readit -o ", +) @click.option("--version", "-V", is_flag=True, help="Check latest version --> readit -V") -@click.option("--export", "-e", multiple=True, nargs=0, help="Export URLs in csv file --> readit -e") +@click.option( + "--export", "-e", multiple=True, nargs=0, help="Export URLs in csv file --> readit -e" +) @click.option("--taglist", "-tl", multiple=True, nargs=0, help="Show all Tags --> readit -tl") @click.argument("insert", nargs=-1, required=False) def main( @@ -61,7 +76,10 @@ def main( if not tag: tag = "general" # Default tag if none provided if not url: - print("\033[91m\nError: URL not provided. Please use the following format: readit -a -t \033[0m") + print( + """\033[91m\nError: URL not provided. Please use the following format: + readit -a -t \033[0m""" + ) sys.exit(0) try: validate_code = check_url_validation(url) @@ -71,19 +89,25 @@ def main( if is_url_added: print(f"\033[92m\nSuccess! Bookmarked URL `{url}` with tag `{tag}`. 🎉\033[0m") else: - print("\033[93m\nWarning: The URL seems to be inaccessible at the moment.\033[0m") # Warning in yellow + print( + "\033[93m\nWarning: The URL seems to be inaccessible at the moment.\033[0m" + ) # Warning in yellow if option_yes_no(): is_url_added = database_connection.tag_url(url, tag) if is_url_added: - print(f"\033[92m\nSuccess! Bookmarked URL `{url}` with tag `{tag}`. 🎉\033[0m") + print( + f"\033[92m\nSuccess! Bookmarked URL `{url}` with tag `{tag}`. 🎉\033[0m" + ) except Exception: - print("\033[93m\nWarning: The URL seems to be inaccessible at the moment.\033[0m") # Warning in yellow + print( + "\033[93m\nWarning: The URL seems to be inaccessible at the moment.\033[0m" + ) # Warning in yellow if option_yes_no(): is_url_added = database_connection.tag_url(url, tag) if is_url_added: print(f"\033[92m\nSuccess! Bookmarked URL `{url}` with tag `{tag}`. 🎉\033[0m") elif delete: - database_connection.delete_url(delete) + database_connection.delete_url(delete) elif update: url_list = [] for update_to_url in update: @@ -96,11 +120,15 @@ def main( if validate_code == 200: database_connection.update_url(url_id, url) else: - print("\033[93m\nWarning: The URL seems to be inaccessible at the moment.\033[0m") # Warning in yellow + print( + "\033[93m\nWarning: The URL seems to be inaccessible at the moment.\033[0m" + ) # Warning in yellow if option_yes_no(): database_connection.update_url(url_id, url) except Exception: - print("\033[93m\nWarning: The URL seems to be inaccessible at the moment.\033[0m") # Warning in yellow + print( + "\033[93m\nWarning: The URL seems to be inaccessible at the moment.\033[0m" + ) # Warning in yellow if option_yes_no(): is_url_updated = database_connection.update_url(url_id, url) if is_url_updated: @@ -131,19 +159,35 @@ def main( if validate_code == 200: is_url_added = database_connection.add_url(url) if is_url_added: - print(f"\033[92mSuccess! The URL '{url}' has been successfully bookmarked. 🎉\033[0m") + print( + f""" + \033[92mSuccess! The URL '{url}' + has been successfully bookmarked. 🎉\033[0m + """ + ) else: - print("\033[93m\nWarning: The URL seems to be inaccessible at the moment.\033[0m") # Warning in yellow + print( + "\033[93m\nWarning: The URL seems to be inaccessible at the moment.\033[0m" + ) # Warning in yellow if option_yes_no(): is_url_added = database_connection.add_url(url) if is_url_added: - print(f"\033[92mSuccess! The URL '{url}' has been successfully bookmarked. 🎉\033[0m") + print( + f"""\033[92mSuccess! The URL '{url}' + has been successfully bookmarked. 🎉\033[0m""" + ) except Exception: - print("\033[93m\nWarning: The URL seems to be inaccessible at the moment.\033[0m") # Warning in yellow + print( + "\033[93m\nWarning: The URL seems to be inaccessible at the moment.\033[0m" + ) # Warning in yellow if option_yes_no(): is_url_added = database_connection.add_url(url) if is_url_added: - print(f"\033[92mSuccess! The URL '{url}' has been successfully bookmarked. 🎉\033[0m") + print( + f"""\033[92mSuccess! The URL '{url}' + has been successfully bookmarked. 🎉\033[0m""" + ) + def option_yes_no(): """ @@ -155,12 +199,13 @@ def option_yes_no(): else: sys.exit(0) + def check_url_validation(url_given): url = url_given.strip() # Strip any leading/trailing whitespace if not url: print("\033[91m\nError: Cannot add an empty URL.\033[0m") sys.exit(0) - + # Initialize the validation code validate_code = 0 @@ -174,9 +219,9 @@ def check_url_validation(url_given): url = full_url # Update URL with valid prefix break else: - validate_code = 0 + validate_code = 0 else: # If URL starts with http or https, validate directly response = requests.get(url) validate_code = response.status_code - return validate_code \ No newline at end of file + return validate_code diff --git a/readit/database.py b/readit/database.py index 064bb1e..cfcc689 100644 --- a/readit/database.py +++ b/readit/database.py @@ -87,14 +87,14 @@ def init_db(self): print("\033[91m\nERROR: Failed to create table in the database.\033[0m") print(f"\nSQLite Error: {str(e)}") sys.exit(1) - + except Exception as e: print(f"\nAn unexpected error occurred: {str(e)}") sys.exit(1) def add_url(self, url): """ - Add a URL to the database. + Add a URL to the database. If the URL already exists, provide a user-friendly error message. """ try: @@ -112,7 +112,7 @@ def add_url(self, url): return True except sqlite3.IntegrityError as e: - if 'UNIQUE constraint failed' in str(e): + if "UNIQUE constraint failed" in str(e): print(f"\033[91m\nError: The URL '{url}' already bookmarked.\033[0m") return False else: @@ -125,7 +125,8 @@ def add_url(self, url): def tag_url(self, tagged_url, tag_name): """ - URLs can be tagged with multiple tags. If the URL already exists, associate it with the new tag. + URLs can be tagged with multiple tags. + If the URL already exists, associate it with the new tag. """ self.tag = tag_name.lower() self.url = tagged_url @@ -144,7 +145,7 @@ def tag_url(self, tagged_url, tag_name): self.cursor.execute( """INSERT INTO bookmarks(url, date, time) VALUES(?, ?, ?)""", - (self.url, date, time) + (self.url, date, time), ) url_id = self.cursor.lastrowid else: @@ -156,10 +157,7 @@ def tag_url(self, tagged_url, tag_name): if not tag_row: # If tag does not exist, insert it - self.cursor.execute( - """INSERT INTO tags(tag_name) VALUES(?)""", - (self.tag,) - ) + self.cursor.execute("""INSERT INTO tags(tag_name) VALUES(?)""", (self.tag,)) tag_id = self.cursor.lastrowid else: tag_id = tag_row[0] @@ -168,15 +166,18 @@ def tag_url(self, tagged_url, tag_name): self.cursor.execute( """INSERT OR IGNORE INTO url_tags(url_id, tag_id) VALUES(?, ?)""", - (url_id, tag_id) + (url_id, tag_id), ) self.db.commit() return True except sqlite3.IntegrityError as e: - if 'UNIQUE constraint failed' in str(e): - print(f"\033[91m\nError: The URL '{self.url}' has already been taggedwith '{self.tag}'.\033[0m") + if "UNIQUE constraint failed" in str(e): + print( + f"""\033[91m\nError: The URL '{self.url}' + has already been taggedwith '{self.tag}'.\033[0m""" + ) return False else: print(f"\nDatabase error: {str(e)}") @@ -200,22 +201,23 @@ def list_all_tags(self): tag_list.sort() return tag_list - + except sqlite3.OperationalError as e: print(f"\nDatabase error occurred: {str(e)}") return [] - + except Exception as e: print(f"\nAn unexpected error occurred: {str(e)}") return [] def delete_url(self, url_id): """ - Deletes the URL and its associated tags (from the url_tags table) based on the provided URL ID. + Deletes the URL and its associated tags + (from the url_tags table) based on the provided URL ID. """ try: self.url_id = url_id - + # Check if the URL exists before attempting to delete self.cursor.execute("""SELECT url FROM bookmarks WHERE id=?""", (self.url_id,)) deleted_url = self.cursor.fetchone() @@ -223,17 +225,23 @@ def delete_url(self, url_id): if deleted_url: # Delete the associated entries from url_tags table self.cursor.execute("""DELETE FROM url_tags WHERE url_id=?""", (self.url_id,)) - + # Delete the URL from bookmarks table self.cursor.execute("""DELETE FROM bookmarks WHERE id=?""", (self.url_id,)) - + # Commit the transaction self.db.commit() - - print(f"\033[92m\nSuccessfully deleted URL '{deleted_url[0]}' and its associated tags.\033[0m") + + print( + f"""\033[92m\nSuccessfully deleted URL '{deleted_url[0]}' and + its associated tags.\033[0m""" + ) return True else: - print(f"\033[91m\nError: No URL found with ID `{self.url_id}`. Nothing was deleted.\033[0m") + print( + f"""\033[91m\nError: No URL found with ID `{self.url_id}`. + Nothing was deleted.\033[0m""" + ) return False except sqlite3.OperationalError as e: @@ -266,10 +274,16 @@ def update_url(self, url_id, new_url): print(f"\033[92m\nSuccess: URL updated to '{self.new_url}'.\033[0m") return True else: - print("\033[92m\nNo changes: The provided URL is already bookmarked. No update needed.\033[0m") + print( + """\033[92m\nNo changes: The provided URL is already bookmarked. + No update needed.\033[0m""" + ) return False else: - print(f"\033[91m\nError: No URL found with ID `{self.url_id}`. Update failed.\033[0m") + print( + f"""\033[91m\nError: No URL found with ID `{self.url_id}`. + Update failed.\033[0m""" + ) return False except sqlite3.OperationalError as e: @@ -282,7 +296,8 @@ def update_url(self, url_id, new_url): def show_urls(self): """ - Fetches all URLs along with their associated tags, date, and time from the database. + Fetches all URLs along with their associated tags, date, and + time from the database. """ try: # Join bookmarks with url_tags and tags to fetch the required data @@ -310,25 +325,29 @@ def show_urls(self): def search_url(self, search_value): """ - Displays a group of URLs associated with the provided search_value (tag or url substring). + Displays a group of URLs associated with the provided + search_value (tag or url substring). """ try: self.search = search_value.lower() all_bookmarks = [] # Search for URLs directly by tag - self.cursor.execute(""" + self.cursor.execute( + """ SELECT b.id, b.url, t.tag_name, b.date, b.time FROM bookmarks AS b JOIN url_tags AS ut ON b.id = ut.url_id JOIN tags AS t ON ut.tag_id = t.id WHERE t.tag_name = ? - """, (self.search,)) # Use a tuple with a trailing comma + """, + (self.search,), + ) # Use a tuple with a trailing comma all_bookmarks = self.cursor.fetchall() if not all_bookmarks: # If no bookmarks found with the tag, search in all URLs by given value as substring - bookmarks = self.show_urls() # Fetch all URls + bookmarks = self.show_urls() # Fetch all URls for bookmark in bookmarks: if self.search in bookmark[1].lower(): # Case-insensitive match all_bookmarks.append(bookmark) @@ -359,15 +378,22 @@ def delete_all_url(self): if url_count > 0: # If URLs exist, proceed to delete all records self.cursor.execute("""DELETE FROM bookmarks""") - self.cursor.execute("""DELETE FROM url_tags""") # Delete all associations in the junction table - self.cursor.execute("""DELETE FROM tags WHERE tag_name NOT IN (SELECT tag_name FROM url_tags)""") # Delete orphaned tags + self.cursor.execute( + """DELETE FROM url_tags""" + ) # Delete all associations in the junction table + self.cursor.execute( + """DELETE FROM tags WHERE tag_name NOT IN (SELECT tag_name FROM url_tags)""" + ) # Delete orphaned tags self.db.commit() - print(f"\033[92m\nSuccess: All {url_count} URLs and their associated tags have been successfully deleted.\033[0m") + print( + f"""\033[92m\nSuccess: All {url_count} URLs and + their associated tags have been successfully deleted.\033[0m""" + ) return True else: print("\033[92m\nSuccess: No URLs found in the database to delete.\033[0m") return False - + except sqlite3.OperationalError as e: print(f"\nDatabase error occurred: {str(e)}") return False @@ -383,7 +409,10 @@ def open_url(self, part_of_url): urls = self.search_url(part_of_url) if urls: # Prompt the user before opening multiple URLs - print(f"\033[96m\nFound {len(urls)} URLs. Do you want to open them all? (yes/no) \033[0m") + print( + f"""\033[96m\nFound {len(urls)} URLs. + Do you want to open them all? (yes/no) \033[0m""" + ) user_confirmation = input().strip().lower() if user_confirmation in ["yes", "y"]: @@ -419,7 +448,8 @@ def select_directory(): if folder_path: print(f"\n\033[92mSelected folder: {folder_path}.\033[0m") else: - print("\n\033[92mNo folder selected.\033[0m") + print("\n\033[92mNo folder selected. No Bookmarks are exported.\033[0m") + sys.exit(0) return folder_path def export_urls(self): @@ -435,11 +465,11 @@ def export_urls(self): if not os.path.exists(folder_path): msg = f"\033[91m\nFile path does not exist: {folder_path}.\033[0m" return False, msg - + except OSError as e: msg = f"\033[91m\nError: Finding directory: {str(e)}.\033[0m" return False, msg - + database_file = os.path.join(os.path.expanduser("~/.config/readit"), "bookmarks.db") try: # Ensure the database file exists @@ -451,16 +481,15 @@ def export_urls(self): with sqlite3.connect(db_file_paths[0]) as conn: cursor = conn.cursor() cursor.execute("SELECT * FROM bookmarks") - + # Export to CSV csv_file_path = os.path.join(folder_path, "exported_bookmarks.csv") with open(csv_file_path, "w", newline="") as csv_file: csv_writer = csv.writer(csv_file, delimiter="\t") csv_writer.writerow([i[0] for i in cursor.description]) # Headers csv_writer.writerows(cursor) # Data rows - return csv_file_path - + except sqlite3.OperationalError as e: msg = f"Database operation failed: {str(e)}" return False, msg @@ -468,10 +497,3 @@ def export_urls(self): except Exception as e: msg = f"Unexpected error: {str(e)}" return False, msg - - exporter = BookmarkExporter() - result = exporter.export_urls() - if isinstance(result, tuple) and result[0] is False: - print(result[1]) # Print error message - else: - print(f"\033[92m\nSuccess: Bookmarks exported successfully to: {result}.\033[0m") diff --git a/tests/test_db.py b/tests/test_db.py index 98444c3..1c815a1 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -1,7 +1,10 @@ -import pytest import os + +import pytest + from readit.database import DatabaseConnection + @pytest.fixture def db_connection(): # Setup: Create a temporary database file for testing @@ -12,52 +15,60 @@ def db_connection(): if os.path.exists(db.databasefile): os.remove(db.databasefile) + def test_init_db_creates_tables(db_connection): # Test if the tables are created during initialization cursor = db_connection.cursor cursor.execute("SELECT name FROM sqlite_master WHERE type='table'") tables = cursor.fetchall() assert len(tables) == 4 # bookmarks, tags, url_tags and considering sqlite_sequence table too - assert ('bookmarks',) in tables - assert ('tags',) in tables - assert ('url_tags',) in tables + assert ("bookmarks",) in tables + assert ("tags",) in tables + assert ("url_tags",) in tables + def test_add_url_success(db_connection): # Test successful addition of a URL url = "http://example.com" result = db_connection.add_url(url) - assert result == True + assert result # Verify the URL was added to the database cursor = db_connection.cursor cursor.execute("SELECT url FROM bookmarks WHERE url=?", (url,)) assert cursor.fetchone()[0] == url + def test_add_url_duplicate(db_connection): # Test adding a duplicate URL url = "http://example.com" db_connection.add_url(url) # First time result = db_connection.add_url(url) # Second time (should fail) - assert result == False + assert not result + def test_tag_url_success(db_connection): # Test tagging a URL with a new tag url = "http://example.com" tag = "test" result = db_connection.tag_url(url, tag) - assert result == True + assert result # Verify the tag was added to the database cursor = db_connection.cursor - cursor.execute(""" - SELECT b.url, t.tag_name - FROM bookmarks b - JOIN url_tags ut ON b.id = ut.url_id + cursor.execute( + """ + SELECT b.url, t.tag_name + FROM bookmarks b + JOIN url_tags ut ON b.id = ut.url_id JOIN tags t ON ut.tag_id = t.id WHERE b.url=? AND t.tag_name=? - """, (url, tag)) + """, + (url, tag), + ) assert cursor.fetchone() == (url, tag) + def test_list_all_tags(db_connection): # Test listing all tags db_connection.tag_url("http://example.com", "test") @@ -68,6 +79,7 @@ def test_list_all_tags(db_connection): assert "test" in tags assert "example" in tags + def test_delete_url(db_connection): # Test deleting a URL and associated tags url = "http://example.com" @@ -81,16 +93,17 @@ def test_delete_url(db_connection): # Delete the URL result = db_connection.delete_url(url_id) - assert result == True + assert result # Verify the URL and associated tags were deleted cursor.execute("SELECT url FROM bookmarks WHERE id=?", (url_id,)) assert cursor.fetchone() is None + def test_update_url_success(db_connection): # Test updating an existing URL db_connection.add_url("http://example.com") - + # Get the URL ID cursor = db_connection.cursor cursor.execute("SELECT id FROM bookmarks WHERE url=?", ("http://example.com",)) @@ -98,32 +111,34 @@ def test_update_url_success(db_connection): # Update the URL result = db_connection.update_url(url_id, "http://updated.com") - assert result == True + assert result # Verify the URL was updated in the database cursor.execute("SELECT url FROM bookmarks WHERE id=?", (url_id,)) assert cursor.fetchone()[0] == "http://updated.com" + def test_search_url(db_connection): # Test searching for URLs by tag or substring db_connection.tag_url("http://example.com", "test") db_connection.tag_url("http://example-test.org", "mytag") - # Search by tag + # Search by tag results = db_connection.search_url("test") assert len(results) == 1 assert results[0][1] == "http://example.com" - # Search by substring + # Search by substring # (if given search value is tag and if it is available then it does not search in url substring) results = db_connection.search_url("example") assert len(results) == 2 + def test_open_url(db_connection, mocker): # Test opening a URL in the browser - mocker.patch('builtins.input', return_value='yes') # Mock user confirmation input - mocker.patch('webbrowser.open', return_value=True) # Mock web browser opening + mocker.patch("builtins.input", return_value="yes") # Mock user confirmation input + mocker.patch("webbrowser.open", return_value=True) # Mock web browser opening db_connection.add_url("http://example.com") result = db_connection.open_url("example") - assert result == True + assert result diff --git a/tests/test_readit.py b/tests/test_readit.py index 5c7ba57..733bc64 100644 --- a/tests/test_readit.py +++ b/tests/test_readit.py @@ -26,6 +26,7 @@ def test_by_version(runner): # Check if version number is correctly outputted assert "readit v0.3" in result.output + # Testing --help option def test_help_option(runner): """Test the help output of the readit CLI tool"""