diff --git a/.gitignore b/.gitignore index 6f23daa..17b1d29 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ wheels/ # Virtual environments .venv +data/*.csv diff --git a/app.py b/app.py index 3b36348..f9ca679 100644 --- a/app.py +++ b/app.py @@ -1,7 +1,17 @@ -import argparse +import csv import os +import time +from datetime import datetime +from typing import Annotated + +import questionary +import typer +from rich import print +from rich.console import Console +from rich.progress import Progress, SpinnerColumn, TextColumn +from rich.prompt import Prompt +from rich.table import Table -from libwardenpy.colors import colored_string, print_colored from libwardenpy.db import get_connection from libwardenpy.funtionality import ( AuthenticatedData, @@ -14,7 +24,6 @@ list_passwords, register_user, ) -from libwardenpy.migrations import migrate_DB from libwardenpy.passgen import generate_password authenticated = False @@ -22,47 +31,95 @@ data: UnAuthData = UnAuthData("", "") auth_data: AuthenticatedData = AuthenticatedData("", b"") +app = typer.Typer() + + +@app.command() +def login( + username: Annotated[ + str, + typer.Option("--username", "-u"), + ], + password: Annotated[ + str, + typer.Option("--password", "-p", prompt=True, hide_input=True), + ], +): + global authenticated, auth_data, data + data.username = username + data.master_password = password + auth_data.username = username + key = authenticate_user(get_connection(), data) + if key is not None: + auth_data.key = key + authenticated = True + main() + -def init_store(args) -> None: +@app.command() +def create( + username: str, + password: Annotated[ + str, + typer.Option( + "--password", "-p", prompt=True, confirmation_prompt=True, hide_input=True + ), + ], +): global data - migrate_DB() - USER_NAME_EXIST = False + data.username, data.master_password = username, password with get_connection() as conn: - cursor = conn.execute("SELECT username FROM users;") - data.username = args.username - if (data.username,) in cursor.fetchall(): - USER_NAME_EXIST = True - if not USER_NAME_EXIST: - tips_text1 = ( - "create a strong and memorable password\n" - "guide: https://anonymousplanet.org/guide.html#appendix-a2-guidelines-for-passwords-and-passphrases\n" + cursor = conn.execute( + "SELECT COUNT(*) FROM users WHERE username = ?", (username,) ) - print_colored(tips_text1, "red") - data.master_password = input("Create Strong and Memorable Master Password: ") - register_user(get_connection(), data) - else: - print("Username exit") - exit() + + if cursor.fetchone()[0] == 0: + if register_user(get_connection(), data): + print(f"User {username} registered successfully!") + else: + print("Username exists") + + +@app.command() +def export( + username: str, + password: Annotated[ + str, + typer.Option( + "--password", "-p", prompt=True, confirmation_prompt=True, hide_input=True + ), + ], +): + global authenticated, auth_data, data + data.username = username + data.master_password = password + auth_data.username = username + key = authenticate_user(get_connection(), data) + if key is not None: + auth_data.key = key + authenticated = True + passwords = list_passwords(get_connection(), auth_data) + if passwords is not None: + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + transient=True, + ) as progress: + progress.add_task(description="Writing data to csv...", total=None) + time.sleep(1.5) + with open( + f"data/export-{datetime.today().date()}.csv", "w", newline="" + ) as csvfile: + writer = csv.writer( + csvfile, delimiter=",", quotechar="|", quoting=csv.QUOTE_MINIMAL + ) + for entry in passwords: + writer.writerow(entry) + print("Done!") def main() -> None: - global authenticated, auth_data, data, pool - args = parse_arguments() - data.username = args.username - data.master_password = args.password - if args.password is not None and args.username is not None: - key = authenticate_user(get_connection(), data) - if key is not None: - auth_data.key = key - authenticated = True - elif args.username is not None and args.password is None: - data.master_password = input("Enter Master Password: ") - key = authenticate_user(get_connection(), data) - if key is not None: - authenticated = True - auth_data.key = key - - auth_data.username = data.username + global authenticated if authenticated: banner = r""" __ __ _ ______ __ @@ -71,13 +128,14 @@ def main() -> None: \ V V / (_| | | | (_| | __/ | | | __/ | | \_/\_/ \__,_|_| \__,_|\___|_| |_|_| |_| -- created by supun - type .help or ? for help and x or .exit to exit. 1.) Add a Entry [A] 2.) Search Entry [S] 3.) List Entries [L] 4.) Delete Entry [D] + + PRESS X TO QUITE """ print(banner) main_logic() @@ -85,22 +143,20 @@ def main() -> None: def main_logic(): global auth_data - print(auth_data.key) while True: - user_input = input("> ") - if user_input.upper() == ".CLEAR": + user_input = Prompt.ask("[bold green]> [/bold green]").upper() + + if user_input == ".CLEAR": os.system("clear") - if user_input == "?" or user_input.upper() == ".HELP": + + if user_input in ("?", ".HELP"): print(help_msg) - if ( - user_input == "1" - or user_input.upper() == "A" - or user_input.upper() == ".ADD" - ): + + if user_input in ("1", "A", ".ADD"): while True: site = input(".add website_url > ").strip() if not site: - print(colored_string("You Must Add a Website .^.", "RED")) + print("[bold red]You Must Add a Website .^.[/bold red]") else: break site_pass = input(".add password (leave this blank for random password) > ") @@ -108,69 +164,63 @@ def main_logic(): site_pass = generate_password() entry: Entry = Entry(site, site_pass) add_password(get_connection(), auth_data, entry) - if ( - user_input == "2" - or user_input.upper() == "S" - or user_input.upper() == ".SEARCH" - ): + + if user_input in ("2", "S", ".SEARCH"): site = input(".search > ") a = get_password(get_connection(), auth_data, site) print(a) - if ( - user_input == "3" - or user_input.upper() == "L" - or user_input.upper() == ".LIST" - ): + if user_input in ("3", "L", ".LIST"): passwords = list_passwords(get_connection(), auth_data) if passwords is None: print(f"No passwords for user {auth_data.username}") - print(passwords) - if user_input.upper() == "D" or user_input.upper() == ".DEL": + else: + table = Table(title="List of Passwords") + table.add_column("Site/Url", style="blue") + table.add_column("Password", style="red") + for item in passwords: + table.add_row(item[0], item[1]) + console = Console() + console.print(table) + + if user_input in ("D", "4", ".DEL"): while True: site = input(".search entry > ").strip() if not site: - print(colored_string("You Must Add a Website .^.", "RED")) + print("[bold red]You Must Add a Website .^.[bold red]") else: break list_of_entries = get_password(get_connection(), auth_data, site) + if list_of_entries is None: print(f"no entries have {site}") if list_of_entries is not None: - print(list_of_entries) - id = input("Give the id of the entry you want to delete > ") - id = str(id).strip() - delete_passwod(get_connection(), auth_data, id) - if user_input.upper() == "X" or user_input.upper() == ".EXIT": + table = Table(title="List of Entries") + table.add_column("id", style="bold blue") + table.add_column("Site/Url", style="green") + table.add_column("Password", style="red") + for item in list_of_entries: + table.add_row(str(item[0]), item[1], item[2].decode("utf-8")) + console = Console() + console.print(table) + choices = [str(id[0]) for id in list_of_entries] + id = questionary.select( + "Which entry do you want to delet?", + choices=choices, + qmark="> ", + pointer=">", + ).ask() + delete_the_entry = Prompt.ask( + f"Are You sure You want to delete [bold red]{id}[/bold red] {'[y/N]'} ?", + default=False, + ) + if delete_the_entry: + delete_passwod(get_connection(), auth_data, id) + + if user_input in ("X", ".EXIT"): break -def parse_arguments() -> argparse.Namespace: - parser = argparse.ArgumentParser() - subparser = parser.add_subparsers( - title="Commands", - ) - parser.add_argument("-u", "--username", help="use the username given here") - parser.add_argument("-p", "--password", help="use the password given here") - parser.add_argument("-a", "--add", help="add password") - - init_parser = subparser.add_parser( - "init", aliases="i", help="Inizialize password repo" - ) - init_parser.add_argument( - "username", help="username for initialize the password store" - ) - init_parser.set_defaults(func=init_store) - subparser.add_parser("new", help="Inizialize new password repo").set_defaults( - func=init_store - ) - - args = parser.parse_args() - if hasattr(args, "func"): - args.func(args) - return args - - help_msg = """ .help, ? Show this menu .clear Clear the screen @@ -183,4 +233,4 @@ def parse_arguments() -> argparse.Namespace: if __name__ == "__main__": - main() + app() diff --git a/libwardenpy/colors.py b/libwardenpy/colors.py deleted file mode 100644 index 0654cd7..0000000 --- a/libwardenpy/colors.py +++ /dev/null @@ -1,25 +0,0 @@ -import sys - - -# stole it from https://medium.com/ai-does-it-better/print-colored-text-in-python-enhance-terminal-output-b90aede058c8 -def print_colored(text, color, end="\n"): - colors = { - "red": "\x1b[31m", - "green": "\x1b[32m", - "yellow": "\x1b[33m", - "blue": "\x1b[34m", - } - reset = "\x1b[0m" - sys.stdout.write(colors.get(color, "") + text + reset + end) - - -# funtion i wrote -### this append reset code and prepend the color code on to a string -def colored_string(text: str, color: str) -> str: - colors = { - "RED": "\033[31m", - "GREEN": "\033[32m", - "BLUE": "\033[34m", - } - RESET = "\033[0m" - return f"{colors[color]}{text}{RESET}" diff --git a/libwardenpy/configuration.py b/libwardenpy/configuration.py new file mode 100644 index 0000000..dcf0039 --- /dev/null +++ b/libwardenpy/configuration.py @@ -0,0 +1,7 @@ +from dataclasses import dataclass + + +@dataclass +class AppConfig: + EXPORT_DIRECTORY: str = "data/" + PASSWORD_LENGTH: int = 20 diff --git a/libwardenpy/db.py b/libwardenpy/db.py index c90b8f3..b9b776e 100644 --- a/libwardenpy/db.py +++ b/libwardenpy/db.py @@ -8,11 +8,33 @@ def get_connection(DB_PATH="data/db.sqlite3"): conn = sqlite3.connect(DB_PATH) try: + conn.execute( + """ + CREATE TABLE IF NOT EXISTS users ( + username TEXT PRIMARY KEY, + password_hash TEXT NOT NULL, + salt BLOB NOT NULL) + """ + ) + conn.execute( + """ + CREATE TABLE IF NOT EXISTS passwords ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL, + site TEXT NOT NULL, + encrypted_password BLOB NOT NULL, + nonce BLOB NOT NULL, + FOREIGN KEY (username) REFERENCES users(username)) + """ + ) yield conn + except Exception as e: conn.rollback() raise e + else: conn.commit() + finally: conn.close() diff --git a/libwardenpy/funtionality.py b/libwardenpy/funtionality.py index 5e3feab..486101e 100644 --- a/libwardenpy/funtionality.py +++ b/libwardenpy/funtionality.py @@ -1,13 +1,11 @@ import secrets import sqlite3 from dataclasses import dataclass -from typing import List +from typing import Optional import argon2 from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 -from libwardenpy.colors import colored_string - @dataclass class UnAuthData: @@ -23,9 +21,9 @@ def __init__(self, username: str, master_password: str) -> None: @dataclass class AuthenticatedData: username: str - key: bytes | None + key: Optional[bytes] - def __init__(self, username: str, key: bytes | None) -> None: + def __init__(self, username: str, key: Optional[bytes]) -> None: self.username = username self.key = key @@ -46,7 +44,7 @@ def __init__(self, url: str, password: str) -> None: ### this funtion get username and password ### then create salt(random bytes) and a hash for password using argon2 ### save them in the sqlite3 database -def register_user(connection, data: UnAuthData) -> None | str: +def register_user(connection, data: UnAuthData) -> bool: salt = secrets.token_bytes(16) password_hash = argon2.PasswordHasher().hash(data.master_password) @@ -56,10 +54,11 @@ def register_user(connection, data: UnAuthData) -> None | str: "INSERT INTO users (username, password_hash, salt) VALUES (?, ?, ?)", (data.username, password_hash, salt), ) - print(colored_string(f"User {data.username} registered successfully!", "GREEN")) + return True except sqlite3.IntegrityError as e: print(e) + return False ### this funtion get username and passwod @@ -67,7 +66,7 @@ def register_user(connection, data: UnAuthData) -> None | str: ### verify the password hash matches given password and if matches create a new ### key for to encrypt the child items ( passwords, sites under given user) else ### if given user not found exit the program and if passwod doesnot match give a err -def authenticate_user(connection, data: UnAuthData) -> bytes | None: +def authenticate_user(connection, data: UnAuthData) -> Optional[bytes]: with connection as conn: cursor = conn.execute( "SELECT password_hash, salt FROM users WHERE username = ?", @@ -129,7 +128,7 @@ def add_password(connection, data: AuthenticatedData, entry: Entry) -> None: ### get the password for the site from sqlite database and decrept them ### and show them -def get_password(connection, data: AuthenticatedData, site: str) -> List | None: +def get_password(connection, data: AuthenticatedData, site: str) -> Optional[list]: if not data.key: return @@ -155,7 +154,7 @@ def get_password(connection, data: AuthenticatedData, site: str) -> List | None: ### get the passwords of all the sites from sqlite database and decrept them ### and show them -def list_passwords(connection, data: AuthenticatedData) -> List | None: +def list_passwords(connection, data: AuthenticatedData) -> Optional[list]: if not data.key: return @@ -172,7 +171,9 @@ def list_passwords(connection, data: AuthenticatedData) -> List | None: return [ ( site, - ChaCha20Poly1305(data.key).decrypt(nonce, encrypted_password, None), + ChaCha20Poly1305(data.key) + .decrypt(nonce, encrypted_password, None) + .decode("utf-8"), ) for site, encrypted_password, nonce in result ] diff --git a/libwardenpy/migrations.py b/libwardenpy/migrations.py deleted file mode 100644 index a0d8032..0000000 --- a/libwardenpy/migrations.py +++ /dev/null @@ -1,19 +0,0 @@ -import os - -from libwardenpy.db import get_connection - - -# all this funtion does is create and init the sqlite3 database -# it read the SQL files in the migration directory and run them against the db -def migrate_DB(): - filenames = next(os.walk("migrations/"))[2] - for file in filenames: - with open(f"migrations/{file}", "r") as file: - query = file.read() - - try: - with get_connection() as conn: - conn.execute(query) - - except Exception as err: - print(f"Some thing is worng with the migration query?\nError -> {err}") diff --git a/libwardenpy/passgen.py b/libwardenpy/passgen.py index 881915c..3e11313 100644 --- a/libwardenpy/passgen.py +++ b/libwardenpy/passgen.py @@ -1,12 +1,16 @@ import secrets import string +from libwardenpy.configuration import AppConfig + +config = AppConfig() + ### this create a password with default length 14 and has digis uppercase and lowecase letters -def generate_password(lenght: int = 14) -> str: +def generate_password(lenght: int = config.PASSWORD_LENGTH) -> str: alphabet = string.ascii_letters + string.digits while True: - password = "".join(secrets.choice(alphabet) for _i in range(lenght)) + password = "".join(secrets.choice(alphabet) for _ in range(lenght)) if ( any(c.islower() for c in password) and any(c.isupper() for c in password) diff --git a/migrations/20241109071102_create_user_table.sql b/migrations/20241109071102_create_user_table.sql deleted file mode 100644 index 4acb557..0000000 --- a/migrations/20241109071102_create_user_table.sql +++ /dev/null @@ -1,5 +0,0 @@ -CREATE TABLE IF NOT EXISTS users ( - username TEXT PRIMARY KEY, - password_hash TEXT NOT NULL, - salt BLOB NOT NULL -); diff --git a/migrations/20241109071443_create_password_table.sql b/migrations/20241109071443_create_password_table.sql deleted file mode 100644 index 218d83f..0000000 --- a/migrations/20241109071443_create_password_table.sql +++ /dev/null @@ -1,8 +0,0 @@ -CREATE TABLE IF NOT EXISTS passwords ( - id INTEGER PRIMARY KEY, - username TEXT NOT NULL, - site TEXT NOT NULL, - encrypted_password BLOB NOT NULL, - nonce BLOB NOT NULL, - FOREIGN KEY (username) REFERENCES users(username) -); diff --git a/pyproject.toml b/pyproject.toml index 8079b55..c754187 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,9 @@ requires-python = ">=3.12" dependencies = [ "argon2-cffi>=23.1.0", "cryptography>=43.0.3", + "questionary>=2.0.1", + "rich>=13.9.4", + "typer>=0.13.1", ] [dependency-groups] @@ -14,4 +17,5 @@ dev = [ "pyright>=1.1.389", "pytest-cov>=6.0.0", "pytest>=8.3.3", + "pyinstaller>=6.11.1", ] diff --git a/tests/test_functionality.py b/tests/test_functionality.py index 5a88463..b7973ac 100644 --- a/tests/test_functionality.py +++ b/tests/test_functionality.py @@ -139,8 +139,8 @@ def test_list_passwords(): passwords = list_passwords(conn, auth_data) assert passwords is not None assert len(passwords) == 2 - assert any(p[0] == "site1.com" and p[1].decode() == "pass1" for p in passwords) - assert any(p[0] == "site2.com" and p[1].decode() == "pass2" for p in passwords) + assert any(p[0] == "site1.com" and p[1] == "pass1" for p in passwords) + assert any(p[0] == "site2.com" and p[1] == "pass2" for p in passwords) def test_delete_password(): diff --git a/tests/test_password_generation.py b/tests/test_password_generation.py index 0e9f2d8..f5cd311 100644 --- a/tests/test_password_generation.py +++ b/tests/test_password_generation.py @@ -1,10 +1,11 @@ import random + from libwardenpy.passgen import generate_password def test_default_password_generation() -> None: default_password = generate_password() - assert len(default_password) == 14 + assert len(default_password) == 20 def test_password_generation_with_length() -> None: diff --git a/uv.lock b/uv.lock index 8cfd014..0f2d4fc 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,15 @@ version = 1 requires-python = ">=3.12" +[[package]] +name = "altgraph" +version = "0.17.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/a8/7145824cf0b9e3c28046520480f207df47e927df83aa9555fb47f8505922/altgraph-0.17.4.tar.gz", hash = "sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406", size = 48418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/3f/3bc3f1d83f6e4a7fcb834d3720544ca597590425be5ba9db032b2bf322a2/altgraph-0.17.4-py2.py3-none-any.whl", hash = "sha256:642743b4750de17e655e6711601b077bc6598dbfa3ba5fa2b2a35ce12b508dff", size = 21212 }, +] + [[package]] name = "argon2-cffi" version = "23.1.0" @@ -67,6 +76,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, ] +[[package]] +name = "click" +version = "8.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -152,6 +173,39 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, ] +[[package]] +name = "macholib" +version = "1.16.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "altgraph" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/ee/af1a3842bdd5902ce133bd246eb7ffd4375c38642aeb5dc0ae3a0329dfa2/macholib-1.16.3.tar.gz", hash = "sha256:07ae9e15e8e4cd9a788013d81f5908b3609aa76f9b1421bae9c4d7606ec86a30", size = 59309 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/5d/c059c180c84f7962db0aeae7c3b9303ed1d73d76f2bfbc32bc231c8be314/macholib-1.16.3-py2.py3-none-any.whl", hash = "sha256:0e315d7583d38b8c77e815b1ecbdbf504a8258d8b3e17b61165c6feb60d18f2c", size = 38094 }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + [[package]] name = "nodeenv" version = "1.9.1" @@ -170,6 +224,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, ] +[[package]] +name = "pefile" +version = "2023.2.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/c5/3b3c62223f72e2360737fd2a57c30e5b2adecd85e70276879609a7403334/pefile-2023.2.7.tar.gz", hash = "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc", size = 74854 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/26/d0ad8b448476d0a1e8d3ea5622dc77b916db84c6aa3cb1e1c0965af948fc/pefile-2023.2.7-py3-none-any.whl", hash = "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6", size = 71791 }, +] + [[package]] name = "pluggy" version = "1.5.0" @@ -179,6 +242,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, ] +[[package]] +name = "prompt-toolkit" +version = "3.0.36" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/93/180be2342f89f16543ec4eb3f25083b5b84eba5378f68efff05409fb39a9/prompt_toolkit-3.0.36.tar.gz", hash = "sha256:3e163f254bef5a03b146397d7c1963bd3e2812f0964bb9a24e6ec761fd28db63", size = 423863 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/37/791f1a6edd13c61cac85282368aa68cb0f3f164440fdf60032f2cc6ca34e/prompt_toolkit-3.0.36-py3-none-any.whl", hash = "sha256:aa64ad242a462c5ff0363a7b9cfe696c20d55d9fc60c11fd8e632d064804d305", size = 386414 }, +] + [[package]] name = "pycparser" version = "2.22" @@ -188,6 +263,56 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, ] +[[package]] +name = "pygments" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, +] + +[[package]] +name = "pyinstaller" +version = "6.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "altgraph" }, + { name = "macholib", marker = "sys_platform == 'darwin'" }, + { name = "packaging" }, + { name = "pefile", marker = "sys_platform == 'win32'" }, + { name = "pyinstaller-hooks-contrib" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/d4/54f5f5c73b803e6256ea97ffc6ba8a305d9a5f57f85f9b00b282512bf18a/pyinstaller-6.11.1.tar.gz", hash = "sha256:491dfb4d9d5d1d9650d9507daec1ff6829527a254d8e396badd60a0affcb72ef", size = 4249772 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/15/b0f1c0985ee32fcd2f6ad9a486ef94e4db3fef9af025a3655e76cb708009/pyinstaller-6.11.1-py3-none-macosx_10_13_universal2.whl", hash = "sha256:44e36172de326af6d4e7663b12f71dbd34e2e3e02233e181e457394423daaf03", size = 991780 }, + { url = "https://files.pythonhosted.org/packages/fd/0f/9f54cb18abe2b1d89051bc9214c0cb40d7b5f4049c151c315dacc067f4a2/pyinstaller-6.11.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:6d12c45a29add78039066a53fb05967afaa09a672426072b13816fe7676abfc4", size = 711739 }, + { url = "https://files.pythonhosted.org/packages/32/f7/79d10830780eff8339bfa793eece1df4b2459e35a712fc81983e8536cc29/pyinstaller-6.11.1-py3-none-manylinux2014_i686.whl", hash = "sha256:ddc0fddd75f07f7e423da1f0822e389a42af011f9589e0269b87e0d89aa48c1f", size = 714053 }, + { url = "https://files.pythonhosted.org/packages/25/f7/9961ef02cdbd2dbb1b1a215292656bd0ea72a83aafd8fb6373513849711e/pyinstaller-6.11.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:0d6475559c4939f0735122989611d7f739ed3bf02f666ce31022928f7a7e4fda", size = 719133 }, + { url = "https://files.pythonhosted.org/packages/6f/4d/7f854842a1ce798de762a0b0bc5d5a4fc26ad06164a98575dc3c54abed1f/pyinstaller-6.11.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:e21c7806e34f40181e7606926a14579f848bfb1dc52cbca7eea66eccccbfe977", size = 709591 }, + { url = "https://files.pythonhosted.org/packages/7f/e0/00d29fc90c3ba50620c61554e26ebb4d764569507be7cd1c8794aa696f9a/pyinstaller-6.11.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:32c742a24fe65d0702958fadf4040f76de85859c26bec0008766e5dbabc5b68f", size = 710068 }, + { url = "https://files.pythonhosted.org/packages/3e/57/d14b44a69f068d2caaee49d15e45f9fa0f37c6a2d2ad778c953c1722a1ca/pyinstaller-6.11.1-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:208c0ef6dab0837a0a273ea32d1a3619a208e3d1fe3fec3785eea71a77fd00ce", size = 714439 }, + { url = "https://files.pythonhosted.org/packages/88/01/256824bb57ca208099c86c2fb289f888ca7732580e91ced48fa14e5903b2/pyinstaller-6.11.1-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:ad84abf465bcda363c1d54eafa76745d77b6a8a713778348377dc98d12a452f7", size = 710457 }, + { url = "https://files.pythonhosted.org/packages/7c/f0/98c9138f5f0ff17462f1ad6d712dcfa643b9a283d6238d464d8145bc139d/pyinstaller-6.11.1-py3-none-win32.whl", hash = "sha256:2e8365276c5131c9bef98e358fbc305e4022db8bedc9df479629d6414021956a", size = 1280261 }, + { url = "https://files.pythonhosted.org/packages/7d/08/f43080614b3e8bce481d4dfd580e579497c7dcdaf87656d9d2ad912e5796/pyinstaller-6.11.1-py3-none-win_amd64.whl", hash = "sha256:7ac83c0dc0e04357dab98c487e74ad2adb30e7eb186b58157a8faf46f1fa796f", size = 1340482 }, + { url = "https://files.pythonhosted.org/packages/ed/56/953c6594cb66e249563854c9cc04ac5a055c6c99d1614298feeaeaa9b87e/pyinstaller-6.11.1-py3-none-win_arm64.whl", hash = "sha256:35e6b8077d240600bb309ed68bb0b1453fd2b7ab740b66d000db7abae6244423", size = 1267519 }, +] + +[[package]] +name = "pyinstaller-hooks-contrib" +version = "2024.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/6a/9d0057e312b85fbd17a79e1c1955d115fd9bbc78b85bab757777c8ef2307/pyinstaller_hooks_contrib-2024.10.tar.gz", hash = "sha256:8a46655e5c5b0186b5e527399118a9b342f10513eb1425c483fa4f6d02e8800c", size = 140592 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/64/445861ee7a5fd32874c0f6cfe8222aacc8feda22539332e0d8ff50dadec6/pyinstaller_hooks_contrib-2024.10-py3-none-any.whl", hash = "sha256:ad47db0e153683b4151e10d231cb91f2d93c85079e78d76d9e0f57ac6c8a5e10", size = 338417 }, +] + [[package]] name = "pyright" version = "1.1.389" @@ -229,6 +354,73 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 }, ] +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756 }, +] + +[[package]] +name = "questionary" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "prompt-toolkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/d0/d73525aeba800df7030ac187d09c59dc40df1c878b4fab8669bdc805535d/questionary-2.0.1.tar.gz", hash = "sha256:bcce898bf3dbb446ff62830c86c5c6fb9a22a54146f0f5597d3da43b10d8fc8b", size = 24726 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/e7/2dd8f59d1d328773505f78b85405ddb1cfe74126425d076ce72e65540b8b/questionary-2.0.1-py3-none-any.whl", hash = "sha256:8ab9a01d0b91b68444dff7f6652c1e754105533f083cbe27597c8110ecc230a2", size = 34248 }, +] + +[[package]] +name = "rich" +version = "13.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, +] + +[[package]] +name = "setuptools" +version = "75.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/54/292f26c208734e9a7f067aea4a7e282c080750c4546559b58e2e45413ca0/setuptools-75.6.0.tar.gz", hash = "sha256:8199222558df7c86216af4f84c30e9b34a61d8ba19366cc914424cdbd28252f6", size = 1337429 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/21/47d163f615df1d30c094f6c8bbb353619274edccf0327b185cc2493c2c33/setuptools-75.6.0-py3-none-any.whl", hash = "sha256:ce74b49e8f7110f9bf04883b730f4765b774ef3ef28f722cce7c273d253aaf7d", size = 1224032 }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, +] + +[[package]] +name = "typer" +version = "0.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/41/49ead3ad3310545e767bcb917c759b026ca9eada5d5c150c2fb6fc555568/typer-0.13.1.tar.gz", hash = "sha256:9d444cb96cc268ce6f8b94e13b4335084cef4c079998a9f4851a90229a3bd25c", size = 98631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/69/e90a0b4d0c16e095901679216c8ecdc728110c7c54e7b5f43a623bc4c789/typer-0.13.1-py3-none-any.whl", hash = "sha256:5b59580fd925e89463a29d363e0a43245ec02765bde9fb77d39e5d0f29dd7157", size = 44723 }, +] + [[package]] name = "typing-extensions" version = "4.12.2" @@ -245,10 +437,14 @@ source = { virtual = "." } dependencies = [ { name = "argon2-cffi" }, { name = "cryptography" }, + { name = "questionary" }, + { name = "rich" }, + { name = "typer" }, ] [package.dev-dependencies] dev = [ + { name = "pyinstaller" }, { name = "pyright" }, { name = "pytest" }, { name = "pytest-cov" }, @@ -258,11 +454,24 @@ dev = [ requires-dist = [ { name = "argon2-cffi", specifier = ">=23.1.0" }, { name = "cryptography", specifier = ">=43.0.3" }, + { name = "questionary", specifier = ">=2.0.1" }, + { name = "rich", specifier = ">=13.9.4" }, + { name = "typer", specifier = ">=0.13.1" }, ] [package.metadata.requires-dev] dev = [ + { name = "pyinstaller", specifier = ">=6.11.1" }, { name = "pyright", specifier = ">=1.1.389" }, { name = "pytest", specifier = ">=8.3.3" }, { name = "pytest-cov", specifier = ">=6.0.0" }, ] + +[[package]] +name = "wcwidth" +version = "0.2.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, +]