jobhunter-cli 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.
Potentially problematic release.
This version of jobhunter-cli might be problematic. Click here for more details.
- jobhunter_cli-0.1.0/.env.example +6 -0
- jobhunter_cli-0.1.0/.github/workflows/job-maintenance.yml +78 -0
- jobhunter_cli-0.1.0/.github/workflows/job-pipeline.yml +139 -0
- jobhunter_cli-0.1.0/.gitignore +23 -0
- jobhunter_cli-0.1.0/.python-version +1 -0
- jobhunter_cli-0.1.0/LICENSE +21 -0
- jobhunter_cli-0.1.0/PKG-INFO +126 -0
- jobhunter_cli-0.1.0/README.md +109 -0
- jobhunter_cli-0.1.0/config/.gitkeep +0 -0
- jobhunter_cli-0.1.0/data/.gitkeep +0 -0
- jobhunter_cli-0.1.0/docs/architecture.md +115 -0
- jobhunter_cli-0.1.0/docs/configuration.md +304 -0
- jobhunter_cli-0.1.0/examples/config.example.json +96 -0
- jobhunter_cli-0.1.0/examples/cv.example.md +24 -0
- jobhunter_cli-0.1.0/examples/preferences.example.md +19 -0
- jobhunter_cli-0.1.0/pyproject.toml +48 -0
- jobhunter_cli-0.1.0/src/jobhunter/__init__.py +0 -0
- jobhunter_cli-0.1.0/src/jobhunter/__main__.py +4 -0
- jobhunter_cli-0.1.0/src/jobhunter/core/__init__.py +0 -0
- jobhunter_cli-0.1.0/src/jobhunter/core/filters.py +110 -0
- jobhunter_cli-0.1.0/src/jobhunter/core/http.py +57 -0
- jobhunter_cli-0.1.0/src/jobhunter/core/logger.py +54 -0
- jobhunter_cli-0.1.0/src/jobhunter/core/paths.py +124 -0
- jobhunter_cli-0.1.0/src/jobhunter/core/settings.py +123 -0
- jobhunter_cli-0.1.0/src/jobhunter/core/textutil.py +33 -0
- jobhunter_cli-0.1.0/src/jobhunter/main.py +94 -0
- jobhunter_cli-0.1.0/src/jobhunter/models.py +81 -0
- jobhunter_cli-0.1.0/src/jobhunter/pipeline/__init__.py +0 -0
- jobhunter_cli-0.1.0/src/jobhunter/pipeline/evaluate.py +284 -0
- jobhunter_cli-0.1.0/src/jobhunter/pipeline/fetch.py +233 -0
- jobhunter_cli-0.1.0/src/jobhunter/pipeline/send.py +191 -0
- jobhunter_cli-0.1.0/src/jobhunter/sources/__init__.py +21 -0
- jobhunter_cli-0.1.0/src/jobhunter/sources/arbeitnow.py +77 -0
- jobhunter_cli-0.1.0/src/jobhunter/sources/ashby.py +75 -0
- jobhunter_cli-0.1.0/src/jobhunter/sources/base.py +108 -0
- jobhunter_cli-0.1.0/src/jobhunter/sources/greenhouse.py +70 -0
- jobhunter_cli-0.1.0/src/jobhunter/sources/lever.py +85 -0
- jobhunter_cli-0.1.0/src/jobhunter/sources/personio.py +124 -0
- jobhunter_cli-0.1.0/src/jobhunter/sources/recruitee.py +80 -0
- jobhunter_cli-0.1.0/src/jobhunter/sources/remoteok.py +69 -0
- jobhunter_cli-0.1.0/src/jobhunter/sources/remotive.py +61 -0
- jobhunter_cli-0.1.0/src/jobhunter/sources/workable.py +94 -0
- jobhunter_cli-0.1.0/src/jobhunter/tools/__init__.py +0 -0
- jobhunter_cli-0.1.0/src/jobhunter/tools/discover.py +275 -0
- jobhunter_cli-0.1.0/src/jobhunter/tools/prompt.py +106 -0
- jobhunter_cli-0.1.0/src/jobhunter/tools/prune.py +124 -0
- jobhunter_cli-0.1.0/uv.lock +637 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Telegram: create a bot at https://t.me/BotFather and get the bot token, then start a chat with your bot and get the chat ID.
|
|
2
|
+
TELEGRAM_BOT_TOKEN=
|
|
3
|
+
TELEGRAM_CHAT_ID=
|
|
4
|
+
|
|
5
|
+
# OpenAI: create an account at https://platform.openai.com/ and generate an API key.
|
|
6
|
+
LLM_API_KEY=
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
name: Job Maintenance & Discovery
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
schedule:
|
|
5
|
+
# Runs once a week: every Sunday at 3:00 AM UTC
|
|
6
|
+
- cron: '0 3 * * 0'
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
inputs:
|
|
9
|
+
source:
|
|
10
|
+
description: 'Run discovery/prune for a specific source only (optional)'
|
|
11
|
+
type: string
|
|
12
|
+
required: false
|
|
13
|
+
default: ''
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
maintenance:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
permissions:
|
|
19
|
+
contents: write
|
|
20
|
+
|
|
21
|
+
steps:
|
|
22
|
+
- name: Checkout repository
|
|
23
|
+
uses: actions/checkout@v4
|
|
24
|
+
|
|
25
|
+
- name: Set up UV and Python
|
|
26
|
+
uses: astral-sh/setup-uv@v5
|
|
27
|
+
with:
|
|
28
|
+
enable-cache: true
|
|
29
|
+
version: "latest"
|
|
30
|
+
|
|
31
|
+
- name: Set up Python
|
|
32
|
+
uses: actions/setup-python@v5
|
|
33
|
+
with:
|
|
34
|
+
python-version-file: ".python-version"
|
|
35
|
+
cache: "uv"
|
|
36
|
+
|
|
37
|
+
- name: Install dependencies
|
|
38
|
+
run: uv sync
|
|
39
|
+
|
|
40
|
+
- name: Run Discover and Prune if config.json exists
|
|
41
|
+
run: |
|
|
42
|
+
if [ ! -f "config/config.json" ]; then
|
|
43
|
+
echo "config/config.json does not exist. Skipping maintenance tasks."
|
|
44
|
+
exit 0
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
SOURCE_FLAG=""
|
|
48
|
+
if [ "${{ github.event_name }}" == "workflow_dispatch" ] && [ -n "${{ inputs.source }}" ]; then
|
|
49
|
+
SOURCE_FLAG="--source ${{ inputs.source }}"
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
echo "Running discovery..."
|
|
53
|
+
uv run jobhunter discover --write $SOURCE_FLAG
|
|
54
|
+
|
|
55
|
+
echo "Running pruning..."
|
|
56
|
+
uv run jobhunter prune --write $SOURCE_FLAG
|
|
57
|
+
|
|
58
|
+
- name: Commit and push updated config.json
|
|
59
|
+
run: |
|
|
60
|
+
if [ ! -f "config/config.json" ]; then
|
|
61
|
+
exit 0
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
git config --global user.name "github-actions[bot]"
|
|
65
|
+
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
66
|
+
|
|
67
|
+
git status
|
|
68
|
+
|
|
69
|
+
if [ -f "config/config.json" ]; then
|
|
70
|
+
git add config/config.json
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
if git diff --staged --quiet; then
|
|
74
|
+
echo "No changes in config.json to commit."
|
|
75
|
+
else
|
|
76
|
+
git commit -m "chore(maintenance): update company board configuration [skip ci]"
|
|
77
|
+
git push
|
|
78
|
+
fi
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
name: Job Pipeline
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
schedule:
|
|
5
|
+
# 1. First daily run (Aggregators ENABLED)
|
|
6
|
+
- cron: "0 6 * * *"
|
|
7
|
+
# 2. Subsequent daily runs (Aggregators DISABLED)
|
|
8
|
+
- cron: "0 14,22 * * *"
|
|
9
|
+
workflow_dispatch:
|
|
10
|
+
inputs:
|
|
11
|
+
dry_run:
|
|
12
|
+
description: "Run pipeline in dry-run mode (no telegram notifications sent)"
|
|
13
|
+
type: boolean
|
|
14
|
+
required: false
|
|
15
|
+
default: false
|
|
16
|
+
ignore_seen:
|
|
17
|
+
description: "Ignore seen history cache and re-process recent jobs"
|
|
18
|
+
type: boolean
|
|
19
|
+
required: false
|
|
20
|
+
default: false
|
|
21
|
+
no_aggregators:
|
|
22
|
+
description: "Disable aggregator sources (Remotive, RemoteOK, Arbeitnow)"
|
|
23
|
+
type: boolean
|
|
24
|
+
required: false
|
|
25
|
+
default: true
|
|
26
|
+
source:
|
|
27
|
+
description: "Run for a specific source/company only (optional)"
|
|
28
|
+
type: string
|
|
29
|
+
required: false
|
|
30
|
+
default: ""
|
|
31
|
+
limit:
|
|
32
|
+
description: "Limit maximum candidates per run (optional)"
|
|
33
|
+
type: number
|
|
34
|
+
required: false
|
|
35
|
+
default: 0
|
|
36
|
+
|
|
37
|
+
jobs:
|
|
38
|
+
run-pipeline:
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
permissions:
|
|
41
|
+
contents: write
|
|
42
|
+
|
|
43
|
+
steps:
|
|
44
|
+
- name: Checkout repository
|
|
45
|
+
uses: actions/checkout@v4
|
|
46
|
+
|
|
47
|
+
- name: Set up UV and Python
|
|
48
|
+
uses: astral-sh/setup-uv@v5
|
|
49
|
+
with:
|
|
50
|
+
enable-cache: true
|
|
51
|
+
version: "latest"
|
|
52
|
+
|
|
53
|
+
- name: Set up Python
|
|
54
|
+
uses: actions/setup-python@v5
|
|
55
|
+
with:
|
|
56
|
+
python-version-file: ".python-version"
|
|
57
|
+
|
|
58
|
+
- name: Install dependencies
|
|
59
|
+
run: uv sync
|
|
60
|
+
|
|
61
|
+
- name: Check existing state files before run
|
|
62
|
+
id: check-state
|
|
63
|
+
run: |
|
|
64
|
+
if [ -f "data/candidates.json" ]; then
|
|
65
|
+
echo "candidates_exists=true" >> $GITHUB_OUTPUT
|
|
66
|
+
else
|
|
67
|
+
echo "candidates_exists=false" >> $GITHUB_OUTPUT
|
|
68
|
+
fi
|
|
69
|
+
if [ -f "data/evaluated.json" ]; then
|
|
70
|
+
echo "evaluated_exists=true" >> $GITHUB_OUTPUT
|
|
71
|
+
else
|
|
72
|
+
echo "evaluated_exists=false" >> $GITHUB_OUTPUT
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
- name: Run JobHunter Pipeline
|
|
76
|
+
env:
|
|
77
|
+
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
|
78
|
+
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
|
|
79
|
+
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
|
|
80
|
+
CONFIG_JSON: ${{ secrets.CONFIG_JSON }}
|
|
81
|
+
CV: ${{ secrets.CV }}
|
|
82
|
+
PREFERENCES: ${{ secrets.PREFERENCES }}
|
|
83
|
+
run: |
|
|
84
|
+
CMD="uv run jobhunter run"
|
|
85
|
+
|
|
86
|
+
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
|
87
|
+
if [ "${{ inputs.no_aggregators }}" == "true" ]; then
|
|
88
|
+
CMD="$CMD --no-aggregators"
|
|
89
|
+
fi
|
|
90
|
+
if [ "${{ inputs.dry_run }}" == "true" ]; then
|
|
91
|
+
CMD="$CMD --dry-run"
|
|
92
|
+
fi
|
|
93
|
+
if [ "${{ inputs.ignore_seen }}" == "true" ]; then
|
|
94
|
+
CMD="$CMD --ignore-seen"
|
|
95
|
+
fi
|
|
96
|
+
if [ -n "${{ inputs.source }}" ]; then
|
|
97
|
+
CMD="$CMD --source ${{ inputs.source }}"
|
|
98
|
+
fi
|
|
99
|
+
if [ -n "${{ inputs.limit }}" ] && [ "${{ inputs.limit }}" -gt 0 ]; then
|
|
100
|
+
CMD="$CMD --limit ${{ inputs.limit }}"
|
|
101
|
+
fi
|
|
102
|
+
elif [ "${{ github.event_name }}" == "schedule" ]; then
|
|
103
|
+
if [ "${{ github.event.schedule }}" == "0 6 * * *" ]; then
|
|
104
|
+
echo "Morning schedule triggered: Aggregators ENABLED"
|
|
105
|
+
else
|
|
106
|
+
echo "Subsequent schedule triggered: Aggregators DISABLED"
|
|
107
|
+
CMD="$CMD --no-aggregators"
|
|
108
|
+
fi
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
echo "Executing: $CMD"
|
|
112
|
+
eval $CMD
|
|
113
|
+
|
|
114
|
+
- name: Commit and push state files
|
|
115
|
+
env:
|
|
116
|
+
CANDIDATES_EXISTS: ${{ steps.check-state.outputs.candidates_exists }}
|
|
117
|
+
EVALUATED_EXISTS: ${{ steps.check-state.outputs.evaluated_exists }}
|
|
118
|
+
run: |
|
|
119
|
+
git config --global user.name "github-actions[bot]"
|
|
120
|
+
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
121
|
+
|
|
122
|
+
git status
|
|
123
|
+
|
|
124
|
+
git add data/seen.json
|
|
125
|
+
|
|
126
|
+
if [ "$CANDIDATES_EXISTS" == "true" ] && [ -f "data/candidates.json" ]; then
|
|
127
|
+
git add data/candidates.json
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
if [ "$EVALUATED_EXISTS" == "true" ] && [ -f "data/evaluated.json" ]; then
|
|
131
|
+
git add data/evaluated.json
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
if git diff --staged --quiet; then
|
|
135
|
+
echo "No state file changes to commit."
|
|
136
|
+
else
|
|
137
|
+
git commit -m "chore(pipeline): update jobhunter state files [skip ci]"
|
|
138
|
+
git push
|
|
139
|
+
fi
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Python-generated files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[oc]
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
wheels/
|
|
7
|
+
*.egg-info
|
|
8
|
+
.locks
|
|
9
|
+
logs
|
|
10
|
+
todo.txt
|
|
11
|
+
|
|
12
|
+
# Virtual environments
|
|
13
|
+
.venv
|
|
14
|
+
|
|
15
|
+
# Secrets and personal files
|
|
16
|
+
.env
|
|
17
|
+
config/*
|
|
18
|
+
!config/.gitkeep
|
|
19
|
+
|
|
20
|
+
# Generated by the tool. Only data/seen.json is meant to be committed (the GitHub Actions workflow does it).
|
|
21
|
+
data/*
|
|
22
|
+
!data/.gitkeep
|
|
23
|
+
!data/seen.json
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.14
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ziad Aboughaleb
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: jobhunter-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: An AI-powered, zero-database Python CLI that automates job hunting. It scrapes ATS boards, evaluates candidate-job fit using LLMs against your CV, and dispatches real-time Telegram alerts.
|
|
5
|
+
Author-email: ziad-aboughaleb <ziadaboughaleb@gmail.com>
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.14
|
|
8
|
+
Requires-Dist: ddgs>=9.16.1
|
|
9
|
+
Requires-Dist: filelock>=4.2.4
|
|
10
|
+
Requires-Dist: httpx>=0.28.1
|
|
11
|
+
Requires-Dist: openai>=3.41.0
|
|
12
|
+
Requires-Dist: pydantic-settings>=2.16.1
|
|
13
|
+
Requires-Dist: pydantic>=2.14.1
|
|
14
|
+
Requires-Dist: rich>=15.0.0
|
|
15
|
+
Requires-Dist: typer>=0.27.3
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# JobHunter 🎯
|
|
19
|
+
|
|
20
|
+
> **AI-Powered, Highly Concurrent, Zero-Database Automated Job Hunting Pipeline**
|
|
21
|
+
|
|
22
|
+
`JobHunter` is a robust, production-grade Python CLI application designed to fully automate the job search and filtering lifecycle. It continuously scrapes Applicant Tracking Systems (ATS) and job boards, filters opportunities against precise regex and keyword criteria, evaluates candidate-job fit using Large Language Models (LLMs) with strict JSON schemas, and dispatches rich real-time alerts directly to Telegram.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 🚀 Key Features
|
|
27
|
+
|
|
28
|
+
- **High Concurrency & Rate Limiting**: Powered by `concurrent.futures` with dynamic worker bounds, exponential backoff retries on transient network errors, and intelligent request pacing.
|
|
29
|
+
- **ATS WAF & API Bypass**: Bypasses heavy frontend anti-bot walls by querying official public underlying JSON/XML board APIs directly (Greenhouse, Lever, Personio, Workable, Recruitee, Ashby, plus aggregators like Remotive, RemoteOK, and Arbeitnow).
|
|
30
|
+
- **Multi-Stage Filtering**: Filters jobs through regex word-boundary matching for titles, excluded companies, location rules, remote requirements, posting age, and minimum tech stack keyword hits (`stack_hits`).
|
|
31
|
+
- **AI-Driven Evaluation**: Evaluates shortlisted candidates against your exact CV (`config/cv.md`) and preferences (`config/preferences.md`) using OpenAI-compatible LLMs, enforcing hard gates, scoring rubrics, and anti-hallucination guardrails.
|
|
32
|
+
- **Telegram Rich Alerts**: Dispatches beautifully formatted HTML notifications featuring VIP priority badges, score color-coding, AI reasoning, score breakdowns, professional pitches, and direct links.
|
|
33
|
+
- **Autonomous Maintenance Tools**: Includes built-in CLI commands to automatically discover new company career boards via DuckDuckGo and prune dead or broken board URLs.
|
|
34
|
+
- **Zero-Database State Management**: Uses atomic file writes with file locking (`filelock`) and a rolling 90-day retention cache (`seen.json`) to prevent duplicate processing without requiring an external database.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 🛠️ Quickstart Guide
|
|
39
|
+
|
|
40
|
+
### Prerequisites
|
|
41
|
+
|
|
42
|
+
- **Python >= 3.14**
|
|
43
|
+
- **uv** (recommended Python package and project manager)
|
|
44
|
+
|
|
45
|
+
### 1. Clone & Install
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
git clone https://github.com/ziad-aboughaleb/job-hunter-v2.git
|
|
49
|
+
cd job-hunter-v2
|
|
50
|
+
uv sync
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 2. Configure Environment Variables
|
|
54
|
+
|
|
55
|
+
Copy the example environment file and add your API keys:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
cp .env.example .env
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Edit `.env`:
|
|
62
|
+
|
|
63
|
+
```env
|
|
64
|
+
TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here
|
|
65
|
+
TELEGRAM_CHAT_ID=your_telegram_chat_id_here
|
|
66
|
+
LLM_API_KEY=your_openai_or_compatible_api_key_here
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 3. Set Up Configuration & Profile
|
|
70
|
+
|
|
71
|
+
Create or copy configuration files in the `config/` directory (examples are provided in `examples/`):
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
cp examples/config.example.json config/config.json
|
|
75
|
+
cp examples/preferences.example.md config/preferences.md
|
|
76
|
+
# Create your cv.md in config/
|
|
77
|
+
cp examples/cv.example.json config/cv.md # (rename or write as Markdown)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
> **💡 Note on Environment Fallbacks & GitHub Actions:**
|
|
81
|
+
> `JobHunter` automatically falls back to environment variables (`CONFIG_JSON`, `CV`, `PREFERENCES`) if physical configuration files do not exist on disk. This allows deploying via GitHub Actions on public repositories using GitHub Secrets instead of committing sensitive files.
|
|
82
|
+
|
|
83
|
+
### 4. Run the Pipeline
|
|
84
|
+
|
|
85
|
+
Execute the full pipeline (Fetch -> Evaluate -> Send):
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
uv run jobhunter run
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 🔄 The 5-Stage Pipeline
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
[Discover / Configure] ──> [Fetch & Filter] ──> [AI Evaluate] ──> [Send to Telegram] ──> [State Purge & Seen Sync]
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
1. **Discover / Configure**: Resolve targets from `config.json` or discover new company boards using `jobhunter discover`.
|
|
100
|
+
2. **Fetch**: Scrape raw job postings concurrently across configured ATS boards and aggregators.
|
|
101
|
+
3. **Filter & Prune**: Discard duplicates, previously seen jobs, and postings failing title, location, age, or stack keyword criteria. Rank candidates by VIP priority and stack keyword hits.
|
|
102
|
+
4. **Evaluate**: Submit shortlisted candidates to the LLM. Enforce hard gates and score against your CV and preferences.
|
|
103
|
+
5. **Send & Purge**: Dispatch passing job alerts to Telegram. Log processed jobs to `seen.json` with a 90-day retention window and purge completed items from active state files.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## 📖 Documentation Deep Dives
|
|
108
|
+
|
|
109
|
+
For comprehensive technical documentation, consult the `docs/` folder:
|
|
110
|
+
|
|
111
|
+
- **[Architecture Guide](docs/architecture.md)**: Detailed breakdown of the decoupled pipeline modules, concurrency model, and zero-database state lifecycle.
|
|
112
|
+
- **[Configuration & Settings](docs/configuration.md)**: Comprehensive guide to `config.json`, Pydantic settings, environment variables, and prompt engineering setup (`cv.md` / `preferences.md`).
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## 💻 CLI Commands Reference
|
|
117
|
+
|
|
118
|
+
| Command | Description | Key Options |
|
|
119
|
+
| :------------------- | :---------------------------------------------------- | :------------------------------------------------------------------------------ |
|
|
120
|
+
| `jobhunter run` | Runs the full pipeline (fetch -> evaluate -> send) | `-s, --source`, `--no-aggregators`, `--ignore-seen`, `-l, --limit`, `--dry-run` |
|
|
121
|
+
| `jobhunter fetch` | Scrapes and filters jobs from configured boards | `-s, --source`, `--no-aggregators`, `--ignore-seen`, `-l, --limit` |
|
|
122
|
+
| `jobhunter evaluate` | Scores and evaluates pending candidates using AI | `-j, --job <UID>`, `-l, --limit` |
|
|
123
|
+
| `jobhunter send` | Dispatches passing job evaluations to Telegram | `--dry-run` |
|
|
124
|
+
| `jobhunter discover` | Discovers new company ATS slugs via DuckDuckGo | `-w, --write`, `-s, --source`, `-t, --titles` |
|
|
125
|
+
| `jobhunter prune` | Validates and prunes broken company slugs from config | `-w, --write`, `-s, --source` |
|
|
126
|
+
| `jobhunter prompt` | Prints the compiled AI evaluation system prompt | None |
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# JobHunter 🎯
|
|
2
|
+
|
|
3
|
+
> **AI-Powered, Highly Concurrent, Zero-Database Automated Job Hunting Pipeline**
|
|
4
|
+
|
|
5
|
+
`JobHunter` is a robust, production-grade Python CLI application designed to fully automate the job search and filtering lifecycle. It continuously scrapes Applicant Tracking Systems (ATS) and job boards, filters opportunities against precise regex and keyword criteria, evaluates candidate-job fit using Large Language Models (LLMs) with strict JSON schemas, and dispatches rich real-time alerts directly to Telegram.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 🚀 Key Features
|
|
10
|
+
|
|
11
|
+
- **High Concurrency & Rate Limiting**: Powered by `concurrent.futures` with dynamic worker bounds, exponential backoff retries on transient network errors, and intelligent request pacing.
|
|
12
|
+
- **ATS WAF & API Bypass**: Bypasses heavy frontend anti-bot walls by querying official public underlying JSON/XML board APIs directly (Greenhouse, Lever, Personio, Workable, Recruitee, Ashby, plus aggregators like Remotive, RemoteOK, and Arbeitnow).
|
|
13
|
+
- **Multi-Stage Filtering**: Filters jobs through regex word-boundary matching for titles, excluded companies, location rules, remote requirements, posting age, and minimum tech stack keyword hits (`stack_hits`).
|
|
14
|
+
- **AI-Driven Evaluation**: Evaluates shortlisted candidates against your exact CV (`config/cv.md`) and preferences (`config/preferences.md`) using OpenAI-compatible LLMs, enforcing hard gates, scoring rubrics, and anti-hallucination guardrails.
|
|
15
|
+
- **Telegram Rich Alerts**: Dispatches beautifully formatted HTML notifications featuring VIP priority badges, score color-coding, AI reasoning, score breakdowns, professional pitches, and direct links.
|
|
16
|
+
- **Autonomous Maintenance Tools**: Includes built-in CLI commands to automatically discover new company career boards via DuckDuckGo and prune dead or broken board URLs.
|
|
17
|
+
- **Zero-Database State Management**: Uses atomic file writes with file locking (`filelock`) and a rolling 90-day retention cache (`seen.json`) to prevent duplicate processing without requiring an external database.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 🛠️ Quickstart Guide
|
|
22
|
+
|
|
23
|
+
### Prerequisites
|
|
24
|
+
|
|
25
|
+
- **Python >= 3.14**
|
|
26
|
+
- **uv** (recommended Python package and project manager)
|
|
27
|
+
|
|
28
|
+
### 1. Clone & Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
git clone https://github.com/ziad-aboughaleb/job-hunter-v2.git
|
|
32
|
+
cd job-hunter-v2
|
|
33
|
+
uv sync
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 2. Configure Environment Variables
|
|
37
|
+
|
|
38
|
+
Copy the example environment file and add your API keys:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
cp .env.example .env
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Edit `.env`:
|
|
45
|
+
|
|
46
|
+
```env
|
|
47
|
+
TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here
|
|
48
|
+
TELEGRAM_CHAT_ID=your_telegram_chat_id_here
|
|
49
|
+
LLM_API_KEY=your_openai_or_compatible_api_key_here
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 3. Set Up Configuration & Profile
|
|
53
|
+
|
|
54
|
+
Create or copy configuration files in the `config/` directory (examples are provided in `examples/`):
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
cp examples/config.example.json config/config.json
|
|
58
|
+
cp examples/preferences.example.md config/preferences.md
|
|
59
|
+
# Create your cv.md in config/
|
|
60
|
+
cp examples/cv.example.json config/cv.md # (rename or write as Markdown)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
> **💡 Note on Environment Fallbacks & GitHub Actions:**
|
|
64
|
+
> `JobHunter` automatically falls back to environment variables (`CONFIG_JSON`, `CV`, `PREFERENCES`) if physical configuration files do not exist on disk. This allows deploying via GitHub Actions on public repositories using GitHub Secrets instead of committing sensitive files.
|
|
65
|
+
|
|
66
|
+
### 4. Run the Pipeline
|
|
67
|
+
|
|
68
|
+
Execute the full pipeline (Fetch -> Evaluate -> Send):
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
uv run jobhunter run
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 🔄 The 5-Stage Pipeline
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
[Discover / Configure] ──> [Fetch & Filter] ──> [AI Evaluate] ──> [Send to Telegram] ──> [State Purge & Seen Sync]
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
1. **Discover / Configure**: Resolve targets from `config.json` or discover new company boards using `jobhunter discover`.
|
|
83
|
+
2. **Fetch**: Scrape raw job postings concurrently across configured ATS boards and aggregators.
|
|
84
|
+
3. **Filter & Prune**: Discard duplicates, previously seen jobs, and postings failing title, location, age, or stack keyword criteria. Rank candidates by VIP priority and stack keyword hits.
|
|
85
|
+
4. **Evaluate**: Submit shortlisted candidates to the LLM. Enforce hard gates and score against your CV and preferences.
|
|
86
|
+
5. **Send & Purge**: Dispatch passing job alerts to Telegram. Log processed jobs to `seen.json` with a 90-day retention window and purge completed items from active state files.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## 📖 Documentation Deep Dives
|
|
91
|
+
|
|
92
|
+
For comprehensive technical documentation, consult the `docs/` folder:
|
|
93
|
+
|
|
94
|
+
- **[Architecture Guide](docs/architecture.md)**: Detailed breakdown of the decoupled pipeline modules, concurrency model, and zero-database state lifecycle.
|
|
95
|
+
- **[Configuration & Settings](docs/configuration.md)**: Comprehensive guide to `config.json`, Pydantic settings, environment variables, and prompt engineering setup (`cv.md` / `preferences.md`).
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 💻 CLI Commands Reference
|
|
100
|
+
|
|
101
|
+
| Command | Description | Key Options |
|
|
102
|
+
| :------------------- | :---------------------------------------------------- | :------------------------------------------------------------------------------ |
|
|
103
|
+
| `jobhunter run` | Runs the full pipeline (fetch -> evaluate -> send) | `-s, --source`, `--no-aggregators`, `--ignore-seen`, `-l, --limit`, `--dry-run` |
|
|
104
|
+
| `jobhunter fetch` | Scrapes and filters jobs from configured boards | `-s, --source`, `--no-aggregators`, `--ignore-seen`, `-l, --limit` |
|
|
105
|
+
| `jobhunter evaluate` | Scores and evaluates pending candidates using AI | `-j, --job <UID>`, `-l, --limit` |
|
|
106
|
+
| `jobhunter send` | Dispatches passing job evaluations to Telegram | `--dry-run` |
|
|
107
|
+
| `jobhunter discover` | Discovers new company ATS slugs via DuckDuckGo | `-w, --write`, `-s, --source`, `-t, --titles` |
|
|
108
|
+
| `jobhunter prune` | Validates and prunes broken company slugs from config | `-w, --write`, `-s, --source` |
|
|
109
|
+
| `jobhunter prompt` | Prints the compiled AI evaluation system prompt | None |
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Architecture & Pipeline Deep Dive
|
|
2
|
+
|
|
3
|
+
`JobHunter` is engineered as a decoupled, highly concurrent, zero-database Python pipeline. This document provides a deep architectural breakdown of its modules, state management lifecycle, and concurrency model.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🏗️ Core Module Architecture
|
|
8
|
+
|
|
9
|
+
The application logic is organized into clean, single-responsibility modules located in `src/jobhunter/`:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
src/jobhunter/
|
|
13
|
+
├── main.py # Typer CLI entrypoint and command router
|
|
14
|
+
├── models.py # Pydantic data models (Job, Candidate, EvaluationResult, EvaluatedCandidate)
|
|
15
|
+
├── core/
|
|
16
|
+
│ ├── filters.py # Regex word-boundary filters and scoring utilities
|
|
17
|
+
│ ├── http.py # Robust HTTP client with retries and exponential backoff
|
|
18
|
+
│ ├── logger.py # Rich console and logging configuration
|
|
19
|
+
│ ├── paths.py # Atomic file I/O, path resolution, and file locking
|
|
20
|
+
│ ├── settings.py # Pydantic Settings & configuration loading
|
|
21
|
+
│ └── textutil.py # HTML cleaning and date parsing utilities
|
|
22
|
+
├── pipeline/
|
|
23
|
+
│ ├── evaluate.py # Stage 4: AI evaluation via OpenAI/LLM API
|
|
24
|
+
│ ├── fetch.py # Stage 1-3: Scraping, filtering, deduplication, and scoring
|
|
25
|
+
│ └── send.py # Stage 5: Telegram dispatch and state lifecycle management
|
|
26
|
+
├── sources/
|
|
27
|
+
│ ├── arbeitnow.py # Arbeitnow aggregator source adapter
|
|
28
|
+
│ ├── ashby.py # Ashby ATS board source adapter
|
|
29
|
+
│ ├── base.py # Base ATS scraper & slug validator
|
|
30
|
+
│ ├── greenhouse.py # Greenhouse ATS board source adapter
|
|
31
|
+
│ ├── lever.py # Lever ATS board source adapter
|
|
32
|
+
│ ├── personio.py # Personio ATS board source adapter
|
|
33
|
+
│ ├── recruitee.py # Recruitee ATS board source adapter
|
|
34
|
+
│ ├── remoteok.py # RemoteOK aggregator source adapter
|
|
35
|
+
│ ├── remotive.py # Remotive aggregator source adapter
|
|
36
|
+
│ └── workable.py # Workable ATS board source adapter
|
|
37
|
+
└── tools/
|
|
38
|
+
├── discover.py # DuckDuckGo-powered company ATS board discovery
|
|
39
|
+
├── prompt.py # Compiled system prompt inspection tool
|
|
40
|
+
└── prune.py # Automated dead board validation and pruning
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 1. Fetch & Filter Pipeline (`fetch.py`)
|
|
44
|
+
|
|
45
|
+
- **Task Generation**: Iterates through registered sources (`SOURCES`), matching non-aggregator sources with company slugs defined in `config.json` (e.g., `companies.greenhouse = ["stripe"]`). Aggregators (Remotive, RemoteOK, Arbeitnow) operate without slugs.
|
|
46
|
+
- **Concurrent Scraping**: Dispatches fetch tasks across a `ThreadPoolExecutor` bounded by `settings.workers.fetch_workers`.
|
|
47
|
+
- **Filtering & Deduplication**: Passes raw `Job` instances through `passes_all_filters()` (checking title include/exclude, company exclusions, location rules, remote requirements, posting age, and minimum stack hits). Removes cross-source duplicates and jobs already recorded in `seen.json`.
|
|
48
|
+
- **Prioritization & Capping**: Ranks passing jobs by VIP priority status (`company_priority`) and stack keyword hits (`stack_hits`). Enforces `max_candidates_per_run` and atomically writes the resulting candidates to `data/candidates.json`.
|
|
49
|
+
|
|
50
|
+
### 2. AI Evaluation Pipeline (`evaluate.py`)
|
|
51
|
+
|
|
52
|
+
- **Task Loading & Filtering**: Reads `data/candidates.json` and `data/evaluated.json`, isolating pending candidates not yet evaluated. Supports evaluating specific candidates via `--job <UID>` or overriding run candidate limits via `--limit <INT>`.
|
|
53
|
+
- **Structured LLM Calls & Resilient Retries**: Submits each candidate along with the compiled system prompt to the OpenAI-compatible LLM client (`response_format={"type": "json_object"}`). Features automatic exponential backoff retries on transient network errors, rate limits (`RateLimitError`, 429), and server errors (`APIStatusError`, 5xx).
|
|
54
|
+
- **Validation & Fallback Parsing**: Parses and validates LLM responses against the `EvaluationResult` Pydantic model, with built-in markdown block cleanup (`json ... `) and fallback parsing. On schema validation failure, logs offending raw JSON for easy debugging.
|
|
55
|
+
- **Concurrency & Persistence**: Executes evaluation tasks across `settings.workers.evaluation_workers`, sorts successfully evaluated candidates by match score descending, and atomically persists them to `data/evaluated.json`.
|
|
56
|
+
|
|
57
|
+
### 3. Telegram Dispatch & State Management (`send.py`)
|
|
58
|
+
|
|
59
|
+
- **Dispatch**: Loads evaluated candidates from `evaluated.json` where `evaluation.hard_fail` is `False`. Formats rich HTML messages (including priority badges, score emojis, reasoning, score explanation, pitch, and job URL) and sends them to Telegram via `httpx` with 1-second request pacing.
|
|
60
|
+
- **State Lifecycle & Pruning**: Upon successful processing:
|
|
61
|
+
1. Records all evaluated job UIDs into `data/seen.json` mapped to their current Unix timestamp (`int(time.time())`).
|
|
62
|
+
2. Executes a **90-day retention prune** on `seen.json`, discarding any entry older than `settings.limits.seen_retention_days * 86400` seconds to prevent file bloat.
|
|
63
|
+
3. Purges processed UIDs from `data/candidates.json`.
|
|
64
|
+
4. Cleans up `evaluated.json` (preserving any items that failed delivery).
|
|
65
|
+
|
|
66
|
+
### 4. Discovery Tool (`discover.py`)
|
|
67
|
+
|
|
68
|
+
- Queries DuckDuckGo (`site:{domain} "{title}"`) for active company career boards across supported ATS providers (Greenhouse, Lever, Ashby, Personio, Workable, Recruitee).
|
|
69
|
+
- Extracts company slugs using regular expression patterns.
|
|
70
|
+
- Concurrently validates discovered slugs against ATS APIs. If run with `--write`, automatically merges valid new slugs into `config/config.json`. Includes a circuit breaker triggering after 3 consecutive search errors.
|
|
71
|
+
|
|
72
|
+
### 5. Pruning Tool (`prune.py`)
|
|
73
|
+
|
|
74
|
+
- Reads configured company slugs from `config/config.json`.
|
|
75
|
+
- Concurrently validates each slug against ATS APIs. Treats 404 and 410 HTTP status codes as definitive dead links (`SlugStatus.INVALID`).
|
|
76
|
+
- If run with `--write`, removes broken slugs from `config/config.json`.
|
|
77
|
+
|
|
78
|
+
### 6. Source Adapters (`sources/`)
|
|
79
|
+
|
|
80
|
+
- **Lightweight API-First Design**: `JobHunter` entirely avoids heavy, resource-intensive headless browsers (such as Selenium or Playwright) by querying public, lightweight JSON and XML ATS APIs directly. This ensures lightning-fast execution, minimal memory footprint, and high concurrency.
|
|
81
|
+
- **Aggregators vs. ATS Board Scrapers**:
|
|
82
|
+
- **Aggregators** (`remotive.py`, `remoteok.py`, `arbeitnow.py`): Pull global remote job feeds without requiring company slugs (`is_aggregator = True`).
|
|
83
|
+
- **ATS Board Scrapers** (`greenhouse.py`, `lever.py`, `ashby.py`, `personio.py`, `workable.py`, `recruitee.py`): Fetch job openings specifically via company board slugs (e.g., Greenhouse API endpoint `https://boards-api.greenhouse.io/v1/boards/{slug}/jobs`).
|
|
84
|
+
- **Slug Validation & Lifecycle (`validate_slug`)**: Both the `discover` and `prune` tools inherit from `BaseSource` and utilize the `validate_slug(slug)` method. By executing lightweight test queries or checking HTTP status codes (where 404 and 410 represent definitive dead links), `validate_slug` returns a `SlugStatus` enum (`VALID`, `INVALID`, or `UNKNOWN`) to ensure all tracked boards remain active.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## 🔄 State Management Lifecycle
|
|
89
|
+
|
|
90
|
+
`JobHunter` maintains state entirely in flat JSON files within the `data/` directory, protected against race conditions and concurrent execution anomalies:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
[candidates.json] ──(Evaluated)──> [evaluated.json] ──(Sent & Synced)──> [seen.json (90-day retention)]
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
- **`data/candidates.json`**: Active pool of filtered, un-evaluated candidates waiting for AI scoring.
|
|
97
|
+
- **data/evaluated.json**: Storage for candidates that have undergone AI evaluation.
|
|
98
|
+
- **data/seen.json**: Persistent dictionary mapping job unique identifiers (`uid`) to their discovery/processing timestamp (`{uid: timestamp}`). Prevents re-scraping or re-evaluating the same job across runs, bounded by a 90-day rolling window.
|
|
99
|
+
- **Concurrency & Atomic Safety (`paths.py`)**: All write operations utilize `filelock.FileLock` (using lockfiles stored in `.locks/`) combined with atomic replacement (`tempfile.mkstemp` + `os.replace` + `os.fsync`/`fdatasync`), ensuring zero file corruption even under high concurrency or abrupt termination.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## ⚡ Concurrency & Rate-Limiting Model
|
|
104
|
+
|
|
105
|
+
`JobHunter` leverages Python's `concurrent.futures.ThreadPoolExecutor` for maximum concurrency while respecting API rate limits:
|
|
106
|
+
|
|
107
|
+
- **Configurable Worker Bounds**: Each pipeline stage reads worker allocations from `settings.workers`, strictly clamped between 1 and 20 threads (discovery clamped to 10):
|
|
108
|
+
- `fetch_workers` (default: 8)
|
|
109
|
+
- `evaluation_workers` (default: 1)
|
|
110
|
+
- `discovery_workers` (default: 8)
|
|
111
|
+
- `pruning_workers` (default: 8)
|
|
112
|
+
- **Exponential Backoff**: HTTP requests (`http.py`) automatically retry transient errors (HTTP 408 Request Timeout, 429 Too Many Requests, and 5xx Server Errors) with exponential backoff (`backoff * (2 ** attempt)`).
|
|
113
|
+
- **Request Pacing**:
|
|
114
|
+
- LLM requests include an optional configurable delay (`settings.llm.request_delay`, default 5s).
|
|
115
|
+
- Telegram dispatches include a fixed 1-second delay (`time.sleep(1)`) to comply with Telegram Bot API rate limits (1 message/second per chat).
|