Kishore Vancheeshwaran
Resume in yaml and orgmode

In my previous post we created the resume using orgmode. Although it was very versatile and friendly with version control, I noticed that I had trouble with blocks of text having tables like below where I had to mix in the latex and html settings along with the content which wasn’t very pretty with git diff.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
* FirstName LastName
#+attr_html: :class mytable meta :rules all :border nil :cellspacing nil :cellpadding nil :frame nil
#+attr_latex: :align c|c|c|c
| [[mailto:email@gmail.com][email@gmail.com]] | [[https://linkedin.com/in/username][linkedin.com/in/username]] | +91-9876543210 | City, Country |
** Experience
*** Company 1
#+attr_html: :class mytable exp :rules nil :border nil :cellspacing nil :cellpadding nil :frame nil
#+attr_latex: :align L{0.27\textwidth}C{0.40\textwidth}R{0.25\textwidth}
| *Software Engineer* | *Company Inc.* | *Feb 2015 -- Present* |
| Software Team       | City           |                       |

In addition to that, to get anonymized resume reviews, I had to manually copy paste parts of the content into another org file and anonymize the relevant info before publishing it. I don’t find this to be that efficient use of my time. I ended up doing what the programmer stereotype does - automate this task of generating my resume which plays well with version control.

Yaml seemed to be the popular format which is (al)most human readable and easy to parse.

The gist of this automation is as follows.

  • Write resume in yaml
  • Parse yaml with python. Python code will have string snippets for org mode. Write this to an org file
  • Run emacs from the command line with a custom init set up and export to both html and pdf
  • Have a Makefile to automate all of this.

Seems straightforward enough.

Yaml structure

resume.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
name: Kishore Vancheeshwaran
contact:
  email: personal.email@gmail.com
  linkedin: linkedin.com/in/kishvanchee
  phone: +91-0000000000
  location: Bangalore, IN
experience:
  - company: Company 2
    position: Software Engineer
    team: Awesome
    start_date: Jan 2015
    location: Bangalore
    summary:
      - I did something awesome
      - I automated some work
  - company: Company 1
    position: Software Engineer
    team: Awesome
    start_date: Jan 2005
    to_date: Dec 2014
    location: Bangalore
    summary:
      - I did something awesome
      - I automated some work
technical_skills:
  languages: Python
  frameworks: Django
  databases: PostgreSQL
  dev_tools: Git
education:
  - degree: Fancy degree
    university: University of freedom
    from: Jul 2000
    to: Jun 2004

The yaml file is pretty self explanatory. You can have multiple experience/companies, multiple education entries, etc.

Here’s a a not so pretty but works python script which takes care of parsing the yaml and writing it to org file.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#!/usr/bin/env python3
import yaml
import sys
import argparse

with open("resume.yaml", "r") as fin:
    resume = yaml.safe_load(fin)

parser = argparse.ArgumentParser()
parser.add_argument("-t", "--type", required=False, help="type of resume")
args = parser.parse_args()

outfile = "resume_kishore.org"

if args.type == "anon":
    outfile = "resume_anon.org"
    resume["name"] = "First LastName"
    resume["contact"]["email"] = "first.lastname@gmail.com"
    resume["contact"]["linkedin"] = "linkedin.com/in/anon"
    resume["contact"]["phone"] = "+91-9876543210"
    resume["contact"]["location"] = "Earth, SS"
    for i, _ in enumerate(resume["education"]):
        resume["education"][i]["university"] = "University Name"
    for i, _ in enumerate(resume["experience"]):
        resume["experience"][i]["company"] = f"Company Name"
elif args.type == "recent":
    resume["experience"][0]["summary"] = ["I currently work here"]

foreword = r"""
#+TITLE: Resume
#+author: {name}
#+options: toc:nil num:nil title:nil author:nil timestamp:nil html-style:nil prop:nil html-postamble:nil
#+latex_compiler: xelatex
#+latex_class: article
#+latex_class_options: [letterpaper,10pt]
#+latex_header: \include{{latexTemplate.tex}}
#+begin_src python :exports (if (org-export-derived-backend-p org-export-current-backend 'html) "results" "none") :results raw :eval yes
comment = '''
#+ATTR_HTML: :class linktopdf :title Kishore's Resume
[[https://kishvanchee.com/resume_kishore.pdf][Click here for pdf]]'''
return comment
#+end_src
"""
foreword = foreword.format(name=resume["name"])

header = """
* {name}
#+attr_html: :class mytable meta :rules all :border nil :cellspacing nil :cellpadding nil :frame nil
#+attr_latex: :align c|c|c|c
| [[mailto:{email}][_{email}_]] | [[https://{linkedin}][_{linkedin}_]] | {phone} | {location} |
"""
header = header.format(
    name=resume["name"],
    email=resume["contact"]["email"],
    linkedin=resume["contact"]["linkedin"],
    phone=resume["contact"]["phone"],
    location=resume["contact"]["location"],
)

tbl_posn_format = r"""
#+attr_html: :class mytable exp :rules nil :border nil :cellspacing nil :cellpadding nil :frame nil
#+attr_latex: :align L{0.27\textwidth}C{0.40\textwidth}R{0.25\textwidth}
"""

experience_header = """
** Experience
"""

exp = []
for e in resume["experience"]:
    company = "*** {company}".format(company=e["company"])
    posn_header = (
        f"| *{e['position']}* | *{e['company']}* | *{e['start_date']} -- {e['to_date'] if e.get('to_date') else 'Present'}* |"
        + "\n"
        + f"| {e['team']} | {e['location']} |   |"
    )
    summary = "\n".join(["- " + s for s in e["summary"]])
    components = company + tbl_posn_format + posn_header + "\n" + summary
    # components = "".join([company, tbl_posn_format, posn_header, summary]) + "\n"
    exp.append(components)

experience = experience_header + "\n".join(exp)

tech_skills = """
** Technical Skills
- *Languages*  -- {languages}
- *Frameworks* -- {frameworks}
- *Databases*  -- {databases}
- *Dev tools*  -- {dev_tools}
"""
tech_skills = tech_skills.format(
    languages=resume["technical_skills"]["languages"],
    frameworks=resume["technical_skills"]["frameworks"],
    dev_tools=resume["technical_skills"]["dev_tools"],
    databases=resume["technical_skills"]["databases"],
)

edu = [
    f"| *{ed['degree']}* | {ed['university']} | {ed['from']} -- {ed['to']} |"
    for ed in resume["education"]
]
final_edu = "\n".join(edu)
education = r"""
** Education
#+attr_html: :class mytable education :rules nil :border nil :cellspacing nil :cellpadding nil :frame nil
#+attr_latex: :align L{0.27\textwidth}C{0.40\textwidth}R{0.25\textwidth}
"""
education = education + final_edu

final_resume = "".join([foreword, header, experience, tech_skills, education])

with open(outfile, "w") as fout:
    fout.write(final_resume)

Now we have the Makefile which automates the entire process. The recent option is so that you can have your most recent work experience listed in your yaml but not the actual pdf/html. It will be prepopulated with I currently work here as the entry. This way you don’t have to worry about having two separate entries with one having a noexport in the org file or even having two separate org files for the same. I wanted this feature to avoid disclosing what I am currently working on in the current company unless it was necessary.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.PHONY: clean pdf anon recent html

all: clean pdf anon html

pdf:
	python generate_resume.py
	emacs resume_kishore.org --batch -f org-latex-export-to-pdf

html:
	python generate_resume.py
	emacs resume_kishore.org --batch -l custominit.el -f org-html-export-to-html

anon:
	python generate_resume.py -t anon
	emacs resume_anon.org --batch -f org-latex-export-to-pdf

recent:
	python generate_resume.py -t recent
	emacs resume_kishore.org --batch -f org-latex-export-to-pdf
	emacs resume_kishore.org --batch -l custominit.el -f org-html-export-to-html

clean:
	find . -maxdepth 1 ! -iname 'generate_resume.py' ! -iname 'resume.yaml' ! -iname 'Makefile' ! -iname 'latexTemplate.tex' ! -iname 'style.css' ! -iname 'custominit.el' -and -type f -exec rm "{}" \;

The custominit file is below. The file is necessary because -batch mode runs emacs with -q option which means without the default init file. Since we require only parts of the init file, we can have a custom file with the bare necessity to run the command.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
;; This function is to run the eval without confirmation, at the top of the org
;; file to generate a link for the pdf file from the html file.
(defun org-confirm-babel-evaluate (lang body)
  (not (or (string= lang "python") )))
(setq org-confirm-babel-evaluate 'org-confirm-babel-evaluate)

(org-babel-do-load-languages 'org-babel-load-languages '((python . t)))

(defun my-org-inline-css-hook (exporter)
  "Insert custom inline css"
  (when (eq exporter 'html)
    (let* ((dir (ignore-errors (file-name-directory (buffer-file-name))))
           (path (concat dir "style.css"))
           (homestyle (or (null dir) (null (file-exists-p path))))
           (final (if homestyle "~/.emacs.d/org-style.css" path)))
      (setq org-html-head-include-default-style nil)
      (setq org-html-head (concat
                           "<style type=\"text/css\">\n"
                           "<!--/*--><![CDATA[/*><!--*/\n"
                           (with-temp-buffer
                             (insert-file-contents final)
                             (buffer-string))
                           "/*]]>*/-->\n"
                           "</style>\n")))))

(add-hook 'org-export-before-processing-hook 'my-org-inline-css-hook)

The style.css and latexTemplate.tex remain the same as in the previous post.

Phew, now we can run make and it automates the entire build. And we have perfect diffs.

If you would like to leave a comment, contact me via email.
Post 12/99 - part of 100DaystoOffload