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.

Files changed (47) hide show
  1. jobhunter_cli-0.1.0/.env.example +6 -0
  2. jobhunter_cli-0.1.0/.github/workflows/job-maintenance.yml +78 -0
  3. jobhunter_cli-0.1.0/.github/workflows/job-pipeline.yml +139 -0
  4. jobhunter_cli-0.1.0/.gitignore +23 -0
  5. jobhunter_cli-0.1.0/.python-version +1 -0
  6. jobhunter_cli-0.1.0/LICENSE +21 -0
  7. jobhunter_cli-0.1.0/PKG-INFO +126 -0
  8. jobhunter_cli-0.1.0/README.md +109 -0
  9. jobhunter_cli-0.1.0/config/.gitkeep +0 -0
  10. jobhunter_cli-0.1.0/data/.gitkeep +0 -0
  11. jobhunter_cli-0.1.0/docs/architecture.md +115 -0
  12. jobhunter_cli-0.1.0/docs/configuration.md +304 -0
  13. jobhunter_cli-0.1.0/examples/config.example.json +96 -0
  14. jobhunter_cli-0.1.0/examples/cv.example.md +24 -0
  15. jobhunter_cli-0.1.0/examples/preferences.example.md +19 -0
  16. jobhunter_cli-0.1.0/pyproject.toml +48 -0
  17. jobhunter_cli-0.1.0/src/jobhunter/__init__.py +0 -0
  18. jobhunter_cli-0.1.0/src/jobhunter/__main__.py +4 -0
  19. jobhunter_cli-0.1.0/src/jobhunter/core/__init__.py +0 -0
  20. jobhunter_cli-0.1.0/src/jobhunter/core/filters.py +110 -0
  21. jobhunter_cli-0.1.0/src/jobhunter/core/http.py +57 -0
  22. jobhunter_cli-0.1.0/src/jobhunter/core/logger.py +54 -0
  23. jobhunter_cli-0.1.0/src/jobhunter/core/paths.py +124 -0
  24. jobhunter_cli-0.1.0/src/jobhunter/core/settings.py +123 -0
  25. jobhunter_cli-0.1.0/src/jobhunter/core/textutil.py +33 -0
  26. jobhunter_cli-0.1.0/src/jobhunter/main.py +94 -0
  27. jobhunter_cli-0.1.0/src/jobhunter/models.py +81 -0
  28. jobhunter_cli-0.1.0/src/jobhunter/pipeline/__init__.py +0 -0
  29. jobhunter_cli-0.1.0/src/jobhunter/pipeline/evaluate.py +284 -0
  30. jobhunter_cli-0.1.0/src/jobhunter/pipeline/fetch.py +233 -0
  31. jobhunter_cli-0.1.0/src/jobhunter/pipeline/send.py +191 -0
  32. jobhunter_cli-0.1.0/src/jobhunter/sources/__init__.py +21 -0
  33. jobhunter_cli-0.1.0/src/jobhunter/sources/arbeitnow.py +77 -0
  34. jobhunter_cli-0.1.0/src/jobhunter/sources/ashby.py +75 -0
  35. jobhunter_cli-0.1.0/src/jobhunter/sources/base.py +108 -0
  36. jobhunter_cli-0.1.0/src/jobhunter/sources/greenhouse.py +70 -0
  37. jobhunter_cli-0.1.0/src/jobhunter/sources/lever.py +85 -0
  38. jobhunter_cli-0.1.0/src/jobhunter/sources/personio.py +124 -0
  39. jobhunter_cli-0.1.0/src/jobhunter/sources/recruitee.py +80 -0
  40. jobhunter_cli-0.1.0/src/jobhunter/sources/remoteok.py +69 -0
  41. jobhunter_cli-0.1.0/src/jobhunter/sources/remotive.py +61 -0
  42. jobhunter_cli-0.1.0/src/jobhunter/sources/workable.py +94 -0
  43. jobhunter_cli-0.1.0/src/jobhunter/tools/__init__.py +0 -0
  44. jobhunter_cli-0.1.0/src/jobhunter/tools/discover.py +275 -0
  45. jobhunter_cli-0.1.0/src/jobhunter/tools/prompt.py +106 -0
  46. jobhunter_cli-0.1.0/src/jobhunter/tools/prune.py +124 -0
  47. 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).