offerprinter 0.3.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {offerprinter-0.3.0 → offerprinter-0.4.0}/CHANGELOG.md +38 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/PKG-INFO +71 -1
- {offerprinter-0.3.0 → offerprinter-0.4.0}/README.md +70 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/offerprinter/cli.py +130 -5
- {offerprinter-0.3.0 → offerprinter-0.4.0}/offerprinter/config.py +1 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/offerprinter/controllers/pipeline.py +6 -1
- {offerprinter-0.3.0 → offerprinter-0.4.0}/offerprinter/models/schemas.py +2 -0
- offerprinter-0.4.0/offerprinter/services/linkedin.py +267 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/offerprinter/services/pdf_writer.py +109 -95
- offerprinter-0.4.0/offerprinter/services/themes.py +232 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/offerprinter/services/writer.py +5 -4
- {offerprinter-0.3.0 → offerprinter-0.4.0}/pyproject.toml +1 -1
- offerprinter-0.4.0/tests/test_templates_and_linkedin.py +329 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/.gitignore +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/LICENSE +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/app.py +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/cli.py +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/config.example.toml +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/deploy/README.md +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/deploy/huggingface/README.md +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/docs/skill/SKILL.md +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/evals/README.md +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/examples/README.md +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/examples/output/northbank-senior-product-analyst/ats-keyword-report.docx +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/examples/output/northbank-senior-product-analyst/ats-keyword-report.md +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/examples/output/northbank-senior-product-analyst/cover-letter.docx +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/examples/output/northbank-senior-product-analyst/cover-letter.md +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/examples/output/northbank-senior-product-analyst/fit-memo.docx +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/examples/output/northbank-senior-product-analyst/fit-memo.md +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/examples/output/northbank-senior-product-analyst/full-package.docx +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/examples/output/northbank-senior-product-analyst/full-package.md +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/examples/output/northbank-senior-product-analyst/interview-prep-pack.docx +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/examples/output/northbank-senior-product-analyst/interview-prep-pack.md +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/examples/output/northbank-senior-product-analyst/tailored-cv.docx +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/examples/output/northbank-senior-product-analyst/tailored-cv.md +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/examples/sample_cv.md +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/examples/sample_jd.md +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/extension/README.md +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/offerprinter/__init__.py +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/offerprinter/controllers/__init__.py +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/offerprinter/llm/__init__.py +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/offerprinter/llm/anthropic_provider.py +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/offerprinter/llm/base.py +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/offerprinter/llm/factory.py +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/offerprinter/llm/gemini_provider.py +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/offerprinter/llm/kimi_provider.py +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/offerprinter/llm/ollama_provider.py +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/offerprinter/llm/openai_provider.py +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/offerprinter/mcp_server.py +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/offerprinter/models/__init__.py +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/offerprinter/pricing.py +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/offerprinter/prompts/__init__.py +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/offerprinter/prompts/templates.py +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/offerprinter/services/__init__.py +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/offerprinter/services/cache.py +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/offerprinter/services/cv_parser.py +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/offerprinter/services/differ.py +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/offerprinter/services/generator.py +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/offerprinter/services/jd_fetcher.py +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/offerprinter/services/ranker.py +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/offerprinter/services/redactor.py +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/offerprinter/services/tracker.py +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/offerprinter/services/verifier.py +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/offerprinter/ui/__init__.py +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/offerprinter/ui/printer.py +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/tests/__init__.py +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/tests/conftest.py +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/tests/test_new_services.py +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/tests/test_offerprinter.py +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/tests/test_pdf_writer.py +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/tests/test_pricing_and_retries.py +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/tests/test_tracker.py +0 -0
- {offerprinter-0.3.0 → offerprinter-0.4.0}/tests/test_verifier.py +0 -0
|
@@ -7,6 +7,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] — 2026-08-25
|
|
11
|
+
|
|
12
|
+
The "it has to look like something" release. v0.3.0 made the output provable;
|
|
13
|
+
this one makes it presentable, and makes getting your history in a two-minute job
|
|
14
|
+
rather than an afternoon.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **Six PDF templates**: `classic`, `compact`, `serif`, `rule`, `accent`, `mono`.
|
|
19
|
+
Choose with `--template`, list with `offerprinter templates`, and see them with
|
|
20
|
+
`offerprinter templates --sample ./samples`. Set a default under `[output]` in
|
|
21
|
+
`config.toml`, or `OFFERPRINTER_TEMPLATE`.
|
|
22
|
+
- **LinkedIn import** (`--linkedin`). Takes either export LinkedIn offers: the PDF
|
|
23
|
+
from *More → Save to PDF*, or the CSV archive from *Get a copy of your data*.
|
|
24
|
+
Nothing is scraped and nothing is fetched — you export your own data and this
|
|
25
|
+
reads the file.
|
|
26
|
+
|
|
27
|
+
### Notes on the templates
|
|
28
|
+
|
|
29
|
+
Every template stays inside what an ATS can parse: one of the 14 PDF base fonts,
|
|
30
|
+
no columns, no tables, no images, no text boxes. What varies is typeface, spacing,
|
|
31
|
+
rules, capitalisation and one accent colour. The test suite checks, per template,
|
|
32
|
+
that the PDF embeds no font program, contains no `/Image` or `/XObject`, and that
|
|
33
|
+
a naive text extraction still returns the name, the email and every bullet.
|
|
34
|
+
|
|
35
|
+
`accent` is labelled *email only*: it extracts cleanly, but carries enough colour
|
|
36
|
+
that a strict parser could lose a heading.
|
|
37
|
+
|
|
38
|
+
The LinkedIn importer is held to the no-fabrication guarantee too. A test takes
|
|
39
|
+
every word of its output and asserts it appeared in the input, because an importer
|
|
40
|
+
that quietly tidied up your history would break the guarantee before the generator
|
|
41
|
+
ever ran.
|
|
42
|
+
|
|
43
|
+
### Unchanged
|
|
44
|
+
|
|
45
|
+
`classic` renders byte-for-byte what v0.3.0 rendered. There is a test for that, so
|
|
46
|
+
upgrading changes nothing unless you ask it to.
|
|
47
|
+
|
|
10
48
|
## [0.3.0] — 2026-08-20
|
|
11
49
|
|
|
12
50
|
The "prove it" release. v0.2.0 made OfferPrinter installable; this one makes its
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: offerprinter
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Free, local-first AI job application generator: one CV + one job description, and it prints a tailored CV, cover letter, fit memo, ATS keyword report and interview prep pack — without fabricating anything.
|
|
5
5
|
Project-URL: Homepage, https://github.com/mohitagw15856/OfferPrinter
|
|
6
6
|
Project-URL: Repository, https://github.com/mohitagw15856/OfferPrinter
|
|
@@ -177,6 +177,76 @@ have hand-edited it, say — and makes no API calls at all, so it is free.
|
|
|
177
177
|
|
|
178
178
|
---
|
|
179
179
|
|
|
180
|
+
## I have not updated my CV in three years
|
|
181
|
+
|
|
182
|
+
Neither has anyone. Your LinkedIn is current though, and LinkedIn will give it
|
|
183
|
+
back to you:
|
|
184
|
+
|
|
185
|
+
```sh
|
|
186
|
+
# More > Save to PDF on your own profile
|
|
187
|
+
offerprinter --linkedin ~/Downloads/Profile.pdf --jd https://…
|
|
188
|
+
|
|
189
|
+
# or the full archive: Settings > Data privacy > Get a copy of your data
|
|
190
|
+
offerprinter --linkedin ~/Downloads/Basic_LinkedInDataExport.zip --jd https://…
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Both shapes are normalised into the Markdown the rest of the pipeline expects.
|
|
194
|
+
Nothing is scraped — you export your own data from your own account and this reads
|
|
195
|
+
the file.
|
|
196
|
+
|
|
197
|
+
The importer holds to the same rule as the generator: **it invents nothing.** A
|
|
198
|
+
role with no description gets no description; an empty section stays empty. There
|
|
199
|
+
is a test that takes every word of the output and asserts it appeared in the
|
|
200
|
+
input, because an importer that quietly "tidied up" your history would break the
|
|
201
|
+
no-fabrication guarantee before the generator ever ran.
|
|
202
|
+
|
|
203
|
+
## Can I make it look like something other than every other CV?
|
|
204
|
+
|
|
205
|
+
Six templates, all of which still parse:
|
|
206
|
+
|
|
207
|
+
```sh
|
|
208
|
+
offerprinter templates # what they are
|
|
209
|
+
offerprinter templates --sample ./samples # one PDF of each, to look at
|
|
210
|
+
offerprinter --cv cv.pdf --jd URL --template serif
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
| Template | Typeface | Portal-safe | For |
|
|
214
|
+
|---|---|---|---|
|
|
215
|
+
| `classic` | Helvetica | yes | the default; nothing to go wrong |
|
|
216
|
+
| `compact` | Helvetica | yes | when two pages need to be one |
|
|
217
|
+
| `serif` | Times | yes | law, academia, policy |
|
|
218
|
+
| `rule` | Helvetica | yes | a hairline under each section, for skim-readers |
|
|
219
|
+
| `accent` | Helvetica | email only | one colour, headings only |
|
|
220
|
+
| `mono` | Courier | yes | reads as engineering |
|
|
221
|
+
|
|
222
|
+
**Why only six, and why so restrained.** A CV is read twice: once by a parser that
|
|
223
|
+
wants plain text in a standard font, and once by a person with forty tabs open. The
|
|
224
|
+
compromise most builders make is to serve the human and quietly fail the machine —
|
|
225
|
+
two columns, an icon font, a text box for the sidebar, and an ATS that returns your
|
|
226
|
+
name and nothing else.
|
|
227
|
+
|
|
228
|
+
So every template here stays on the safe side of that line. All of them use one of
|
|
229
|
+
the 14 PDF base fonts, so nothing is embedded and the text always extracts; none
|
|
230
|
+
use columns, tables, images or text boxes. What varies is typeface, spacing, rules,
|
|
231
|
+
capitalisation and one accent colour — the parts a human notices and a parser
|
|
232
|
+
ignores.
|
|
233
|
+
|
|
234
|
+
`accent` is labelled *email only* rather than being left off the list: it extracts
|
|
235
|
+
cleanly, but carries enough colour that a strict parser could lose a heading. Send
|
|
236
|
+
that one to a person, not a portal.
|
|
237
|
+
|
|
238
|
+
The test suite asserts, for every template, that the rendered PDF contains no
|
|
239
|
+
embedded font program, no `/Image` or `/XObject`, and that the name, email and
|
|
240
|
+
every bullet come back out of a naive text extraction. That is the claim, and it
|
|
241
|
+
is checked rather than asserted.
|
|
242
|
+
|
|
243
|
+
Set a default in `config.toml`:
|
|
244
|
+
|
|
245
|
+
```toml
|
|
246
|
+
[output]
|
|
247
|
+
template = "rule"
|
|
248
|
+
```
|
|
249
|
+
|
|
180
250
|
## How do I install OfferPrinter?
|
|
181
251
|
|
|
182
252
|
<!-- AEO Answer Capsule — 57 words -->
|
|
@@ -130,6 +130,76 @@ have hand-edited it, say — and makes no API calls at all, so it is free.
|
|
|
130
130
|
|
|
131
131
|
---
|
|
132
132
|
|
|
133
|
+
## I have not updated my CV in three years
|
|
134
|
+
|
|
135
|
+
Neither has anyone. Your LinkedIn is current though, and LinkedIn will give it
|
|
136
|
+
back to you:
|
|
137
|
+
|
|
138
|
+
```sh
|
|
139
|
+
# More > Save to PDF on your own profile
|
|
140
|
+
offerprinter --linkedin ~/Downloads/Profile.pdf --jd https://…
|
|
141
|
+
|
|
142
|
+
# or the full archive: Settings > Data privacy > Get a copy of your data
|
|
143
|
+
offerprinter --linkedin ~/Downloads/Basic_LinkedInDataExport.zip --jd https://…
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Both shapes are normalised into the Markdown the rest of the pipeline expects.
|
|
147
|
+
Nothing is scraped — you export your own data from your own account and this reads
|
|
148
|
+
the file.
|
|
149
|
+
|
|
150
|
+
The importer holds to the same rule as the generator: **it invents nothing.** A
|
|
151
|
+
role with no description gets no description; an empty section stays empty. There
|
|
152
|
+
is a test that takes every word of the output and asserts it appeared in the
|
|
153
|
+
input, because an importer that quietly "tidied up" your history would break the
|
|
154
|
+
no-fabrication guarantee before the generator ever ran.
|
|
155
|
+
|
|
156
|
+
## Can I make it look like something other than every other CV?
|
|
157
|
+
|
|
158
|
+
Six templates, all of which still parse:
|
|
159
|
+
|
|
160
|
+
```sh
|
|
161
|
+
offerprinter templates # what they are
|
|
162
|
+
offerprinter templates --sample ./samples # one PDF of each, to look at
|
|
163
|
+
offerprinter --cv cv.pdf --jd URL --template serif
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
| Template | Typeface | Portal-safe | For |
|
|
167
|
+
|---|---|---|---|
|
|
168
|
+
| `classic` | Helvetica | yes | the default; nothing to go wrong |
|
|
169
|
+
| `compact` | Helvetica | yes | when two pages need to be one |
|
|
170
|
+
| `serif` | Times | yes | law, academia, policy |
|
|
171
|
+
| `rule` | Helvetica | yes | a hairline under each section, for skim-readers |
|
|
172
|
+
| `accent` | Helvetica | email only | one colour, headings only |
|
|
173
|
+
| `mono` | Courier | yes | reads as engineering |
|
|
174
|
+
|
|
175
|
+
**Why only six, and why so restrained.** A CV is read twice: once by a parser that
|
|
176
|
+
wants plain text in a standard font, and once by a person with forty tabs open. The
|
|
177
|
+
compromise most builders make is to serve the human and quietly fail the machine —
|
|
178
|
+
two columns, an icon font, a text box for the sidebar, and an ATS that returns your
|
|
179
|
+
name and nothing else.
|
|
180
|
+
|
|
181
|
+
So every template here stays on the safe side of that line. All of them use one of
|
|
182
|
+
the 14 PDF base fonts, so nothing is embedded and the text always extracts; none
|
|
183
|
+
use columns, tables, images or text boxes. What varies is typeface, spacing, rules,
|
|
184
|
+
capitalisation and one accent colour — the parts a human notices and a parser
|
|
185
|
+
ignores.
|
|
186
|
+
|
|
187
|
+
`accent` is labelled *email only* rather than being left off the list: it extracts
|
|
188
|
+
cleanly, but carries enough colour that a strict parser could lose a heading. Send
|
|
189
|
+
that one to a person, not a portal.
|
|
190
|
+
|
|
191
|
+
The test suite asserts, for every template, that the rendered PDF contains no
|
|
192
|
+
embedded font program, no `/Image` or `/XObject`, and that the name, email and
|
|
193
|
+
every bullet come back out of a naive text extraction. That is the claim, and it
|
|
194
|
+
is checked rather than asserted.
|
|
195
|
+
|
|
196
|
+
Set a default in `config.toml`:
|
|
197
|
+
|
|
198
|
+
```toml
|
|
199
|
+
[output]
|
|
200
|
+
template = "rule"
|
|
201
|
+
```
|
|
202
|
+
|
|
133
203
|
## How do I install OfferPrinter?
|
|
134
204
|
|
|
135
205
|
<!-- AEO Answer Capsule — 57 words -->
|
|
@@ -30,6 +30,7 @@ from __future__ import annotations
|
|
|
30
30
|
from pathlib import Path
|
|
31
31
|
|
|
32
32
|
import typer
|
|
33
|
+
from rich import box
|
|
33
34
|
from rich.console import Console
|
|
34
35
|
from rich.panel import Panel
|
|
35
36
|
from rich.table import Table
|
|
@@ -80,7 +81,22 @@ def _version_cb(value: bool) -> None:
|
|
|
80
81
|
raise typer.Exit()
|
|
81
82
|
|
|
82
83
|
|
|
83
|
-
def _load_cv(
|
|
84
|
+
def _load_cv(
|
|
85
|
+
cv: Path | None, cv_text: str | None, linkedin: Path | None = None
|
|
86
|
+
) -> ResumeInput:
|
|
87
|
+
if linkedin:
|
|
88
|
+
from offerprinter.services.linkedin import import_profile
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
markdown = import_profile(linkedin)
|
|
92
|
+
except Exception as exc: # noqa: BLE001
|
|
93
|
+
console.print(f"[red]Could not read that LinkedIn export:[/red] {exc}")
|
|
94
|
+
raise typer.Exit(code=1) from exc
|
|
95
|
+
console.print(
|
|
96
|
+
f"[grey62]Imported {len(markdown):,} characters from {linkedin.name}. "
|
|
97
|
+
"Nothing was added that was not in the export.[/grey62]"
|
|
98
|
+
)
|
|
99
|
+
return cv_from_text(markdown)
|
|
84
100
|
try:
|
|
85
101
|
return cv_from_text(cv_text) if cv_text else extract_cv(cv) # type: ignore[arg-type]
|
|
86
102
|
except Exception as exc: # noqa: BLE001
|
|
@@ -121,6 +137,7 @@ def _apply_overrides(
|
|
|
121
137
|
redact: bool = False,
|
|
122
138
|
no_verify: bool = False,
|
|
123
139
|
no_cache: bool = False,
|
|
140
|
+
template: str | None = None,
|
|
124
141
|
) -> None:
|
|
125
142
|
if provider:
|
|
126
143
|
config.llm.provider = provider
|
|
@@ -135,6 +152,14 @@ def _apply_overrides(
|
|
|
135
152
|
config.output.formats = [
|
|
136
153
|
f.strip().lower().lstrip(".") for f in formats.split(",") if f.strip()
|
|
137
154
|
]
|
|
155
|
+
if template:
|
|
156
|
+
from offerprinter.services.themes import get_theme
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
config.output.template = get_theme(template).name
|
|
160
|
+
except KeyError as exc:
|
|
161
|
+
console.print(f"[red]{exc.args[0]}[/red]")
|
|
162
|
+
raise typer.Exit(code=2) from exc
|
|
138
163
|
if no_track:
|
|
139
164
|
config.output.track = False
|
|
140
165
|
if sequential:
|
|
@@ -299,6 +324,14 @@ def run(
|
|
|
299
324
|
cv_text: str | None = typer.Option(
|
|
300
325
|
None, "--cv-text", help="Paste your CV as raw text instead of a file."
|
|
301
326
|
),
|
|
327
|
+
linkedin: Path | None = typer.Option(
|
|
328
|
+
None,
|
|
329
|
+
"--linkedin",
|
|
330
|
+
help=(
|
|
331
|
+
"A LinkedIn export instead of a CV: the .pdf from More > Save to PDF, "
|
|
332
|
+
"or the .zip from Get a copy of your data."
|
|
333
|
+
),
|
|
334
|
+
),
|
|
302
335
|
jd: str | None = typer.Option(
|
|
303
336
|
None,
|
|
304
337
|
"--jd",
|
|
@@ -327,6 +360,11 @@ def run(
|
|
|
327
360
|
output_dir: Path | None = typer.Option(
|
|
328
361
|
None, "--output-dir", "-o", help="Where to write the package (default ./output)."
|
|
329
362
|
),
|
|
363
|
+
template: str | None = typer.Option(
|
|
364
|
+
None,
|
|
365
|
+
"--template",
|
|
366
|
+
help="PDF template: classic, compact, serif, rule, accent, mono. See `offerprinter templates`.",
|
|
367
|
+
),
|
|
330
368
|
formats: str | None = typer.Option(
|
|
331
369
|
None, "--formats", help="Comma-separated output formats: md, docx, pdf."
|
|
332
370
|
),
|
|
@@ -373,17 +411,22 @@ def run(
|
|
|
373
411
|
return
|
|
374
412
|
|
|
375
413
|
# ---- resolve inputs ---------------------------------------------------
|
|
376
|
-
if not cv and not cv_text:
|
|
377
|
-
console.print(
|
|
414
|
+
if not cv and not cv_text and not linkedin:
|
|
415
|
+
console.print(
|
|
416
|
+
"[red]Error:[/red] provide a CV with --cv or --cv-text, "
|
|
417
|
+
"or a LinkedIn export with --linkedin."
|
|
418
|
+
)
|
|
378
419
|
raise typer.Exit(code=2)
|
|
379
420
|
if not jd and not jd_file and not jd_dir:
|
|
380
421
|
console.print("[red]Error:[/red] provide a job with --jd, --jd-file, or --jd-dir.")
|
|
381
422
|
raise typer.Exit(code=2)
|
|
382
423
|
|
|
383
424
|
config = load_config(config_file)
|
|
384
|
-
_apply_overrides(
|
|
425
|
+
_apply_overrides(
|
|
426
|
+
config, provider, model, locale, output_dir, formats, no_track, sequential, template=template
|
|
427
|
+
)
|
|
385
428
|
|
|
386
|
-
resume = _load_cv(cv, cv_text)
|
|
429
|
+
resume = _load_cv(cv, cv_text, linkedin)
|
|
387
430
|
|
|
388
431
|
# ---- batch mode -------------------------------------------------------
|
|
389
432
|
if jd_dir:
|
|
@@ -899,6 +942,88 @@ def practice_command(
|
|
|
899
942
|
console.print(f"[grey62]Saved to {path}[/grey62]")
|
|
900
943
|
|
|
901
944
|
|
|
945
|
+
@app.command("templates")
|
|
946
|
+
def templates_command(
|
|
947
|
+
sample: Path | None = typer.Option(
|
|
948
|
+
None, "--sample", help="Render one PDF per template into this folder."
|
|
949
|
+
),
|
|
950
|
+
) -> None:
|
|
951
|
+
"""List the PDF templates, and optionally print a sample of each.
|
|
952
|
+
|
|
953
|
+
Every one of them stays inside what an ATS can parse -- base-14 fonts, no
|
|
954
|
+
columns, no tables, no images. What varies is typeface, spacing, rules and
|
|
955
|
+
one accent colour, which is the part a human notices and a parser ignores.
|
|
956
|
+
"""
|
|
957
|
+
from offerprinter.services.pdf_writer import write_pdf
|
|
958
|
+
from offerprinter.services.themes import THEMES
|
|
959
|
+
|
|
960
|
+
table = Table(box=box.SIMPLE, show_header=True, header_style="bold")
|
|
961
|
+
table.add_column("Template")
|
|
962
|
+
table.add_column("Typeface")
|
|
963
|
+
table.add_column("Portal-safe")
|
|
964
|
+
table.add_column("What it is for")
|
|
965
|
+
for theme in THEMES.values():
|
|
966
|
+
table.add_row(
|
|
967
|
+
theme.name,
|
|
968
|
+
theme.typeface.regular,
|
|
969
|
+
"[green]yes[/green]" if theme.ats else "[yellow]email only[/yellow]",
|
|
970
|
+
theme.description,
|
|
971
|
+
)
|
|
972
|
+
console.print()
|
|
973
|
+
console.print(table)
|
|
974
|
+
console.print(
|
|
975
|
+
"[grey62]'email only' still extracts cleanly; it just carries enough colour that a "
|
|
976
|
+
"strict parser could lose a heading. Send it to a person, not a portal.[/grey62]"
|
|
977
|
+
)
|
|
978
|
+
|
|
979
|
+
if sample:
|
|
980
|
+
sample.mkdir(parents=True, exist_ok=True)
|
|
981
|
+
for theme in THEMES.values():
|
|
982
|
+
write_pdf(_SAMPLE_CV, sample / f"sample-{theme.name}.pdf", theme)
|
|
983
|
+
console.print(f"\n[green]Wrote {len(THEMES)} samples to[/green] {sample}")
|
|
984
|
+
else:
|
|
985
|
+
console.print("[grey62]offerprinter templates --sample ./samples prints one of each.[/grey62]")
|
|
986
|
+
console.print()
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
#: Used only by `templates --sample`. Invented person, so nothing real is written.
|
|
990
|
+
_SAMPLE_CV = """# Ada Lovelace
|
|
991
|
+
|
|
992
|
+
Analytical Engines - London - ada@example.com - linkedin.com/in/example
|
|
993
|
+
|
|
994
|
+
## Summary
|
|
995
|
+
|
|
996
|
+
Engineer with ten years turning mechanical specifications into working programs.
|
|
997
|
+
Published the first algorithm intended for a machine.
|
|
998
|
+
|
|
999
|
+
## Experience
|
|
1000
|
+
|
|
1001
|
+
### Principal Engineer - Difference Engine Co
|
|
1002
|
+
*Jan 2019 - Present*
|
|
1003
|
+
|
|
1004
|
+
- Shipped the **first published algorithm**, six weeks ahead of schedule
|
|
1005
|
+
- Cut crank turns per calculation by 40%, which halved operator fatigue
|
|
1006
|
+
- Wrote the notes that three generations of engineers learned the machine from
|
|
1007
|
+
|
|
1008
|
+
### Analyst - Royal Society
|
|
1009
|
+
*2016 - 2019*
|
|
1010
|
+
|
|
1011
|
+
- Translated Menabrea's memoir and added notes three times its length
|
|
1012
|
+
|
|
1013
|
+
## Education
|
|
1014
|
+
|
|
1015
|
+
- University of London, Mathematics
|
|
1016
|
+
|
|
1017
|
+
## Skills
|
|
1018
|
+
|
|
1019
|
+
Analytical engines, algorithm design, technical writing, translation
|
|
1020
|
+
|
|
1021
|
+
---
|
|
1022
|
+
|
|
1023
|
+
> Every line above came from a source document. Nothing was invented.
|
|
1024
|
+
"""
|
|
1025
|
+
|
|
1026
|
+
|
|
902
1027
|
@app.command("verify")
|
|
903
1028
|
def verify_command(
|
|
904
1029
|
package_dir: Path = typer.Argument(..., help="An output/<company>-<role> folder to check."),
|
|
@@ -162,6 +162,7 @@ def load_config(config_path: str | os.PathLike[str] | None = None) -> Config:
|
|
|
162
162
|
),
|
|
163
163
|
track=_env_bool("OFFERPRINTER_TRACK", bool(out_raw.get("track", True))),
|
|
164
164
|
redact=_env_bool("OFFERPRINTER_REDACT", bool(out_raw.get("redact", False))),
|
|
165
|
+
template=_first_env("OFFERPRINTER_TEMPLATE") or out_raw.get("template", "classic"),
|
|
165
166
|
)
|
|
166
167
|
|
|
167
168
|
# ---- generation --------------------------------------------------------
|
|
@@ -115,7 +115,12 @@ class Pipeline:
|
|
|
115
115
|
package.usage = Usage(**self.provider.usage.model_dump())
|
|
116
116
|
|
|
117
117
|
# 7. Write to disk.
|
|
118
|
-
written = write_package(
|
|
118
|
+
written = write_package(
|
|
119
|
+
package,
|
|
120
|
+
self.config.output.dir,
|
|
121
|
+
self.config.output.formats,
|
|
122
|
+
self.config.output.template,
|
|
123
|
+
)
|
|
119
124
|
out_dir = Path(self.config.output.dir) / slug
|
|
120
125
|
yield PipelineEvent(
|
|
121
126
|
kind="written",
|
|
@@ -53,6 +53,8 @@ class OutputConfig(BaseModel):
|
|
|
53
53
|
track: bool = True
|
|
54
54
|
#: Strip name, email, phone and links before the provider ever sees the CV.
|
|
55
55
|
redact: bool = False
|
|
56
|
+
#: PDF template name. See `offerprinter templates`.
|
|
57
|
+
template: str = "classic"
|
|
56
58
|
|
|
57
59
|
|
|
58
60
|
class GenerationConfig(BaseModel):
|