codeurcv 0.1.0__py3-none-any.whl
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.
- codeurcv/__init__.py +0 -0
- codeurcv/__main__.py +4 -0
- codeurcv/cli.py +69 -0
- codeurcv/core/constants.py +3 -0
- codeurcv/core/logger.py +16 -0
- codeurcv/core/markdown_converter.py +13 -0
- codeurcv/core/plugin_loader.py +39 -0
- codeurcv/core/renderer.py +140 -0
- codeurcv/core/schema.py +53 -0
- codeurcv/core/template_loader.py +19 -0
- codeurcv/plugins/__init__.py +0 -0
- codeurcv/plugins/base.py +30 -0
- codeurcv/plugins/minimalist/__init__.py +0 -0
- codeurcv/plugins/minimalist/plugin.py +23 -0
- codeurcv/plugins/minimalist/template.tex +178 -0
- codeurcv-0.1.0.dist-info/METADATA +44 -0
- codeurcv-0.1.0.dist-info/RECORD +19 -0
- codeurcv-0.1.0.dist-info/WHEEL +4 -0
- codeurcv-0.1.0.dist-info/entry_points.txt +3 -0
codeurcv/__init__.py
ADDED
|
File without changes
|
codeurcv/__main__.py
ADDED
codeurcv/cli.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from importlib.metadata import version
|
|
5
|
+
|
|
6
|
+
from .core.renderer import ResumeRenderer
|
|
7
|
+
from .core.constants import DEFAULT_CONFIG_FILE, DEFAULT_OUT_DIR
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(help="Generate LaTeX resumes from YAML configs.")
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_version() -> str:
|
|
14
|
+
try:
|
|
15
|
+
return version("codeurcv")
|
|
16
|
+
except Exception:
|
|
17
|
+
return "0.0.0-dev"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def version_callback(value: bool):
|
|
21
|
+
if value:
|
|
22
|
+
console.print(f"[bold cyan]codeurcv version:[/bold cyan] {get_version()}")
|
|
23
|
+
raise typer.Exit()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@app.callback()
|
|
27
|
+
def main(
|
|
28
|
+
version: bool = typer.Option(
|
|
29
|
+
False,
|
|
30
|
+
"-v",
|
|
31
|
+
"--version",
|
|
32
|
+
help="Show version and exit",
|
|
33
|
+
is_eager=True,
|
|
34
|
+
callback=version_callback,
|
|
35
|
+
),
|
|
36
|
+
):
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@app.command()
|
|
41
|
+
def run(
|
|
42
|
+
filename: Path = typer.Option(
|
|
43
|
+
DEFAULT_CONFIG_FILE, "--file", "-f", help="Path to resume YAML config"
|
|
44
|
+
),
|
|
45
|
+
out_dir: Path = typer.Option(
|
|
46
|
+
DEFAULT_OUT_DIR, "--out-dir", "-o", help="Directory to save generated resume"
|
|
47
|
+
),
|
|
48
|
+
):
|
|
49
|
+
"""Render resume."""
|
|
50
|
+
console.print("[bold cyan]Starting resume generation...[/bold cyan]")
|
|
51
|
+
|
|
52
|
+
renderer = ResumeRenderer()
|
|
53
|
+
|
|
54
|
+
renderer.render(filename, out_dir)
|
|
55
|
+
|
|
56
|
+
console.print("[bold green]Done![/bold green]")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@app.command()
|
|
60
|
+
def templates():
|
|
61
|
+
"""List available resume templates."""
|
|
62
|
+
from .core.plugin_loader import load_builtin_plugins
|
|
63
|
+
|
|
64
|
+
plugins = load_builtin_plugins()
|
|
65
|
+
|
|
66
|
+
console.print("\n[bold]Available Templates:[/bold]\n")
|
|
67
|
+
|
|
68
|
+
for name, plugin in plugins.items():
|
|
69
|
+
console.print(f"• [cyan]{name}[/cyan] — {plugin.description}")
|
codeurcv/core/logger.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def setup_logger(
|
|
6
|
+
debug: bool = False, log_file: Path = Path("app.log")
|
|
7
|
+
) -> logging.Logger:
|
|
8
|
+
level = logging.DEBUG if debug else logging.INFO
|
|
9
|
+
|
|
10
|
+
logging.basicConfig(
|
|
11
|
+
level=level,
|
|
12
|
+
format="%(asctime)s | %(levelname)s | %(message)s",
|
|
13
|
+
handlers=[logging.FileHandler(log_file, encoding="utf-8")],
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
return logging.getLogger("codeurcv")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class MarkdownConverter:
|
|
2
|
+
@staticmethod
|
|
3
|
+
def convert(text: str) -> str:
|
|
4
|
+
import subprocess
|
|
5
|
+
|
|
6
|
+
result = subprocess.run(
|
|
7
|
+
["pandoc", "-f", "markdown", "-t", "latex"],
|
|
8
|
+
input=text,
|
|
9
|
+
text=True,
|
|
10
|
+
capture_output=True,
|
|
11
|
+
check=True,
|
|
12
|
+
)
|
|
13
|
+
return result.stdout.strip()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import pkgutil
|
|
2
|
+
import importlib
|
|
3
|
+
from typing import Dict
|
|
4
|
+
|
|
5
|
+
from codeurcv.plugins.base import TemplatePlugin
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def load_builtin_plugins() -> Dict[str, TemplatePlugin]:
|
|
9
|
+
"""
|
|
10
|
+
Dynamically discover built-in template plugins.
|
|
11
|
+
"""
|
|
12
|
+
plugins = {}
|
|
13
|
+
|
|
14
|
+
package = "codeurcv.plugins"
|
|
15
|
+
|
|
16
|
+
for _, module_name, is_pkg in pkgutil.iter_modules(
|
|
17
|
+
importlib.import_module(package).__path__
|
|
18
|
+
):
|
|
19
|
+
if not is_pkg:
|
|
20
|
+
continue
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
module = importlib.import_module(f"{package}.{module_name}.plugin")
|
|
24
|
+
|
|
25
|
+
for attr in dir(module):
|
|
26
|
+
obj = getattr(module, attr)
|
|
27
|
+
|
|
28
|
+
if (
|
|
29
|
+
isinstance(obj, type)
|
|
30
|
+
and issubclass(obj, TemplatePlugin)
|
|
31
|
+
and obj is not TemplatePlugin
|
|
32
|
+
):
|
|
33
|
+
instance = obj()
|
|
34
|
+
plugins[instance.name] = instance
|
|
35
|
+
|
|
36
|
+
except ModuleNotFoundError:
|
|
37
|
+
continue
|
|
38
|
+
|
|
39
|
+
return plugins
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import yaml
|
|
2
|
+
import shutil
|
|
3
|
+
import logging
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
from codeurcv.core.template_loader import TemplateEngine
|
|
10
|
+
from codeurcv.core.logger import setup_logger
|
|
11
|
+
from codeurcv.core.plugin_loader import load_builtin_plugins
|
|
12
|
+
from codeurcv.core.schema import ResumeConfig
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ResumeRenderer:
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self.plugins = load_builtin_plugins()
|
|
20
|
+
self._check_dependencies()
|
|
21
|
+
log_dir = Path("logs")
|
|
22
|
+
log_dir.mkdir(exist_ok=True)
|
|
23
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
24
|
+
self.log_file = log_dir / f"codeurcv_{timestamp}.log"
|
|
25
|
+
self.logger = setup_logger(debug=False, log_file=self.log_file)
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def _check_dependencies():
|
|
29
|
+
for tool in ("pandoc", "pdflatex"):
|
|
30
|
+
if shutil.which(tool) is None:
|
|
31
|
+
raise RuntimeError(f"{tool} is not installed or not in PATH.")
|
|
32
|
+
|
|
33
|
+
def render(self, config_path: Path, output_dir: Path):
|
|
34
|
+
console.print("\n[bold green]🚀 Building your resume...[/bold green]\n")
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
# STEP 1
|
|
38
|
+
raw_data = self._step(
|
|
39
|
+
"Configuration loaded",
|
|
40
|
+
lambda: yaml.safe_load(config_path.read_text()),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# STEP 2
|
|
44
|
+
config = self._step(
|
|
45
|
+
"Data validated",
|
|
46
|
+
lambda: ResumeConfig(**raw_data),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# STEP 3
|
|
50
|
+
plugin = self._step(
|
|
51
|
+
f"Template applied: {config.template}",
|
|
52
|
+
lambda: self._get_plugin(config.template),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# STEP 4
|
|
56
|
+
data = plugin.preprocess(config)
|
|
57
|
+
|
|
58
|
+
template_path = plugin.template_path()
|
|
59
|
+
template_engine = TemplateEngine(template_path.parent)
|
|
60
|
+
|
|
61
|
+
rendered_tex = self._step(
|
|
62
|
+
"LaTeX rendered",
|
|
63
|
+
lambda: template_engine.render(
|
|
64
|
+
template_path.name,
|
|
65
|
+
data.model_dump(),
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
tex_file = output_dir / f"{config.filename}.tex"
|
|
71
|
+
tex_file.write_text(plugin.postprocess(rendered_tex))
|
|
72
|
+
|
|
73
|
+
# STEP 5
|
|
74
|
+
console.print("\n📄 Generating PDF...\n")
|
|
75
|
+
self._generate_pdf(tex_file, output_dir)
|
|
76
|
+
|
|
77
|
+
console.print("\n[bold green]✔ PDF generated[/bold green]\n")
|
|
78
|
+
console.print("[bold green]🎉 Done![/bold green]")
|
|
79
|
+
console.print(
|
|
80
|
+
f"[cyan]📁 {output_dir.resolve() / f'{config.filename}.pdf'}[/cyan]\n"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
except Exception:
|
|
84
|
+
console.print("\n[bold red]❌ Resume generation failed.[/bold red]")
|
|
85
|
+
console.print(f"[yellow]See full logs at:[/yellow] {self.log_file}\n")
|
|
86
|
+
raise
|
|
87
|
+
|
|
88
|
+
def _step(self, message: str, func):
|
|
89
|
+
try:
|
|
90
|
+
result = func()
|
|
91
|
+
console.print(f"[green]✔[/green] {message}")
|
|
92
|
+
logging.info("SUCCESS: %s", message)
|
|
93
|
+
return result
|
|
94
|
+
except Exception as e:
|
|
95
|
+
logging.exception("FAILED: %s", message)
|
|
96
|
+
raise RuntimeError(f"{message} failed.") from e
|
|
97
|
+
|
|
98
|
+
def _get_plugin(self, template_name: str):
|
|
99
|
+
if template_name not in self.plugins:
|
|
100
|
+
available = ", ".join(self.plugins.keys())
|
|
101
|
+
raise RuntimeError(
|
|
102
|
+
f"Template '{template_name}' not found.\n"
|
|
103
|
+
f"Available templates: {available}"
|
|
104
|
+
)
|
|
105
|
+
return self.plugins[template_name]
|
|
106
|
+
|
|
107
|
+
def _generate_pdf(self, tex_file: Path, output_dir: Path):
|
|
108
|
+
"""
|
|
109
|
+
Runs pdflatex to generate PDF from the .tex file.
|
|
110
|
+
- Uses subprocess to call pdflatex with appropriate arguments.
|
|
111
|
+
- Captures and logs output in real-time.
|
|
112
|
+
- Raises an error if PDF generation fails.
|
|
113
|
+
"""
|
|
114
|
+
command = [
|
|
115
|
+
"pdflatex",
|
|
116
|
+
"-interaction=nonstopmode",
|
|
117
|
+
"-output-directory",
|
|
118
|
+
str(output_dir),
|
|
119
|
+
str(tex_file),
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
console.print(f"[bold] > Running:[/bold] {' '.join(command)}")
|
|
123
|
+
logging.info("Running command: %s", " ".join(command))
|
|
124
|
+
|
|
125
|
+
process = subprocess.Popen(
|
|
126
|
+
command,
|
|
127
|
+
stdout=subprocess.PIPE,
|
|
128
|
+
stderr=subprocess.STDOUT,
|
|
129
|
+
text=True,
|
|
130
|
+
bufsize=1,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
with console.status("[green]Generating PDF...[/green]", spinner="dots"):
|
|
134
|
+
stdout, _ = process.communicate()
|
|
135
|
+
|
|
136
|
+
for line in stdout.splitlines():
|
|
137
|
+
logging.info(line)
|
|
138
|
+
|
|
139
|
+
if process.returncode != 0:
|
|
140
|
+
raise RuntimeError("PDF generation failed.")
|
codeurcv/core/schema.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
from pydantic import BaseModel, Field
|
|
3
|
+
|
|
4
|
+
from codeurcv.core.constants import DEFAULT_TEMPLATE
|
|
5
|
+
|
|
6
|
+
class ResumeSection(BaseModel):
|
|
7
|
+
"""Base model for resume sections with optional ordering."""
|
|
8
|
+
priority: Optional[int] = None
|
|
9
|
+
|
|
10
|
+
class BasicDetails(BaseModel):
|
|
11
|
+
name: str
|
|
12
|
+
email: str
|
|
13
|
+
phone: Optional[str] = None
|
|
14
|
+
website: Optional[str] = None
|
|
15
|
+
github: Optional[str] = None
|
|
16
|
+
linkedin: Optional[str] = None
|
|
17
|
+
location: Optional[str] = None
|
|
18
|
+
|
|
19
|
+
class Education(ResumeSection):
|
|
20
|
+
institution: str
|
|
21
|
+
location: str
|
|
22
|
+
degree: str
|
|
23
|
+
duration: str
|
|
24
|
+
gpa: Optional[str] = None
|
|
25
|
+
additional_information: List[str] = Field(default_factory=list)
|
|
26
|
+
|
|
27
|
+
class Job(ResumeSection):
|
|
28
|
+
company: str
|
|
29
|
+
role: str
|
|
30
|
+
duration: str
|
|
31
|
+
achievements: List[str] = Field(default_factory=list)
|
|
32
|
+
technologies: List[str] = Field(default_factory=list)
|
|
33
|
+
|
|
34
|
+
class Project(ResumeSection):
|
|
35
|
+
name: str
|
|
36
|
+
description: List[str] = Field(default_factory=list)
|
|
37
|
+
technologies: List[str] = Field(default_factory=list)
|
|
38
|
+
link: Optional[str] = None
|
|
39
|
+
date: Optional[str] = None
|
|
40
|
+
|
|
41
|
+
class Skill(ResumeSection):
|
|
42
|
+
name: str
|
|
43
|
+
featured_skills: List[str] = Field(default_factory=list)
|
|
44
|
+
|
|
45
|
+
class ResumeConfig(BaseModel):
|
|
46
|
+
template: str = DEFAULT_TEMPLATE
|
|
47
|
+
filename: str = "resume"
|
|
48
|
+
basic_details: BasicDetails
|
|
49
|
+
summary: Optional[str] = None
|
|
50
|
+
education: List[Education] = Field(default_factory=list)
|
|
51
|
+
work: List[Job] = Field(default_factory=list)
|
|
52
|
+
projects: List[Project] = Field(default_factory=list)
|
|
53
|
+
skills: List[Skill] = Field(default_factory=list)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from jinja2 import Environment, FileSystemLoader
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TemplateEngine:
|
|
6
|
+
def __init__(self, template_dir: Path):
|
|
7
|
+
self.env = Environment(
|
|
8
|
+
loader=FileSystemLoader(template_dir),
|
|
9
|
+
block_start_string="((*",
|
|
10
|
+
block_end_string="*))",
|
|
11
|
+
variable_start_string="(((",
|
|
12
|
+
variable_end_string=")))",
|
|
13
|
+
comment_start_string="((#",
|
|
14
|
+
comment_end_string="#))",
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
def render(self, template_name: str, context: dict) -> str:
|
|
18
|
+
template = self.env.get_template(template_name)
|
|
19
|
+
return template.render(**context)
|
|
File without changes
|
codeurcv/plugins/base.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Dict, Any
|
|
4
|
+
from codeurcv.core.schema import ResumeConfig
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TemplatePlugin(ABC):
|
|
8
|
+
"""
|
|
9
|
+
Base class for resume template plugins.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
name: str
|
|
13
|
+
description: str
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def template_path(self) -> Path:
|
|
17
|
+
"""Return path to LaTeX template file."""
|
|
18
|
+
|
|
19
|
+
def preprocess(self, data: ResumeConfig) -> ResumeConfig:
|
|
20
|
+
"""
|
|
21
|
+
Optional hook to modify validated config
|
|
22
|
+
before rendering.
|
|
23
|
+
"""
|
|
24
|
+
return data
|
|
25
|
+
|
|
26
|
+
def postprocess(self, tex_content: str) -> str:
|
|
27
|
+
"""
|
|
28
|
+
Optional hook to modify final rendered LaTeX.
|
|
29
|
+
"""
|
|
30
|
+
return tex_content
|
|
File without changes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from codeurcv.core.schema import ResumeConfig
|
|
4
|
+
from codeurcv.plugins.base import TemplatePlugin
|
|
5
|
+
from codeurcv.core.markdown_converter import MarkdownConverter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MinimalistTemplate(TemplatePlugin):
|
|
9
|
+
name = "minimalist"
|
|
10
|
+
description = "Clean minimalist resume style"
|
|
11
|
+
|
|
12
|
+
def template_path(self) -> Path:
|
|
13
|
+
return Path(__file__).parent / "template.tex"
|
|
14
|
+
|
|
15
|
+
def preprocess(self, data: ResumeConfig) -> ResumeConfig:
|
|
16
|
+
converter = MarkdownConverter()
|
|
17
|
+
if data.summary:
|
|
18
|
+
data.summary = converter.convert(data.summary)
|
|
19
|
+
for job in data.work:
|
|
20
|
+
job.achievements = [converter.convert(achievement) for achievement in job.achievements]
|
|
21
|
+
for proj in data.projects:
|
|
22
|
+
proj.description = [converter.convert(desc) for desc in proj.description]
|
|
23
|
+
return data
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
%-------------------------
|
|
2
|
+
% Resume in LateX
|
|
3
|
+
% Author : Subhomoy Roy Choudhury
|
|
4
|
+
% License : MIT
|
|
5
|
+
%------------------------
|
|
6
|
+
|
|
7
|
+
\documentclass[letterpaper,11pt]{article}
|
|
8
|
+
|
|
9
|
+
\usepackage{latexsym}
|
|
10
|
+
\usepackage[empty]{fullpage}
|
|
11
|
+
\usepackage{titlesec}
|
|
12
|
+
\usepackage{marvosym}
|
|
13
|
+
\usepackage[usenames,dvipsnames]{color}
|
|
14
|
+
\usepackage{verbatim}
|
|
15
|
+
\usepackage{enumitem}
|
|
16
|
+
\usepackage[hidelinks]{hyperref}
|
|
17
|
+
\usepackage{fancyhdr}
|
|
18
|
+
\usepackage[english]{babel}
|
|
19
|
+
\usepackage{tabularx}
|
|
20
|
+
\input{glyphtounicode}
|
|
21
|
+
|
|
22
|
+
\pagestyle{fancy}
|
|
23
|
+
\fancyhf{} % Clear all header and footer fields
|
|
24
|
+
\fancyfoot{}
|
|
25
|
+
\renewcommand{\headrulewidth}{0pt}
|
|
26
|
+
\renewcommand{\footrulewidth}{0pt}
|
|
27
|
+
|
|
28
|
+
% Adjust margins
|
|
29
|
+
\addtolength{\oddsidemargin}{-0.5in}
|
|
30
|
+
\addtolength{\evensidemargin}{-0.5in}
|
|
31
|
+
\addtolength{\textwidth}{1in}
|
|
32
|
+
\addtolength{\topmargin}{-.5in}
|
|
33
|
+
\addtolength{\textheight}{1.0in}
|
|
34
|
+
\setlength{\footskip}{12pt}
|
|
35
|
+
|
|
36
|
+
\urlstyle{same}
|
|
37
|
+
|
|
38
|
+
\raggedbottom
|
|
39
|
+
\raggedright
|
|
40
|
+
\setlength{\tabcolsep}{0in}
|
|
41
|
+
|
|
42
|
+
% Sections formatting
|
|
43
|
+
\titleformat{\section}{
|
|
44
|
+
\vspace{-4pt}\scshape\raggedright\large
|
|
45
|
+
}{}{0em}{}[\color{black}\titlerule \vspace{-5pt}]
|
|
46
|
+
|
|
47
|
+
% Ensure that generate PDF is machine readable/ATS parsable
|
|
48
|
+
\pdfgentounicode=1
|
|
49
|
+
|
|
50
|
+
%-------------------------
|
|
51
|
+
% Custom commands
|
|
52
|
+
% \newcommand{\resumeItem}[2]{
|
|
53
|
+
% \item\small{
|
|
54
|
+
% \textbf{#1}{: #2 \vspace{-2pt}}
|
|
55
|
+
% }
|
|
56
|
+
% }
|
|
57
|
+
\newcommand{\resumeItem}[1]{
|
|
58
|
+
\item\small{
|
|
59
|
+
{#1 \vspace{-2pt}}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
% Just in case someone needs a heading that does not need to be in a list
|
|
64
|
+
\newcommand{\resumeHeading}[4]{
|
|
65
|
+
\begin{tabular*}{0.99\textwidth}[t]{l@{\extracolsep{\fill}}r}
|
|
66
|
+
\textbf{#1} & #2 \\
|
|
67
|
+
\textit{\small#3} & \textit{\small #4} \\
|
|
68
|
+
\end{tabular*}\vspace{-5pt}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
\newcommand{\resumeSubheading}[4]{
|
|
72
|
+
\vspace{-2pt}\item
|
|
73
|
+
\begin{tabular*}{0.97\textwidth}[t]{l@{\extracolsep{\fill}}r}
|
|
74
|
+
\textbf{#1} & #2 \\
|
|
75
|
+
\textit{\small#3} & \textit{\small #4} \\
|
|
76
|
+
\end{tabular*}\vspace{-7pt}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
\newcommand{\resumeSubSubheading}[2]{
|
|
80
|
+
\begin{tabular*}{0.97\textwidth}{l@{\extracolsep{\fill}}r}
|
|
81
|
+
\textit{\small#1} & \textit{\small #2} \\
|
|
82
|
+
\end{tabular*}\vspace{-7pt}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
\newcommand{\resumeSubItem}[1]{\resumeItem{#1}\vspace{-4pt}}
|
|
86
|
+
|
|
87
|
+
\renewcommand{\labelitemii}{$\circ$}
|
|
88
|
+
|
|
89
|
+
\newcommand{\resumeSubHeadingListStart}{\begin{itemize}[leftmargin=0.15in, label={}]}
|
|
90
|
+
\newcommand{\resumeSubHeadingListEnd}{\end{itemize}}
|
|
91
|
+
\newcommand{\resumeItemListStart}{\begin{itemize}}
|
|
92
|
+
\newcommand{\resumeItemListEnd}{\end{itemize}\vspace{-5pt}}
|
|
93
|
+
|
|
94
|
+
\newcommand{\technologies}[1]{
|
|
95
|
+
\noindent\textbf{Technologies:} #1
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
\newcommand{\resumeProjectHeading}[2]{
|
|
99
|
+
\item
|
|
100
|
+
\begin{tabular*}{0.97\textwidth}{l@{\extracolsep{\fill}}r}
|
|
101
|
+
\small#1 & #2 \\
|
|
102
|
+
\end{tabular*}\vspace{-7pt}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
\begin{document}
|
|
106
|
+
|
|
107
|
+
%----------HEADING-----------------
|
|
108
|
+
\begin{center}
|
|
109
|
+
{\LARGE \textbf{((( basic_details.name )))}} \\[4pt]
|
|
110
|
+
\normalsize
|
|
111
|
+
\href{mailto:((( basic_details.email )))}{((( basic_details.email )))} $|$
|
|
112
|
+
\href{tel:((( basic_details.phone )))}{((( basic_details.phone )))} $|$
|
|
113
|
+
\href{((( "https://www." + basic_details.github )))}{((( basic_details.github )))} $|$
|
|
114
|
+
\href{((( "https://www." + basic_details.linkedin )))}{((( basic_details.linkedin )))}
|
|
115
|
+
|
|
116
|
+
\end{center}
|
|
117
|
+
|
|
118
|
+
%----------SUMMARY-----------------
|
|
119
|
+
\section{Summary}
|
|
120
|
+
\resumeSubHeadingListStart
|
|
121
|
+
\item\small
|
|
122
|
+
((( summary | safe ))) \\
|
|
123
|
+
\resumeSubHeadingListEnd
|
|
124
|
+
|
|
125
|
+
%-----------EDUCATION-----------------
|
|
126
|
+
\section{Education}
|
|
127
|
+
\resumeSubHeadingListStart
|
|
128
|
+
((* for school in education *))
|
|
129
|
+
\resumeSubheading
|
|
130
|
+
{ ((( school.institution ))) }{ ((( school.location ))) }
|
|
131
|
+
{ ((( school.degree ))); GPA: ((( school.gpa ))) }{ ((( school.duration ))) }
|
|
132
|
+
((* endfor *))
|
|
133
|
+
\resumeSubHeadingListEnd
|
|
134
|
+
|
|
135
|
+
%-----------EXPERIENCE-----------------
|
|
136
|
+
\section{Experience}
|
|
137
|
+
\resumeSubHeadingListStart
|
|
138
|
+
((* for job in work *))
|
|
139
|
+
\resumeSubheading
|
|
140
|
+
{ ((( job.company ))) }{ ((( job.location ))) }
|
|
141
|
+
{ ((( job.role ))) }{ ((( job.duration ))) }
|
|
142
|
+
\resumeItemListStart
|
|
143
|
+
((* for item in job.achievements *))
|
|
144
|
+
\resumeItem{ ((( item | safe ))) }
|
|
145
|
+
((* endfor *))
|
|
146
|
+
\resumeItem{ \technologies{((( job.technologies | join(", ") ))) } }
|
|
147
|
+
\resumeItemListEnd
|
|
148
|
+
((* endfor *))
|
|
149
|
+
\resumeSubHeadingListEnd
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
%-----------PROJECTS-----------------
|
|
153
|
+
\section{Projects}
|
|
154
|
+
\resumeSubHeadingListStart
|
|
155
|
+
((* for proj in projects *))
|
|
156
|
+
\resumeProjectHeading
|
|
157
|
+
{\href{ ((( proj.link ))) }{ \textbf{((( proj.name )))}}((* if proj.technologies *)) $|$ \emph{((( proj.technologies | join(', ') )))}((* endif *))}
|
|
158
|
+
{((( proj.duration )))}
|
|
159
|
+
\resumeItemListStart
|
|
160
|
+
((* for item in proj.description *))
|
|
161
|
+
\resumeItem{((( item | safe )))}
|
|
162
|
+
((* endfor *))
|
|
163
|
+
\resumeItemListEnd
|
|
164
|
+
((* endfor *))
|
|
165
|
+
\resumeSubHeadingListEnd
|
|
166
|
+
|
|
167
|
+
%-----------TECHNICAL SKILLS-----------------
|
|
168
|
+
\section{Technical Skills}
|
|
169
|
+
\resumeSubHeadingListStart
|
|
170
|
+
((* for category in skills *))
|
|
171
|
+
\item\small
|
|
172
|
+
\begin{tabular*}{0.97\textwidth}{l@{\extracolsep{\fill}}r}
|
|
173
|
+
\textbf{ ((( category.name|capitalize )))}: {((( category.featured_skills | join(", ") )))} \\
|
|
174
|
+
\end{tabular*}\vspace{-7pt}
|
|
175
|
+
((* endfor *))
|
|
176
|
+
\resumeSubHeadingListEnd
|
|
177
|
+
|
|
178
|
+
\end{document}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: codeurcv
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python library to generate LaTeX resumes from YAML configuration files using Jinja2 templates.
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: python,digipin,coordinates
|
|
7
|
+
Author: crackedngineer
|
|
8
|
+
Author-email: subhomoyrchoudhury@gmail.com
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
|
+
Classifier: Topic :: Security
|
|
16
|
+
Requires-Dist: jinja2 (>=3.1.6)
|
|
17
|
+
Requires-Dist: pydantic (>=2.12.5)
|
|
18
|
+
Requires-Dist: pyyaml (>=6.0.3)
|
|
19
|
+
Requires-Dist: rich (>=14.3.3)
|
|
20
|
+
Requires-Dist: typer (>=0.24.1)
|
|
21
|
+
Project-URL: Documentation, https://github.com/crackedngineer/code-ur-cv/
|
|
22
|
+
Project-URL: Homepage, https://github.com/crackedngineer/code-ur-cv
|
|
23
|
+
Project-URL: Repository, https://github.com/crackedngineer/code-ur-cv
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# Subhomoy Roy Choudhury Resume
|
|
27
|
+
|
|
28
|
+
## Local Setup
|
|
29
|
+
|
|
30
|
+
To build and compile the resume locally using Docker:
|
|
31
|
+
|
|
32
|
+
1. **Build the Docker image:**
|
|
33
|
+
```bash
|
|
34
|
+
make docker-build
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
2. **Compile the LaTeX resume inside the container:**
|
|
38
|
+
```bash
|
|
39
|
+
docker run --rm -v $(pwd):/workspace ghcr.io/youruser/latex-ci:latest make compile
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
> Ensure you have [Docker](https://www.docker.com/) and [Make](https://www.gnu.org/software/make/) installed on your system.
|
|
43
|
+
|
|
44
|
+
The compiled PDF will be available in your project directory.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
codeurcv/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
codeurcv/__main__.py,sha256=DK2zjOWykPtZIvszMtx2GBYbnuIq98qpYt8I1p0BBig,66
|
|
3
|
+
codeurcv/cli.py,sha256=2sdOHQJsfGE40PObtg19QfyJRC0z_YHl6qR_VbGo3i8,1667
|
|
4
|
+
codeurcv/core/constants.py,sha256=UQSPmRP9efKYB5dq--KIxatGrsTG9BIfJzQ1aJUTFb0,93
|
|
5
|
+
codeurcv/core/logger.py,sha256=x9_K9UwzZc7ygXqEBRwfj5Ze6OOn447eyWdJ51GH7Cc,415
|
|
6
|
+
codeurcv/core/markdown_converter.py,sha256=sNHIcHRM2wdIMfczO2ATwREnBGLiVdk2kVxhUhNyazc,345
|
|
7
|
+
codeurcv/core/plugin_loader.py,sha256=0TNullA-4FFKLbK9ANzt_8HpczzMZqmWPNmwBzl3GoQ,976
|
|
8
|
+
codeurcv/core/renderer.py,sha256=l7NvcBSL323ZqjzIbpOoF14WgEg0Tp7UCKwWHaWQqA4,4706
|
|
9
|
+
codeurcv/core/schema.py,sha256=7lEW5Fx4VC56eqR0gidv44sZQfCS_Tq4Flhru5cNP6s,1598
|
|
10
|
+
codeurcv/core/template_loader.py,sha256=zJytY-S8dn9WuMRl1-4XACsVdQ5cHVp1fauMfhRGQu0,630
|
|
11
|
+
codeurcv/plugins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
codeurcv/plugins/base.py,sha256=allYvP1gGXanGwo9ndLZrFb6MIfoROFsUAITGLx1kp8,708
|
|
13
|
+
codeurcv/plugins/minimalist/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
codeurcv/plugins/minimalist/plugin.py,sha256=Pzf6t0ca41xNiAr8qvoqHBiF7bKN_nd6OQZOpr1ouiM,855
|
|
15
|
+
codeurcv/plugins/minimalist/template.tex,sha256=hKNBWdjgWXP6CX3eusr56MTI8UBCROiRVAosGKCDipA,5036
|
|
16
|
+
codeurcv-0.1.0.dist-info/METADATA,sha256=OyYjzJJhYfYlgzkMAD7bG5MaxeURmWRraPuylBB-DVo,1537
|
|
17
|
+
codeurcv-0.1.0.dist-info/WHEEL,sha256=kJCRJT_g0adfAJzTx2GUMmS80rTJIVHRCfG0DQgLq3o,88
|
|
18
|
+
codeurcv-0.1.0.dist-info/entry_points.txt,sha256=aKyQXg_Xg0fHPN8T6N0wFrRzbSxc1JS2WBexEZL-NJU,55
|
|
19
|
+
codeurcv-0.1.0.dist-info/RECORD,,
|