Last active
March 7, 2023 22:55
-
-
Save breard-r/82263814d65b6c7bb12a5e95000cffb9 to your computer and use it in GitHub Desktop.
Curriculum vitæ - Python/Jinja2/WeasyPrint
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
1. Install dependencies | |
wget 'https://use.fontawesome.com/releases/v5.9.0/fontawesome-free-5.9.0-web.zip' | |
unzip fontawesome-free-5.9.0-web.zip | |
rm fontawesome-free-5.9.0-web.zip | |
pipenv install jinja2 | |
pipenv install weasyprint | |
2. Edit content.py | |
3. Generate the PDF | |
pipenv run python gen.py |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name = 'Nestor Burma' | |
subtitle = { | |
'fr': 'Détective privé', | |
'en': 'Private investigator', | |
} | |
infos = { | |
'fr': [ | |
{ | |
'icon': 'solid/home', | |
'value': ['27, rue de Mogador', '75009 Paris'], | |
}, | |
{ | |
'icon': 'solid/phone', | |
'value': ['01 23 45 67 89'], | |
}, | |
{ | |
'icon': 'solid/envelope', | |
'value': ['nestor.burma@example.com'], | |
'link': 'mailto:nestor.burma@example.com', | |
}, | |
{ | |
'icon': 'brands/wikipedia-w', | |
'value': ['Nestor Burma'], | |
'link': 'https://fr.wikipedia.org/wiki/Nestor_Burma', | |
}, | |
], | |
'en': [ | |
{ | |
'icon': 'solid/home', | |
'value': ['27, rue de Mogador', '75009 Paris', 'France'], | |
}, | |
{ | |
'icon': 'solid/phone', | |
'value': ['+33 12 345 6789'], | |
}, | |
{ | |
'icon': 'solid/envelope', | |
'value': ['nestor.burma@example.com'], | |
'link': 'mailto:nestor.burma@example.com', | |
}, | |
{ | |
'icon': 'brands/wikipedia-w', | |
'value': ['Nestor Burma'], | |
'link': 'https://en.wikipedia.org/wiki/Nestor_Burma', | |
}, | |
], | |
} | |
sections = { | |
# ---------- | |
# Français | |
# ---------- | |
'fr': [ | |
{ | |
'title': 'Expérience professionnelle', | |
'ctn_type': 'job_list', | |
'ctn': [ | |
{ | |
'date': 'Depuis octobre 1942', | |
'entity': 'Agence Fiat Lux', | |
'title': 'Détective privé', | |
'description': 'Enquêtes, filatures, recherches, surveillances.', | |
}, | |
{ | |
'date': '1941 - 1942', | |
'entity': 'Armée française', | |
'title': 'Prisonier de guerre', | |
'description': 'Stalag VIII-B, Lamsdorf, Haute-Silésie', | |
}, | |
{ | |
'date': '1940', | |
'entity': 'Armée française', | |
'title': 'Appelé du contingent', | |
'description': '67<sup>e</sup> régiment d\'infanterie', | |
}, | |
{ | |
'date': '1935 - 1940', | |
'entity': 'Agence Fiat Lux', | |
'title': 'Détective privé', | |
'description': 'Enquêtes, filatures, recherches, surveillances.', | |
}, | |
], | |
}, | |
{ | |
'title': 'Formation', | |
'ctn_type': 'detailed_list', | |
'ctn': [ | |
{ | |
'abstract': '1935', | |
'title': 'Diplôme universitaire professionnel d\'enquêteur privé', | |
'subtitle': 'Avec mention', | |
'description': 'Université Panthéon-Assas, Paris', | |
}, | |
], | |
}, | |
{ | |
'title': 'Langues', | |
'ctn_type': 'detailed_list', | |
'ctn': [ | |
{ | |
'abstract': 'Français', | |
'title': '', | |
'subtitle': '', | |
'description': 'Langue maternelle', | |
}, | |
{ | |
'abstract': 'Anglais', | |
'title': '', | |
'subtitle': '', | |
'description': 'Courant', | |
}, | |
], | |
}, | |
{ | |
'title': 'Loisirs', | |
'ctn_type': 'simple_list', | |
'ctn': [ | |
'Calembour et de la contrepèterie', | |
'Photographie', | |
], | |
}, | |
], | |
# ---------- | |
# Anglais | |
# ---------- | |
'en': [ | |
{ | |
'title': 'Professional experience', | |
'ctn_type': 'job_list', | |
'ctn': [ | |
{ | |
'date': 'Since octobre 1942', | |
'entity': 'Fiat Lux Agency', | |
'title': 'Private investigator', | |
'description': 'Investigations, skip tracing, information retrieval, surveillance.', | |
}, | |
{ | |
'date': '1941 - 1942', | |
'entity': 'French army', | |
'title': 'Prisoner of war', | |
'description': 'Stalag VIII-B, Lamsdorf, Silesia', | |
}, | |
{ | |
'date': '1940', | |
'entity': 'French army', | |
'title': 'Conscripted', | |
'description': '67<sup>th</sup> Infantry Regiment', | |
}, | |
{ | |
'date': '1935 - 1940', | |
'entity': 'Fiat Lux Agency', | |
'title': 'Private investigator', | |
'description': 'Investigations, skip tracing, information retrieval, surveillance.', | |
}, | |
], | |
}, | |
{ | |
'title': 'Education', | |
'ctn_type': 'detailed_list', | |
'ctn': [ | |
{ | |
'abstract': '1935', | |
'title': 'Private investigator professional university degree', | |
'subtitle': 'With honors', | |
'description': 'Panthéon-Assas University, Paris', | |
}, | |
], | |
}, | |
{ | |
'title': 'Languages', | |
'ctn_type': 'detailed_list', | |
'ctn': [ | |
{ | |
'abstract': 'French', | |
'title': '', | |
'subtitle': '', | |
'description': 'Native language', | |
}, | |
{ | |
'abstract': 'English', | |
'title': '', | |
'subtitle': '', | |
'description': 'Fluent', | |
}, | |
], | |
}, | |
{ | |
'title': 'Hobbies', | |
'ctn_type': 'simple_list', | |
'ctn': [ | |
'Puns and spoonerism', | |
'Photography', | |
], | |
}, | |
], | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
from pathlib import Path | |
from jinja2 import Environment, PackageLoader, select_autoescape | |
from weasyprint import HTML, CSS | |
from weasyprint.fonts import FontConfiguration | |
import content | |
import logging | |
import os | |
out_fmt = 'cv_{}.pdf' | |
tpl_path = 'template.html' | |
css_path = 'style.css' | |
fontawesome_root = 'fontawesome-free-5.9.0-web' | |
def get_context(lang): | |
ctx = { | |
'lang': lang, | |
'name': content.name, | |
'subtitle': content.subtitle[lang], | |
'infos': content.infos[lang], | |
'sections': content.sections[lang], | |
} | |
return ctx | |
def render_templates(lang): | |
mod_name = Path(__file__).stem | |
ctx = get_context(lang) | |
ctx['fontawesome_root'] = fontawesome_root | |
env = Environment( | |
loader=PackageLoader(mod_name, '.'), | |
autoescape=select_autoescape([]) | |
) | |
template = env.get_template(tpl_path) | |
return template.render(**ctx) | |
def render_pdf(html_str, file_name): | |
path = os.path.dirname(os.path.realpath(__file__)) | |
base_url = 'file://{}/'.format(path) | |
font_config = FontConfiguration() | |
html = HTML(string=html_str, base_url=base_url) | |
css = [ | |
CSS(filename=css_path), | |
] | |
html.write_pdf(file_name, stylesheets=css, font_config=font_config) | |
def gen_cv(langs): | |
logger = logging.getLogger('weasyprint') | |
logger.addHandler(logging.lastResort) | |
for lang in langs: | |
html = render_templates(lang) | |
outfile = out_fmt.format(lang) | |
render_pdf(html, outfile) | |
if __name__ == "__main__": | |
gen_cv(['fr', 'en']) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Adapted from the "Freeman Curriculum Vitae" LaTeX template by Alessandro Plasmati. | |
* https://www.latextemplates.com/template/freeman-cv | |
*/ | |
@page { | |
margin: 1cm; | |
} | |
html, body { | |
color: #444; | |
/* | |
font-family: 'C059'; | |
font-weight: lighter; | |
*/ | |
font-size: 12px; | |
} | |
p { | |
margin: 0; | |
padding: 0; | |
word-wrap: break-word; | |
} | |
a, a:hover { | |
color: #444; | |
text-decoration: none; | |
} | |
sup { | |
font-size: 10px; | |
} | |
ul { | |
list-style: none; | |
} | |
h1, h2, h3, h4, h5 { | |
font-family: 'C059'; | |
font-weight: lighter; | |
} | |
h1, h2 { | |
margin: 0; | |
padding: 0 0 0.2cm 0; | |
text-align: center; | |
} | |
h1 { | |
font-size: 33px; | |
} | |
h2 { | |
padding-bottom: 1cm; | |
font-style: italic; | |
font-size: 20px; | |
color: #666; | |
} | |
h3 { | |
margin-top: 0.7cm; | |
font-size: 20px; | |
font-variant: small-caps; | |
color: #801026; | |
border-bottom: 1px solid #666; | |
} | |
h5 { | |
margin: 0; | |
padding: 0; | |
font-weight: bold !important; | |
font-size: 14px; | |
} | |
article { | |
break-inside: avoid; | |
} | |
#body { | |
column-count: 2; | |
} | |
#personal { | |
float: right; | |
width: 9cm; | |
padding: 0; | |
margin-bottom: 0.8cm; | |
background-color: #f5dd9d; | |
border: 1px solid #666; | |
} | |
#personal > li { | |
margin: 0.2cm 0.2cm 0.2cm 0.2cm; | |
display: flex; | |
flex-direction: row; | |
} | |
.personal_left { | |
padding: 0 0.4cm 0 0.2cm; | |
text-align: center; | |
} | |
.icon { | |
width: 0.4cm; | |
opacity: 0.7; | |
} | |
.simple_list, .detailed_list, .job_list { | |
padding: 0; | |
margin: 0; | |
} | |
.simple_list > li { | |
padding: 0; | |
margin-top: 0.2cm; | |
} | |
.detailed_list > li { | |
margin-top: 0.4cm; | |
display: flex; | |
flex-direction: row; | |
} | |
.list_left { | |
padding-right: 0.2cm; | |
display: block; | |
width: 2.3cm; | |
text-align: right; | |
} | |
.list_right { | |
display: block; | |
width: 7cm; | |
} | |
.list_right > span { | |
font-style: italic; | |
color: #666; | |
} | |
.job_separator { | |
text-align: center; | |
margin: 0.3cm 1.5cm 0.3cm 1.5cm; | |
border-top: 1px dotted #888; | |
} | |
.job_list > li { | |
padding: 0; | |
} | |
.job_date { | |
font-style: italic; | |
color: #666; | |
text-align: right; | |
} | |
.job_title { | |
padding: 0.2cm 0 0.2cm 0; | |
font-size: 16px; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html lang="{{ lang }}"> | |
<head> | |
<meta charset="utf-8"> | |
<title>{{ name }} – {{ subtitle }}</title> | |
</head> | |
<body> | |
<h1>{{ name }}</h1> | |
<h2>{{ subtitle }}</h2> | |
<ul id="personal"> | |
{% for item in infos %} | |
<li> | |
<div class="personal_left"> | |
<img class="icon" src="{{ fontawesome_root }}/svgs/{{ item.icon }}.svg"> | |
</div> | |
<div class="personal_right"> | |
{% with value = item.value|join('<br>') %} | |
{% if item.link %} | |
<a href="{{ item.link }}">{{ value }}</a> | |
{% else %} | |
{{ value }} | |
{% endif %} | |
{% endwith %} | |
</div> | |
</li> | |
{% endfor %} | |
</ul> | |
<div id="body"> | |
{% for section in sections %} | |
<article> | |
<h3>{{ section.title }}</h3> | |
<ul class="{{ section.ctn_type }}"> | |
{% for item in section.ctn %} | |
{% if section.ctn_type == 'simple_list' %} | |
<li>{{ item }}</li> | |
{% elif section.ctn_type == 'detailed_list' %} | |
<li> | |
<span class="list_left">{{ item.abstract }}</span> | |
<span class="list_right"> | |
{% if item.title %} | |
<h5>{{ item.title }}</h5> | |
{% endif %} | |
{% if item.subtitle %} | |
<span>{{ item.subtitle }}</span> | |
{% endif %} | |
{% if item.description %} | |
<p>{{ item.description }}</p> | |
{% endif %} | |
</span> | |
</li> | |
{% elif section.ctn_type == 'job_list' %} | |
<li> | |
{% if not loop.first %} | |
<div class="job_separator"></div> | |
{% endif %} | |
<div class="job_date">{{ item.date }}</div> | |
<div class="job_entity">{{ item.entity}}</div> | |
<h5 class="job_title">{{ item.title }}</h5> | |
<p>{{ item.description }}</p> | |
</li> | |
{% endif %} | |
{% endfor %} | |
</ul> | |
</article> | |
{% endfor %} | |
</div> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment