Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions data/samples/jobs_match_demo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
[
{
"id": "match-demo-1",
"source": "fixture",
"title": "Senior Python Backend Engineer",
"company": "Remote APIs Co",
"location": "Remote",
"url": "https://example.com/jobs/python-backend",
"description": "Build FastAPI services backed by PostgreSQL and Docker-based delivery.",
"tags": ["python", "fastapi", "postgresql", "docker"],
"salary": "$110k-$145k",
"remote": true
},
{
"id": "match-demo-2",
"source": "fixture",
"title": "Frontend Product Engineer",
"company": "Web Studio",
"location": "Remote",
"url": "https://example.com/jobs/frontend-product",
"description": "Build customer-facing React and TypeScript interfaces.",
"tags": ["react", "typescript", "css"],
"salary": "$90k-$125k",
"remote": true
},
{
"id": "match-demo-3",
"source": "fixture",
"title": "Data Platform Engineer",
"company": "Data Flow Labs",
"location": "Hybrid",
"url": "https://example.com/jobs/data-platform",
"description": "Operate data pipelines with Python, SQL, Airflow, and cloud infrastructure.",
"tags": ["python", "sql", "airflow", "cloud"],
"salary": "$120k-$150k",
"remote": false
}
]
8 changes: 8 additions & 0 deletions data/samples/resume_match_demo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"full_name": "Jordan Python",
"email": "jordan@example.com",
"location": "Remote",
"headline": "Python Backend Engineer",
"summary": "Backend engineer focused on APIs, data services, and reliable delivery.",
"skills": ["Python", "FastAPI", "PostgreSQL", "Docker"]
}
88 changes: 88 additions & 0 deletions src/nerajob/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,94 @@ def jobs_match(
console.print(table)


@app.command("match")
def match_cmd(
top: int = typer.Option(10, "--top", "-k", min=1, max=50),
job_id: str | None = typer.Option(None, "--job-id", "-j"),
resume_file: Path | None = typer.Option(
None,
"--resume-file",
"-r",
exists=True,
readable=True,
help="Offline: profile JSON file (instead of stored profile)",
),
jobs_file: Path | None = typer.Option(
None,
"--jobs-file",
"-f",
exists=True,
readable=True,
help="Offline: jobs JSON file (instead of stored jobs)",
),
skill_weight: float = typer.Option(
DEFAULT_MATCH_WEIGHTS.skills,
"--skill-weight",
min=0.0,
help="Maximum score contribution from profile skill matches",
),
title_weight: float = typer.Option(
DEFAULT_MATCH_WEIGHTS.title,
"--title-weight",
min=0.0,
help="Maximum score contribution from headline/title overlap",
),
location_weight: float = typer.Option(
DEFAULT_MATCH_WEIGHTS.location,
"--location-weight",
min=0.0,
help="Maximum score contribution from location or remote fit",
),
) -> None:
"""Rank jobs against a profile, including offline resume/jobs files."""
import json

from nerajob.match import match_score, rank_jobs
from nerajob.models import Profile

if not resume_file or not jobs_file:
console.print(
"[red]Offline files are required.[/red] Run: nerajob match --resume-file data/samples/resume_match_demo.json --jobs-file data/samples/jobs_match_demo.json"
)
raise typer.Exit(code=1)

profile_data = json.loads(resume_file.read_text(encoding="utf-8"))
profile = Profile(**profile_data)
jobs_data = json.loads(jobs_file.read_text(encoding="utf-8"))
jobs = [JobPosting(**j) for j in jobs_data]
console.print(f"[dim]Offline match: {len(jobs)} jobs x {len(profile.skills or [])} skills[/dim]")

weights = MatchWeights(
skills=skill_weight,
title=title_weight,
location=location_weight,
)
if job_id:
job = next((j for j in jobs if j.id == job_id), None)
if not job:
console.print(f"[red]Unknown job id:[/red] {job_id}")
raise typer.Exit(1)
console.print_json(data=match_score(profile, job, weights=weights))
return

ranked = rank_jobs(profile, jobs, top_k=top, weights=weights)
table = Table(title=f"Job matches (top {len(ranked)})")
table.add_column("Score")
table.add_column("Band")
table.add_column("Title")
table.add_column("Company")
table.add_column("Hits")
for row in ranked:
table.add_row(
str(row["score"]),
str(row["band"]),
str(row["title"])[:40],
str(row["company"])[:24],
", ".join(row["skill_hits"][:5]),
)
console.print(table)


@app_app.command("list")
def app_list() -> None:
"""List all applications with status, job_id, created_at."""
Expand Down
26 changes: 26 additions & 0 deletions tests/test_offline_match_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,32 @@ def test_offline_match_with_files(tmp_path: Path) -> None:
assert "TestCo" in result.stdout


def test_top_level_offline_match_with_sample_fixtures() -> None:
root = Path(__file__).parent.parent
result = sp.run(
[
"python",
"-m",
"nerajob",
"match",
"--resume-file",
str(root / "data" / "samples" / "resume_match_demo.json"),
"--jobs-file",
str(root / "data" / "samples" / "jobs_match_demo.json"),
"--top",
"2",
],
capture_output=True,
text=True,
cwd=root,
)
assert result.returncode == 0
assert "Offline match: 3 jobs x 4 skills" in result.stdout
assert "Senior Python" in result.stdout
assert "Backend Engineer" in result.stdout
assert "Frontend Product Engineer" not in result.stdout


def test_offline_match_python_profile_vs_frontend_jobs():
profile = Profile(
headline="Python Backend Engineer",
Expand Down
Loading