Skip to content

Instantly share code, notes, and snippets.

@breard-r
Last active March 7, 2023 22:55
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save breard-r/82263814d65b6c7bb12a5e95000cffb9 to your computer and use it in GitHub Desktop.
Save breard-r/82263814d65b6c7bb12a5e95000cffb9 to your computer and use it in GitHub Desktop.
Curriculum vitæ - Python/Jinja2/WeasyPrint
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
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',
],
},
],
}
#!/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'])
/*
* 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;
}
<!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