hiringcafe-cli 0.1.4__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.
- hiringcafe_cli-0.1.4/LICENSE +21 -0
- hiringcafe_cli-0.1.4/PKG-INFO +145 -0
- hiringcafe_cli-0.1.4/README.md +122 -0
- hiringcafe_cli-0.1.4/pyproject.toml +74 -0
- hiringcafe_cli-0.1.4/pyproject.toml.orig +62 -0
- hiringcafe_cli-0.1.4/src/hiringcafe_cli/__init__.py +152 -0
- hiringcafe_cli-0.1.4/src/hiringcafe_cli/auth.py +95 -0
- hiringcafe_cli-0.1.4/src/hiringcafe_cli/cache.py +98 -0
- hiringcafe_cli-0.1.4/src/hiringcafe_cli/count.py +37 -0
- hiringcafe_cli-0.1.4/src/hiringcafe_cli/firebase.py +93 -0
- hiringcafe_cli-0.1.4/src/hiringcafe_cli/http_client.py +245 -0
- hiringcafe_cli-0.1.4/src/hiringcafe_cli/io_limits.py +23 -0
- hiringcafe_cli-0.1.4/src/hiringcafe_cli/job.py +63 -0
- hiringcafe_cli-0.1.4/src/hiringcafe_cli/probe.py +140 -0
- hiringcafe_cli-0.1.4/src/hiringcafe_cli/saved_jobs.py +101 -0
- hiringcafe_cli-0.1.4/src/hiringcafe_cli/saved_jobs_list.py +161 -0
- hiringcafe_cli-0.1.4/src/hiringcafe_cli/saved_jobs_remove.py +51 -0
- hiringcafe_cli-0.1.4/src/hiringcafe_cli/saved_jobs_save.py +59 -0
- hiringcafe_cli-0.1.4/src/hiringcafe_cli/saved_jobs_stage.py +64 -0
- hiringcafe_cli-0.1.4/src/hiringcafe_cli/search.py +140 -0
- hiringcafe_cli-0.1.4/src/hiringcafe_cli/search_state.py +36 -0
- hiringcafe_cli-0.1.4/src/hiringcafe_cli/session.py +82 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 chen-gdp
|
|
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,145 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hiringcafe-cli
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Summary: Spec-driven Python CLI for Hiring Cafe job search and saved-job tracking
|
|
5
|
+
Author: chen-gdp
|
|
6
|
+
Author-email: chen-gdp <146709874+chen-gdp@users.noreply.github.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Project-URL: Homepage, https://github.com/chen-gdp/hiringcafe-cli
|
|
19
|
+
Project-URL: Repository, https://github.com/chen-gdp/hiringcafe-cli
|
|
20
|
+
Project-URL: Issues, https://github.com/chen-gdp/hiringcafe-cli/issues
|
|
21
|
+
Project-URL: Changelog, https://github.com/chen-gdp/hiringcafe-cli/blob/main/CHANGELOG.md
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# HiringCafe CLI
|
|
25
|
+
|
|
26
|
+
Give your AI agent Hiring Cafe superpowers: search jobs, inspect postings, and keep your Hiring Cafe saved-job board in sync without a browser.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
hiringcafe search "python developer" --pages 3 --jsonl
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The result is stable JSON/JSONL, explicit exit codes, cached anonymous reads, bounded retries, and authenticated actions against your real Hiring Cafe account.
|
|
33
|
+
|
|
34
|
+
> **Unofficial client.** This project is not affiliated with, endorsed by, or supported by Hiring Cafe. Use your own account, respect the service, and expect upstream APIs to change.
|
|
35
|
+
|
|
36
|
+
## For agents
|
|
37
|
+
|
|
38
|
+
Agents can install and verify the CLI in three commands:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
uv tool install hiringcafe-cli
|
|
42
|
+
hiringcafe search "python developer" --page 0 --json
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
For persistent installations, use `uv tool install hiringcafe-cli` or `pipx install hiringcafe-cli`.
|
|
46
|
+
|
|
47
|
+
Feed the output directly to your tooling:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# One JSON object per page
|
|
51
|
+
hiringcafe search "python developer" --pages 3 --jsonl
|
|
52
|
+
|
|
53
|
+
# One line per job
|
|
54
|
+
hiringcafe search "python developer" --pages 3 --json | jq -c '.jobs[]'
|
|
55
|
+
|
|
56
|
+
# Machine-readable result count
|
|
57
|
+
hiringcafe count "python developer" --json
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Import an existing Firebase refresh token without an interactive password prompt:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
printf '%s\n' "$HIRINGCAFE_REFRESH_TOKEN" | hiringcafe auth import
|
|
64
|
+
hiringcafe saved-jobs list --json
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Safe automation contract:
|
|
68
|
+
|
|
69
|
+
- JSON success output goes to stdout; cache diagnostics go to stderr.
|
|
70
|
+
- Stable exit codes describe transport, input, rate-limit, auth, and conflict outcomes.
|
|
71
|
+
- Passwords and Firebase ID tokens are never written to disk.
|
|
72
|
+
- Authenticated mutations are never retried or cached.
|
|
73
|
+
- Anonymous search/count responses use a 15-minute cache; job details use 24 hours.
|
|
74
|
+
|
|
75
|
+
## For humans
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
hiringcafe auth login --email you@example.com
|
|
79
|
+
hiringcafe search "python developer" --page 0 --json
|
|
80
|
+
hiringcafe job show <objectID> --json
|
|
81
|
+
hiringcafe saved-jobs list --json
|
|
82
|
+
hiringcafe auth logout
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The password is read through hidden input, exchanged once, and discarded.
|
|
86
|
+
|
|
87
|
+
## Saved jobs
|
|
88
|
+
|
|
89
|
+
These commands modify your real Hiring Cafe account. Review object IDs and confirmation prompts before changing data.
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
hiringcafe saved-jobs save <objectID> --stage saved
|
|
93
|
+
hiringcafe saved-jobs stage <objectID> --stage interviewing
|
|
94
|
+
hiringcafe saved-jobs remove <objectID> --yes
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Valid stages are `saved`, `applied`, `interviewing`, `offer`, `rejected`, `ghosted`, and `hidden`.
|
|
98
|
+
|
|
99
|
+
## Local data
|
|
100
|
+
|
|
101
|
+
| Data | Location | Notes |
|
|
102
|
+
|---|---|---|
|
|
103
|
+
| Refresh token | `~/.config/hiringcafe-cli/session.json` | owner-only file; removed by logout |
|
|
104
|
+
| Anonymous cache | `~/.cache/hiringcafe-cli/cache/v1/` | public search/job data only |
|
|
105
|
+
|
|
106
|
+
Set `HIRINGCAFE_CONFIG_HOME` or `HIRINGCAFE_CACHE_HOME` to use different roots.
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
hiringcafe search "python developer" --no-cache
|
|
110
|
+
hiringcafe search "python developer" --cache-ttl 3600
|
|
111
|
+
hiringcafe cache purge
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Exit codes
|
|
115
|
+
|
|
116
|
+
| Code | Agent action |
|
|
117
|
+
|---:|---|
|
|
118
|
+
| `0` | success |
|
|
119
|
+
| `1` | transport, HTTP, parsing, or API error; inspect stderr |
|
|
120
|
+
| `2` | invalid arguments or search state; fix the request |
|
|
121
|
+
| `3` | rate limited; wait and retry |
|
|
122
|
+
| `4` | no stored credential; authenticate first |
|
|
123
|
+
| `5` | stored credential rejected; re-authenticate |
|
|
124
|
+
| `6` | write conflict or prerequisite missing; refresh state |
|
|
125
|
+
|
|
126
|
+
## Development
|
|
127
|
+
|
|
128
|
+
Requires Python 3.11+ and [uv](https://docs.astral.sh/uv/).
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
uv sync
|
|
132
|
+
uv run pytest
|
|
133
|
+
uv run hiringcafe --help
|
|
134
|
+
uv run pre-commit run --all-files
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Normal tests do not access the network. The verified anonymous endpoint probe is opt-in:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
LIVE=1 uv run hiringcafe probe
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Every implementation change traces to `specs/pr-*/spec.md` and the roadmap. Never commit credentials, Firebase tokens, cookies, real UIDs, emails, or private account data.
|
|
144
|
+
|
|
145
|
+
Beta status: search, job details, authentication, and saved-job workflows are implemented. Browser OAuth login remains intentionally deferred. See [`roadmap.md`](roadmap.md) and [`specs/`](specs/).
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# HiringCafe CLI
|
|
2
|
+
|
|
3
|
+
Give your AI agent Hiring Cafe superpowers: search jobs, inspect postings, and keep your Hiring Cafe saved-job board in sync without a browser.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
hiringcafe search "python developer" --pages 3 --jsonl
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
The result is stable JSON/JSONL, explicit exit codes, cached anonymous reads, bounded retries, and authenticated actions against your real Hiring Cafe account.
|
|
10
|
+
|
|
11
|
+
> **Unofficial client.** This project is not affiliated with, endorsed by, or supported by Hiring Cafe. Use your own account, respect the service, and expect upstream APIs to change.
|
|
12
|
+
|
|
13
|
+
## For agents
|
|
14
|
+
|
|
15
|
+
Agents can install and verify the CLI in three commands:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
uv tool install hiringcafe-cli
|
|
19
|
+
hiringcafe search "python developer" --page 0 --json
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
For persistent installations, use `uv tool install hiringcafe-cli` or `pipx install hiringcafe-cli`.
|
|
23
|
+
|
|
24
|
+
Feed the output directly to your tooling:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# One JSON object per page
|
|
28
|
+
hiringcafe search "python developer" --pages 3 --jsonl
|
|
29
|
+
|
|
30
|
+
# One line per job
|
|
31
|
+
hiringcafe search "python developer" --pages 3 --json | jq -c '.jobs[]'
|
|
32
|
+
|
|
33
|
+
# Machine-readable result count
|
|
34
|
+
hiringcafe count "python developer" --json
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Import an existing Firebase refresh token without an interactive password prompt:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
printf '%s\n' "$HIRINGCAFE_REFRESH_TOKEN" | hiringcafe auth import
|
|
41
|
+
hiringcafe saved-jobs list --json
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Safe automation contract:
|
|
45
|
+
|
|
46
|
+
- JSON success output goes to stdout; cache diagnostics go to stderr.
|
|
47
|
+
- Stable exit codes describe transport, input, rate-limit, auth, and conflict outcomes.
|
|
48
|
+
- Passwords and Firebase ID tokens are never written to disk.
|
|
49
|
+
- Authenticated mutations are never retried or cached.
|
|
50
|
+
- Anonymous search/count responses use a 15-minute cache; job details use 24 hours.
|
|
51
|
+
|
|
52
|
+
## For humans
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
hiringcafe auth login --email you@example.com
|
|
56
|
+
hiringcafe search "python developer" --page 0 --json
|
|
57
|
+
hiringcafe job show <objectID> --json
|
|
58
|
+
hiringcafe saved-jobs list --json
|
|
59
|
+
hiringcafe auth logout
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The password is read through hidden input, exchanged once, and discarded.
|
|
63
|
+
|
|
64
|
+
## Saved jobs
|
|
65
|
+
|
|
66
|
+
These commands modify your real Hiring Cafe account. Review object IDs and confirmation prompts before changing data.
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
hiringcafe saved-jobs save <objectID> --stage saved
|
|
70
|
+
hiringcafe saved-jobs stage <objectID> --stage interviewing
|
|
71
|
+
hiringcafe saved-jobs remove <objectID> --yes
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Valid stages are `saved`, `applied`, `interviewing`, `offer`, `rejected`, `ghosted`, and `hidden`.
|
|
75
|
+
|
|
76
|
+
## Local data
|
|
77
|
+
|
|
78
|
+
| Data | Location | Notes |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| Refresh token | `~/.config/hiringcafe-cli/session.json` | owner-only file; removed by logout |
|
|
81
|
+
| Anonymous cache | `~/.cache/hiringcafe-cli/cache/v1/` | public search/job data only |
|
|
82
|
+
|
|
83
|
+
Set `HIRINGCAFE_CONFIG_HOME` or `HIRINGCAFE_CACHE_HOME` to use different roots.
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
hiringcafe search "python developer" --no-cache
|
|
87
|
+
hiringcafe search "python developer" --cache-ttl 3600
|
|
88
|
+
hiringcafe cache purge
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Exit codes
|
|
92
|
+
|
|
93
|
+
| Code | Agent action |
|
|
94
|
+
|---:|---|
|
|
95
|
+
| `0` | success |
|
|
96
|
+
| `1` | transport, HTTP, parsing, or API error; inspect stderr |
|
|
97
|
+
| `2` | invalid arguments or search state; fix the request |
|
|
98
|
+
| `3` | rate limited; wait and retry |
|
|
99
|
+
| `4` | no stored credential; authenticate first |
|
|
100
|
+
| `5` | stored credential rejected; re-authenticate |
|
|
101
|
+
| `6` | write conflict or prerequisite missing; refresh state |
|
|
102
|
+
|
|
103
|
+
## Development
|
|
104
|
+
|
|
105
|
+
Requires Python 3.11+ and [uv](https://docs.astral.sh/uv/).
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
uv sync
|
|
109
|
+
uv run pytest
|
|
110
|
+
uv run hiringcafe --help
|
|
111
|
+
uv run pre-commit run --all-files
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Normal tests do not access the network. The verified anonymous endpoint probe is opt-in:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
LIVE=1 uv run hiringcafe probe
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Every implementation change traces to `specs/pr-*/spec.md` and the roadmap. Never commit credentials, Firebase tokens, cookies, real UIDs, emails, or private account data.
|
|
121
|
+
|
|
122
|
+
Beta status: search, job details, authentication, and saved-job workflows are implemented. Browser OAuth login remains intentionally deferred. See [`roadmap.md`](roadmap.md) and [`specs/`](specs/).
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "hiringcafe-cli"
|
|
3
|
+
version = "0.1.4"
|
|
4
|
+
description = "Spec-driven Python CLI for Hiring Cafe job search and saved-job tracking"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
requires-python = ">=3.11"
|
|
9
|
+
dependencies = []
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Environment :: Console",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.11",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"Programming Language :: Python :: 3.13",
|
|
18
|
+
"Programming Language :: Python :: 3.14",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[[project.authors]]
|
|
22
|
+
name = "chen-gdp"
|
|
23
|
+
email = "146709874+chen-gdp@users.noreply.github.com"
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/chen-gdp/hiringcafe-cli"
|
|
27
|
+
Repository = "https://github.com/chen-gdp/hiringcafe-cli"
|
|
28
|
+
Issues = "https://github.com/chen-gdp/hiringcafe-cli/issues"
|
|
29
|
+
Changelog = "https://github.com/chen-gdp/hiringcafe-cli/blob/main/CHANGELOG.md"
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
hiringcafe = "hiringcafe_cli:main"
|
|
33
|
+
|
|
34
|
+
[dependency-groups]
|
|
35
|
+
dev = [
|
|
36
|
+
"pre-commit>=4.3.0",
|
|
37
|
+
"pytest>=9.0.0",
|
|
38
|
+
"ruff>=0.14.0",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[build-system]
|
|
42
|
+
requires = ["uv_build>=0.11.16,<0.12.0"]
|
|
43
|
+
build-backend = "uv_build"
|
|
44
|
+
|
|
45
|
+
[tool.ruff]
|
|
46
|
+
line-length = 120
|
|
47
|
+
target-version = "py311"
|
|
48
|
+
exclude = [
|
|
49
|
+
".venv",
|
|
50
|
+
"dist",
|
|
51
|
+
"__pycache__",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
[tool.ruff.lint]
|
|
55
|
+
select = [
|
|
56
|
+
"E",
|
|
57
|
+
"F",
|
|
58
|
+
"I",
|
|
59
|
+
"B",
|
|
60
|
+
"UP",
|
|
61
|
+
"S",
|
|
62
|
+
]
|
|
63
|
+
ignore = [
|
|
64
|
+
"S310",
|
|
65
|
+
"S311",
|
|
66
|
+
"UP017",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
[tool.ruff.lint.per-file-ignores]
|
|
70
|
+
"tests/*" = [
|
|
71
|
+
"S101",
|
|
72
|
+
"S105",
|
|
73
|
+
"S106",
|
|
74
|
+
]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "hiringcafe-cli"
|
|
3
|
+
version = "0.1.4"
|
|
4
|
+
description = "Spec-driven Python CLI for Hiring Cafe job search and saved-job tracking"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "chen-gdp", email = "146709874+chen-gdp@users.noreply.github.com" }
|
|
10
|
+
]
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
dependencies = []
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Environment :: Console",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Programming Language :: Python :: 3.14",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://github.com/chen-gdp/hiringcafe-cli"
|
|
26
|
+
Repository = "https://github.com/chen-gdp/hiringcafe-cli"
|
|
27
|
+
Issues = "https://github.com/chen-gdp/hiringcafe-cli/issues"
|
|
28
|
+
Changelog = "https://github.com/chen-gdp/hiringcafe-cli/blob/main/CHANGELOG.md"
|
|
29
|
+
|
|
30
|
+
[dependency-groups]
|
|
31
|
+
dev = [
|
|
32
|
+
"pre-commit>=4.3.0",
|
|
33
|
+
"pytest>=9.0.0",
|
|
34
|
+
"ruff>=0.14.0",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
hiringcafe = "hiringcafe_cli:main"
|
|
39
|
+
|
|
40
|
+
[build-system]
|
|
41
|
+
requires = ["uv_build>=0.11.16,<0.12.0"]
|
|
42
|
+
build-backend = "uv_build"
|
|
43
|
+
|
|
44
|
+
[tool.ruff]
|
|
45
|
+
line-length = 120
|
|
46
|
+
target-version = "py311"
|
|
47
|
+
exclude = [".venv", "dist", "__pycache__"]
|
|
48
|
+
|
|
49
|
+
[tool.ruff.lint]
|
|
50
|
+
select = ["E", "F", "I", "B", "UP", "S"]
|
|
51
|
+
ignore = [
|
|
52
|
+
"S310", # all transport URLs are fixed HTTPS constants or verified site URLs
|
|
53
|
+
"S311", # random jitter is for retry timing, not cryptography
|
|
54
|
+
"UP017", # timezone.utc is the Python 3.11-compatible spelling
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
[tool.ruff.lint.per-file-ignores]
|
|
58
|
+
"tests/*" = [
|
|
59
|
+
"S101", # assertions are the project's test contract
|
|
60
|
+
"S105", # synthetic credential names in isolated tests
|
|
61
|
+
"S106", # synthetic credential values in isolated tests
|
|
62
|
+
]
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""HiringCafe CLI package interface."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from hiringcafe_cli import (
|
|
7
|
+
auth,
|
|
8
|
+
count,
|
|
9
|
+
job,
|
|
10
|
+
probe,
|
|
11
|
+
saved_jobs_list,
|
|
12
|
+
saved_jobs_remove,
|
|
13
|
+
saved_jobs_save,
|
|
14
|
+
saved_jobs_stage,
|
|
15
|
+
search,
|
|
16
|
+
)
|
|
17
|
+
from hiringcafe_cli.cache import CacheStore, active_cache_root
|
|
18
|
+
from hiringcafe_cli.firebase import FirebaseAuthError, FirebaseTransportError
|
|
19
|
+
from hiringcafe_cli.http_client import HireCafeHTTPError, HireCafeTransportError
|
|
20
|
+
from hiringcafe_cli.saved_jobs import VALID_STAGES
|
|
21
|
+
from hiringcafe_cli.saved_jobs_list import FirestoreError
|
|
22
|
+
from hiringcafe_cli.search_state import InvalidSearchState
|
|
23
|
+
|
|
24
|
+
__version__ = "0.1.0"
|
|
25
|
+
|
|
26
|
+
EXIT_CONTRACT = """\
|
|
27
|
+
Exit codes:
|
|
28
|
+
0 success
|
|
29
|
+
1 transport, HTTP, parsing, or API error
|
|
30
|
+
2 invalid arguments or search state
|
|
31
|
+
3 rate limited
|
|
32
|
+
4 no stored credential
|
|
33
|
+
5 stored credential rejected
|
|
34
|
+
6 write conflict or prerequisite missing
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
39
|
+
"""Build the root CLI parser.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
An argparse parser implementing the PR 0.1 bootstrap contract.
|
|
43
|
+
"""
|
|
44
|
+
parser = argparse.ArgumentParser(
|
|
45
|
+
prog="hiringcafe",
|
|
46
|
+
description="Hiring Cafe command-line interface",
|
|
47
|
+
epilog=EXIT_CONTRACT,
|
|
48
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
49
|
+
)
|
|
50
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
51
|
+
cache_options = argparse.ArgumentParser(add_help=False)
|
|
52
|
+
cache_options.add_argument("--no-cache", action="store_true")
|
|
53
|
+
cache_options.add_argument("--cache-ttl", type=int)
|
|
54
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
55
|
+
probe_parser = subparsers.add_parser("probe", help="run the opt-in live endpoint probe")
|
|
56
|
+
probe_parser.set_defaults(handler=probe.main)
|
|
57
|
+
search_parser = subparsers.add_parser("search", help="search jobs", parents=[cache_options])
|
|
58
|
+
search_parser.add_argument("query", nargs="?", default="")
|
|
59
|
+
search_parser.add_argument("--page", type=int, default=0)
|
|
60
|
+
search_parser.add_argument("--pages", type=int, default=1)
|
|
61
|
+
search_parser.add_argument("--raw-search-state")
|
|
62
|
+
search_parser.add_argument("--json", action="store_true")
|
|
63
|
+
search_parser.add_argument("--jsonl", action="store_true")
|
|
64
|
+
search_parser.set_defaults(handler=search.run)
|
|
65
|
+
count_parser = subparsers.add_parser("count", help="print total result count", parents=[cache_options])
|
|
66
|
+
count_parser.add_argument("query", nargs="?", default="")
|
|
67
|
+
count_parser.add_argument("--raw-search-state")
|
|
68
|
+
count_parser.add_argument("--json", action="store_true")
|
|
69
|
+
count_parser.set_defaults(handler=count.run)
|
|
70
|
+
job_parser = subparsers.add_parser("job", help="inspect job details")
|
|
71
|
+
job_subparsers = job_parser.add_subparsers(dest="job_command", required=True)
|
|
72
|
+
job_show_parser = job_subparsers.add_parser("show", help="show a job description", parents=[cache_options])
|
|
73
|
+
job_show_parser.add_argument("object_id")
|
|
74
|
+
job_show_parser.add_argument("--json", action="store_true")
|
|
75
|
+
job_show_parser.set_defaults(handler=job.run)
|
|
76
|
+
auth_parser = subparsers.add_parser("auth", help="manage Firebase authentication")
|
|
77
|
+
auth_subparsers = auth_parser.add_subparsers(dest="auth_command", required=True)
|
|
78
|
+
auth_login_parser = auth_subparsers.add_parser("login", help="log in to Firebase")
|
|
79
|
+
auth_login_parser.add_argument("--email", required=True)
|
|
80
|
+
auth_login_parser.set_defaults(handler=auth.login_email)
|
|
81
|
+
auth_import_parser = auth_subparsers.add_parser("import", help="import a refresh token")
|
|
82
|
+
auth_import_parser.set_defaults(handler=auth.import_token)
|
|
83
|
+
auth_status_parser = auth_subparsers.add_parser("status", help="show credential status")
|
|
84
|
+
auth_status_parser.add_argument("--json", action="store_true")
|
|
85
|
+
auth_status_parser.set_defaults(handler=auth.status)
|
|
86
|
+
auth_logout_parser = auth_subparsers.add_parser("logout", help="remove the local session")
|
|
87
|
+
auth_logout_parser.set_defaults(handler=auth.logout)
|
|
88
|
+
saved_parser = subparsers.add_parser("saved-jobs", help="manage saved jobs")
|
|
89
|
+
saved_subparsers = saved_parser.add_subparsers(dest="saved_command", required=True)
|
|
90
|
+
saved_list_parser = saved_subparsers.add_parser("list", help="list saved jobs")
|
|
91
|
+
saved_list_parser.add_argument("--stage", choices=sorted(VALID_STAGES))
|
|
92
|
+
saved_list_parser.add_argument("--json", action="store_true")
|
|
93
|
+
saved_list_parser.set_defaults(handler=saved_jobs_list.run)
|
|
94
|
+
saved_save_parser = saved_subparsers.add_parser("save", help="save a job")
|
|
95
|
+
saved_save_parser.add_argument("object_id")
|
|
96
|
+
saved_save_parser.add_argument("--stage", default="saved")
|
|
97
|
+
saved_save_parser.set_defaults(handler=saved_jobs_save.run)
|
|
98
|
+
saved_stage_parser = saved_subparsers.add_parser("stage", help="update a job stage")
|
|
99
|
+
saved_stage_parser.add_argument("object_id")
|
|
100
|
+
saved_stage_parser.add_argument("--stage", required=True)
|
|
101
|
+
saved_stage_parser.set_defaults(handler=saved_jobs_stage.run)
|
|
102
|
+
saved_remove_parser = saved_subparsers.add_parser("remove", help="remove a saved job")
|
|
103
|
+
saved_remove_parser.add_argument("object_id")
|
|
104
|
+
saved_remove_parser.add_argument("--yes", action="store_true")
|
|
105
|
+
saved_remove_parser.set_defaults(handler=saved_jobs_remove.run)
|
|
106
|
+
cache_parser = subparsers.add_parser("cache", help="manage anonymous read cache")
|
|
107
|
+
cache_subparsers = cache_parser.add_subparsers(dest="cache_command", required=True)
|
|
108
|
+
cache_purge_parser = cache_subparsers.add_parser("purge", help="remove cached anonymous reads")
|
|
109
|
+
cache_purge_parser.set_defaults(handler=cache_purge)
|
|
110
|
+
return parser
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def cache_purge(argv=None, json_output: bool = False) -> int:
|
|
114
|
+
"""Purge all anonymous cache files."""
|
|
115
|
+
CacheStore(active_cache_root()).purge()
|
|
116
|
+
print("Cache purged")
|
|
117
|
+
return 0
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def main(argv: list[str] | None = None) -> None:
|
|
121
|
+
"""Run the command-line interface.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
argv: Optional argv list; defaults to ``sys.argv[1:]``.
|
|
125
|
+
"""
|
|
126
|
+
arguments = build_parser().parse_args(argv)
|
|
127
|
+
handler = getattr(arguments, "handler", None)
|
|
128
|
+
if handler is not None:
|
|
129
|
+
json_output = getattr(arguments, "json", False)
|
|
130
|
+
try:
|
|
131
|
+
raise SystemExit(handler(arguments, json_output))
|
|
132
|
+
except InvalidSearchState as error:
|
|
133
|
+
print(f"hiringcafe: {error}", file=sys.stderr)
|
|
134
|
+
raise SystemExit(2) from error
|
|
135
|
+
except ValueError as error:
|
|
136
|
+
print(f"hiringcafe: {error}", file=sys.stderr)
|
|
137
|
+
raise SystemExit(1) from error
|
|
138
|
+
except FirebaseAuthError as error:
|
|
139
|
+
print(f"hiringcafe: {error}", file=sys.stderr)
|
|
140
|
+
raise SystemExit(5) from error
|
|
141
|
+
except FirebaseTransportError as error:
|
|
142
|
+
print(f"hiringcafe: {error}", file=sys.stderr)
|
|
143
|
+
raise SystemExit(1) from error
|
|
144
|
+
except FirestoreError as error:
|
|
145
|
+
print(f"hiringcafe: {error}", file=sys.stderr)
|
|
146
|
+
raise SystemExit(1) from error
|
|
147
|
+
except HireCafeHTTPError as error:
|
|
148
|
+
print(f"hiringcafe: {error}", file=sys.stderr)
|
|
149
|
+
raise SystemExit(3 if error.status == 429 else 1) from error
|
|
150
|
+
except HireCafeTransportError as error:
|
|
151
|
+
print(f"hiringcafe: {error}", file=sys.stderr)
|
|
152
|
+
raise SystemExit(1) from error
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Firebase authentication commands."""
|
|
2
|
+
|
|
3
|
+
import getpass
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from hiringcafe_cli.firebase import FirebaseClient
|
|
10
|
+
from hiringcafe_cli.session import SessionStore
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def default_session_path() -> Path:
|
|
14
|
+
"""Return the platform user config session path."""
|
|
15
|
+
config_home = os.environ.get("HIRINGCAFE_CONFIG_HOME")
|
|
16
|
+
if config_home:
|
|
17
|
+
return Path(config_home) / "session.json"
|
|
18
|
+
return Path.home() / ".config" / "hiringcafe-cli" / "session.json"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def store() -> SessionStore:
|
|
22
|
+
"""Create the session store using the selected configuration root."""
|
|
23
|
+
return SessionStore(default_session_path())
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def import_token(argv: object, json_output: bool = False) -> int:
|
|
27
|
+
"""Import a refresh token through stdin or the nonprinting environment."""
|
|
28
|
+
token = sys.stdin.readline().strip() or os.environ.get("HIRINGCAFE_REFRESH_TOKEN", "")
|
|
29
|
+
if not token:
|
|
30
|
+
print("hiringcafe: no refresh token provided on stdin or HIRINGCAFE_REFRESH_TOKEN", file=sys.stderr)
|
|
31
|
+
return 2
|
|
32
|
+
credentials = FirebaseClient().refresh(token)
|
|
33
|
+
store().save(credentials["refreshToken"])
|
|
34
|
+
print("Authenticated Firebase session saved")
|
|
35
|
+
return 0
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def login_email(argv: object, json_output: bool = False) -> int:
|
|
39
|
+
"""Exchange Firebase email/password credentials once and save only the refresh token."""
|
|
40
|
+
password = os.environ.get("HIRINGCAFE_PASSWORD") or getpass.getpass("Firebase password: ")
|
|
41
|
+
credentials = FirebaseClient().login_with_password(argv.email, password)
|
|
42
|
+
del password
|
|
43
|
+
store().save(credentials["refreshToken"])
|
|
44
|
+
print("Authenticated Firebase session saved")
|
|
45
|
+
return 0
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def status(argv: object, json_output: bool = False) -> int:
|
|
49
|
+
"""Report whether the stored Firebase credential can obtain an ID token.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
argv: Unused CLI namespace.
|
|
53
|
+
json_output: Whether stdout must be machine-readable.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Exit code 0 when usable, 4 when no session exists.
|
|
57
|
+
"""
|
|
58
|
+
session = store().load()
|
|
59
|
+
if session is None:
|
|
60
|
+
if json_output:
|
|
61
|
+
print(json.dumps({"version": 1, "authenticated": False}, separators=(",", ":")))
|
|
62
|
+
else:
|
|
63
|
+
print("No stored credential", file=sys.stderr)
|
|
64
|
+
return 4
|
|
65
|
+
FirebaseClient().refresh(session.refresh_token)
|
|
66
|
+
if json_output:
|
|
67
|
+
payload = json.dumps(
|
|
68
|
+
{"version": 1, "authenticated": True},
|
|
69
|
+
separators=(",", ":"),
|
|
70
|
+
)
|
|
71
|
+
print(payload)
|
|
72
|
+
else:
|
|
73
|
+
print("Authenticated")
|
|
74
|
+
return 0
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def logout(argv: object, json_output: bool = False) -> int:
|
|
78
|
+
"""Delete the local Firebase session.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
argv: Unused CLI namespace.
|
|
82
|
+
json_output: Unused; logout output remains intentionally human-only.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Exit code 0 after local credential removal.
|
|
86
|
+
"""
|
|
87
|
+
session_store = store()
|
|
88
|
+
had_session = session_store.load() is not None
|
|
89
|
+
try:
|
|
90
|
+
session_store.delete()
|
|
91
|
+
except OSError as error:
|
|
92
|
+
print(f"hiringcafe: cannot remove session: {error}", file=sys.stderr)
|
|
93
|
+
return 1
|
|
94
|
+
print("Local Firebase session removed" if had_session else "No local Firebase session")
|
|
95
|
+
return 0
|