From 3ddd3dec8ef1d08c4ed4ba0e1f2e4ebd49626e24 Mon Sep 17 00:00:00 2001 From: Skye Date: Sat, 2 Dec 2023 16:29:40 -0500 Subject: [PATCH] Use pathlib.Path, remove img extensions, fix under strict typing --- .gitignore | 1 + format.py | 115 ++++++++++++++++++++++++++++++----------------------- 2 files changed, 66 insertions(+), 50 deletions(-) diff --git a/.gitignore b/.gitignore index 5f4061c..9035255 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ result .direnv +.vscode/* *.pdf addresses.csv diff --git a/format.py b/format.py index 4e543c4..95f6dca 100755 --- a/format.py +++ b/format.py @@ -13,10 +13,19 @@ import xml.etree.ElementTree as ET import requests import imb +from pathlib import Path +from typing import TypedDict + # A lot of stuff needs to be in the same directory, just chdir os.chdir(os.path.dirname(os.path.realpath(__file__))) +def cache_dir() -> Path: + cache = Path("cache") + cache.mkdir(exist_ok=True) + return cache + + def iso_code(s: str) -> str: if len(s) != 2: raise ValueError("must be 2 characters long") @@ -27,74 +36,70 @@ def iso_code(s: str) -> str: def get_discord_avatar( - url: urllib.parse.ParseResult, secrets: dict -) -> (typing.Optional[str], str): + url: urllib.parse.ParseResult, secrets: dict[str, str] +) -> typing.Optional[str]: try: + uid = url.path token = secrets["discord_token"] user_info = requests.get( - f"https://discord.com/api/users/{url.path}", + f"https://discord.com/api/users/{uid}", headers={"Authorization": f"Bot {token}"}, ).json() avatar_hash = user_info["avatar"] - return ( - f"https://cdn.discordapp.com/avatars/{url.path}/{avatar_hash}.png?size=4096", - "png", - ) + return f"https://cdn.discordapp.com/avatars/{uid}/{avatar_hash}.png?size=4096" except KeyError: - return None, "" + return None def get_fedi_avatar( - url: urllib.parse.ParseResult, secrets: dict -) -> (typing.Optional[str], str): + url: urllib.parse.ParseResult, secrets: dict[str, str] +) -> typing.Optional[str]: try: mastodon_api = secrets["mastodon_api"] user_info = requests.get( f"{mastodon_api}/api/v1/accounts/lookup", params={"acct": url.path} ).json() avatar_url = user_info["avatar_static"] - extension = avatar_url.split(".")[-1] - return avatar_url, extension + return avatar_url except KeyError: - return None, "" + return None -def get_orig_avatar(url: str, basename: str, secrets: dict) -> typing.Optional[bytes]: - if not os.path.exists("cache"): - os.mkdir("cache") - +def get_orig_avatar( + url: str, basename: str, secrets: dict[str, str] +) -> typing.Optional[bytes]: url_parts = urllib.parse.urlparse(url) if url_parts.scheme == "fedi": - real_url, extension = get_fedi_avatar(url_parts, secrets) + real_url = get_fedi_avatar(url_parts, secrets) elif url_parts.scheme == "discord": - real_url, extension = get_discord_avatar(url_parts, secrets) + real_url = get_discord_avatar(url_parts, secrets) else: real_url = url - extension = url_parts.path.rsplit(".", 1)[1] if real_url is None: return None - img_name = f"cache/{basename}.{extension}" + img_file = cache_dir() / basename - if os.path.exists(img_name): - with open(img_name, "rb") as infile: + if img_file.exists(): + with img_file.open("rb") as infile: return infile.read() result = requests.get(real_url) - if result.ok: - with open(img_name, "wb") as outfile: - outfile.write(result.content) - return result.content - return None + if not result.ok: + return None + with img_file.open("wb") as outfile: + outfile.write(result.content) + return result.content -def get_avatar(url: str, secrets: dict) -> str: +def get_avatar(url: str, secrets: dict[str, str]) -> str | None: basename = hashlib.sha256(url.encode("utf-8")).hexdigest() - if os.path.exists(f"cache/{basename}.svg"): - return f"cache/{basename}.svg" + file_path = cache_dir() / f"{basename}.svg" + if file_path.exists(): + return str(file_path) avatar_raster = get_orig_avatar(url, basename, secrets) if avatar_raster is None: - return "" + return None svg_text = f""" @@ -104,13 +109,13 @@ def get_avatar(url: str, secrets: dict) -> str: xlink:href="data:;base64,{base64.b64encode(avatar_raster).decode("utf-8")}" /> """ - with open(f"cache/{basename}.svg", "w") as svgfile: + with open(file_path, "w") as svgfile: svgfile.write(svg_text) - return f"cache/{basename}.svg" + return str(file_path) def get_country_name( - root: ET.ElementTree, destination: str, alt=None + root: ET.ElementTree, destination: str, alt: str | None = None ) -> typing.Optional[str]: elements = root.findall( f"./localeDisplayNames/territories/territory[@type='{destination.upper()}']" @@ -198,16 +203,24 @@ current_serial = imb.get_first_serial() mid = secrets.get("mailer_id") -cards = [] +class Card(TypedDict): + address: str + avatar: str | None + row: dict[str, str] + imb: str + + +cards: list[Card] = [] for row in rows: if row["Address"] == "": continue - country = ( - [] - if row["Country"].lower() == args.origin - else [get_country_name(cldr_root, row["Country"]).upper()] - ) + if row["Country"].lower() == args.origin: + country = [] + else: + name = get_country_name(cldr_root, row["Country"]) + assert name is not None + country = [name.upper()] address = row["Address"].split("\n") + country @@ -216,14 +229,13 @@ for row in rows: else: avatar = None - cards += [ - { - "address": "\n".join(address), - "avatar": avatar, - "row": row, - "imb": "", - } - ] + card: Card = { + "address": "\n".join(address), + "avatar": avatar, + "row": row, + "imb": "", + } + cards.append(card) # Typst can't access files outside the project root, except through a symlink # Create one in cache to use here @@ -258,6 +270,9 @@ with open("options.json", "w") as options: if args.dont_compile: exit() +font_paths = os.getenv("TYPST_FONT_PATHS") +assert font_paths is not None + os.execlp( "typst", "typst", @@ -265,7 +280,7 @@ os.execlp( "--font-path", args.content_path, "--font-path", - os.getenv("TYPST_FONT_PATHS"), + font_paths, args.template, "output.pdf", )