krz/yoshi-cli

A password manager for the command line.

clone: git clone https://gitbay.org/krz/yoshi-cli.git

c8617ce8aecb40ccf1d13c02dcd7d7e74e79b56b

signed_unknown_key

author: Christian Cleberg <hello@cleberg.net> · 2025-04-05T18:36:49Z
committer: <noreply@github.com>

migrate from pylint to ruff-action (#4)

* migrate from pylint to ruff-action

* temporarily disable push param

* Commit from GitHub Actions (Pylint)

* re-enable push param

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
 .github/workflows/pylint.yml |  26 ++++++++---
 yoshi/__main__.py            |   1 +
 yoshi/account.py             |  24 ++++++----
 yoshi/cli.py                 |  90 ++++++++++++++++---------------------
 yoshi/crypto.py              |  12 ++---
 yoshi/database.py            |  60 +++++++++++++------------
 yoshi/process.py             | 105 ++++++++++++++++++++++++-------------------
 7 files changed, 169 insertions(+), 149 deletions(-)

diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml
index 6f918b6..caef813 100644
--- a/.github/workflows/pylint.yml
+++ b/.github/workflows/pylint.yml
@@ -1,23 +1,37 @@
 name: Pylint
 
-on: [push]
+on:
+  push:
+    paths:
+      - '**.py'
 
 jobs:
   build:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: ["3.8", "3.9", "3.10"]
+        python-version: ["3.x"]
     steps:
     - uses: actions/checkout@v4
     - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v4
+      uses: actions/setup-python@v5
       with:
         python-version: ${{ matrix.python-version }}
+        ref: ${{ github.event.pull_request.head.ref }}
     - name: Install dependencies
       run: |
         python -m pip install --upgrade pip
-        pip install pylint cryptography prettytable
-    - name: Analysing the code with pylint
+        pip install cryptography prettytable
+    - name: Install Ruff
+      uses: astral-sh/ruff-action@v3.2.2
+    - name: Ruff Actions
       run: |
-        pylint -d R0801 $(git ls-files '*.py')
+        ruff check --fix
+        ruff format
+    - name: Add and Commit
+      uses: EndBug/add-and-commit@v9
+      env:
+        GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+      with:
+        default_author: github_actions
+        pathspec_error_handling: ignore
\ No newline at end of file
diff --git a/yoshi/__main__.py b/yoshi/__main__.py
index b7e6c6f..a94bac4 100644
--- a/yoshi/__main__.py
+++ b/yoshi/__main__.py
@@ -4,4 +4,5 @@ Entry point for the yoshi cli app.
 
 if __name__ == "__main__":
     from yoshi.cli import yoshi
+
     yoshi()
diff --git a/yoshi/account.py b/yoshi/account.py
index fb42008..e92c304 100644
--- a/yoshi/account.py
+++ b/yoshi/account.py
@@ -11,9 +11,14 @@ from yoshi import database
 class Account:
     """Represents a login account."""
 
-    def __init__(self, uuid: str, application: str, #pylint: disable=R0913,R0917
-                 username: str, #pylint: disable=R0913,R0917
-                 password: str, url: str) -> None: #pylint: disable=R0913,R0917
+    def __init__(
+        self,
+        uuid: str,
+        application: str,  # pylint: disable=R0913,R0917
+        username: str,  # pylint: disable=R0913,R0917
+        password: str,
+        url: str,
+    ) -> None:  # pylint: disable=R0913,R0917
         self.uuid = uuid
         self.application = application
         self.username = username
@@ -22,16 +27,17 @@ class Account:
 
     def display_account(self) -> None:
         """Print the account details."""
-        print('ID:', self.uuid)
-        print('Application:', self.application)
-        print('Username:', self.username)
-        print('Password:', self.password)
-        print('URL:', self.url)
+        print("ID:", self.uuid)
+        print("Application:", self.application)
+        print("Username:", self.username)
+        print("Password:", self.password)
+        print("URL:", self.url)
 
     def save_account(self) -> None:
         """Save the account details to the database."""
         database.add_account(
-            self.uuid, self.application, self.username, self.password, self.url)
+            self.uuid, self.application, self.username, self.password, self.url
+        )
 
     def delete_account(self) -> bool:
         """Delete the account from the database.
diff --git a/yoshi/cli.py b/yoshi/cli.py
index e140098..2e9ce49 100644
--- a/yoshi/cli.py
+++ b/yoshi/cli.py
@@ -7,91 +7,76 @@ It imports the required modules and sets up a parser with basic options for demo
 import argparse
 from yoshi import crypto, database, process
 
+
 def yoshi():
     """
     CLI entry point logic, used to parse user inputs.
     """
     parser = argparse.ArgumentParser(
-        description='Manage your username and passwords via a convenient CLI vault.'
+        description="Manage your username and passwords via a convenient CLI vault."
     )
 
     # Top-level arguments
     group_one = parser.add_mutually_exclusive_group()
     group_one.add_argument(
-        '-n', '--new',
-        help='Create a new account.',
-        action='store_true'
+        "-n", "--new", help="Create a new account.", action="store_true"
     )
     group_one.add_argument(
-        '-l', '--list',
-        help='List all saved accounts.',
-        action='store_true'
+        "-l", "--list", help="List all saved accounts.", action="store_true"
     )
     group_one.add_argument(
-        '-e', '--edit',
-        help='Edit a saved account.',
-        action='store_true'
+        "-e", "--edit", help="Edit a saved account.", action="store_true"
     )
     group_one.add_argument(
-        '-d', '--delete',
-        help='Delete a saved account.',
-        action='store_true'
+        "-d", "--delete", help="Delete a saved account.", action="store_true"
     )
     group_one.add_argument(
-        '--purge',
+        "--purge",
         help=(
-            'Purge all accounts and delete the vault. '
-            '(Caution: this will irreversibly destroy your data.)'
+            "Purge all accounts and delete the vault. "
+            "(Caution: this will irreversibly destroy your data.)"
         ),
-        action='store_true'
-    )
-    group_one.add_argument(
-        '--encrypt',
-        help='Encrypt the vault.',
-        action='store_true'
-    )
-    group_one.add_argument(
-        '--decrypt',
-        help='Decrypt the vault.',
-        action='store_true'
+        action="store_true",
     )
+    group_one.add_argument("--encrypt", help="Encrypt the vault.", action="store_true")
+    group_one.add_argument("--decrypt", help="Decrypt the vault.", action="store_true")
 
     # Encryption flags
     group_two = parser.add_mutually_exclusive_group()
     group_two.add_argument(
-        '-g', '--generate',
-        help=(
-            'When using the --encrypt option, generate a new encryption key.'
-        ),
-        action='store_true'
+        "-g",
+        "--generate",
+        help=("When using the --encrypt option, generate a new encryption key."),
+        action="store_true",
     )
     group_two.add_argument(
-        '-k', '--keyfile',
-        help='Path to existing key file.',
-        action='store',
+        "-k",
+        "--keyfile",
+        help="Path to existing key file.",
+        action="store",
         nargs=1,
-        type=str
+        type=str,
     )
 
     # Edit flags
     group_three = parser.add_argument_group()
     group_three.add_argument(
-        '-u', '--uuid',
-        help=(
-            'When using the --edit or --delete options, provide the account UUID.'
-        ),
-        action='store',
+        "-u",
+        "--uuid",
+        help=("When using the --edit or --delete options, provide the account UUID."),
+        action="store",
         nargs=1,
-        type=str
+        type=str,
     )
     group_three.add_argument(
-        '-f', '--field',
+        "-f",
+        "--field",
         help=(
-            'When using the --edit option, specify the field to edit (integer index).'
+            "When using the --edit option, specify the field to edit (integer index)."
         ),
-        action='store',
+        action="store",
         nargs=1,
-        type=int
+        type=int,
     )
 
     args = parser.parse_args()
@@ -100,22 +85,22 @@ def yoshi():
         if args.keyfile:
             key = crypto.load_key(args.keyfile[0])
         else:
-            key = input('Please enter your decryption key: ')
+            key = input("Please enter your decryption key: ")
         crypto.decrypt(key)
     elif args.encrypt:
         if args.generate:
             key = crypto.generate_key()
             print(
-                'WRITE THIS KEY DOWN SOMEWHERE SAFE. YOU WILL NOT BE ABLE TO DECRYPT '
-                'YOUR DATA WITHOUT IT!'
+                "WRITE THIS KEY DOWN SOMEWHERE SAFE. YOU WILL NOT BE ABLE TO DECRYPT "
+                "YOUR DATA WITHOUT IT!"
             )
             print(key.decode())
-            print('\n')
+            print("\n")
         else:
             if args.keyfile:
                 key = crypto.load_key(args.keyfile[0])
             else:
-                key = input('Please enter your encryption key: ')
+                key = input("Please enter your encryption key: ")
         crypto.encrypt(key)
     elif database.check_table():
         if args.new:
@@ -130,8 +115,9 @@ def yoshi():
             process.purge_accounts()
         else:
             raise TypeError(
-                'Please specify a command or use the --help flag for more information.'
+                "Please specify a command or use the --help flag for more information."
             )
 
+
 if __name__ == "__main__":
     yoshi()
diff --git a/yoshi/crypto.py b/yoshi/crypto.py
index 9b0a423..d85fbe9 100644
--- a/yoshi/crypto.py
+++ b/yoshi/crypto.py
@@ -6,7 +6,7 @@ It allows for secure encryption and decryption of data using a secret key.
 
 from cryptography.fernet import Fernet
 
-VAULT_FILE = 'vault.sqlite'
+VAULT_FILE = "vault.sqlite"
 
 
 def generate_key() -> bytes:
@@ -21,25 +21,25 @@ def load_key(key_file: str) -> bytes:
     Args:
         key_file (str): Path to the key file.
     """
