feat: initial commit
This commit is contained in:
commit
6bfef61ecc
31 changed files with 1864 additions and 0 deletions
0
src/cv_generator/__init__.py
Normal file
0
src/cv_generator/__init__.py
Normal file
382
src/cv_generator/cv.css
Normal file
382
src/cv_generator/cv.css
Normal file
|
|
@ -0,0 +1,382 @@
|
|||
@font-face {
|
||||
font-family: Fira Sans Condensed;
|
||||
font-weight: 400;
|
||||
src: url(fonts/FiraSansCondensed-Regular.ttf);
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Fira Sans Condensed;
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url(fonts/FiraSansCondensed-Italic.ttf);
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Fira Sans Condensed;
|
||||
font-weight: 300;
|
||||
src: url(fonts/FiraSansCondensed-Light.ttf);
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Fira Sans Condensed;
|
||||
font-weight: 700;
|
||||
src: url(fonts/FiraSansCondensed-Bold.ttf);
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Fira Sans Condensed;
|
||||
font-weight: 500;
|
||||
src: url(fonts/FiraSansCondensed-SemiBold.ttf);
|
||||
}
|
||||
|
||||
html {
|
||||
color: #393939;
|
||||
font-family: Fira Sans Condensed;
|
||||
font-size: 9pt;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
:root {
|
||||
--page-padding: 2rem;
|
||||
--sidebar-width: 50mm;
|
||||
--header-height: 35mm;
|
||||
--background-color: rgb(249, 249, 249);
|
||||
--background-color-highlight: rgb(242, 242, 242);
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@page letter {
|
||||
padding: 80mm 25mm 20mm 20mm;
|
||||
margin: 0;
|
||||
|
||||
@top-center {
|
||||
content: element(letter_top);
|
||||
}
|
||||
|
||||
@bottom-center {
|
||||
content: element(letter_bottom);
|
||||
}
|
||||
}
|
||||
|
||||
@page cv {
|
||||
background-color: var(--background-color);
|
||||
margin: 0 0 0 60mm;
|
||||
padding: var(--page-padding);
|
||||
|
||||
@left-top {
|
||||
content: element(side);
|
||||
}
|
||||
}
|
||||
|
||||
div#address {
|
||||
top: -50mm;
|
||||
font-size: 11pt;
|
||||
position: absolute;
|
||||
page: letter;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
div#address ul {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
footer {
|
||||
page: letter;
|
||||
position: running(letter_bottom);
|
||||
width: 210mm;
|
||||
height: 25mm;
|
||||
padding: 2mm 25mm 2mm 25mm;
|
||||
text-align: center;
|
||||
color: white;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(18, 78, 120, 1) 0%,
|
||||
rgba(14, 44, 69, 1) 100%
|
||||
);
|
||||
}
|
||||
|
||||
footer > a {
|
||||
display: inline-block;
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
footer > a.icon::before {
|
||||
margin-left: 4mm;
|
||||
display: inline-block;
|
||||
width: 8pt;
|
||||
height: 8pt;
|
||||
background-size: 8pt 8pt;
|
||||
margin-right: 1mm;
|
||||
background-repeat: no-repeat;
|
||||
content: "";
|
||||
margin-right: 2mm;
|
||||
}
|
||||
|
||||
div#subject {
|
||||
page: letter;
|
||||
position: absolute;
|
||||
font-weight: 500;
|
||||
top: -10mm;
|
||||
font-size: 11pt;
|
||||
}
|
||||
|
||||
div#date {
|
||||
page: letter;
|
||||
position: absolute;
|
||||
text-align: right;
|
||||
top: -20mm;
|
||||
width: 165mm;
|
||||
font-size: 11pt;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
article#letter {
|
||||
page: letter;
|
||||
break-before: page;
|
||||
text-align: justify;
|
||||
font-size: 10pt;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
div#letter_top {
|
||||
position: running(letter_top);
|
||||
width: 210mm;
|
||||
height: 10mm;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(18, 78, 120, 1) 0%,
|
||||
rgba(14, 44, 69, 1) 100%
|
||||
);
|
||||
}
|
||||
|
||||
div#appended {
|
||||
font-weight: 300;
|
||||
padding-top: 5mm;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
header {
|
||||
height: var(--header-height);
|
||||
page: cv;
|
||||
padding: 8mm 0 4mm 0;
|
||||
}
|
||||
|
||||
header #name {
|
||||
font-size: 40pt;
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
header #name #first_name {
|
||||
padding-right: 6pt;
|
||||
}
|
||||
|
||||
header #description {
|
||||
font-size: 11pt;
|
||||
}
|
||||
|
||||
aside {
|
||||
position: running(side);
|
||||
background: #124e78;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(18, 78, 120, 1) 0%,
|
||||
rgba(14, 44, 69, 1) 100%
|
||||
);
|
||||
width: var(--sidebar-width);
|
||||
height: 100%;
|
||||
color: white;
|
||||
padding: 5mm 5mm 5mm 5mm;
|
||||
}
|
||||
|
||||
aside #profile_image {
|
||||
border-radius: 50%;
|
||||
border: 1mm solid var(--background-color);
|
||||
width: 75%;
|
||||
display: block;
|
||||
margin: auto;
|
||||
margin-bottom: 8mm;
|
||||
}
|
||||
|
||||
aside #contact > a {
|
||||
display: block;
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
aside #contact > a::before {
|
||||
display: inline-block;
|
||||
width: 8pt;
|
||||
height: 8pt;
|
||||
background-size: 8pt 8pt;
|
||||
margin-right: 1mm;
|
||||
background-repeat: no-repeat;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.address_icon::before {
|
||||
background-image: url(icons/house-fill.svg);
|
||||
}
|
||||
|
||||
.phone_icon::before {
|
||||
background-image: url(icons/telephone-fill.svg);
|
||||
}
|
||||
|
||||
.mail_icon::before {
|
||||
background-image: url(icons/envelope-at-fill.svg);
|
||||
}
|
||||
|
||||
.github_icon::before {
|
||||
background-image: url(icons/github.svg);
|
||||
}
|
||||
|
||||
.linkedin_icon::before {
|
||||
background-image: url(icons/linkedin.svg);
|
||||
}
|
||||
|
||||
aside h1 {
|
||||
font-size: 13pt;
|
||||
padding: 2mm 0 0 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
aside ul {
|
||||
margin: 0;
|
||||
padding: 0 0 0 2mm;
|
||||
}
|
||||
|
||||
aside .skills a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
aside .skills ul {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
aside .skills li.skilllevel_4::before {
|
||||
content: "●●●◐";
|
||||
padding-right: 1mm;
|
||||
}
|
||||
|
||||
aside .skills li.skilllevel_3::before {
|
||||
content: "●●●○";
|
||||
padding-right: 1mm;
|
||||
}
|
||||
|
||||
aside .skills li.skilllevel_2::before {
|
||||
content: "●●○○";
|
||||
padding-right: 1mm;
|
||||
}
|
||||
|
||||
aside .skills li.skilllevel_1::before {
|
||||
content: "●○○○";
|
||||
padding-right: 1mm;
|
||||
}
|
||||
|
||||
aside .skills li.other {
|
||||
padding-top: 1mm;
|
||||
}
|
||||
|
||||
aside .skills li.certificate::before {
|
||||
padding-right: 1mm;
|
||||
background-image: url(icons/patch-check-fill.svg);
|
||||
display: inline-block;
|
||||
width: 8pt;
|
||||
height: 8pt;
|
||||
background-size: 8pt 8pt;
|
||||
margin-right: 1mm;
|
||||
background-repeat: no-repeat;
|
||||
content: "";
|
||||
}
|
||||
|
||||
aside .experience {
|
||||
margin-bottom: 4mm;
|
||||
}
|
||||
|
||||
aside .experience > .title {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
aside .experience > .time {
|
||||
text-align: right;
|
||||
font-size: 7pt;
|
||||
opacity: 0.5;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
aside .experience > .description {
|
||||
font-weight: 200;
|
||||
font-size: 7pt;
|
||||
}
|
||||
|
||||
.skills span.specification {
|
||||
display: block;
|
||||
font-weight: 200;
|
||||
opacity: 0.5;
|
||||
font-size: 7pt;
|
||||
}
|
||||
|
||||
.skills span.inline-specification {
|
||||
padding-left: 2mm;
|
||||
font-weight: 200;
|
||||
opacity: 0.5;
|
||||
font-size: 7pt;
|
||||
}
|
||||
|
||||
main section > h1 {
|
||||
padding: 5mm 0 0 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
main .experience {
|
||||
margin-bottom: 2mm;
|
||||
border-radius: 2mm;
|
||||
padding: 1.5mm;
|
||||
background-color: var(--background-color-highlight);
|
||||
padding-left: 30mm;
|
||||
}
|
||||
|
||||
main .experience .sub_experience {
|
||||
padding-top: 1mm;
|
||||
}
|
||||
|
||||
main .experience .sub_experience .title {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
main .experience .sub_experience .time {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
main .experience ul {
|
||||
margin: 0;
|
||||
padding: 0mm 2mm;
|
||||
}
|
||||
|
||||
main .experience .title {
|
||||
font-weight: bolder;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
main .experience .institution {
|
||||
display: inline-block;
|
||||
padding-left: 2mm;
|
||||
}
|
||||
|
||||
main .experience .time {
|
||||
position: absolute;
|
||||
left: 2mm;
|
||||
}
|
||||
|
||||
.signature {
|
||||
max-width: 33%;
|
||||
}
|
||||
196
src/cv_generator/cv.html.j2
Normal file
196
src/cv_generator/cv.html.j2
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>CV {{ cv.first_name }} {{ cv.last_name }}</title>
|
||||
<meta name="author" content="{{ cv.first_name }} {{ cv.last_name }}" />
|
||||
<meta name="generator" content="Weasyprint" />
|
||||
|
||||
<link rel="stylesheet" href="cv.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="letter_top"></div>
|
||||
|
||||
<div id="address">
|
||||
<ul>
|
||||
<li>{{ cv.letter.recipient.name }}</li>
|
||||
<li><strong>{{ cv.letter.recipient.company }}</strong></li>
|
||||
<li>{{ cv.letter.recipient.street }}</li>
|
||||
<li>{{ cv.letter.recipient.city }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div id="subject">
|
||||
{{ cv.letter.subject }}
|
||||
</div>
|
||||
<div id="date">{{ cv.letter.date }}</div>
|
||||
|
||||
<footer>
|
||||
<a>{{ cv.first_name }} {{ cv.last_name }}</a>
|
||||
<a class="icon address_icon">{{ cv.contact.address_full }}</a>
|
||||
<a href="tel:{{ cv.contact.phone_url }}" class="icon phone_icon">{{ cv.contact.phone }}</a>
|
||||
<a href="mailto:{{ cv.contact.email }}" class="icon mail_icon">{{ cv.contact.email }}</a>
|
||||
</footer>
|
||||
|
||||
<article id="letter">
|
||||
<p>{{ cv.letter.greeting }}</p>
|
||||
|
||||
{{ letter_body | safe }}
|
||||
|
||||
<p>{{ cv.letter.closing }}</p>
|
||||
|
||||
<img
|
||||
src="{{ cv.signature }}"
|
||||
class="signature"
|
||||
alt="{{ cv.first_name }} {{ cv.last_name }}"
|
||||
/>
|
||||
</article>
|
||||
|
||||
<div id="appended">
|
||||
Anbei: {{ cv.letter.appended }}
|
||||
</div>
|
||||
|
||||
<header>
|
||||
<div id="name">
|
||||
<span id="first_name">{{ cv.first_name }}</span
|
||||
><span id="last_name">{{ cv.last_name }}</span>
|
||||
</div>
|
||||
|
||||
<div id="description">
|
||||
{{ cv.description }}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<aside>
|
||||
<img src="{{ cv.photo }}" id="profile_image" />
|
||||
|
||||
<section id="contact">
|
||||
<a class="icon address_icon">{{ cv.contact.address }}</a>
|
||||
<a href="tel:{{ cv.contact.phone_url }}" class="icon phone_icon">{{ cv.contact.phone }}</a>
|
||||
<a href="mailto:{{ cv.contact.email }}" class="icon mail_icon">{{ cv.contact.email }}</a>
|
||||
<a href="{{ cv.contact.github_url }}" class="icon github_icon">{{ cv.contact.github }}</a>
|
||||
<a href="{{ cv.contact.linkedin_url }}" class="icon linkedin_icon">{{ cv.contact.linkedin }}</a>
|
||||
</section>
|
||||
|
||||
<section class="skills">
|
||||
<h1>Skills</h1>
|
||||
|
||||
<ul>
|
||||
{% for skill in cv.skills %}
|
||||
<li class="{{ skill.css_class }}">
|
||||
{{ skill.name }}
|
||||
{% if skill.specification %}
|
||||
<span class="specification">{{ skill.specification | replace('\n', '<br>') | safe }}</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% for cert in cv.certificates %}
|
||||
<li class="certificate">
|
||||
<a href="{{ cert.url }}">{{ cert.name }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% for other in cv.other_skills %}
|
||||
<li class="other">{{ other.text }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="skills">
|
||||
<h1>Sprachen</h1>
|
||||
|
||||
<ul>
|
||||
{% for lang in cv.languages %}
|
||||
<li class="{{ lang.css_class }}">
|
||||
{{ lang.name }}{% if lang.specification %}<span class="inline-specification">{{ lang.specification }}</span>{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h1>Sonstiges</h1>
|
||||
|
||||
{% for activity in cv.other_activities %}
|
||||
<div class="experience">
|
||||
<div class="time">{{ activity.time }}</div>
|
||||
<div class="title">{{ activity.title }}</div>
|
||||
{% if activity.institution %}
|
||||
<div class="institution">{{ activity.institution }}</div>
|
||||
{% endif %}
|
||||
{% if activity.description %}
|
||||
<div class="description">
|
||||
{{ activity.description }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
<main>
|
||||
<section id="experiences">
|
||||
<h1>Arbeitserfahrung</h1>
|
||||
|
||||
{% for exp in cv.work_experience %}
|
||||
<div class="experience">
|
||||
<div class="time">{{ exp.time }}</div>
|
||||
<div class="title">
|
||||
{{ exp.title }}
|
||||
</div>
|
||||
{% if exp.institution %}
|
||||
<div class="institution">{{ exp.institution }}</div>
|
||||
{% endif %}
|
||||
{% if exp.description %}
|
||||
<div class="description">
|
||||
{{ exp.description }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if exp.grades %}
|
||||
<div class="grades">{{ exp.grades }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% for sub in exp.sub_experiences %}
|
||||
<div class="sub_experience">
|
||||
<div class="time">{{ sub.time }}</div>
|
||||
<div class="title">
|
||||
{{ sub.title }}
|
||||
</div>
|
||||
{% if sub.bullets %}
|
||||
<ul>
|
||||
{% for bullet in sub.bullets %}
|
||||
<li>{{ bullet }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
|
||||
<section id="education">
|
||||
<h1>Ausbildung</h1>
|
||||
|
||||
{% for edu in cv.education %}
|
||||
<div class="experience">
|
||||
<div class="time">{{ edu.time }}</div>
|
||||
<div class="title">{{ edu.title }}</div>
|
||||
{% if edu.institution %}
|
||||
<div class="institution">{{ edu.institution }}</div>
|
||||
{% endif %}
|
||||
{% if edu.description %}
|
||||
<div class="description">
|
||||
{{ edu.description }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if edu.grades %}
|
||||
<div class="grades">{{ edu.grades }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
BIN
src/cv_generator/fonts/FiraSansCondensed-Bold.ttf
Normal file
BIN
src/cv_generator/fonts/FiraSansCondensed-Bold.ttf
Normal file
Binary file not shown.
BIN
src/cv_generator/fonts/FiraSansCondensed-Italic.ttf
Normal file
BIN
src/cv_generator/fonts/FiraSansCondensed-Italic.ttf
Normal file
Binary file not shown.
BIN
src/cv_generator/fonts/FiraSansCondensed-Light.ttf
Normal file
BIN
src/cv_generator/fonts/FiraSansCondensed-Light.ttf
Normal file
Binary file not shown.
BIN
src/cv_generator/fonts/FiraSansCondensed-Regular.ttf
Normal file
BIN
src/cv_generator/fonts/FiraSansCondensed-Regular.ttf
Normal file
Binary file not shown.
BIN
src/cv_generator/fonts/FiraSansCondensed-SemiBold.ttf
Normal file
BIN
src/cv_generator/fonts/FiraSansCondensed-SemiBold.ttf
Normal file
Binary file not shown.
32
src/cv_generator/generator.py
Normal file
32
src/cv_generator/generator.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
from pathlib import Path
|
||||
|
||||
import frontmatter # pyright: ignore[reportMissingTypeStubs]
|
||||
import markdown
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from weasyprint import HTML # pyright: ignore[reportMissingTypeStubs]
|
||||
|
||||
from .models import CVData
|
||||
|
||||
SRC_DIR = Path(__file__).resolve().parent
|
||||
|
||||
|
||||
def generate_pdf(input_file: Path, output_file: Path | None = None) -> Path:
|
||||
post = frontmatter.load(str(input_file))
|
||||
|
||||
cv = CVData.model_validate(post.metadata)
|
||||
letter_body: str = markdown.markdown(post.content)
|
||||
|
||||
input_dir = Path(input_file).resolve().parent
|
||||
cv.photo = str(input_dir / cv.photo)
|
||||
cv.signature = str(input_dir / cv.signature)
|
||||
|
||||
env = Environment(loader=FileSystemLoader(str(SRC_DIR)))
|
||||
template = env.get_template("cv.html.j2")
|
||||
|
||||
html_str = template.render(cv=cv, letter_body=letter_body)
|
||||
|
||||
if output_file is None:
|
||||
output_file = input_file.with_suffix(".pdf")
|
||||
|
||||
_ = HTML(string=html_str, base_url=str(SRC_DIR)).write_pdf(output_file) # pyright: ignore[reportUnknownMemberType]
|
||||
return output_file
|
||||
4
src/cv_generator/icons/envelope-at-fill.svg
Normal file
4
src/cv_generator/icons/envelope-at-fill.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="white" class="bi bi-envelope-at-fill" viewBox="0 0 16 16">
|
||||
<path d="M2 2A2 2 0 0 0 .05 3.555L8 8.414l7.95-4.859A2 2 0 0 0 14 2zm-2 9.8V4.698l5.803 3.546zm6.761-2.97-6.57 4.026A2 2 0 0 0 2 14h6.256A4.5 4.5 0 0 1 8 12.5a4.49 4.49 0 0 1 1.606-3.446l-.367-.225L8 9.586zM16 9.671V4.697l-5.803 3.546.338.208A4.5 4.5 0 0 1 12.5 8c1.414 0 2.675.652 3.5 1.671"/>
|
||||
<path d="M15.834 12.244c0 1.168-.577 2.025-1.587 2.025-.503 0-1.002-.228-1.12-.648h-.043c-.118.416-.543.643-1.015.643-.77 0-1.259-.542-1.259-1.434v-.529c0-.844.481-1.4 1.26-1.4.585 0 .87.333.953.63h.03v-.568h.905v2.19c0 .272.18.42.411.42.315 0 .639-.415.639-1.39v-.118c0-1.277-.95-2.326-2.484-2.326h-.04c-1.582 0-2.64 1.067-2.64 2.724v.157c0 1.867 1.237 2.654 2.57 2.654h.045c.507 0 .935-.07 1.18-.18v.731c-.219.1-.643.175-1.237.175h-.044C10.438 16 9 14.82 9 12.646v-.214C9 10.36 10.421 9 12.485 9h.035c2.12 0 3.314 1.43 3.314 3.034zm-4.04.21v.227c0 .586.227.8.581.8.31 0 .564-.17.564-.743v-.367c0-.516-.275-.708-.572-.708-.346 0-.573.245-.573.791"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
3
src/cv_generator/icons/github.svg
Normal file
3
src/cv_generator/icons/github.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="white" class="bi bi-github" viewBox="0 0 16 16">
|
||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 701 B |
4
src/cv_generator/icons/house-fill.svg
Normal file
4
src/cv_generator/icons/house-fill.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="white" class="bi bi-house-fill" viewBox="0 0 16 16">
|
||||
<path d="M8.707 1.5a1 1 0 0 0-1.414 0L.646 8.146a.5.5 0 0 0 .708.708L8 2.207l6.646 6.647a.5.5 0 0 0 .708-.708L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293z"/>
|
||||
<path d="m8 3.293 6 6V13.5a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 2 13.5V9.293z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 384 B |
3
src/cv_generator/icons/linkedin.svg
Normal file
3
src/cv_generator/icons/linkedin.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="white" class="bi bi-linkedin" viewBox="0 0 16 16">
|
||||
<path d="M0 1.146C0 .513.526 0 1.175 0h13.65C15.474 0 16 .513 16 1.146v13.708c0 .633-.526 1.146-1.175 1.146H1.175C.526 16 0 15.487 0 14.854zm4.943 12.248V6.169H2.542v7.225zm-1.2-8.212c.837 0 1.358-.554 1.358-1.248-.015-.709-.52-1.248-1.342-1.248S2.4 3.226 2.4 3.934c0 .694.521 1.248 1.327 1.248zm4.908 8.212V9.359c0-.216.016-.432.08-.586.173-.431.568-.878 1.232-.878.869 0 1.216.662 1.216 1.634v3.865h2.401V9.25c0-2.22-1.184-3.252-2.764-3.252-1.274 0-1.845.7-2.165 1.193v.025h-.016l.016-.025V6.169h-2.4c.03.678 0 7.225 0 7.225z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 659 B |
3
src/cv_generator/icons/patch-check-fill.svg
Normal file
3
src/cv_generator/icons/patch-check-fill.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="white" class="bi bi-patch-check-fill" viewBox="0 0 16 16">
|
||||
<path d="M10.067.87a2.89 2.89 0 0 0-4.134 0l-.622.638-.89-.011a2.89 2.89 0 0 0-2.924 2.924l.01.89-.636.622a2.89 2.89 0 0 0 0 4.134l.637.622-.011.89a2.89 2.89 0 0 0 2.924 2.924l.89-.01.622.636a2.89 2.89 0 0 0 4.134 0l.622-.637.89.011a2.89 2.89 0 0 0 2.924-2.924l-.01-.89.636-.622a2.89 2.89 0 0 0 0-4.134l-.637-.622.011-.89a2.89 2.89 0 0 0-2.924-2.924l-.89.01zm.287 5.984-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 1 1 .708-.708L7 8.793l2.646-2.647a.5.5 0 0 1 .708.708"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 601 B |
3
src/cv_generator/icons/telephone-fill.svg
Normal file
3
src/cv_generator/icons/telephone-fill.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="white" class="bi bi-telephone-fill" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M1.885.511a1.745 1.745 0 0 1 2.61.163L6.29 2.98c.329.423.445.974.315 1.494l-.547 2.19a.68.68 0 0 0 .178.643l2.457 2.457a.68.68 0 0 0 .644.178l2.189-.547a1.75 1.75 0 0 1 1.494.315l2.306 1.794c.829.645.905 1.87.163 2.611l-1.034 1.034c-.74.74-1.846 1.065-2.877.702a18.6 18.6 0 0 1-7.01-4.42 18.6 18.6 0 0 1-4.42-7.009c-.362-1.03-.037-2.137.703-2.877z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 514 B |
99
src/cv_generator/models.py
Normal file
99
src/cv_generator/models.py
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
from pydantic import BaseModel, computed_field
|
||||
|
||||
|
||||
class Recipient(BaseModel):
|
||||
name: str
|
||||
company: str
|
||||
street: str
|
||||
city: str
|
||||
|
||||
|
||||
class LetterMeta(BaseModel):
|
||||
recipient: Recipient
|
||||
subject: str
|
||||
date: str
|
||||
greeting: str
|
||||
closing: str
|
||||
appended: str
|
||||
|
||||
|
||||
class ContactInfo(BaseModel):
|
||||
address: str
|
||||
address_full: str
|
||||
phone: str
|
||||
phone_url: str
|
||||
email: str
|
||||
github: str
|
||||
github_url: str
|
||||
linkedin: str
|
||||
linkedin_url: str
|
||||
|
||||
|
||||
class Skill(BaseModel):
|
||||
name: str
|
||||
level: int
|
||||
specification: str | None = None
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def css_class(self) -> str:
|
||||
return f"skilllevel_{self.level}"
|
||||
|
||||
|
||||
class Certificate(BaseModel):
|
||||
name: str
|
||||
url: str
|
||||
|
||||
|
||||
class OtherSkill(BaseModel):
|
||||
text: str
|
||||
|
||||
|
||||
class Language(BaseModel):
|
||||
name: str
|
||||
level: int
|
||||
specification: str | None = None
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def css_class(self) -> str:
|
||||
return f"skilllevel_{self.level}"
|
||||
|
||||
|
||||
class SubExperience(BaseModel):
|
||||
time: str
|
||||
title: str
|
||||
bullets: list[str] = []
|
||||
|
||||
|
||||
class Experience(BaseModel):
|
||||
time: str
|
||||
title: str
|
||||
institution: str | None = None
|
||||
description: str | None = None
|
||||
grades: str | None = None
|
||||
sub_experiences: list[SubExperience] = []
|
||||
|
||||
|
||||
class OtherActivity(BaseModel):
|
||||
time: str
|
||||
title: str
|
||||
institution: str | None = None
|
||||
description: str | None = None
|
||||
|
||||
|
||||
class CVData(BaseModel):
|
||||
first_name: str
|
||||
last_name: str
|
||||
description: str
|
||||
photo: str
|
||||
signature: str
|
||||
contact: ContactInfo
|
||||
letter: LetterMeta
|
||||
skills: list[Skill]
|
||||
certificates: list[Certificate]
|
||||
other_skills: list[OtherSkill] = []
|
||||
languages: list[Language]
|
||||
work_experience: list[Experience]
|
||||
education: list[Experience]
|
||||
other_activities: list[OtherActivity]
|
||||
Loading…
Add table
Add a link
Reference in a new issue