updated instructions
This commit is contained in:
173
readme.md
173
readme.md
@@ -1,26 +1,30 @@
|
|||||||
build a small python cli tool called `usajobs.py` for exploring usajobs results.
|
`usajobs.py` is a python tui for exploring data.usajobs.gov
|
||||||
|
|
||||||
goal:
|
## goal:
|
||||||
- query the official usajobs api
|
- query the official usajobs api
|
||||||
- apply strict local filters
|
- apply strict local filters because usajobs search facets are unreliable
|
||||||
- show results in a readable terminal table
|
- show results in a readable terminal table
|
||||||
- let me select rows to export into an org-mode file
|
- let me interactively mark/unmark jobs for export
|
||||||
|
- export selected jobs to org-mode
|
||||||
|
|
||||||
stack:
|
## stack:
|
||||||
- python 3.11+
|
- python 3.11+
|
||||||
- click for cli args
|
- click for cli args
|
||||||
- requests for api
|
- requests for api
|
||||||
- rich for table + row selection prompt
|
- rich for table output
|
||||||
- pathlib/json/csv stdlib only otherwise
|
- questionary for v1 interactive row marking/export
|
||||||
|
- pathlib/json/csv/stdlib otherwise
|
||||||
|
- do not use typer or pick for v1
|
||||||
|
- stretch/v2: textual tui after the simple questionary flow works
|
||||||
|
|
||||||
env vars:
|
## env vars:
|
||||||
- USAJOBS_EMAIL
|
- USAJOBS_EMAIL
|
||||||
- USAJOBS_KEY
|
- USAJOBS_KEY
|
||||||
|
|
||||||
basic command:
|
### Run:
|
||||||
`python jobs.py search --location "Washington, DC" --radius 25 --salary-min 150 --grade-min 15 --grade-max 15 --series 2210 --series 0340 --clearance 3 --clearance 4`
|
`python usajobs.py search --location "Washington, DC" --radius 25 --salary-min 150 --grade-min 15 --grade-max 15 --series 2210 --series 0340 --clearance 3 --clearance 4`
|
||||||
|
|
||||||
option behavior:
|
### option behavior:
|
||||||
- --radius 25 means 25 miles
|
- --radius 25 means 25 miles
|
||||||
- --salary-min 150 means $150,000
|
- --salary-min 150 means $150,000
|
||||||
- --grade-min/--grade-max filter locally against low/high grade
|
- --grade-min/--grade-max filter locally against low/high grade
|
||||||
@@ -28,15 +32,25 @@ option behavior:
|
|||||||
- --clearance may repeat; pass to api as semicolon list
|
- --clearance may repeat; pass to api as semicolon list
|
||||||
- --pay-plan may repeat, default gs and gg
|
- --pay-plan may repeat, default gs and gg
|
||||||
- --limit defaults 100
|
- --limit defaults 100
|
||||||
- --out defaults jobs.org
|
- --out-dir defaults exports
|
||||||
|
- --out optional explicit org output path
|
||||||
- --cache-dir defaults .cache/usajobs
|
- --cache-dir defaults .cache/usajobs
|
||||||
|
- --interactive / --no-interactive, default true
|
||||||
|
- --select-all preselects every row in the export picker
|
||||||
|
- --dry-run shows what would be exported without writing
|
||||||
|
- --offline reads cached json only and does not call api
|
||||||
|
- --debug prints api params and counts before/after filtering
|
||||||
|
|
||||||
search behavior:
|
search behavior:
|
||||||
1. call https://data.usajobs.gov/api/search using official headers:
|
1. call https://data.usajobs.gov/api/search using official headers:
|
||||||
host: data.usajobs.gov
|
- host: data.usajobs.gov
|
||||||
user-agent: $USAJOBS_EMAIL
|
- user-agent: $USAJOBS_EMAIL
|
||||||
authorization-key: $USAJOBS_KEY
|
- authorization-key: $USAJOBS_KEY
|
||||||
2. request fields=full, resultsperpage=500, sortfield=opendate, sortdirection=desc
|
2. request:
|
||||||
|
- fields=full
|
||||||
|
- resultsperpage=500
|
||||||
|
- sortfield=opendate
|
||||||
|
- sortdirection=desc
|
||||||
3. cache raw json response per query/page under .cache/usajobs
|
3. cache raw json response per query/page under .cache/usajobs
|
||||||
4. apply local filters after fetching:
|
4. apply local filters after fetching:
|
||||||
- pay plan in allowed pay plans
|
- pay plan in allowed pay plans
|
||||||
@@ -44,13 +58,47 @@ search behavior:
|
|||||||
- high_grade <= grade_max
|
- high_grade <= grade_max
|
||||||
- salary max >= salary_min, or salary min >= salary_min if max absent
|
- salary max >= salary_min, or salary min >= salary_min if max absent
|
||||||
- location string contains/near requested location as available
|
- location string contains/near requested location as available
|
||||||
5. output table with columns:
|
5. output rich table with columns:
|
||||||
idx, title, agency, grade, salary, location, close date, clearance match, url
|
- idx
|
||||||
6. selection/export:
|
- title
|
||||||
- after displaying table, allow user to arrow/highlight and mark for export
|
- agency
|
||||||
- pick sensible defaults, eg x or m for mark, u for unmark, a for all, e for export
|
- grade
|
||||||
- export selected jobs to new file with short slug name + datetime stamp
|
- salary
|
||||||
- org output format:
|
- location
|
||||||
|
- close date
|
||||||
|
- clearance match
|
||||||
|
- url
|
||||||
|
|
||||||
|
v1 selection/export behavior:
|
||||||
|
1. render the rich table of filtered jobs
|
||||||
|
2. below the table, open a questionary checkbox prompt:
|
||||||
|
"mark jobs to export"
|
||||||
|
3. each checkbox choice should be a compact one-line label:
|
||||||
|
"[12] dia | gg-15 | $167k-$191k | washington dc | information technology..."
|
||||||
|
4. value should be stable job id / document id, not table index
|
||||||
|
5. preselect nothing by default unless --select-all is passed
|
||||||
|
6. user toggles rows with space, navigates with arrows or j/k, confirms with enter
|
||||||
|
7. after confirmation, export checked jobs to org
|
||||||
|
8. selecting nothing exits without writing
|
||||||
|
9. ctrl-c cancels cleanly
|
||||||
|
|
||||||
|
questionary defaults/keys:
|
||||||
|
- arrows navigate
|
||||||
|
- j/k navigate
|
||||||
|
- ctrl-n/ctrl-p navigate
|
||||||
|
- space toggles mark/unmark
|
||||||
|
- enter confirms/export
|
||||||
|
- ctrl-c cancels
|
||||||
|
- prompt instruction should say:
|
||||||
|
"space=mark/unmark, enter=export, ctrl-c=cancel"
|
||||||
|
|
||||||
|
export naming:
|
||||||
|
- if --out is absent, create a new timestamped file:
|
||||||
|
exports/usajobs_<location-slug>_<filters-slug>_<yyyymmdd-hhmm>.org
|
||||||
|
- example:
|
||||||
|
exports/usajobs_washington-dc_2210-0340_gs15_salary150_20260518-1412.org
|
||||||
|
|
||||||
|
org output format:
|
||||||
```
|
```
|
||||||
** <shortened job title> [[url][link]]
|
** <shortened job title> [[url][link]]
|
||||||
:properties:
|
:properties:
|
||||||
@@ -67,9 +115,8 @@ clearance: <clearance/security text or unknown>
|
|||||||
*** posting
|
*** posting
|
||||||
<raw posting text>
|
<raw posting text>
|
||||||
```
|
```
|
||||||
7. (stretch) Cache each query, allow arrow/scroll through them like a cli, recall, or save filters. could be too much.
|
|
||||||
|
|
||||||
implementation notes:
|
## implementation notes:
|
||||||
- write clean functional code:
|
- write clean functional code:
|
||||||
- build_params()
|
- build_params()
|
||||||
- fetch_page()
|
- fetch_page()
|
||||||
@@ -77,17 +124,75 @@ implementation notes:
|
|||||||
- normalize_job()
|
- normalize_job()
|
||||||
- passes_filters()
|
- passes_filters()
|
||||||
- render_table()
|
- render_table()
|
||||||
- parse_selection()
|
- compact_job_label()
|
||||||
|
- choose_jobs()
|
||||||
- export_org()
|
- export_org()
|
||||||
|
- make_output_path()
|
||||||
- normalize both official api shape and frontend-ish shape if present:
|
- normalize both official api shape and frontend-ish shape if present:
|
||||||
- api jobs may use MatchedObjectDescriptor
|
- api jobs may use MatchedObjectDescriptor
|
||||||
- details may be under UserArea.Details
|
- details may be under UserArea.Details
|
||||||
- raw posting text should combine title, summary, duties, requirements, qualifications, evaluations, other info, key requirements.
|
- normalized job dict should include:
|
||||||
- shortened job title should be max 80 chars, strip all-caps screaming where reasonable, preserve meaning.
|
- document_id
|
||||||
- include helpful errors if env vars missing.
|
- title
|
||||||
- include a --offline flag that only reads cached json and does not call api.
|
- agency
|
||||||
- include a --debug flag that prints api params and counts before/after filtering.
|
- department
|
||||||
|
- pay_plan
|
||||||
|
- low_grade
|
||||||
|
- high_grade
|
||||||
|
- salary_min
|
||||||
|
- salary_max
|
||||||
|
- location
|
||||||
|
- close_date
|
||||||
|
- travel
|
||||||
|
- clearance
|
||||||
|
- clearance_text_match
|
||||||
|
- url
|
||||||
|
- raw_posting_text
|
||||||
|
- raw posting text should combine:
|
||||||
|
- title
|
||||||
|
- summary
|
||||||
|
- duties
|
||||||
|
- requirements
|
||||||
|
- qualifications
|
||||||
|
- evaluations
|
||||||
|
- other information
|
||||||
|
- key requirements
|
||||||
|
- shortened job title should be max 80 chars
|
||||||
|
- compact selection row title should fit reasonably in 120 cols
|
||||||
|
- compact row should include idx, agency, grade, salary, location, title
|
||||||
|
- truncate title to about 55 chars in the picker
|
||||||
|
- avoid putting full url in questionary prompt; keep url in rich table and org output
|
||||||
|
- include helpful errors if env vars are missing
|
||||||
|
- never mutate cached raw results
|
||||||
|
|
||||||
acceptance test:
|
selection implementation hint:
|
||||||
- running the command with `--salary-min 150 --grade-min 15 --grade-max 15 --radius 25` should not show gs/gg-13 jobs after local filtering.
|
|
||||||
- selecting `none` exits without writing.
|
```python
|
||||||
|
import questionary
|
||||||
|
from questionary import Choice
|
||||||
|
|
||||||
|
def choose_jobs(jobs: list[dict], select_all: bool = False) -> list[dict]:
|
||||||
|
by_id = {job["document_id"]: job for job in jobs}
|
||||||
|
|
||||||
|
choices = [
|
||||||
|
Choice(
|
||||||
|
title=compact_job_label(job, idx),
|
||||||
|
value=job["document_id"],
|
||||||
|
checked=select_all,
|
||||||
|
)
|
||||||
|
for idx, job in enumerate(jobs, start=1)
|
||||||
|
]
|
||||||
|
|
||||||
|
selected_ids = questionary.checkbox(
|
||||||
|
"mark jobs to export",
|
||||||
|
choices=choices,
|
||||||
|
instruction="space=mark/unmark, enter=export, ctrl-c=cancel",
|
||||||
|
use_jk_keys=True,
|
||||||
|
use_emacs_keys=True,
|
||||||
|
).ask()
|
||||||
|
|
||||||
|
if not selected_ids:
|
||||||
|
return []
|
||||||
|
|
||||||
|
return [by_id[job_id] for job_id in selected_ids]
|
||||||
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user