diff --git a/readme.md b/readme.md index 0dc3946..fd87b2c 100644 --- a/readme.md +++ b/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 -- apply strict local filters +- apply strict local filters because usajobs search facets are unreliable - 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+ - click for cli args - requests for api -- rich for table + row selection prompt -- pathlib/json/csv stdlib only otherwise +- rich for table output +- 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_KEY -basic command: -`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` +### Run: +`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 - --salary-min 150 means $150,000 - --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 - --pay-plan may repeat, default gs and gg - --limit defaults 100 -- --out defaults jobs.org +- --out-dir defaults exports +- --out optional explicit org output path - --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: 1. call https://data.usajobs.gov/api/search using official headers: - host: data.usajobs.gov - user-agent: $USAJOBS_EMAIL - authorization-key: $USAJOBS_KEY -2. request fields=full, resultsperpage=500, sortfield=opendate, sortdirection=desc + - host: data.usajobs.gov + - user-agent: $USAJOBS_EMAIL + - authorization-key: $USAJOBS_KEY +2. request: + - fields=full + - resultsperpage=500 + - sortfield=opendate + - sortdirection=desc 3. cache raw json response per query/page under .cache/usajobs 4. apply local filters after fetching: - pay plan in allowed pay plans @@ -44,13 +58,47 @@ search behavior: - high_grade <= grade_max - salary max >= salary_min, or salary min >= salary_min if max absent - location string contains/near requested location as available -5. output table with columns: - idx, title, agency, grade, salary, location, close date, clearance match, url -6. selection/export: -- after displaying table, allow user to arrow/highlight and mark for export - - pick sensible defaults, eg x or m for mark, u for unmark, a for all, e for export -- export selected jobs to new file with short slug name + datetime stamp -- org output format: +5. output rich table with columns: + - idx + - title + - agency + - grade + - salary + - 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___.org +- example: + exports/usajobs_washington-dc_2210-0340_gs15_salary150_20260518-1412.org + +org output format: ``` ** [[url][link]] :properties: @@ -67,9 +115,8 @@ clearance: *** posting ``` -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: - build_params() - fetch_page() @@ -77,17 +124,75 @@ implementation notes: - normalize_job() - passes_filters() - render_table() - - parse_selection() + - compact_job_label() + - choose_jobs() - export_org() + - make_output_path() - normalize both official api shape and frontend-ish shape if present: - api jobs may use MatchedObjectDescriptor - details may be under UserArea.Details -- raw posting text should combine title, summary, duties, requirements, qualifications, evaluations, other info, key requirements. -- shortened job title should be max 80 chars, strip all-caps screaming where reasonable, preserve meaning. -- include helpful errors if env vars missing. -- include a --offline flag that only reads cached json and does not call api. -- include a --debug flag that prints api params and counts before/after filtering. +- normalized job dict should include: + - document_id + - title + - agency + - 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: -- 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. +selection implementation hint: + +```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] +```