Skip to main content

Profile-driven CV renderer: one YAML source of truth, tag-filtered tailored profiles, HTML/PDF export, content lint

Project description

CV Renderer

One YAML file. Multiple tailored CVs. One command per application.

PyPI CI License Python uv Jinja2 Playwright

All your CV content lives once in a YAML file. A profile defines what to show and what to emphasize for each application. The render engine produces a clean HTML and optionally a print-ready PDF — one command per role.

Install

pip install 'cv-renderer[pdf]'      # or: uv tool install 'cv-renderer[pdf]'

# One-time: download the Chromium binary for PDF export
playwright install chromium

# Bootstrap your personal data directory (./user-data)
cv-renderer init

The [pdf] extra pulls Playwright; skip it if you only need HTML output.

Working from a clone instead? uv sync --dev, then every command below runs as uv run cv-renderer ... (the old uv run python render.py ... still works too).

Then fill in user-data/data/base_en.yaml with your own content.

Data directory: commands look for ./user-data in the current directory by default. Keep your data elsewhere by setting CV_DATA_DIR (as an env var or in a .env file next to where you run the command).

Usage

# Render to HTML
cv-renderer --profile ai-engineer

# Render and export to PDF
cv-renderer --profile ai-engineer --export pdf

# Render in Turkish
cv-renderer --profile ai-engineer --lang tr

# List available profiles
cv-renderer --list

# Validate your CV data before rendering — tag typos, EN/TR parity,
# wording issues, broken profile references
cv-renderer --lint
cv-renderer --lint --profile ai-engineer

# Machine-readable lint output for scripts and agents
cv-renderer --lint --profile ai-engineer --format json

--lint exits with 0 (clean), 1 (errors) or 2 (warnings only), so scripts can branch on the result.

Output lands in <data dir>/out/ — override per run with the CV_OUT_DIR env var.

How It Works

Single source of truthuser-data/data/base_en.yaml holds all your CV content, with every bullet tagged:

bullets:
  - text: "Shipped multi-agent HR analytics features using LangGraph..."
    tags: [ai, llm, backend]
  - text: "Modeled 56 mart tables through a 109-model dbt Core layer..."
    tags: [data]

Profiles control what surfaces — user-data/profiles/ai-engineer.yaml:

focus_tags: [ai, llm, ml]
deprioritize_tags: [data]
max_bullets_per_job: 4

The render engine merges the two, feeds the result into a Jinja2 template, and produces the output. The visual design never changes — only the content does.

A profile can also override the headline title and summary paragraph without touching the base data file — useful for a one-off application pitch that doesn't belong in the single source of truth:

title_override: Veri Mühendisi
about_override: |
  A summary written for this specific application.

When omitted, the title/summary fall back to the variant lookup in base_<lang>.yaml as usual.

Tag filtering can only pick among bullets that already exist, and can only include or exclude a whole job/project by its tags — it can't reframe a real accomplishment around a different angle, and it can't partially include something whose tags don't match. experience_overrides / project_overrides replace one entry's bullets outright, keyed by company / project name:

project_overrides:
  Agentic RAG:
    - "Isolated SQL access behind an MCP subprocess so agent code never touches database credentials directly."

An override also forces that job/project into the CV even if its tags would normally get it filtered out entirely — useful when a project's primary tag is ai but one real piece of it (e.g. a security boundary around a database) is genuinely relevant to a data-focused application. If nothing in an entry is honestly relevant, skip the override and let tag filtering drop it as usual — don't force it in.

project_order reorders the rendered projects by name, the same way skill_categories reorders skill categories — useful when the most relevant project for an application isn't the first one in the base data:

project_order: [Analytics Copilot, Agentic RAG]

skill_categories can only select and reorder categories that already exist in the base data — it can't reshape them. skill_overrides regroups skill items into new, per-application category labels (e.g. splitting one base category into two), pulling item text from anywhere in the base data's skills regardless of original category, without touching the base file — so general and every other profile still see the base data's original grouping:

skill_overrides:
  Data Warehousing: [Data Modeling, ETL/ELT, Data Profiling]
  Data Tools: [dbt Core, PostgreSQL, Apache Superset]

When set, skill_overrides replaces the skills section entirely — it ignores skill_categories and tag-based filtering.

Tag System