-    with open(key_file, 'rb') as key:
+    with open(key_file, "rb") as key:
         return key.read()
 
 
 def encrypt(key: bytes, filename: str = VAULT_FILE) -> None:
     """Encrypts the data in the specified file using the provided key."""
     f = Fernet(key)
-    with open(filename, 'rb') as vault:
+    with open(filename, "rb") as vault:
         data = vault.read()
     encrypted_data = f.encrypt(data)
-    with open(filename, 'wb') as vault:
+    with open(filename, "wb") as vault:
         vault.write(encrypted_data)
 
 
 def decrypt(key: bytes, filename: str = VAULT_FILE) -> None:
     """Decrypts the data in the specified file using the provided key."""
     f = Fernet(key)
-    with open(filename, 'rb') as vault:
+    with open(filename, "rb") as vault:
         encrypted_data = vault.read()
     decrypted_data = f.decrypt(encrypted_data)
-    with open(filename, 'wb') as vault:
+    with open(filename, "wb") as vault:
         vault.write(decrypted_data)
diff --git a/yoshi/database.py b/yoshi/database.py
index e1e2e78..4591955 100644
--- a/yoshi/database.py
+++ b/yoshi/database.py
@@ -7,8 +7,8 @@ import sqlite3
 import sys
 import os
 
