Skip to content

Commit ed1c16c

Browse files
committed
add cli for wgen, refactors, convert to global methods
1 parent 73ce1a1 commit ed1c16c

File tree

2 files changed

+91
-68
lines changed

2 files changed

+91
-68
lines changed

pwiki/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__all__ = ['wgen', 'wiki']

pwiki/wgen.py

Lines changed: 90 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,97 @@
1+
"""Simple secret manager which provides an interactive way to input, save, and load credentials for use with pwiki."""
2+
import argparse
13
import base64
2-
# import re
34
import getpass
45

56
from pathlib import Path
7+
from pprint import pprint
68

79
_DEFAULT_PX = Path.home() / ".px.txt"
810

9-
class Wgen:
10-
"""Simple secret manager which provides an interactive way to input, save, and load credentials for use with pwiki."""
11-
@staticmethod
12-
def load_px(px_file: Path = _DEFAULT_PX) -> dict:
13-
"""Loads the specified password file if it exists. Returns a dictionary with username/passwords that were found
14-
15-
Args:
16-
px_file (Path, optional): The path to the password file. Defaults to _DEFAULT_PX.
17-
18-
Raises:
19-
FileNotFoundError: If a file at Path `px_file` does not exist on the local file system.
20-
21-
Returns:
22-
dict: A dict with credentials such that each key is the username and each value is the password.
23-
"""
24-
if not px_file.is_file():
25-
raise FileNotFoundError(f"'{px_file}' does not exist or is a directory. Did you run Wgen yet? If there is a dir here, rename it first.")
26-
27-
return dict([line.split("\t") for line in base64.b64decode(px_file.read_text().encode()).decode().strip().splitlines()])
28-
29-
@staticmethod
30-
def setup(out_file: Path = _DEFAULT_PX, allow_continue: bool = True):
31-
"""Interactively creates a credential save file.
32-
33-
Args:
34-
out_file (Path, optional): The path to create the password file at. CAVEAT: If a file exists at this location exists it will be overwritten. Defaults to _DEFAULT_PX.
35-
allow_continue (bool, optional): Set True to allow user to enter more than one user-pass combo. Defaults to True.
36-
"""
37-
pxl = {}
38-
39-
while True:
40-
print("Please enter the username/password combo(s) you would like to use.")
41-
u = input("Username: ")
42-
p = getpass.getpass()
43-
confirm_p = getpass.getpass("Confirm Password: ")
44-
45-
if p != confirm_p:
46-
print("ERROR: Entered passwords do not match")
47-
if Wgen._user_says_no("Try again?"):
48-
break
49-
# if not re.match("(?i)(y|yes)", input("Try again? (y/N): ")):
50-
# break
51-
else:
52-
pxl[u] = p
53-
54-
# if not allow_continue or not re.match("(?i)(y|yes)", input("Continue? (y/N): ")):
55-
if not allow_continue or Wgen._user_says_no("Continue?"): # not re.match("(?i)(y|yes)", input("Continue? (y/N): ")):
56-
break
57-
58-
if not pxl:
59-
print("WARNING: You did not make any entries. Doing nothing.")
60-
return
61-
62-
out_file.write_text(base64.b64encode("\n".join([f"{k}\t{v}" for k, v in pxl.items()]).encode()).decode())
63-
print(f"Successfully created '{out_file}'")
64-
65-
@staticmethod
66-
def _user_says_no(question: str) -> bool:
67-
"""Ask the user a question via interactive command line.
68-
69-
Args:
70-
question (str): The question to ask. `" (y/N): "` will be automatically appended to the question.
71-
72-
Returns:
73-
bool: True if the user responded with something other than `"y"` or `"yes"`.
74-
"""
75-
return input(question + " (y/N): ").strip().lower() not in ("y", "yes")
11+
12+
def load_px(px_file: Path = _DEFAULT_PX) -> dict:
13+
"""Loads the specified password file if it exists. Returns a dictionary with username/passwords that were found
14+
15+
Args:
16+
px_file (Path, optional): The path to the password file. Defaults to _DEFAULT_PX.
17+
18+
Raises:
19+
FileNotFoundError: If a file at Path `px_file` does not exist on the local file system.
20+
21+
Returns:
22+
dict: A dict with credentials such that each key is the username and each value is the password.
23+
"""
24+
if not px_file.is_file():
25+
raise FileNotFoundError(f"'{px_file}' does not exist or is a directory. Did you run Wgen yet? If there is a directory here, rename it before proceeding.")
26+
27+
return dict([line.split("\t") for line in base64.b64decode(px_file.read_text().encode()).decode().strip().splitlines()])
28+
29+
30+
def setup_px(out_file: Path = _DEFAULT_PX, allow_continue: bool = True, edit_mode: bool = False):
31+
"""Interactively creates a credential save file.
32+
33+
Args:
34+
out_file (Path, optional): The path to create the password file at. CAVEAT: If a file exists at this location exists it will be overwritten. Defaults to _DEFAULT_PX.
35+
allow_continue (bool, optional): Set True to allow user to enter more than one user-pass combo. Defaults to True.
36+
edit_mode (bool, optional): Enables edit mode, meaning that entries of an existing file will be modified. Does nothing if `out_file` does not exist. Defaults to False.
37+
"""
38+
pxl = load_px(out_file) if edit_mode and out_file.is_file() else {}
39+
40+
while True:
41+
print("Please enter the username/password combo(s) you would like to use.")
42+
u = input("Username: ")
43+
p = getpass.getpass()
44+
confirm_p = getpass.getpass("Confirm Password: ")
45+
46+
if p != confirm_p:
47+
print("ERROR: Entered passwords do not match")
48+
if _user_says_no("Try again?"):
49+
break
50+
else:
51+
pxl[u] = p
52+
53+
if not allow_continue or _user_says_no("Continue?"):
54+
break
55+
56+
if not pxl:
57+
print("WARNING: You did not make any entries. Doing nothing.")
58+
return
59+
60+
out_file.write_text(base64.b64encode("\n".join([f"{k}\t{v}" for k, v in pxl.items()]).encode()).decode())
61+
print(f"Entries successfully written out to '{out_file}'")
62+
63+
64+
def _user_says_no(question: str) -> bool:
65+
"""Ask the user a question via interactive command line.
66+
67+
Args:
68+
question (str): The question to ask. `" (y/N): "` will be automatically appended to the question.
69+
70+
Returns:
71+
bool: True if the user responded with something other than `"y"` or `"yes"`.
72+
"""
73+
return input(question + " (y/N): ").strip().lower() not in ("y", "yes")
74+
75+
76+
def main():
77+
"""Main driver, to be used when this module is invoked via CLI."""
78+
cli_parser = argparse.ArgumentParser(description="pwiki Wgen credential manager")
79+
cli_parser.add_argument('--px-path', type=Path, default=_DEFAULT_PX, dest="px_path", help="The local path to write the password file to")
80+
cli_parser.add_argument("-e", action='store_true', dest="edit_mode", help="Enables edit/append mode, instead of overwrite (the default)")
81+
cli_parser.add_argument("--show", action='store_true', help="Read and shows the contents of the px file instead of writing/editing. This overrides the -e option.")
82+
args = cli_parser.parse_args()
83+
84+
if args.show:
85+
try:
86+
pprint(load_px(args.px_path))
87+
except FileNotFoundError as e:
88+
print(e)
89+
else:
90+
try:
91+
setup_px(args.px_path, edit_mode=args.edit_mode)
92+
except KeyboardInterrupt:
93+
print("\nkeyboard interrupt, no changes will be made.")
94+
95+
96+
if __name__ == "__main__":
97+
main()

0 commit comments

Comments
 (0)