Out of boredom, I made a decision to start out my private undertaking and I’ve chosen a easy Textual content Password Supervisor.
Notice: For anybody on the market, I strongly suggest you NOT to make use of this for any delicate storage functions because it DOESN’T present encryption but!. That’ll most likely are available a later launch.
About Safer
My undertaking goes to be referred to as Safer and these are the instruments I’ve used thus far:
- Python 3.8
- SQLAlchemy
- SQLite3
Present options:
- Retrieve all saved passwords.
- Create a brand new password.
- Retrieve a single password (by its identify).
- Replace a single password (by its identify).
- Delete a single password (by its identify).
Upcoming options (out of this evaluation’s function but it surely offers the reviewer some context):
- Do the entire above provided that a grasp password is supplied (and it additionally matches the one from the DB).
- Create a grasp password if it does not exist.
- Encrypt all of the passwords.
What I would wish to get out of this evaluation:
- Is there a greater strategy to restructure this undertaking?
- Are the undertaking recordsdata named accurately?
- Is my code modular sufficient?
- What concerning the logic? Would you utilize different method over one other when it comes any of the performance in my code?
- Did I stick with the DRY precept sufficient? If not, what can I enhance?
- Have I used SqlAlchemy as I ought to’ve?
- UX – Consumer expertise
- Wherever is room from enchancment, please do inform ^_^
Proper now, my undertaking seems like this:
├── README.md
├── backend
│ ├── __init__.py // nothing right here
│ ├── major.py // run program from right here (will most likely be moved to root dir sooner or later)
│ ├── fashions.py // all of the fashions utilized by SQLAlchemy
│ └── views.py // probably not views, actions for my fashions.
├── config.py // retailer all of the wanted configs right here
├── necessities.txt // self-explanatory
├── safer.db // sqlite db file
└── setup.cfg // numerous pep8, type, type-annotations config
The code:
major.py
"""Foremost entry to our app.
Incorporates all of the wanted calls.
"""
from typing import Optionally available, Iterable
import sys
from getpass import getpass
from views import (
create_master_password,
create_password,
delete_password,
get_password_by_name,
is_master_password_valid,
list_all_passwords,
update_password,
)
VALID_MASTER_PASS_ANSWERS = (
"Y",
"y",
"Sure",
"sure",
"N",
"n",
"No",
"no",
)
VALID_ACTIONS = (
"1",
"2",
"3",
"4",
"5",
"9",
)
def get_name(immediate: str) -> str:
"""Hold asking for a legitimate identify till one is given.
Arguments:
immediate (str): Immediate message.
Returns:
string - identify of the password
"""
whereas True:
identify = enter(immediate)
if not identify:
print(
"Title can't be empty. We recommend you insert a "
"descriptive identify on your password."
)
proceed
return identify
def get_password(immediate: str) -> str:
"""Hold asking for a legitimate password till one is given.
Arguments:
immediate (str): Immediate message.
Returns:
string - password
"""
whereas True:
password = getpass(immediate)
if not password:
print("Password can't be empty.")
proceed
if len(password) < 8:
print("WARNING! It is a weak password.")
return password
def get_option(immediate: str, choices: Optionally available[Iterable[str]] = None) -> str:
"""Hold asking for a legitimate possibility till one is given.
Arguments:
immediate (str): Immediate message.
choices (tuple): Choices to select from
Returns:
string - legitimate possibility
"""
whereas True:
possibility = enter(immediate)
if not possibility:
print("Please enter an possibility.")
proceed
if possibility not in choices:
valid_options = ", ".be a part of(choices)
print(f"Invalid possibility. Legitimate choices: {valid_options}")
proceed
return possibility
def major() -> None:
"""Foremost entry to our program."""
has_master_password = get_option(
"Do you could have a grasp password? [Y/n]: ",
choices=VALID_MASTER_PASS_ANSWERS,
)
if has_master_password in ("Y", "y", "Sure", "sure"):
master_password = getpass("Insert your grasp password: ")
if not is_master_password_valid(master_password):
elevate ValueError("Please insert a legitimate grasp key.")
what_next = get_option(
"""Select your subsequent motion:
1. View all passwords.
2. Create new password.
3. Present password by identify.
4. Replace password by identify.
5. Delete password by identify.
9. Stop
> """,
choices=VALID_ACTIONS,
)
if what_next == "1":
list_all_passwords()
if what_next == "2":
identify = get_name("New password identify (distinctive!): ")
worth = get_password("New password: ")
create_password(identify, worth)
if what_next == "3":
identify = get_name("Password identify: ")
get_password_by_name(identify)
if what_next == "4":
identify = get_name("Password identify: ")
worth = get_password("New password: ")
update_password(identify, worth)
if what_next == "5":
identify = get_name("Password identify: ")
delete_password(identify)
if what_next == "9":
sys.exit()
else:
master_password = getpass("Insert your new grasp password: ")
create_master_password(master_password)
if __name__ == "__main__":
major()
views.py
"""Views module.
Incorporates primary actions that may be completed towards
MasterPassword and Password fashions.
"""
from typing import Any, Optionally available, Tuple, Union
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from tabulate import tabulate
from config import SQLITE_FILEPATH
from fashions import Base, MasterPassword, Password
ENGINE = create_engine(SQLITE_FILEPATH)
Base.metadata.create_all(ENGINE)
Session = sessionmaker(bind=ENGINE)
class SaferSession:
"""Context supervisor for ease of session administration."""
def __init__(
self, file: Optionally available[Union[MasterPassword, Password]] = None
) -> None:
"""Easy constructor.
Arguments:
file (tuple): Optionally available argument used if supplied.
Returns:
None
"""
self.file = file
def __enter__(self) -> sessionmaker():
"""Create a session object and return it.
Returns:
session object
"""
self.session = Session()
return self.session
def __exit__(self, *args: Tuple[None]) -> None:
"""Be sure the session object will get closed correctly.
Arguments:
args (tuple): Probably not used. Might be None as properly.
Returns:
None
"""
if self.file:
self.session.add(self.file)
self.session.commit()
self.session.shut()
def create_master_password(master_password: str) -> None:
"""Create a grasp password.
Arguments:
master_password (str): Desired grasp password
Returns:
None
"""
with SaferSession(file=MasterPassword(worth=master_password)):
print("Grasp password has been created!")
def create_password(identify: str, worth: str) -> None:
"""Create a password and a reputation for it.
Arguments:
identify (str): Title of the password.
worth (str): The password.
Returns:
None
"""
with SaferSession(file=Password(identify, worth)):
print(f"Efficiently added {identify} file.")
def is_master_password_valid(master_password: str) -> Optionally available[bool]:
"""Verify if supplied grasp password is legitimate or not.
Arguments:
master_password (str): The grasp password.
Returns:
True if the password matches or None in any other case
"""
with SaferSession() as session:
password_obj = session.question(MasterPassword).one_or_none()
return password_obj.worth == master_password if password_obj else None
def get_password_by_name(identify: str) -> Any:
"""Get a password by its identify.
Arguments:
identify (str): Title of the password.
Returns:
password or None
"""
with SaferSession() as session:
strive:
password = session.question(Password)
password = password.filter_by(identify=identify).first().worth
besides AttributeError:
password = None
print(f"{identify} couldn't be discovered!")
return password
def update_password(identify: str, new_value: str) -> None:
"""Replace a selected password.
Arguments:
identify (str): Title of the password that wants updating.
new_value (str): New password.
Returns:
None
"""
with SaferSession() as session:
strive:
password = session.question(Password).filter_by(identify=identify).first()
password.worth = new_value
print(f"Efficiently up to date {identify} file.")
besides AttributeError:
print(f"{identify} couldn't be discovered!")
return
def delete_password(identify: str) -> None:
"""Delete a selected password.
Arguments:
identify (str): NAme of the password that must be deleted.
Returns:
None
"""
with SaferSession() as session:
strive:
session.question(Password).filter(Password.identify == identify).delete()
print(f"Efficiently deleted {identify} file.")
besides AttributeError:
print(f"{identify} couldn't be discovered!")
return
def list_all_passwords() -> None:
"""Checklist all passwords.
Returns:
None
"""
with SaferSession() as session:
passwords = session.question(Password).all()
if not passwords:
print("No passwords saved but!")
return
desk = [
[password_obj.name, password_obj.value]
for password_obj in passwords
]
print(tabulate(desk, ["Password Name", "Password"], tablefmt="grid"))
fashions.py
"""Fashions module.
Incorporates all of the wanted fashions.
"""
from sqlalchemy import Column, DateTime, Integer, String, func
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class Password(Base):
"""Password mannequin."""
__tablename__ = "passwords"
id = Column(Integer, primary_key=True)
identify = Column(String(128), nullable=False, distinctive=True)
worth = Column(String, nullable=False)
up to date = Column(DateTime, default=func.now())
def __init__(self, identify: str, worth: str) -> None:
"""Easy constructor
Arguments:
identify (str): Title of the password.
worth (str): Password.
Returns:
None
"""
self.identify = identify
self.worth = worth
def __repr__(self) -> str:
"""Illustration of the Password object.
Returns:
Illustration of the Password object as str
"""
return f"<Password(identify='{self.identify}', worth='{self.worth}')>"
class MasterPassword(Base):
"""Grasp Password mannequin."""
__tablename__ = "master_password"
id = Column(Integer, primary_key=True)
worth = Column(String, nullable=False)
updated_at = Column(DateTime, default=func.now())
def __init__(self, worth: str) -> None:
"""Easy constructor.
Arguments:
worth (str): Grasp password.
Returns:
None
"""
self.worth = worth
def __repr__(self) -> str:
"""Illustration of the Grasp Password object.
Returns:
Illustration of the Grasp Password object as str
"""
return f"<MasterPassword(worth='{self.worth}')>"
config.py
SQLITE_FILEPATH = 'sqlite:////path_to_project_root/safer.db'
setup.cfg
[pylama]
linters = mccabe,pep8,pycodestyle,pyflakes,mypy,isort
ignore=W293
[pylama:*/__init__.py]
ignore=W0611
[pylama:pydocstyle]
conference = google
[pylama:mccabe]
max-complexity = 2
[pydocstyle]
conference = google
[isort]
multi_line_output=3
include_trailing_comma=True
force_grid_wrap=0
use_parentheses=True
line_length=79
[mypy]
check_untyped_defs = true
disallow_any_generics = true
disallow_untyped_defs = true
ignore_missing_imports = true
no_implicit_optional = true
warn_redundant_casts = true
warn_return_any = true
warn_unused_ignores = true
You can even clone the undertaking from right here. Do not forget to vary the trail within the config.py
!