-VAULT_DECRYPTED = 'vault.sqlite'
-VAULT_ENCRYPTED = 'vault.sqlite.aes'
+VAULT_DECRYPTED = "vault.sqlite"
+VAULT_ENCRYPTED = "vault.sqlite.aes"
 
 
 def create_table() -> None:
@@ -16,8 +16,8 @@ def create_table() -> None:
     db_connection = sqlite3.connect(VAULT_DECRYPTED)
     cursor = db_connection.cursor()
     cursor.execute(
-        ''' CREATE TABLE IF NOT EXISTS accounts (uuid text, application text,
-            username text, password text, url text) '''
+        """ CREATE TABLE IF NOT EXISTS accounts (uuid text, application text,
+            username text, password text, url text) """
     )
     db_connection.commit()
     db_connection.close()
@@ -29,17 +29,18 @@ def check_table() -> bool:
     db_connection = sqlite3.connect(VAULT_DECRYPTED)
     cursor = db_connection.cursor()
     cursor.execute(
-        ''' SELECT count(name) FROM sqlite_master WHERE type='table'
-            AND name='accounts' '''
+        """ SELECT count(name) FROM sqlite_master WHERE type='table'
+            AND name='accounts' """
     )
     if cursor.fetchone()[0] != 1:
         user_choice = input(
-            'Password vault does not exist. Would you like to create it now? (y/n): ')
-        if user_choice.lower() == 'y':
+            "Password vault does not exist. Would you like to create it now? (y/n): "
+        )
+        if user_choice.lower() == "y":
             create_table()
             check = True
         else:
