harshitsingh 0.1.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.
- harshitsingh-0.1.0/PKG-INFO +59 -0
- harshitsingh-0.1.0/README.md +47 -0
- harshitsingh-0.1.0/pyproject.toml +24 -0
- harshitsingh-0.1.0/setup.cfg +4 -0
- harshitsingh-0.1.0/src/harshit_profile/__init__.py +29 -0
- harshitsingh-0.1.0/src/harshit_profile/__main__.py +3 -0
- harshitsingh-0.1.0/src/harshit_profile/_data.py +55 -0
- harshitsingh-0.1.0/src/harshit_profile/blog.py +95 -0
- harshitsingh-0.1.0/src/harshit_profile/cli.py +49 -0
- harshitsingh-0.1.0/src/harshit_profile/matching.py +181 -0
- harshitsingh-0.1.0/src/harshit_profile/models.py +86 -0
- harshitsingh-0.1.0/src/harshitsingh.egg-info/PKG-INFO +59 -0
- harshitsingh-0.1.0/src/harshitsingh.egg-info/SOURCES.txt +15 -0
- harshitsingh-0.1.0/src/harshitsingh.egg-info/dependency_links.txt +1 -0
- harshitsingh-0.1.0/src/harshitsingh.egg-info/entry_points.txt +2 -0
- harshitsingh-0.1.0/src/harshitsingh.egg-info/top_level.txt +1 -0
- harshitsingh-0.1.0/tests/test_profile.py +49 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: harshitsingh
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Offline job-description relevance scoring against Harshit's profile.
|
|
5
|
+
Author: Harshit
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: profile,portfolio,resume,cli
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# harshitsingh
|
|
14
|
+
|
|
15
|
+
Harshit's profile as a small, offline job-description matcher. Give it a job description and it prints one relevance score.
|
|
16
|
+
|
|
17
|
+
```powershell
|
|
18
|
+
pip install harshitsingh
|
|
19
|
+
harshitsingh "Backend role requiring Python, FastAPI, system design, and distributed systems"
|
|
20
|
+
# 100%
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
For a longer description, save it as UTF-8 text and run:
|
|
24
|
+
|
|
25
|
+
```powershell
|
|
26
|
+
harshitsingh --file .\job-description.txt
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or pipe text to it:
|
|
30
|
+
|
|
31
|
+
```powershell
|
|
32
|
+
Get-Content .\job-description.txt -Raw | harshitsingh
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Running `harshitsingh` without arguments opens a one-line prompt. The command prints only the percentage, so its output is easy to copy or use in a script.
|
|
36
|
+
|
|
37
|
+
## Python API
|
|
38
|
+
|
|
39
|
+
The distribution name is `harshitsingh`; the import name is `harshit_profile`.
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from harshit_profile import match_description
|
|
43
|
+
|
|
44
|
+
result = match_description("Python backend role with FastAPI and distributed systems")
|
|
45
|
+
print(result["percentage"]) # 100
|
|
46
|
+
print(result["matched"]) # recognized strengths in Harshit's profile
|
|
47
|
+
print(result["missing"]) # recognized requirements not in the profile
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The matcher uses a local vocabulary of profile skills, aliases, and common engineering requirements. It counts each recognized skill once, gives explicit must-have requirements more weight, and discounts preferred skills. It makes no network calls and does not send job descriptions to an AI service. Unrecognized terms are ignored, so the score is a quick comparison aid rather than a hiring decision.
|
|
51
|
+
|
|
52
|
+
The profile data is also available as `me`, `skills`, and `projects`:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from harshit_profile import me, projects
|
|
56
|
+
|
|
57
|
+
me.name # "Harshit"
|
|
58
|
+
[project.name for project in projects] # includes "nazarGeo"
|
|
59
|
+
```
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# harshitsingh
|
|
2
|
+
|
|
3
|
+
Harshit's profile as a small, offline job-description matcher. Give it a job description and it prints one relevance score.
|
|
4
|
+
|
|
5
|
+
```powershell
|
|
6
|
+
pip install harshitsingh
|
|
7
|
+
harshitsingh "Backend role requiring Python, FastAPI, system design, and distributed systems"
|
|
8
|
+
# 100%
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
For a longer description, save it as UTF-8 text and run:
|
|
12
|
+
|
|
13
|
+
```powershell
|
|
14
|
+
harshitsingh --file .\job-description.txt
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or pipe text to it:
|
|
18
|
+
|
|
19
|
+
```powershell
|
|
20
|
+
Get-Content .\job-description.txt -Raw | harshitsingh
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Running `harshitsingh` without arguments opens a one-line prompt. The command prints only the percentage, so its output is easy to copy or use in a script.
|
|
24
|
+
|
|
25
|
+
## Python API
|
|
26
|
+
|
|
27
|
+
The distribution name is `harshitsingh`; the import name is `harshit_profile`.
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from harshit_profile import match_description
|
|
31
|
+
|
|
32
|
+
result = match_description("Python backend role with FastAPI and distributed systems")
|
|
33
|
+
print(result["percentage"]) # 100
|
|
34
|
+
print(result["matched"]) # recognized strengths in Harshit's profile
|
|
35
|
+
print(result["missing"]) # recognized requirements not in the profile
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The matcher uses a local vocabulary of profile skills, aliases, and common engineering requirements. It counts each recognized skill once, gives explicit must-have requirements more weight, and discounts preferred skills. It makes no network calls and does not send job descriptions to an AI service. Unrecognized terms are ignored, so the score is a quick comparison aid rather than a hiring decision.
|
|
39
|
+
|
|
40
|
+
The profile data is also available as `me`, `skills`, and `projects`:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from harshit_profile import me, projects
|
|
44
|
+
|
|
45
|
+
me.name # "Harshit"
|
|
46
|
+
[project.name for project in projects] # includes "nazarGeo"
|
|
47
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "harshitsingh"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Offline job-description relevance scoring against Harshit's profile."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Harshit" }]
|
|
13
|
+
keywords = ["profile", "portfolio", "resume", "cli"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
]
|
|
18
|
+
dependencies = []
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
harshitsingh = "harshit_profile.cli:main"
|
|
22
|
+
|
|
23
|
+
[tool.setuptools.packages.find]
|
|
24
|
+
where = ["src"]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Harshit's profile, importable.
|
|
2
|
+
|
|
3
|
+
>>> from harshit_profile import me, skills
|
|
4
|
+
>>> me.has("python")
|
|
5
|
+
True
|
|
6
|
+
>>> me.match(["Python", "Rust", "MongoDB"])["missing"]
|
|
7
|
+
['Rust']
|
|
8
|
+
|
|
9
|
+
Command line: harshitsingh "paste a job description" | harshitsingh --file job.txt
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from . import blog
|
|
13
|
+
from ._data import ME as _ME, PROJECTS as projects, SKILLS as skills
|
|
14
|
+
from .models import Profile, Project, Skill
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.0"
|
|
17
|
+
|
|
18
|
+
me = Profile(
|
|
19
|
+
**{k: v for k, v in _ME.__dict__.items() if k not in ("skills", "projects")},
|
|
20
|
+
skills=tuple(skills),
|
|
21
|
+
projects=tuple(projects),
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def match_description(description: str) -> dict:
|
|
26
|
+
"""Return Harshit's skill match score and recognized skill details."""
|
|
27
|
+
return me.match_description(description)
|
|
28
|
+
|
|
29
|
+
__all__ = ["me", "skills", "projects", "blog", "Profile", "Project", "Skill", "match_description", "__version__"]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""The single source of truth for the profile. Edit this file, bump the version, republish."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .models import Profile, Project, Skill
|
|
6
|
+
|
|
7
|
+
ME = Profile(
|
|
8
|
+
name="Harshit",
|
|
9
|
+
headline="System design, backend engineering, and applied AI for reliable production systems.",
|
|
10
|
+
employer="Aditya Birla Money Limited",
|
|
11
|
+
role="Senior Software Engineer",
|
|
12
|
+
education="VIT",
|
|
13
|
+
location="India",
|
|
14
|
+
site="https://falcon7-theta.vercel.app", # update if you add a custom domain
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
SKILLS = [
|
|
18
|
+
Skill("C++20", "systems", "Kernel and engine work."),
|
|
19
|
+
Skill("Python", "languages", "Simulation, tooling, services."),
|
|
20
|
+
Skill("FastAPI", "backend", "Backend services for production APIs."),
|
|
21
|
+
Skill("System design", "backend", "Designing reliable, production-scale services."),
|
|
22
|
+
Skill("Backend engineering", "backend", "Services, gateways and production data systems."),
|
|
23
|
+
Skill("Distributed systems", "backend", "Reliable systems built for real-world constraints."),
|
|
24
|
+
Skill("Applied AI", "ai", "Computer vision, intelligent tools and agent systems."),
|
|
25
|
+
Skill("Computer vision", "ai", "Vision models, image analysis and spatial perception."),
|
|
26
|
+
Skill("Oracle", "data", "On-premise relational databases."),
|
|
27
|
+
Skill("SQL Server", "data", "On-premise relational databases."),
|
|
28
|
+
Skill("MongoDB", "data", "Document store for internal data flows."),
|
|
29
|
+
Skill("MCP", "ai", "Model Context Protocol servers and tools."),
|
|
30
|
+
Skill("Agent systems", "ai", "Agents as first-class objects."),
|
|
31
|
+
Skill("Execution engines", "markets", "Software-in-the-loop trading systems."),
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
PROJECTS = [
|
|
35
|
+
Project(
|
|
36
|
+
"SHAURYA",
|
|
37
|
+
"A heterogeneous software-in-the-loop execution engine for high-frequency trading.",
|
|
38
|
+
("HFT", "SIL", "research"),
|
|
39
|
+
),
|
|
40
|
+
Project(
|
|
41
|
+
"nazarGeo",
|
|
42
|
+
"A real-time building geolocation pipeline fusing vision, LiDAR, and IMU/GNSS.",
|
|
43
|
+
("computer vision", "LiDAR", "geospatial"),
|
|
44
|
+
),
|
|
45
|
+
Project(
|
|
46
|
+
"HershyOS",
|
|
47
|
+
"A 64-bit agentic operating system where agents are first-class kernel objects.",
|
|
48
|
+
("C++20", "kernel", "agents"),
|
|
49
|
+
),
|
|
50
|
+
Project(
|
|
51
|
+
"KYC Broker Sim",
|
|
52
|
+
"A simulation of the eKYC flow between an investor and a broker.",
|
|
53
|
+
("Python", "eKYC"),
|
|
54
|
+
),
|
|
55
|
+
]
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Read the published blog straight from Supabase's REST API using only the standard library.
|
|
2
|
+
|
|
3
|
+
Set two environment variables (or edit the constants below before publishing):
|
|
4
|
+
|
|
5
|
+
HARSHIT_SUPABASE_URL e.g. https://abcdxyz.supabase.co
|
|
6
|
+
HARSHIT_SUPABASE_ANON_KEY the public anon key (safe to ship; row level security
|
|
7
|
+
only lets it read published posts)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import urllib.error
|
|
15
|
+
import urllib.parse
|
|
16
|
+
import urllib.request
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from typing import List, Optional
|
|
19
|
+
|
|
20
|
+
SUPABASE_URL = ""
|
|
21
|
+
SUPABASE_ANON_KEY = ""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BlogNotConfigured(RuntimeError):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class Post:
|
|
30
|
+
slug: str
|
|
31
|
+
title: str
|
|
32
|
+
genre: str
|
|
33
|
+
excerpt: str
|
|
34
|
+
published_at: str
|
|
35
|
+
reading_minutes: int
|
|
36
|
+
body_md: str = ""
|
|
37
|
+
|
|
38
|
+
def __str__(self) -> str:
|
|
39
|
+
return f"{self.published_at[:10]} [{self.genre}] {self.title}"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _config():
|
|
43
|
+
url = os.environ.get("HARSHIT_SUPABASE_URL") or SUPABASE_URL
|
|
44
|
+
key = os.environ.get("HARSHIT_SUPABASE_ANON_KEY") or SUPABASE_ANON_KEY
|
|
45
|
+
if not url or not key:
|
|
46
|
+
raise BlogNotConfigured(
|
|
47
|
+
"Blog is not configured. Set HARSHIT_SUPABASE_URL and HARSHIT_SUPABASE_ANON_KEY."
|
|
48
|
+
)
|
|
49
|
+
return url.rstrip("/"), key
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _get(path: str, params: dict) -> list:
|
|
53
|
+
url, key = _config()
|
|
54
|
+
query = urllib.parse.urlencode(params, safe="(),.*")
|
|
55
|
+
req = urllib.request.Request(
|
|
56
|
+
f"{url}/rest/v1/{path}?{query}",
|
|
57
|
+
headers={"apikey": key, "Authorization": f"Bearer {key}", "Accept": "application/json"},
|
|
58
|
+
)
|
|
59
|
+
try:
|
|
60
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
61
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
62
|
+
except urllib.error.URLError as exc:
|
|
63
|
+
raise ConnectionError(f"Could not reach the blog: {exc}") from exc
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _to_post(row: dict) -> Post:
|
|
67
|
+
return Post(
|
|
68
|
+
slug=row["slug"],
|
|
69
|
+
title=row["title"],
|
|
70
|
+
genre=row.get("genre", ""),
|
|
71
|
+
excerpt=row.get("excerpt", ""),
|
|
72
|
+
published_at=row.get("published_at") or "",
|
|
73
|
+
reading_minutes=row.get("reading_minutes") or 1,
|
|
74
|
+
body_md=row.get("body_md", ""),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def posts(genre: Optional[str] = None, limit: int = 20) -> List[Post]:
|
|
79
|
+
"""Latest published posts, newest first. Optionally filter by genre."""
|
|
80
|
+
params = {
|
|
81
|
+
"select": "slug,title,genre,excerpt,published_at,reading_minutes",
|
|
82
|
+
"order": "published_at.desc",
|
|
83
|
+
"limit": str(limit),
|
|
84
|
+
}
|
|
85
|
+
if genre:
|
|
86
|
+
params["genre"] = f"eq.{genre}"
|
|
87
|
+
return [_to_post(r) for r in _get("posts", params)]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def post(slug: str) -> Post:
|
|
91
|
+
"""One full post, including its Markdown body."""
|
|
92
|
+
rows = _get("posts", {"select": "*", "slug": f"eq.{slug}", "limit": "1"})
|
|
93
|
+
if not rows:
|
|
94
|
+
raise KeyError(f"No published post with slug {slug!r}")
|
|
95
|
+
return _to_post(rows[0])
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Command line interface for scoring a job description."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from . import __version__, match_description
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main(argv=None) -> int:
|
|
13
|
+
parser = argparse.ArgumentParser(
|
|
14
|
+
prog="harshitsingh",
|
|
15
|
+
description="Score how closely a job description matches Harshit's profile.",
|
|
16
|
+
)
|
|
17
|
+
parser.add_argument("--version", action="version", version=f"harshitsingh {__version__}")
|
|
18
|
+
parser.add_argument("description", nargs="*", help="job description text")
|
|
19
|
+
parser.add_argument("--file", "-f", help="read a long job description from a UTF-8 text file")
|
|
20
|
+
args = parser.parse_args(argv)
|
|
21
|
+
|
|
22
|
+
if args.file and args.description:
|
|
23
|
+
parser.error("pass either description text or --file, not both")
|
|
24
|
+
|
|
25
|
+
if args.file:
|
|
26
|
+
try:
|
|
27
|
+
description = Path(args.file).read_text(encoding="utf-8-sig")
|
|
28
|
+
except OSError as exc:
|
|
29
|
+
parser.error(f"cannot read {args.file!r}: {exc}")
|
|
30
|
+
elif args.description:
|
|
31
|
+
description = " ".join(args.description)
|
|
32
|
+
elif sys.stdin.isatty():
|
|
33
|
+
try:
|
|
34
|
+
description = input("Job description: ")
|
|
35
|
+
except EOFError:
|
|
36
|
+
description = ""
|
|
37
|
+
else:
|
|
38
|
+
description = sys.stdin.read()
|
|
39
|
+
|
|
40
|
+
if not description.strip():
|
|
41
|
+
parser.error("provide a job description as text, via --file, or on standard input")
|
|
42
|
+
|
|
43
|
+
result = match_description(description)
|
|
44
|
+
print(f"{result['percentage']}%")
|
|
45
|
+
return 0
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
if __name__ == "__main__":
|
|
49
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Offline job-description matching against a curated skill vocabulary."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Dict, Iterable, List, Tuple
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# Alternate spellings and common names used in job descriptions. These aliases
|
|
10
|
+
# map to a single profile skill, so mentioning several aliases never inflates
|
|
11
|
+
# the score.
|
|
12
|
+
PROFILE_ALIASES = {
|
|
13
|
+
"c++20": ("c++", "cpp", "c plus plus", "c++ 20"),
|
|
14
|
+
"python": ("python 3", "python3"),
|
|
15
|
+
"fastapi": ("fast api",),
|
|
16
|
+
"system design": ("systems design", "system architecture", "systems architecture", "software architecture"),
|
|
17
|
+
"backend engineering": (
|
|
18
|
+
"backend development", "back-end engineering", "back end engineering",
|
|
19
|
+
"server-side engineering", "server side development", "microservice", "microservices",
|
|
20
|
+
"api design", "api gateway", "api gateways",
|
|
21
|
+
),
|
|
22
|
+
"distributed systems": ("distributed system", "distributed architecture"),
|
|
23
|
+
"applied ai": ("artificial intelligence", "ai"),
|
|
24
|
+
"oracle": ("oracle database",),
|
|
25
|
+
"sql server": ("microsoft sql server", "mssql"),
|
|
26
|
+
"mongodb": ("mongo db",),
|
|
27
|
+
"mcp": ("model context protocol",),
|
|
28
|
+
"agent systems": ("ai agents", "agentic systems", "multi-agent systems"),
|
|
29
|
+
"execution engines": (
|
|
30
|
+
"execution engine", "trading engine", "trading systems", "low-latency trading",
|
|
31
|
+
"low latency trading", "high-frequency trading", "hft",
|
|
32
|
+
),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# A compact comparison vocabulary lets the score account for commonly stated
|
|
37
|
+
# requirements the profile does not list. Unknown words are deliberately
|
|
38
|
+
# ignored instead of being guessed as skills.
|
|
39
|
+
OTHER_SKILLS = {
|
|
40
|
+
"Java": ("Java SE",),
|
|
41
|
+
"JavaScript": ("JS",),
|
|
42
|
+
"TypeScript": ("TS",),
|
|
43
|
+
"Rust": (),
|
|
44
|
+
"C#": ("C sharp",),
|
|
45
|
+
"Kotlin": (),
|
|
46
|
+
"Swift": (),
|
|
47
|
+
"Golang": ("Go language",),
|
|
48
|
+
"Bash": ("shell scripting",),
|
|
49
|
+
"Django": (),
|
|
50
|
+
"Flask": (),
|
|
51
|
+
"Spring Boot": ("Spring",),
|
|
52
|
+
"Node.js": ("NodeJS", "Node JS"),
|
|
53
|
+
"Express.js": ("ExpressJS", "Express JS"),
|
|
54
|
+
"React": ("React.js", "ReactJS"),
|
|
55
|
+
"Next.js": ("NextJS", "Next JS"),
|
|
56
|
+
".NET": ("dotnet", "ASP.NET"),
|
|
57
|
+
"REST API": ("REST APIs", "RESTful API", "RESTful APIs"),
|
|
58
|
+
"GraphQL": (),
|
|
59
|
+
"gRPC": (),
|
|
60
|
+
"PostgreSQL": ("Postgres",),
|
|
61
|
+
"MySQL": (),
|
|
62
|
+
"Redis": (),
|
|
63
|
+
"SQLite": (),
|
|
64
|
+
"Kafka": ("Apache Kafka",),
|
|
65
|
+
"Apache Spark": ("Spark",),
|
|
66
|
+
"Docker": (),
|
|
67
|
+
"Kubernetes": ("K8s",),
|
|
68
|
+
"AWS": ("Amazon Web Services",),
|
|
69
|
+
"Azure": ("Microsoft Azure",),
|
|
70
|
+
"Google Cloud": ("GCP",),
|
|
71
|
+
"Terraform": (),
|
|
72
|
+
"Linux": (),
|
|
73
|
+
"CI/CD": ("continuous integration", "continuous delivery", "continuous deployment"),
|
|
74
|
+
"machine learning": ("ML",),
|
|
75
|
+
"deep learning": (),
|
|
76
|
+
"natural language processing": ("NLP",),
|
|
77
|
+
"large language models": ("LLM", "LLMs", "large language model"),
|
|
78
|
+
"PyTorch": (),
|
|
79
|
+
"TensorFlow": (),
|
|
80
|
+
"data engineering": (),
|
|
81
|
+
"algorithmic trading": (),
|
|
82
|
+
"market microstructure": (),
|
|
83
|
+
"FIX protocol": ("FIX",),
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
_REQUIRED_CUE = re.compile(
|
|
87
|
+
r"\b(?:required|requirements?|must(?:\s+have)?|minimum|essential|mandatory|"
|
|
88
|
+
r"proficient(?:\s+in)?|expertise|strong\s+(?:experience|knowledge|skills?)\s+(?:with|in))\b",
|
|
89
|
+
re.IGNORECASE,
|
|
90
|
+
)
|
|
91
|
+
_PREFERRED_CUE = re.compile(
|
|
92
|
+
r"\b(?:preferred|preferably|nice\s+to\s+have|bonus|ideally|optional|a\s+plus)\b",
|
|
93
|
+
re.IGNORECASE,
|
|
94
|
+
)
|
|
95
|
+
_NOT_REQUIRED_CUE = re.compile(r"\bnot\s+(?:required|necessary|essential)\b", re.IGNORECASE)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _phrase_pattern(phrase: str) -> re.Pattern:
|
|
99
|
+
"""Compile a case-insensitive phrase matcher with word-safe boundaries."""
|
|
100
|
+
words = re.split(r"\s+", phrase.strip())
|
|
101
|
+
body = r"\s+".join(re.escape(word) for word in words if word)
|
|
102
|
+
return re.compile(r"(?<![\w])" + body + r"(?![\w])", re.IGNORECASE)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _importance(text: str, start: int, end: int) -> float:
|
|
106
|
+
"""Weight explicit must-have language more than optional preferences."""
|
|
107
|
+
left = text[max(0, start - 96):start]
|
|
108
|
+
right = text[end:min(len(text), end + 48)]
|
|
109
|
+
context = left + " " + right
|
|
110
|
+
if _NOT_REQUIRED_CUE.search(context):
|
|
111
|
+
return 0.5
|
|
112
|
+
if _REQUIRED_CUE.search(left) or _REQUIRED_CUE.search(right):
|
|
113
|
+
return 1.5
|
|
114
|
+
if _PREFERRED_CUE.search(context):
|
|
115
|
+
return 0.5
|
|
116
|
+
return 1.0
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _catalog(profile_skills: Iterable) -> List[Tuple[str, bool, Tuple[re.Pattern, ...]]]:
|
|
120
|
+
profile_names = {skill.name.casefold(): skill.name for skill in profile_skills}
|
|
121
|
+
aliases_by_name: Dict[str, Tuple[str, ...]] = {}
|
|
122
|
+
|
|
123
|
+
for folded_name, canonical in profile_names.items():
|
|
124
|
+
aliases = PROFILE_ALIASES.get(folded_name, ())
|
|
125
|
+
aliases_by_name[canonical] = (canonical,) + tuple(aliases)
|
|
126
|
+
|
|
127
|
+
for canonical, aliases in OTHER_SKILLS.items():
|
|
128
|
+
if canonical.casefold() not in profile_names:
|
|
129
|
+
aliases_by_name[canonical] = (canonical,) + tuple(aliases)
|
|
130
|
+
|
|
131
|
+
catalog = []
|
|
132
|
+
for canonical, aliases in aliases_by_name.items():
|
|
133
|
+
patterns = tuple(_phrase_pattern(alias) for alias in aliases)
|
|
134
|
+
catalog.append((canonical, canonical in profile_names.values(), patterns))
|
|
135
|
+
return catalog
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def match_description(description: str, profile_skills: Iterable) -> dict:
|
|
139
|
+
"""Score a job description using distinct recognized skills.
|
|
140
|
+
|
|
141
|
+
The score is the weighted share of recognized skill requirements that are
|
|
142
|
+
present in the profile. Repeated mentions count once. Required skills carry
|
|
143
|
+
1.5 weight, optional/preferred ones carry 0.5, and other mentions carry 1.
|
|
144
|
+
"""
|
|
145
|
+
if not isinstance(description, str):
|
|
146
|
+
raise TypeError("description must be a string")
|
|
147
|
+
|
|
148
|
+
text = description.strip()
|
|
149
|
+
if not text:
|
|
150
|
+
return {
|
|
151
|
+
"score": 0.0,
|
|
152
|
+
"percentage": 0,
|
|
153
|
+
"matched": [],
|
|
154
|
+
"missing": [],
|
|
155
|
+
"recognized": [],
|
|
156
|
+
"recognized_count": 0,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
detected: Dict[str, Tuple[bool, float]] = {}
|
|
160
|
+
for canonical, is_profile_skill, patterns in _catalog(profile_skills):
|
|
161
|
+
weight = 0.0
|
|
162
|
+
for pattern in patterns:
|
|
163
|
+
for match in pattern.finditer(text):
|
|
164
|
+
weight = max(weight, _importance(text, match.start(), match.end()))
|
|
165
|
+
if weight:
|
|
166
|
+
detected[canonical] = (is_profile_skill, weight)
|
|
167
|
+
|
|
168
|
+
matched = sorted(name for name, (is_profile_skill, _) in detected.items() if is_profile_skill)
|
|
169
|
+
missing = sorted(name for name, (is_profile_skill, _) in detected.items() if not is_profile_skill)
|
|
170
|
+
total_weight = sum(weight for _, weight in detected.values())
|
|
171
|
+
matched_weight = sum(weight for is_profile_skill, weight in detected.values() if is_profile_skill)
|
|
172
|
+
score = matched_weight / total_weight if total_weight else 0.0
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
"score": round(score, 4),
|
|
176
|
+
"percentage": int(round(score * 100)),
|
|
177
|
+
"matched": matched,
|
|
178
|
+
"missing": missing,
|
|
179
|
+
"recognized": sorted(detected),
|
|
180
|
+
"recognized_count": len(detected),
|
|
181
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Small immutable value types used across the package."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import asdict, dataclass, field
|
|
6
|
+
from typing import Iterable
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class Skill:
|
|
11
|
+
name: str
|
|
12
|
+
category: str
|
|
13
|
+
note: str = ""
|
|
14
|
+
|
|
15
|
+
def __str__(self) -> str:
|
|
16
|
+
return self.name
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class Project:
|
|
21
|
+
name: str
|
|
22
|
+
summary: str
|
|
23
|
+
tags: tuple = ()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class Profile:
|
|
28
|
+
name: str
|
|
29
|
+
headline: str
|
|
30
|
+
employer: str
|
|
31
|
+
role: str
|
|
32
|
+
education: str
|
|
33
|
+
location: str
|
|
34
|
+
site: str
|
|
35
|
+
skills: tuple = field(default=(), compare=False)
|
|
36
|
+
projects: tuple = field(default=(), compare=False)
|
|
37
|
+
|
|
38
|
+
def has(self, skill: str) -> bool:
|
|
39
|
+
"""Case-insensitive check: does Harshit list this skill?"""
|
|
40
|
+
wanted = skill.strip().lower()
|
|
41
|
+
return any(s.name.lower() == wanted for s in self.skills)
|
|
42
|
+
|
|
43
|
+
def skills_by_category(self) -> dict:
|
|
44
|
+
grouped: dict = {}
|
|
45
|
+
for s in self.skills:
|
|
46
|
+
grouped.setdefault(s.category, []).append(s.name)
|
|
47
|
+
return grouped
|
|
48
|
+
|
|
49
|
+
def match(self, needed: Iterable[str]) -> dict:
|
|
50
|
+
"""Compare a list of required skills against this profile."""
|
|
51
|
+
needed = [n for n in needed]
|
|
52
|
+
have = [n for n in needed if self.has(n)]
|
|
53
|
+
missing = [n for n in needed if not self.has(n)]
|
|
54
|
+
return {"have": have, "missing": missing, "score": len(have) / len(needed) if needed else 0.0}
|
|
55
|
+
|
|
56
|
+
def match_description(self, description: str) -> dict:
|
|
57
|
+
"""Score a job description against this profile's skills.
|
|
58
|
+
|
|
59
|
+
The matcher uses a local, curated vocabulary of skill names and aliases;
|
|
60
|
+
it does not send the description to an external service.
|
|
61
|
+
"""
|
|
62
|
+
from .matching import match_description
|
|
63
|
+
|
|
64
|
+
return match_description(description, self.skills)
|
|
65
|
+
|
|
66
|
+
def to_dict(self) -> dict:
|
|
67
|
+
return asdict(self)
|
|
68
|
+
|
|
69
|
+
def card(self) -> str:
|
|
70
|
+
"""A plain-text card for terminals."""
|
|
71
|
+
rule = "-" * 56
|
|
72
|
+
lines = [
|
|
73
|
+
self.name,
|
|
74
|
+
self.headline,
|
|
75
|
+
rule,
|
|
76
|
+
f"{self.role}, {self.employer}",
|
|
77
|
+
f"{self.education} · {self.location}",
|
|
78
|
+
self.site,
|
|
79
|
+
rule,
|
|
80
|
+
]
|
|
81
|
+
for category, names in self.skills_by_category().items():
|
|
82
|
+
lines.append(f"{category:<10} {', '.join(names)}")
|
|
83
|
+
lines.append(rule)
|
|
84
|
+
for p in self.projects:
|
|
85
|
+
lines.append(f"{p.name}: {p.summary}")
|
|
86
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: harshitsingh
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Offline job-description relevance scoring against Harshit's profile.
|
|
5
|
+
Author: Harshit
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: profile,portfolio,resume,cli
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# harshitsingh
|
|
14
|
+
|
|
15
|
+
Harshit's profile as a small, offline job-description matcher. Give it a job description and it prints one relevance score.
|
|
16
|
+
|
|
17
|
+
```powershell
|
|
18
|
+
pip install harshitsingh
|
|
19
|
+
harshitsingh "Backend role requiring Python, FastAPI, system design, and distributed systems"
|
|
20
|
+
# 100%
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
For a longer description, save it as UTF-8 text and run:
|
|
24
|
+
|
|
25
|
+
```powershell
|
|
26
|
+
harshitsingh --file .\job-description.txt
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or pipe text to it:
|
|
30
|
+
|
|
31
|
+
```powershell
|
|
32
|
+
Get-Content .\job-description.txt -Raw | harshitsingh
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Running `harshitsingh` without arguments opens a one-line prompt. The command prints only the percentage, so its output is easy to copy or use in a script.
|
|
36
|
+
|
|
37
|
+
## Python API
|
|
38
|
+
|
|
39
|
+
The distribution name is `harshitsingh`; the import name is `harshit_profile`.
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from harshit_profile import match_description
|
|
43
|
+
|
|
44
|
+
result = match_description("Python backend role with FastAPI and distributed systems")
|
|
45
|
+
print(result["percentage"]) # 100
|
|
46
|
+
print(result["matched"]) # recognized strengths in Harshit's profile
|
|
47
|
+
print(result["missing"]) # recognized requirements not in the profile
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The matcher uses a local vocabulary of profile skills, aliases, and common engineering requirements. It counts each recognized skill once, gives explicit must-have requirements more weight, and discounts preferred skills. It makes no network calls and does not send job descriptions to an AI service. Unrecognized terms are ignored, so the score is a quick comparison aid rather than a hiring decision.
|
|
51
|
+
|
|
52
|
+
The profile data is also available as `me`, `skills`, and `projects`:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from harshit_profile import me, projects
|
|
56
|
+
|
|
57
|
+
me.name # "Harshit"
|
|
58
|
+
[project.name for project in projects] # includes "nazarGeo"
|
|
59
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/harshit_profile/__init__.py
|
|
4
|
+
src/harshit_profile/__main__.py
|
|
5
|
+
src/harshit_profile/_data.py
|
|
6
|
+
src/harshit_profile/blog.py
|
|
7
|
+
src/harshit_profile/cli.py
|
|
8
|
+
src/harshit_profile/matching.py
|
|
9
|
+
src/harshit_profile/models.py
|
|
10
|
+
src/harshitsingh.egg-info/PKG-INFO
|
|
11
|
+
src/harshitsingh.egg-info/SOURCES.txt
|
|
12
|
+
src/harshitsingh.egg-info/dependency_links.txt
|
|
13
|
+
src/harshitsingh.egg-info/entry_points.txt
|
|
14
|
+
src/harshitsingh.egg-info/top_level.txt
|
|
15
|
+
tests/test_profile.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
harshit_profile
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import unittest
|
|
3
|
+
|
|
4
|
+
import harshit_profile as hp
|
|
5
|
+
from harshit_profile import blog, cli
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ProfileTests(unittest.TestCase):
|
|
9
|
+
def test_has_is_case_insensitive(self):
|
|
10
|
+
self.assertTrue(hp.me.has("python"))
|
|
11
|
+
self.assertTrue(hp.me.has(" C++20 "))
|
|
12
|
+
self.assertFalse(hp.me.has("cobol"))
|
|
13
|
+
|
|
14
|
+
def test_match_reports_gaps(self):
|
|
15
|
+
result = hp.me.match(["Python", "Rust", "MongoDB"])
|
|
16
|
+
self.assertEqual(result["missing"], ["Rust"])
|
|
17
|
+
self.assertAlmostEqual(result["score"], 2 / 3)
|
|
18
|
+
|
|
19
|
+
def test_match_empty_is_zero(self):
|
|
20
|
+
self.assertEqual(hp.me.match([])["score"], 0.0)
|
|
21
|
+
|
|
22
|
+
def test_grouping_covers_every_skill(self):
|
|
23
|
+
grouped = hp.me.skills_by_category()
|
|
24
|
+
self.assertEqual(sum(len(v) for v in grouped.values()), len(hp.skills))
|
|
25
|
+
|
|
26
|
+
def test_json_roundtrip(self):
|
|
27
|
+
data = json.loads(json.dumps(hp.me.to_dict()))
|
|
28
|
+
self.assertEqual(data["name"], "Harshit")
|
|
29
|
+
self.assertEqual(len(data["projects"]), len(hp.projects))
|
|
30
|
+
|
|
31
|
+
def test_card_mentions_projects(self):
|
|
32
|
+
card = hp.me.card()
|
|
33
|
+
for p in hp.projects:
|
|
34
|
+
self.assertIn(p.name, card)
|
|
35
|
+
|
|
36
|
+
def test_blog_requires_config(self):
|
|
37
|
+
import os
|
|
38
|
+
|
|
39
|
+
for k in ("HARSHIT_SUPABASE_URL", "HARSHIT_SUPABASE_ANON_KEY"):
|
|
40
|
+
os.environ.pop(k, None)
|
|
41
|
+
with self.assertRaises(blog.BlogNotConfigured):
|
|
42
|
+
blog.posts()
|
|
43
|
+
|
|
44
|
+
def test_cli_default_prints_card(self):
|
|
45
|
+
self.assertEqual(cli.main([]), 0)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
if __name__ == "__main__":
|
|
49
|
+
unittest.main()
|