From d8a7a022df815d8254c4a9674ef1143f50caa277 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Mon, 28 Nov 2022 19:34:35 +0100 Subject: Documentation. --- html_render.py | 44 +++++++++++++++++++++++++++++++++++--------- main.py | 2 -- mu.py | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++- password.py | 23 +++++++++++++++++++++-- 4 files changed, 113 insertions(+), 14 deletions(-) diff --git a/html_render.py b/html_render.py index 533ae7c..99140ad 100644 --- a/html_render.py +++ b/html_render.py @@ -1,23 +1,24 @@ import html from typing import ( + Callable, TypeAlias, Union, - Callable, - Optional, - cast, ) -HTML: TypeAlias = Union[tuple, list, Callable[[], str], None, - str, int, float] +HTML: TypeAlias = Union[tuple, + list['HTML'], + Callable[[], str], + None, str, int, float] standalones = ['hr', 'br', 'meta'] +"""Tags which can't have a closing tag.""" def _render_document(document: HTML) -> str: - if type(document) == tuple: + if isinstance(document, tuple): tag, *body = document - if body and type(body[0]) == dict: + if body and isinstance(body[0], dict): print(body[0]) attributes = ' '.join(f'{a}="{html.escape(b)}"' for a, b in body[0].items()) @@ -25,14 +26,18 @@ def _render_document(document: HTML) -> str: start = f'<{tag} {attributes}>' else: start = f'<{tag}>' + if tag in standalones: return start else: - items = ''.join(_render_document(b) for b in body) + if body: + items = ''.join(_render_document(b) for b in body) + else: + items = '' return start + f'{items}' elif callable(document): return str(document()) - elif type(document) == list: + elif isinstance(document, list): return ''.join(_render_document(e) for e in document) elif document is None: return '' @@ -42,4 +47,25 @@ def _render_document(document: HTML) -> str: def render_document(document: HTML) -> str: + """ + Render an HTML structure to an Html string. + + The following Python types are converted as follows: + - Tuples + - The first value becomes the tags name + - The second value, if a dictionary, becomes the tags attributes + - All following values (including the second if not a dictionary) + gets individually passed to render_document. + - Lists + Each element gets passed to render_document + - Callable[[], str] + Gets called, and its output is included verbatim. Useful for + including strings which shouldn't be escaped. + - str + Gets escaped, and included + - int, float + Gets included as their default string representation. + - None + Becomes an empty string + """ return '\n' + _render_document(document) diff --git a/main.py b/main.py index f1c7b27..8315d3b 100644 --- a/main.py +++ b/main.py @@ -1,10 +1,8 @@ from email.message import EmailMessage from email.headerregistry import Address from urllib.parse import urlencode -import http.cookies import password from password import Passwords -from uuid import uuid4 import os from typing import ( Optional, diff --git a/mu.py b/mu.py index 93816f4..9337747 100644 --- a/mu.py +++ b/mu.py @@ -1,3 +1,7 @@ +""" +Wrapper for the `mu` command line. +""" + import email.message import email.policy from email.parser import BytesParser @@ -6,16 +10,29 @@ from subprocess import PIPE import xml.dom.minidom import xml.dom +from typing import ( + Literal, + Union, +) parser = BytesParser(policy=email.policy.default) def get_mail(id: str) -> email.message.Message: + """ + Lookup email by Message-ID. + + [Raises] + MuError + """ cmd = subprocess.run(['mu', 'find', '-u', f'i:{id}', '--fields', 'l'], stdout=PIPE) filename = cmd.stdout.decode('UTF-8').strip() + if cmd.returncode != 0: + raise MuError(cmd.returncode) + with open(filename, "rb") as f: mail = parser.parse(f) return mail @@ -36,7 +53,46 @@ class MuError(Exception): return f'MuError({self.returncode}, "{self.msg}")' -def mu_search(query, sortfield='subject', reverse=False): +Sortfield = Union[Literal['cc'], + Literal['c'], + Literal['bcc'], + Literal['h'], + Literal['date'], + Literal['d'], + Literal['from'], + Literal['f'], + Literal['maildir'], + Literal['m'], + Literal['msgid'], + Literal['i'], + Literal['prio'], + Literal['p'], + Literal['subject'], + Literal['s'], + Literal['to'], + Literal['t'], + Literal['list'], + Literal['v']] + + + +def mu_search(query, sortfield: Sortfield = 'subject', reverse: bool = False) -> list[dict[str, str]]: + """ + [Parameters] + query - Search query as per mu-find(1). + sortfield - Field to sort the values by + reverse - If the sort should be reversed + + [Returns] + >>> {'from': 'Hugo Hörnquist ', + 'date': '1585678375', + 'size': '377', + 'msgid': 'SAMPLE-ID@localhost', + 'path': '/home/hugo/.local/var/mail/INBOX/cur/filename', + 'maildir': '/INBOX' + } + """ + if not query: raise ValueError('Query required for mu_search') cmdline = ['mu', 'find', diff --git a/password.py b/password.py index 16192be..ff0df21 100755 --- a/password.py +++ b/password.py @@ -1,4 +1,11 @@ #!/usr/bin/env python3 + +""" +Simple password store, backed by a JSON file. + +Also contains an entry point for managing the store. +""" + import hashlib import json import os @@ -28,6 +35,12 @@ class PasswordEntry(TypedDict): class Passwords: + """ + Simple password store. + + [Parameters] + fname - Path of json file to load and store from. + """ def __init__(self, fname: os.PathLike): self.fname = fname self.db: dict[str, PasswordEntry] @@ -38,6 +51,7 @@ class Passwords: self.db = {} def save(self) -> None: + """Dump current data to disk.""" try: with open(os.fspath(self.fname) + '.tmp', 'w') as f: json.dump(self.db, f) @@ -47,6 +61,7 @@ class Passwords: print(f'Saving password failed {e}') def add(self, username: str, password: str) -> None: + """Add (or modify) entry in store.""" if cur := self.db.get(username): salt = cur['salt'] hashed = hashlib.sha256((salt + password).encode('UTF-8')) @@ -65,6 +80,7 @@ class Passwords: } def validate(self, username: str, password: str) -> bool: + """Check if user exists, and if it has a correct password.""" # These shall fail when key is missing data = self.db[username] proc = hash_methods[data['method']] @@ -72,8 +88,7 @@ class Passwords: return data['hash'] == digest -if __name__ == '__main__': - +def main(): import argparse parser = argparse.ArgumentParser() @@ -99,3 +114,7 @@ if __name__ == '__main__': print(passwords.validate(args.username, args.password)) else: parser.print_help() + + +if __name__ == '__main__': + main() -- cgit v1.2.3