-            sys.exit('Program aborted upon user request.')
+            sys.exit("Program aborted upon user request.")
     else:
         check = True
     db_connection.commit()
@@ -47,17 +48,22 @@ def check_table() -> bool:
     return check
 
 
-def add_account(uuid: str, application: str, username: str, password: str,
-                   url: str) -> None:
+def add_account(
+    uuid: str, application: str, username: str, password: str, url: str
+) -> None:
     """Add a new account within the vault database."""
     db_connection = sqlite3.connect(VAULT_DECRYPTED)
     cursor = db_connection.cursor()
     cursor.execute(
-        ''' INSERT INTO accounts VALUES (:uuid,:application,:username,
-            :password,:url) ''', {
-            'uuid': uuid, 'application': application, 'username': username,
-            'password': password, 'url': url
-        }
+        """ INSERT INTO accounts VALUES (:uuid,:application,:username,
+            :password,:url) """,
+        {
+            "uuid": uuid,
+            "application": application,
+            "username": username,
+            "password": password,
+            "url": url,
+        },
     )
     db_connection.commit()
     db_connection.close()
@@ -67,9 +73,7 @@ def delete_account(uuid: str) -> None:
     """Delete an account within the vault database by its unique ID."""
     db_connection = sqlite3.connect(VAULT_DECRYPTED)
     cursor = db_connection.cursor()
-    cursor.execute(
-        ''' DELETE FROM accounts WHERE uuid = :uuid ''', {'uuid': uuid}
-    )
+    cursor.execute(""" DELETE FROM accounts WHERE uuid = :uuid """, {"uuid": uuid})
     db_connection.commit()
     db_connection.close()
 
@@ -78,9 +82,7 @@ def find_account(uuid: str) -> list:
     """Find an account within the vault database by its unique ID."""
     db_connection = sqlite3.connect(VAULT_DECRYPTED)
     cursor = db_connection.cursor()
-    cursor.execute(
-        ''' SELECT * FROM accounts WHERE uuid = :uuid ''', {'uuid': uuid}
-    )
+    cursor.execute(""" SELECT * FROM accounts WHERE uuid = :uuid """, {"uuid": uuid})
     account = cursor.fetchall()
     db_connection.close()
     return account
@@ -90,7 +92,7 @@ def find_accounts() -> list:
     """Return all accounts stored within the vault database."""
     db_connection = sqlite3.connect(VAULT_DECRYPTED)
     cursor = db_connection.cursor()
-    cursor.execute(''' SELECT * FROM accounts ''')
+    cursor.execute(""" SELECT * FROM accounts """)
     accounts = cursor.fetchall()
     db_connection.close()
     return accounts