The tag vocabulary lives in user-data/tags.yaml (falls back to the packaged example vocabulary if you haven't created one), not hardcoded anywhere — cv-renderer --lint validates every tag in your data and profiles against it:

Tag Covers
ai LLM systems, agentic workflows, RAG, evaluation
llm Prompt engineering, tool use, LLM-as-judge
ml Training, distillation, PyTorch, model deployment
data dbt, SQL, warehouse, BI dashboards, ETL/ELT
backend FastAPI, API design, Redis
devops Docker, CI/CD, OTEL
always Appears in every profile regardless of focus

Add a legitimate new tag by adding one line to user-data/tags.yaml — no code change needed, and --lint still catches genuine typos.

Tags are the only interface between data and profiles. A bullet that covers both FastAPI and PostgreSQL gets [backend, data], not one or the other.

Skill categories can be filtered the same way at two levels — which categories show (skill_categories in the profile, or the category's own tags), and which items inside a shown category show:

- category: AI and LLM Systems
  tags: [ai, llm]
  items:
    - text: LangChain
      tags: [ai]
    - text: Prompt Engineering
      tags: [ai, llm]

An item with no tags is generic and always shown once its category is included. An item whose tags are entirely inside deprioritize_tags is dropped; a category left with zero items after filtering is dropped too. The legacy shorthand — items: "Python, FastAPI, ..." — still works and treats every item as untagged.

Profiles

The examples ship two archetypes and one sample company profile — add as many of each as you need:

Profile Purpose
general Full CV, no filtering
ai-engineer AI/LLM work foregrounded
companies/spotify Per-application, typically agent-generated

Languages

The data directory holds one file per language: base_en.yaml for English, base_tr.yaml for Turkish. Both share the same structure — only the text differs. Tags, field names, and all structural keys stay in English across all files.

Add any language by creating base_<lang>.yaml and selecting it with --lang <lang>.

Agentic Workflow

The profile schema is small and structured — an agent can generate a company-specific profile directly from a job description. It reads user-data/data/base_en.yaml to see the available tags and bullets, then produces user-data/profiles/companies/<company>.yaml. Lint it before rendering — it catches tag typos, wording mistakes, and broken references an agent can introduce:

cv-renderer --lint --profile companies/spotify --format json
cv-renderer --profile companies/spotify --export pdf

--format json plus the exit codes make the linter usable as a validation gate in a pipeline: an orchestrator can install this package, invoke python -m cv_renderer as a subprocess, and feed the findings back to whatever generated the profile.

Writing rules for bullet generation and profile creation are in AGENTS.md.

Project Structure

cv-renderer/
├── user-data/                    ← your personal data (gitignored)
│   ├── data/
│   │   ├── base_en.yaml          ← CV content in English
│   │   ├── base_tr.yaml          ← same structure, Turkish text
│   │   └── base_<lang>.yaml      ← add more languages as needed
│   ├── profiles/
│   └── out/                      ← rendered outputs
├── src/cv_renderer/              ← render engine (filter, loader, lint, render)
│   ├── templates/main.html.j2    ← Jinja2 template (visual design only)
│   └── examples/                 ← scaffold used by `init` (ships in the wheel)
├── docs/references/              ← Harvard OCS and XYZ writing guides
└── tests/                        ← pytest suite (runs against the packaged examples, never user-data/)

Development

uv sync --dev

uv run ruff check .
uv run ruff format --check .
uv run mypy src/cv_renderer/
uv run pytest

CI runs all four on every push and pull request — see .github/workflows/ci.yml.

Releasing: publish a GitHub Release tagged vX.Y.Z (with the matching version in pyproject.toml) — publish.yml builds and uploads to PyPI via Trusted Publishing, no API token involved.

License

Apache 2.0

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cv_renderer-0.1.0.tar.gz (542.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cv_renderer-0.1.0-py3-none-any.whl (31.0 kB view details)

Uploaded Python 3

File details

Details for the file cv_renderer-0.1.0.tar.gz.

File metadata

  • Download URL: cv_renderer-0.1.0.tar.gz
  • Upload date:
  • Size: 542.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for cv_renderer-0.1.0.tar.gz
Algorithm Hash digest
SHA256 7044f6ef41f25aa155b51b50bcad5c0929286a16523ff7f53cfa4de7eb4a2d7e
MD5 9290ef0946777d5035c40295efdf981b
BLAKE2b-256 80a9bc6e3e6ef8395fd44446280ccdc50bc10847cb51bde565a748b620a37aa7

See more details on using hashes here.

Provenance

The following attestation bundles were made for cv_renderer-0.1.0.tar.gz:

Publisher: publish.yml on melisklc0/cv-renderer

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file cv_renderer-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: cv_renderer-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 31.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for cv_renderer-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ef95980edb2a78bec47f3c0f7bbf1e5a96bedbaa8ded5b60dc47734d735ff7f2
MD5 b4d71d7d90eb96d7bedc631d469d0c41
BLAKE2b-256 d6d92c778af7ece3601b38f18a7f1550e4ac93f3b3637f63a515addd92c69d0d

See more details on using hashes here.

Provenance

The following attestation bundles were made for cv_renderer-0.1.0-py3-none-any.whl:

Publisher: publish.yml on melisklc0/cv-renderer

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page