#!/usr/bin/env python3 """Render resume.yaml -> a .odt file. Two output flavors: --level=short (default) -> condensed single-page resume. --level=full -> detailed resume; concatenates each list with its `_full` sibling (e.g. bullets + bullets_full). Builds the ODT zip from scratch (mimetype, manifest, meta, styles, content). The ODT styling lives inline as STYLES below; content comes from resume.yaml. DOCX is produced by piping this output through LibreOffice: soffice --headless --convert-to docx .odt """ import argparse import os import yaml import zipfile HERE = os.path.dirname(os.path.abspath(__file__)) DATA = os.path.join(HERE, "resume.yaml") DEFAULT_OUT = { "short": os.path.join(HERE, "MikeEberlein_Resume.odt"), "full": os.path.join(HERE, "MikeEberlein_Resume_Detailed.odt"), } MIMETYPE = "application/vnd.oasis.opendocument.text" MANIFEST = ''' ''' META = ''' Mike Eberlein Resumerender_odt.py ''' STYLES = ''' ''' CONTENT_HEAD = ''' ''' CONTENT_TAIL = '' # ---------- XML helpers ---------- XML_ESCAPES = {"&": "&", "<": "<", ">": ">"} def xesc(s: str) -> str: return "".join(XML_ESCAPES.get(c, c) for c in s) def p(style, content): return f'{content}' def b(text): return f'{xesc(text)}' def i(text): return f'{xesc(text)}' TAB = '' def bullet(text, style="Bullet"): return (f'' f'{xesc(text)}' f'') def merged(item, key, level): """Return item[key] for short, or item[key] + item[key + '_full'] for full.""" base = item.get(key) or [] if level == "full": base = list(base) + list(item.get(key + "_full") or []) return base def render(data, level="short") -> str: parts = [] h = data["header"] parts.append(p("Name", xesc(h["name"]))) contact = f'{xesc(h["tagline"])} • {xesc(h["email"])}' parts.append(p("Contact", contact)) parts.append(p("Summary", xesc(data["summary"].strip()))) parts.append(p("SectionHead", "EXPERIENCE")) for company in merged(data, "experience", level): parts.append(f'{b(company["company"])}{TAB}{xesc(company["dates"])}') for role in merged(company, "roles", level): parts.append(f'{i(role["title"])}{TAB}{i(role["dates"])}') subroles = merged(role, "subroles", level) if subroles: for sr in subroles: parts.append(f'{xesc(sr["title"])}{TAB}{i(sr["dates"])}') for blt in merged(sr, "bullets", level): parts.append(bullet(blt, style="SubBullet")) else: for blt in merged(role, "bullets", level): parts.append(bullet(blt, style="Bullet")) parts.append(p("SectionHead", "EDUCATION")) for ed in merged(data, "education", level): parts.append(f'{b(ed["school"])}{TAB}{xesc(ed["dates"])}') for blt in merged(ed, "bullets", level): parts.append(bullet(blt, style="Bullet")) parts.append(p("SectionHead", "CERTIFICATIONS")) parts.append(p("Standard", " • ".join(xesc(c) for c in merged(data, "certifications", level)))) return CONTENT_HEAD + "".join(parts) + CONTENT_TAIL def main(): ap = argparse.ArgumentParser() ap.add_argument("--level", choices=["short", "full"], default="short") ap.add_argument("--output", default=None, help="Output .odt path (default: MikeEberlein_Resume.odt / MikeEberlein_Resume_Detailed.odt)") args = ap.parse_args() out_path = args.output or DEFAULT_OUT[args.level] with open(DATA, "r", encoding="utf-8") as f: data = yaml.safe_load(f) content = render(data, level=args.level) with zipfile.ZipFile(out_path, "w", zipfile.ZIP_DEFLATED) as z: zi = zipfile.ZipInfo("mimetype") zi.compress_type = zipfile.ZIP_STORED z.writestr(zi, MIMETYPE) z.writestr("META-INF/manifest.xml", MANIFEST) z.writestr("meta.xml", META) z.writestr("styles.xml", STYLES) z.writestr("content.xml", content) print(f"wrote {out_path} (level={args.level})") if __name__ == "__main__": main()