@@ -99,14 +101,14 @@ def find_accounts() -> list:
 def update_account(field_name: str, new_value: str, uuid: str) -> None:
     """Update an account within the vault database by its unique ID."""
     queries = {
-        'application': 'UPDATE accounts SET application = :new_value WHERE uuid = :uuid',
-        'username': 'UPDATE accounts SET username = :new_value WHERE uuid = :uuid',
-        'password': 'UPDATE accounts SET password = :new_value WHERE uuid = :uuid',
-        'url': 'UPDATE accounts SET url = :new_value WHERE uuid = :uuid'
+        "application": "UPDATE accounts SET application = :new_value WHERE uuid = :uuid",
+        "username": "UPDATE accounts SET username = :new_value WHERE uuid = :uuid",
+        "password": "UPDATE accounts SET password = :new_value WHERE uuid = :uuid",
+        "url": "UPDATE accounts SET url = :new_value WHERE uuid = :uuid",
     }
     db_connection = sqlite3.connect(VAULT_DECRYPTED)
     cursor = db_connection.cursor()
-    cursor.execute(queries[field_name], {'new_value': new_value, 'uuid': uuid})
+    cursor.execute(queries[field_name], {"new_value": new_value, "uuid": uuid})
     db_connection.commit()
     db_connection.close()
 
@@ -115,7 +117,7 @@ def purge_table() -> None:
     """Purge the 'accounts' table within the vault database."""
     db_connection = sqlite3.connect(VAULT_DECRYPTED)
     cursor = db_connection.cursor()
-    cursor.execute(''' DROP TABLE accounts ''')
+    cursor.execute(""" DROP TABLE accounts """)
     db_connection.commit()
     db_connection.close()
 
diff --git a/yoshi/process.py b/yoshi/process.py
index f09c036..ed9f456 100644
--- a/yoshi/process.py
+++ b/yoshi/process.py
@@ -32,6 +32,7 @@ from yoshi.account import Account
 from yoshi import database
 from yoshi.wordlist import WORDLIST
 
+
 def generate_characters(n: int) -> list:
     """
     Generates a list of n random characters from the set of ASCII letters,
@@ -61,7 +62,7 @@ def shuffle_characters(characters: list) -> str:
         str: A string representation of the shuffled characters
     """
     random.shuffle(characters)
-    character_string = ''.join(characters)
+    character_string = "".join(characters)
     return character_string
 
 
@@ -96,7 +97,7 @@ def list_accounts() -> None:
         None
     """
     accounts = database.find_accounts()
-    t = PrettyTable(['UUID', 'Application', 'Username', 'Password', 'URL'])
+    t = PrettyTable(["UUID", "Application", "Username", "Password", "URL"])
     for account in accounts:
         t.add_row([account[0], account[1], account[2], account[3], account[4]])
     print(t)
@@ -113,13 +114,15 @@ def delete_account(account_uuid: str) -> None:
         None
     """
     account_record = database.find_account(account_uuid)
-    account = Account(account_record[0][0],
-                               account_record[0][1],
-                               account_record[0][2],
-                               account_record[0][3],
-                               account_record[0][4])
+    account = Account(
+        account_record[0][0],
+        account_record[0][1],
+        account_record[0][2],
+        account_record[0][3],
+        account_record[0][4],
+    )
     if account.delete_account():
-        print('Account successfully deleted.')
+        print("Account successfully deleted.")
 
 
 def purge_accounts() -> None:
@@ -130,12 +133,15 @@ def purge_accounts() -> None:
         None
     """
     check = input(
-        '''Are you absolutely sure you want to delete your password vault?
-        This action is irreversible. (y/n): ''')
-    if check.lower() == 'y':
+        """Are you absolutely sure you want to delete your password vault?
+        This action is irreversible. (y/n): """
+    )
+    if check.lower() == "y":
         database.purge_table()
         database.purge_database()
-        print('The password vault has been purged. You may now exit or create a new one.')
+        print(
+            "The password vault has been purged. You may now exit or create a new one."
+        )
 
 
 def create_account() -> None:
@@ -145,46 +151,50 @@ def create_account() -> None:
     Returns:
         None
     """
-    application_string = input('Please enter a name for this account: ')
-    username_string = input('Please enter your username for this account: ')
-    url_string = input('(Optional) Please enter a URL for this account: ')
+    application_string = input("Please enter a name for this account: ")
+    username_string = input("Please enter your username for this account: ")
+    url_string = input("(Optional) Please enter a URL for this account: ")
 
     password_type = input(
-        '''Do you want a random character password (p), an XKCD-style passphrase
-(x), or a custom password (c)? (p|x|c): '''
+        """Do you want a random character password (p), an XKCD-style passphrase
+(x), or a custom password (c)? (p|x|c): """
     )
