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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -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,2 @@
1
+ [console_scripts]
2
+ harshitsingh = harshit_profile.cli:main
@@ -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()