<repo_name>atla5/resume<file_sep>/README.md
# <NAME>

### Description ###

##### short #####
store and export information about my education, work experience, projects, and goals.

##### long #####
This repository is meant to act as both:
 - a central hub to store the data objects about my personal and professional career in one location.
 - a repeatable and simple way to generate PDF resumes without dealing with formatting hassles (allowing me to only update the json and run a command)
 - a simplified way to host/serve resume info and pdf download on [my site](http://aidan-sawyer.com).

### Content ###

As a "resume", the repository is meant to store information relevant to my work
  and career. Following the traditional divisions as well as those I found most
  desirable to my site, the information is stored as follows:

|file|description|
|:---|:---------------|
|experience.json|jobs i've had in a number of fields|
|education.json|formal schooling i've completed|
|projects.json|software projects i've worked on over the years|
|additional.json|listing of languages, skills, interests, and links that didn't fit elsewhere|


### How to Use ###

#### Getting Started

Clone the repository or download the zip from the master branch
```
$ cd ~/your/projects/folder
$ git clone https://github.com/atla5/resume.git
```

This project runs on `python` 2.6 - 3.5 and requires `pdflatex` as a helper library.

I use/prefer macs, so the commands are in/for that. I'm sure you could get it running
  on PC or linux, and if you do, please send in a ticket or Pull Request explaining 
  how you did.

For mac, make sure you have homebrew package manager installed:
```bash
$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
```

And use it to install the `basictex` [brew cask](https://brew.sh/).  
  
```bash
$ brew install basictex --cask
```

#### Running the Builder

The main script for this repository, responsible for reading the json, 
  populating the new `.tex` file, and outputing the actual `.pdf` at the end 
  is in `src/build_resume.pdf`:

```bash
$ python src/build_resume.py
```

Once you run it, look in the `./build` folder to find the following main outputs:
  -  `Resume_NAME.pdf` 
  -  `CoverLetter_NAME` 
  -  `References_NAME.pdf`

#### Rolling Your Own

You probably don't just want to build _my_ resume, so if you'd like to create one 
  that looks like it but contains your own information, simply edit the `.json` files 
  in the `data/` directory.

If you don't like how I laid out my `json` schemas and want to fiddle with how the information 
  is stored, look into editing the `src/update_values_helpers.py` methods. 

More extensive edits can be made to the _content_ of the resume by editing `build/resume.tex` 
  directly. The `.tex` is similar to most other markup once you get the hang of it, and 
  doesn't take you long to learn.

If you'd like to make changes to the _styling_ of the resume, you're looking at changing 
  the `build/resume.cls` file, and more power to you. I've yet to wrestle that beast, and 
  will not be able to provide you much assistance (at least yet).
<file_sep>/src/update_values_helpers.py
# author: <NAME> (atla5)
# date_created: 2017-08-12
# license: MIT
# purpose: customize dict_values according to passed values

import logging
logger = logging.getLogger(__name__)

months = ["Jan", "Feb", "March", "April", "May", "June", "July", "Aug", "Sept", "Oct", "Nov", "Dec"]
months_full = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]


def humanize_date(yyyy_mm, formalize=False):
    output = yyyy_mm

    try:
        if '-' not in yyyy_mm:
            return yyyy_mm
        else:
            tokens = yyyy_mm.split('-')
            year = tokens[0]
            month = int(tokens[1])

        if 0 < month <= 12:
            str_month = months_full[month-1] if formalize else months[month-1]
            output = "{} {}".format(str_month, year)
        else:
            logger.warning("Invalid month: {}\n".format(yyyy_mm))

    except IndexError:
        logger.warning("Improperly formatted date: {}\n".format(yyyy_mm))

    finally:
        return output


def humanize_list(ls):
    return ", ".join(str(s) for s in ls)


def generate_about(dict_values, about):
    contact = about['contact']
    accounts = about['accounts']
    highlights = about['overview']
    dict_values.update({
        "FULL~NAME": about['name'],
        "OBJECTIVE": about['objective'],
        "EMAIL": contact['email'] if contact['email'] else "",
        "PHONE": contact['phone'] if contact['phone'] else "",
        "GITHUB": accounts['github'],
        "WEBSITE": about['url'].replace('http://', ''),
        "HIGHLIGHT~1": highlights[0],
        "HIGHLIGHT~2": highlights[1]
    })


def generate_school_info(dict_values, school, id=1):
    logging.debug("updating school values...")
    prefix = "S{}~".format(id)
    school_notes = school['notes']

    dict_values.update({
        prefix + "NAME": school['school_name'],
        prefix + "DEGREE": school['degree'],
        prefix + "TIME~START": humanize_date(school['time_start']),
        prefix + "TIME~END": humanize_date(school['time_end']),
        prefix + "SUMMARY": school['summary'] if 'summary' in school else school_notes[0],
        prefix + "NOTE~1": school_notes[0],
        prefix + "NOTE~2": school_notes[1] if len(school_notes) >= 2 else ""
    })



def generate_work_experience(dict_values, work, pre="WA", id=1):
    logging.debug("updating work experience values for work '{}'".format(id))
    prefix = "{}{}~".format(pre,id)  # e.g. 'WA1~', 'WB2~'
    responsibilities = work['responsibilities']
    num_responsibilities = len(responsibilities)

    dict_values.update({
        prefix + "NAME": work['company_name'],
        prefix + "POSITION": work['position'],
        prefix + "TIME~START": humanize_date(work['time_start']),
        prefix + "TIME~END": humanize_date(work['time_end']) if 'time_end' in work else "Present",
        prefix + "ADVISOR~NAME": work['advisor_name'],
        prefix + "ADVISOR~POSITION": work['advisor_position'],
        prefix + "ADVISOR~CONTACT": work['advisor_contact'],
        prefix + "RESPONSIBILITY~1": responsibilities[0] if num_responsibilities >= 1 else work['summary_short'],
        prefix + "RESPONSIBILITY~2": responsibilities[1] if num_responsibilities >= 2 else "",
        prefix + "RESPONSIBILITY~3": responsibilities[2] if num_responsibilities >= 3 else "",
        prefix + "SUMMARY": work['summary_short'] if 'summary_short' in work else ""
    })


def generate_reference(dict_values, reference, id=1):
    logging.debug("updating reference '{}'".format(id))
    prefix = "R{}~".format(id)

    contact = reference['email']
    if 'phone' in reference and reference['phone']:
        contact += " - {}".format(reference['phone'])
    dict_values.update({
        prefix + "NAME": reference['name'],
        prefix + "CONTACT": contact,
        prefix + "POSITION": reference['position'],
        prefix + "DATE~START": humanize_date(reference['date_start']),
        prefix + "DATE~END": humanize_date(reference['date_end']) if 'date_end' in reference else "Present",
        prefix + "RELATIONSHIP": reference['relationship'],
        prefix + "IMPORTANCE": reference['importance'],
    })


def generate_project(dict_values, project, id=1):
    logging.debug("updating project info for project '{}'".format(id))
    prefix = "P{}~".format(id)
    dict_values.update({
        prefix+"NAME": project['name'],
        prefix+"DESCRIPTION": project['description_short']
    })


def generate_certificate(dict_values, certificate, id=1):
    logging.debug("updating project info for certificate '{}'".format(id))
    prefix = "C{}~".format(str(id))
    dict_values.update({
        prefix+"CODE": certificate['name'],
        prefix+"DESCRIPTION": certificate['description']
    })


def generate_language_entry(dict_values, level, languages, id=1):
    logging.debug("updating language entry for level '{}'".format(level))
    suffix = "~{}".format(id)
    dict_values.update({
        "LEVEL" + suffix: level if languages else "",
        "LANGUAGES" + suffix: humanize_list([lang['name'] for lang in languages] if languages else "")
    })


def generate_languages(dict_values, languages):
    # establish name for proficiencies
    lvl_1 = "Intermediate"
    lvl_2 = "Functional"
    lvl_3 = "Novice"

    # sort languages into lists based on proficiency
    ls_intermediate = []
    ls_functional = []
    ls_limited = []
    for language in languages:
        if language['proficiency'] == lvl_1:
            ls_intermediate.append(language)
        elif language['proficiency'] == lvl_2:
            ls_functional.append(language)
        else:
            ls_limited.append(language)

    # update dict_values with each grouping of languages
    generate_language_entry(dict_values, lvl_1, ls_intermediate, 1)
    generate_language_entry(dict_values, lvl_2, ls_functional, 2)
    generate_language_entry(dict_values, lvl_3, ls_limited, 3)
<file_sep>/src/build_resume.py
# author: <NAME> (atla5)
# date_created: 2017-08-12
# license: MIT
# purpose: build custom resume from LaTeX template and json

import json, time, logging
from os import path, getcwd, system, chdir
from sys import stdout
from shutil import copyfile

from subprocess import check_call, STDOUT, DEVNULL
from update_values_helpers import *

logging.basicConfig(stream=stdout, level=logging.INFO)
logger = logging.getLogger("build_resume")

# set absolute paths for 'build/' and 'data/' directories
src_dir = path.abspath(path.dirname(__file__))
build_dir = path.abspath(path.join(src_dir, "../build"))
data_dir = path.abspath(path.join(src_dir, "../data"))


def get_json_from_data_file(filename):
    json_to_return = {}
    try:
        data_file = path.join(data_dir, filename)
        json_to_return = json.load(open(data_file))
    except FileNotFoundError:
        logger.error("Error loading file: {}".format(filename), exc_info=True)
    finally:
        return json_to_return


def sanitize_latex_syntax(line):
    return line.replace("#", "\#")


def update_shared_values(dict_values):
    logger.debug("adding header, date data to 'dict_values'")

    # about me
    about = get_json_from_data_file('about.json')
    generate_about(dict_values, about)

    # date created
    dict_values.update({
        "DATE~CREATED": time.strftime("%Y-%m-%d")
    })


def update_resume_values(dict_values):
    logger.debug("adding resume values data to 'dict_values'")
    filter_a = "craft"
    filter_b = "software"

    # education
    educations = get_json_from_data_file('education.json')
    filtered = [ed for ed in educations if filter_a in ed['tags'] or filter_b in ed['tags']]
    generate_school_info(dict_values, filtered[0], 1)
    if filtered[1]:
        generate_school_info(dict_values, filtered[1], 2)

    # work experiences A (e.g. "craft")
    experiences = get_json_from_data_file('experience.json')
    work_xp = [xp for xp in experiences if filter_a in xp['tags']]
    for i, work_experience in enumerate(work_xp, start=1):
        generate_work_experience(dict_values, work_experience, pre="WA", id=i)

    # work experiences B (e.g. "software")
    pro_xp = [xp for xp in experiences if filter_b in xp['tags']]
    for i, work_experience in enumerate(pro_xp[:3], start=1):
        generate_work_experience(dict_values, work_experience, "WB", i)

    # projects
    projects = get_json_from_data_file('projects.json')
    for i, project in enumerate(projects[:3], start=1):
        generate_project(dict_values, project, i)

    # languages
    additional = get_json_from_data_file('additional.json')
    languages = additional['languages']
    generate_languages(dict_values, languages)

    # certificates
    certificates = additional['certifications']
    for i, certificate in enumerate(certificates[:4], start=1):
        generate_certificate(dict_values, certificate, i)

def update_references_values(dict_values):
    logger.debug("adding references data to 'dict_values'")
    references = get_json_from_data_file('references.json')
    for i, project in enumerate(references[:3], start=1):
        generate_reference(dict_values, project, i)


def generate_new_tex_file_with_values(values, input_template, output_filename):
    logger.debug("generating new tex file '{}' using input template '{}'".format(output_filename, input_template))

    # copy .tex template into a new 'output_filename.tex'
    copyfile(input_template, output_filename)

    # use `dict_values` to replace placeholders in template with real values in the new one
    resume_template = open(input_template, 'r')
    output_tex = open(output_filename, 'w')
    for line in resume_template:
        for key in values:
            line = line.replace(key, values[key])
        output_tex.write(sanitize_latex_syntax(line))

    # close files
    resume_template.close()
    output_tex.close()


def generate_pdf_from_tex_template(output_tex_filename):
    logger.debug("generating pdf from tex file '{}'".format(output_tex_filename))

    chdir(build_dir)

    # export filename.tex into a pdf
    check_call(['pdflatex', '-interaction=nonstopmode', output_tex_filename], stdout=DEVNULL, stderr=STDOUT)
    logger.info("pdf created at {}".format(output_tex_filename.replace('.tex','.pdf')))


def build_resume():
    logger.info("\n\nbuilding resume...")

    # create and update value dictionary from json files
    dict_values = {}
    update_shared_values(dict_values)
    update_resume_values(dict_values)

    # manage/generate filenames and paths
    tex_template_filepath = path.join(build_dir, "resume.tex")
    last_name = dict_values['FULL~NAME'].split()[-1]
    filename = "Resume{}".format("_"+last_name if last_name else "")
    tex_new_filepath = path.join(build_dir, filename + ".tex")

    # use values to generate a pdf
    generate_new_tex_file_with_values(dict_values, tex_template_filepath, tex_new_filepath)
    generate_pdf_from_tex_template(tex_new_filepath)


def build_references():
    logger.info("\n\nbuilding references...")

    # create and update value dictionary from json files
    dict_values = {}
    update_shared_values(dict_values)
    update_references_values(dict_values)

    # manage/generate filenames and paths
    tex_template_filepath = path.join(build_dir, "references.tex")
    last_name = dict_values['FULL~NAME'].split()[-1]
    filename = "References{}".format("_" + last_name if last_name else "")
    tex_new_filepath = path.join(build_dir, filename + ".tex")

    # use values to generate a pdf
    generate_new_tex_file_with_values(dict_values, tex_template_filepath, tex_new_filepath)
    generate_pdf_from_tex_template(tex_new_filepath)


def build_cover_letter():
    logger.info("\n\nbuilding cover letter...")
    dict_values = {}
    update_shared_values(dict_values)
    with open(path.join(data_dir, "coverletter.txt"),'r') as cover_letter_text:
        cl_text = ""
        for line in cover_letter_text:
            cl_text += line.replace("’", "'").replace("‘","'")

        dict_values.update({ 
            "CL~TEXT": cl_text,
            "FORMAL~DATE": humanize_date(dict_values["DATE~CREATED"], formalize=True)
        })

    # manage/generate filenames and paths
    tex_template_filepath = path.join(build_dir, "coverletter.tex")
    last_name = dict_values['FULL~NAME'].split()[-1]
    filename = "CoverLetter{}".format("_" + last_name if last_name else "")
    tex_new_filepath = path.join(build_dir, filename + ".tex")

    # use values to generate a pdf
    generate_new_tex_file_with_values(dict_values, tex_template_filepath, tex_new_filepath)
    generate_pdf_from_tex_template(tex_new_filepath)


def clean_up():
    system("rm *.aux")
    system("rm *.fls")
    system("rm *.aux")
    system("rm *.gz")
    system("rm *latexmk")

if __name__ == "__main__":
    build_resume()
    build_references()
    build_cover_letter()
    clean_up()
<file_sep>/src/test_build.py
import unittest
import os, json, csv

from update_values_helpers import humanize_date, humanize_list
from build_resume import build_dir, data_dir, build_resume, build_references


def get_time_created_or_zero(filename):
    return os.path.getctime(filename) if os.path.exists(filename) else 0


class TestBuild(unittest.TestCase):

    def test_humanize_date(self):
        self.assertEqual("Jan 2017", humanize_date("2017-01"))
        self.assertEqual("2017-13",  humanize_date("2017-13"))
        self.assertEqual("2017-00",  humanize_date("2017-00"))

    def test_humanize_list(self):
        self.assertEqual("one fish, two fish, red fish, blue fish", humanize_list(["one fish", "two fish", "red fish", "blue fish"]))
        self.assertEqual("one fish, two fish", humanize_list(["one fish", "two fish"]))
        self.assertEqual("one fish", humanize_list(["one fish"]))
        self.assertEqual("", humanize_list([""]))
        self.assertEqual("1, 2, 3, 4", humanize_list([1, 2, 3, "4"]))

    def test_valid_json_data(self):
        for filename in os.listdir(data_dir):
            if filename.endswith('.json'):
                try:
                    with open(os.path.join(data_dir, filename), 'r') as file:
                        json.loads(file.read())
                except ValueError:
                    self.fail("linting error found in {}".format(filename))

    def test_valid_csv_data(self):
        for filename in os.listdir(data_dir):
            if filename.endswith('.csv'):
                try:
                    with open(os.path.join(data_dir, filename), 'r') as csvfile:
                        for row in csv.reader(csvfile, delimiter=','):
                            if not bool(row) or not isinstance(row, list) or not all(isinstance(elem, str) for elem in row):
                                self.fail("error rendering csv file '{}' into list of strings.".format(filename))

                except Exception:
                    self.fail("error reading csv file: {}".format(filename))

    def build_test_helper(self, filename_prefix, build_function):
        # get the 'created' timestamp any existing output files were created, defaulting to zero
        output_tex_timestamp_before = get_time_created_or_zero("{}.tex".format(filename_prefix))
        output_pdf_timestamp_before = get_time_created_or_zero("{}.pdf".format(filename_prefix))

        try:
            build_function()
        except Exception:
            self.fail("Exception occurred while attempting to build {} output".format(filename_prefix))

        # get the 'created' timestamp of our fresh output files
        output_tex_timestamp_after = get_time_created_or_zero("{}.tex".format(filename_prefix))
        output_pdf_timestamp_after = get_time_created_or_zero("{}.pdf".format(filename_prefix))

        # ensure that build_resume() effectively created both files
        if output_tex_timestamp_after != 0:
            self.assertGreater(output_tex_timestamp_after, output_tex_timestamp_before)
        else:
            self.fail("Error Creating {}.tex".format(filename_prefix))

        if output_pdf_timestamp_after != 0:
            self.assertGreater(output_pdf_timestamp_after, output_pdf_timestamp_before)
        else:
            self.fail("Error creating {}.pdf".format(filename_prefix))

    def test_build_resume(self):
        self.build_test_helper("Resume_Sawyer", build_resume)  # todo: make this filename prefix generic

    def test_build_references(self):
        self.build_test_helper("References_Sawyer", build_references)  # todo: make this filename prefix generic