-    if password_type not in ['p', 'x', 'c']:
-        print('Error: Invalid choice. Please choose p, x, or c.')
+    if password_type not in ["p", "x", "c"]:
+        print("Error: Invalid choice. Please choose p, x, or c.")
         return
 
-    if password_type == 'x':
+    if password_type == "x":
         password_length = int(
-            input('Please enter number of words to include (min. 2): ')
+            input("Please enter number of words to include (min. 2): ")
         )
         if password_length < 3:
-            print('Error: Your passphrase length must be at least 3 words.')
+            print("Error: Your passphrase length must be at least 3 words.")
             return
         password_separator = input(
-            'Please enter your desired separator symbol (_,-, ~, etc.): '
+            "Please enter your desired separator symbol (_,-, ~, etc.): "
         )
         password_string = generate_passphrase(password_length, password_separator)
-    elif password_type == 'p':
+    elif password_type == "p":
         password_length = int(
-            input('Please enter your desired password length (min. 8): ')
+            input("Please enter your desired password length (min. 8): ")
         )
         if password_length < 8:
-            print('Error: Your password length must be at least 8 characters.')
+            print("Error: Your password length must be at least 8 characters.")
             return
         password_characters = generate_characters(password_length)
         password_string = shuffle_characters(password_characters)
     else:
-        password_string = input('Please enter your desired password: ')
-
-    account = Account(str(uuid.uuid4()), application_string,
-                      username_string, password_string, url_string)
+        password_string = input("Please enter your desired password: ")
+
+    account = Account(
+        str(uuid.uuid4()),
+        application_string,
+        username_string,
+        password_string,
+        url_string,
+    )
     account.save_account()
-    print('Account saved to the vault. Use `--list` to see all saved accounts.')
-
+    print("Account saved to the vault. Use `--list` to see all saved accounts.")
 
 
 def edit_account(account_uuid: str, edit_parameter: int) -> None:
@@ -197,29 +207,30 @@ def edit_account(account_uuid: str, edit_parameter: int) -> None:
             Valid values are 1 for application name, 2 for username,
             3 for password, and 4 for URL.
     """
-    field_name, new_value = ''
+    field_name, new_value = ""
     if edit_parameter == 1:
-        field_name = 'application'
-        new_value = input('Please enter your desired Application name: ')
+        field_name = "application"
+        new_value = input("Please enter your desired Application name: ")
     elif edit_parameter == 2:
-        field_name = 'username'
-        new_value = input('Please enter your desired username: ')
+        field_name = "username"
+        new_value = input("Please enter your desired username: ")
     elif edit_parameter == 3:
-        field_name = 'password'
+        field_name = "password"
         type_check = input(
-            'Do you want a new random password or to enter a custom password? '
-            '(random/custom): ').lower()
-        if type_check == 'random':
-            password_length = int(input('Please enter your desired password length: '))
+            "Do you want a new random password or to enter a custom password? "
+            "(random/custom): "
+        ).lower()
+        if type_check == "random":
+            password_length = int(input("Please enter your desired password length: "))
             if password_length < 8:
-                print('Error: Your password length must be at least 8 characters.')
+                print("Error: Your password length must be at least 8 characters.")
             else:
                 password_characters = generate_characters(password_length)
                 new_value = shuffle_characters(password_characters)
         else:
-            new_value = input('Please enter your desired password: ')
+            new_value = input("Please enter your desired password: ")
     elif edit_parameter == 4:
-        field_name = 'url'
-        new_value = input('Please enter your desired URL: ')
+        field_name = "url"
+        new_value = input("Please enter your desired URL: ")
     database.update_account(field_name, new_value, account_uuid)
-    print('Account successfully updated.')
+    print("Account successfully updated.")