usagov-search 0.2.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.
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ build/
5
+ dist/
6
+ .venv/
7
+ .pytest_cache/
8
+ .ruff_cache/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 usagov-search contributors
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,487 @@
1
+ Metadata-Version: 2.5
2
+ Name: usagov-search
3
+ Version: 0.2.0
4
+ Summary: Search USA.gov, extract page content, archive HTML, and ask Groq-powered questions
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Keywords: ai,cli,groq,rag,scraper,search,usa.gov,usagov
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Internet :: WWW/HTTP
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.9
23
+ Requires-Dist: playwright>=1.40
24
+ Provides-Extra: agent
25
+ Requires-Dist: groq>=0.20; extra == 'agent'
26
+ Provides-Extra: dev
27
+ Requires-Dist: build; extra == 'dev'
28
+ Requires-Dist: pytest>=8; extra == 'dev'
29
+ Requires-Dist: ruff; extra == 'dev'
30
+ Requires-Dist: twine; extra == 'dev'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # usagov-search
34
+
35
+ Search [USA.gov](https://www.usa.gov/) from Python or the terminal, extract the
36
+ full text of individual result pages, and archive rendered HTML. Supports the
37
+ English and Spanish editions, pagination, JSON/CSV output, and polite rate
38
+ limiting.
39
+
40
+ It also includes an optional AI assistant (`usagov-ask`) that answers questions
41
+ from USA.gov search results or from pages you point it at, with citations.
42
+
43
+ The PyPI distribution is `usagov-search`; the import package is
44
+ `usagov_search`; the commands are `usagov-search` and `usagov-ask`.
45
+
46
+ ## Installation
47
+
48
+ ```bash
49
+ python -m pip install usagov-search
50
+ python -m playwright install chromium
51
+ ```
52
+
53
+ Playwright is installed as a dependency, but its Chromium binary is a separate
54
+ download. The second command is required once per machine.
55
+
56
+ ## CLI quick start
57
+
58
+ ```bash
59
+ usagov-search "passport"
60
+ usagov-search "student loans" --limit 5
61
+ usagov-search "impuestos" --lang es
62
+
63
+ # Also works as a module
64
+ python -m usagov_search "passport" --json
65
+ ```
66
+
67
+ ## Library usage
68
+
69
+ ```python
70
+ from usagov_search import search
71
+
72
+ response = search(
73
+ "passport",
74
+ limit=5,
75
+ extract=[1],
76
+ save_dir="pages",
77
+ )
78
+
79
+ print(response.total) # reported result count
80
+ print(response.returned) # results returned here
81
+ print(response.results[0].title)
82
+ print(response.extracted[0].h1)
83
+ print(response.extracted[0].headings)
84
+ print(response.extracted[0].text) # full page text
85
+ print(response.saved_files) # written HTML paths
86
+ ```
87
+
88
+ `search()` accepts `extract=[1, 3]`, `extract="1-3"`, or `extract="2,4,7"`.
89
+ Use `save_dir=None` (default) to skip HTML archiving.
90
+
91
+ Lower-level helpers are exported too:
92
+
93
+ ```python
94
+ from usagov_search import parse_extract_values, save_html
95
+
96
+ ranks = parse_extract_values(["1-3", "5"]) # [1, 2, 3, 5]
97
+ path = save_html("pages", "custom.html", html)
98
+ ```
99
+
100
+ `save_html()` creates the directory and appends `_2`, `_3`, … instead of
101
+ overwriting an existing file.
102
+
103
+ ## AI answers (optional, requires your own Groq key)
104
+
105
+ `usagov-search` itself makes no network calls beyond USA.gov. The assistant is an
106
+ optional extra that uses the Groq API. **The package does not ship a key.** Every
107
+ user supplies their own free key from
108
+ [console.groq.com/keys](https://console.groq.com/keys).
109
+
110
+ ```bash
111
+ python -m pip install "usagov-search[agent]"
112
+ ```
113
+
114
+ ### Set your key
115
+
116
+ ```powershell
117
+ # PowerShell (current session)
118
+ $env:GROQ_API_KEY="gsk_your_key_here"
119
+
120
+ # PowerShell (persist for new terminals)
121
+ setx GROQ_API_KEY "gsk_your_key_here"
122
+ ```
123
+
124
+ ```bash
125
+ # bash / zsh
126
+ export GROQ_API_KEY="gsk_your_key_here"
127
+ ```
128
+
129
+ You can also pass `api_key=` to `USAgovAgent(...)` or let `usagov-ask` prompt for
130
+ it. Prefer the environment variable: keys on the command line end up in shell
131
+ history.
132
+
133
+ ### CLI
134
+
135
+ ```bash
136
+ # Search USA.gov, then answer with citations
137
+ usagov-ask "How do I renew my passport?"
138
+
139
+ # Ask about specific pages instead of searching (USA.gov links only)
140
+ usagov-ask "How long does renewal take?" --url https://www.usa.gov/renew-adult-passport
141
+
142
+ # Several pages at once
143
+ usagov-ask "Compare the options" \
144
+ --url https://www.usa.gov/renew-adult-passport \
145
+ --url https://www.usa.gov/child-passport
146
+
147
+ # Spanish search edition, JSON output
148
+ usagov-ask "¿Cómo renuevo mi pasaporte?" --lang es --json
149
+
150
+ # Interactive session (keeps conversation context)
151
+ usagov-ask
152
+ ```
153
+
154
+ If `GROQ_API_KEY` is unset, `usagov-ask` asks for it with a hidden prompt and
155
+ never writes it to disk.
156
+
157
+ | Option | Default | Description |
158
+ |---|---|---|
159
+ | `--url URL` | off | Read this page instead of searching; repeatable |
160
+ | `--lang {en,es}` | `en` | Search the English or Spanish edition |
161
+ | `--model NAME` | `openai/gpt-oss-20b` | Groq model; `USAGOV_MODEL` also works |
162
+ | `--max-sources N` | `4` | Pages to retrieve and read |
163
+ | `--json` | off | Emit `{text, sources, model, warnings}` |
164
+ | `--timeout SEC` | `45` | Per-page browser timeout |
165
+ | `--headed` | off | Show the Chromium window |
166
+
167
+ ### Library
168
+
169
+ ```python
170
+ import os
171
+
172
+ from usagov_search import USAgovAgent
173
+
174
+ agent = USAgovAgent(api_key=os.environ["GROQ_API_KEY"])
175
+
176
+ answer = agent.ask("What do I need to renew an adult passport?")
177
+ print(answer.text)
178
+ for source in answer.sources:
179
+ print(f"[{source.index}] {source.title} - {source.url}")
180
+
181
+ # Ask about one or more specific pages (must be usa.gov links)
182
+ answer = agent.ask(
183
+ "Can I renew by mail?",
184
+ urls="https://www.usa.gov/renew-adult-passport",
185
+ )
186
+ print(answer.text)
187
+
188
+ agent.reset() # clear conversation history
189
+ ```
190
+
191
+ `USAgovAgent` accepts `model`, `max_sources`, `max_source_chars`,
192
+ `max_context_chars`, `temperature`, `max_tokens`, `history_turns`, `timeout`,
193
+ `retries`, `delay`, and `headed`. Pass `client=` to inject your own Groq client
194
+ (tests use this to avoid network calls).
195
+
196
+ How it works:
197
+
198
+ 1. Your question is reduced to search keywords and run through the normal
199
+ `usagov-search` retrieval path (or your `urls` are fetched directly).
200
+ 2. Page text is trimmed to a character budget and numbered as `[1]`, `[2]`, …
201
+ 3. One Groq chat completion receives the numbered sources plus the question and
202
+ is instructed to answer only from them and cite `[n]`.
203
+ 4. The answer and its source list are returned together.
204
+
205
+ What leaves your machine: the question and the trimmed page excerpts are sent to
206
+ Groq. Page fetching stays between you and USA.gov. Do not paste secrets into
207
+ questions.
208
+
209
+ Limitations: answers reflect the page text at request time, not a live policy
210
+ database; USA.gov can change deadlines and fees. The model is told to refuse
211
+ when the sources do not contain the answer, but verify official details on the
212
+ linked page. Extracted text is plain text, so links and tables lose structure.
213
+
214
+ ## Why Playwright?
215
+
216
+ USA.gov search is served by `search.usa.gov`, which sits behind an AWS WAF
217
+ JavaScript challenge. Plain HTTP clients (`requests`, `curl`, `urllib`) receive
218
+ HTTP 202 with an empty body. The package drives Chromium through
219
+ [Playwright](https://playwright.dev/python/) so the challenge can complete, then
220
+ parses the rendered DOM.
221
+
222
+ ## CLI examples
223
+
224
+ ```bash
225
+ # English search, default 20 results
226
+ usagov-search "passport"
227
+
228
+ # Limit results
229
+ usagov-search "passport" --limit 5
230
+
231
+ # Spanish edition
232
+ usagov-search "pasaporte" --lang es
233
+
234
+ # Start from page 2
235
+ usagov-search "taxes" --page 2 --limit 3
236
+
237
+ # Fetch across pages (20 results per page)
238
+ usagov-search "passport" --limit 45 --delay 1
239
+
240
+ # All available pages, capped at 5 pages
241
+ usagov-search "passport" --limit 0 --max-pages 5
242
+
243
+ # Machine-readable output
244
+ usagov-search "student loans" --json
245
+ usagov-search "student loans" --csv > results.csv
246
+
247
+ # Extract full contents of specific result pages (1-based ranks)
248
+ usagov-search "passport" --limit 5 --extract 1
249
+ usagov-search "passport" --limit 5 --extract 1-3
250
+ usagov-search "passport" --limit 10 --extract 2,4,7
251
+
252
+ # Full body text in table output (default limit: 4000 characters)
253
+ usagov-search "passport" --extract 1 --extract-chars 0
254
+
255
+ # Save search pages and extracted pages as rendered HTML
256
+ usagov-search "passport" --extract 2 --save-html pages
257
+
258
+ # Show the browser window (useful if the WAF challenge stalls)
259
+ usagov-search "passport" --headed
260
+ ```
261
+
262
+ `--extract` refers to ranks in the returned result list, so referenced ranks
263
+ must exist within `--limit` (default 20). Put `QUERY` before `--save-html`;
264
+ the option takes an optional directory name.
265
+
266
+ ## Options
267
+
268
+ | Option | Default | Description |
269
+ |---|---|---|
270
+ | `--lang {en,es}` | `en` | Search the English or Spanish edition |
271
+ | `--affiliate ID` | auto | Override the `search.usa.gov` affiliate ID |
272
+ | `--page N` | `1` | First results page (1-based) |
273
+ | `--limit N` | `20` | Maximum results; `0` means all pages up to `--max-pages` |
274
+ | `--max-pages N` | `10` | Safety cap on pages fetched |
275
+ | `--timeout SEC` | `45` | Per-page navigation/result timeout |
276
+ | `--retries N` | `2` | Retries when the WAF stalls a request |
277
+ | `--delay SEC` | `1.0` | Delay between page requests |
278
+ | `--json` | off | Emit JSON |
279
+ | `--csv` | off | Emit CSV |
280
+ | `--snippet-width N` | `160` | Truncate titles/snippets in table output |
281
+ | `--extract SPEC` | off | Extract page contents for ranks, e.g. `1`, `1,3`, `1-3`; repeatable |
282
+ | `--extract-chars N` | `4000` | Truncate extracted text in table output; `0` means all |
283
+ | `--save-html [DIR]` | off | Save rendered HTML to `DIR` (default `saved_pages`) |
284
+ | `--headed` | off | Show the Chromium window |
285
+
286
+ `--json` and `--csv` are mutually exclusive.
287
+
288
+ ## Output
289
+
290
+ ### Table
291
+
292
+ ```text
293
+ Query: passport (en) - 31 reported, 3 returned
294
+
295
+ 1. Renew an adult passport | USAGov
296
+ https://www.usa.gov/renew-adult-passport
297
+ Find out if you can renew your passport or if you have to get a new one...
298
+ ```
299
+
300
+ ### Extracted content
301
+
302
+ `--extract` prints the result list, then a detail block per requested page:
303
+ final URL, metadata, heading outline, an excerpt of the body text (up to
304
+ `--extract-chars`), and the HTML path when `--save-html` is used. JSON and CSV
305
+ always contain the full extracted text.
306
+
307
+ ```text
308
+ Extracted result 1: Renew an adult passport | USAGov
309
+ URL: https://www.usa.gov/renew-adult-passport
310
+ H1: Renew an adult passport
311
+ Description: Find out if you can renew your passport or if you have to get a new one...
312
+ Last updated: March 23, 2026
313
+ Headings (15):
314
+ H1: Renew an adult passport
315
+ H2: Warning
316
+ H2: Determine if you can renew your passport
317
+ ...
318
+
319
+ Renew an adult passport
320
+
321
+ Find out if you can renew your passport or need a new one...
322
+
323
+ LAST UPDATED: March 23, 2026
324
+
325
+ HTML: pages\page_01_renew-adult-passport.html
326
+ ```
327
+
328
+ ### JSON
329
+
330
+ ```json
331
+ {
332
+ "query": "student loans",
333
+ "lang": "en",
334
+ "affiliate": "usagov_en_internal",
335
+ "total": 10,
336
+ "returned": 2,
337
+ "results": [
338
+ {
339
+ "rank": 1,
340
+ "title": "Resolve student loan payment problems | USAGov",
341
+ "url": "https://www.usa.gov/student-loan-problems",
342
+ "display_url": "www.usa.gov/student-loan-problems",
343
+ "snippet": "Contact your loan servicer if you are struggling to repay..."
344
+ }
345
+ ],
346
+ "extracted": [
347
+ {
348
+ "rank": 1,
349
+ "requested_url": "https://www.usa.gov/student-loan-problems",
350
+ "final_url": "https://www.usa.gov/student-loan-problems",
351
+ "title": "Resolve student loan payment problems | USAGov",
352
+ "h1": "Resolve student loan payment problems",
353
+ "description": "Contact your loan servicer if you are struggling...",
354
+ "canonical": "https://www.usa.gov/student-loan-problems",
355
+ "last_updated": "August 15, 2026",
356
+ "headings": [{"level": "h1", "text": "Resolve student loan payment problems"}],
357
+ "text": "Full page text...",
358
+ "html_file": null
359
+ }
360
+ ],
361
+ "saved_files": [],
362
+ "warnings": []
363
+ }
364
+ ```
365
+
366
+ `extracted` is empty unless `--extract` is used; `saved_files` lists every file
367
+ written by `--save-html`.
368
+
369
+ ### CSV
370
+
371
+ ```csv
372
+ rank,title,url,display_url,snippet,content,html_file
373
+ 1,Taxes | USAGov,https://www.usa.gov/taxes,www.usa.gov/taxes,"income tax. Check the status...",,
374
+ 2,Renew an adult passport | USAGov,https://www.usa.gov/renew-adult-passport,www.usa.gov/renew-adult-passport,"Find out if you can renew...","Renew an adult passport...",pages\page_02_renew-adult-passport.html
375
+ ```
376
+
377
+ Extracted rows repeat the search result fields and add `content` (the body
378
+ text) and `html_file` (blank unless the page was saved).
379
+
380
+ ## How it works
381
+
382
+ 1. Builds a request to:
383
+
384
+ ```text
385
+ https://search.usa.gov/search?affiliate=usagov_en_internal&query=...&page=N
386
+ ```
387
+
388
+ The Spanish affiliate is `usagov_es_internal`.
389
+
390
+ 2. Loads the page in Chromium and waits for the WAF challenge to finish.
391
+ 3. Extracts results from the rendered DOM:
392
+
393
+ | Data | Selector |
394
+ |---|---|
395
+ | Result cards | `#results a.no-underline` |
396
+ | Title | `.result-title` |
397
+ | Snippet | `.result-desc p` |
398
+ | Display URL | `.result-url-text` |
399
+ | Total count | `.results-count` |
400
+ | Next page | `.usa-pagination__next-page` |
401
+
402
+ 4. Follows the "Next" link until `limit` is reached or `max_pages` is hit.
403
+ 5. De-duplicates by URL and assigns sequential ranks.
404
+ 6. For each extracted rank, opens the result page and clones its main content
405
+ (`<main id="main-content">`, falling back to `<main>` or `<body>`). It removes
406
+ chrome and scripts (`nav`, `header`, `footer`, `aside`, `form`, `button`,
407
+ `svg`, `iframe`, `[aria-hidden="true"]`), then reads `innerText` to produce
408
+ readable text, alongside the title, meta description, canonical URL,
409
+ last-updated value, and the H1–H4 outline.
410
+ 7. With `save_dir` / `--save-html`, writes the fully rendered DOM for each
411
+ search page and extracted page to the target directory.
412
+
413
+ Search-result parsing lives in `client.extract_search_page()`; page extraction
414
+ lives in `client.extract_page()`. If USA.gov changes its markup, update the
415
+ selectors there.
416
+
417
+ ## Notes and limitations
418
+
419
+ - **Rate limits:** `search.usa.gov` rate-limits bursts. Run one instance at a
420
+ time and keep `--delay` at 1 second or higher when paging or extracting.
421
+ - **WAF stalls:** If the challenge does not complete, retry after a minute,
422
+ increase `--timeout`, or use `--headed`.
423
+ - **Result order** can vary slightly between runs.
424
+ - **Snippets** include the site's own duplicated fragments
425
+ (e.g. `Learn...Learn how to renew...`).
426
+ - **Rank** is relative to the returned set, so `--page 2` starts again at 1.
427
+ - **Extraction is text-only:** links, tables, and layout are flattened into
428
+ plain text. Use HTML saving when you need the original structure.
429
+ - **One request per extracted page.** Larger extraction sets take longer; keep
430
+ `--delay` at 1 second or higher.
431
+ - **Saved HTML is a rendered snapshot.** Assets and links may still be relative
432
+ to `www.usa.gov`, so opening the file offline can look unstyled.
433
+ - This is an unofficial convenience client. Respect the site's terms and do not
434
+ hammer it.
435
+
436
+ ## Development
437
+
438
+ ```bash
439
+ git clone <your-repo>
440
+ cd <repo-checkout>
441
+ python -m pip install -e ".[dev,agent]"
442
+ python -m playwright install chromium
443
+ pytest
444
+ ```
445
+
446
+ The package uses a src layout:
447
+
448
+ ```text
449
+ src/usagov_search/
450
+ ├── __init__.py # public API and version
451
+ ├── __main__.py # python -m usagov_search
452
+ ├── agent.py # USAgovAgent: retrieval + Groq answer with citations
453
+ ├── cli.py # argument parsing and output formatting
454
+ ├── cli_agent.py # usagov-ask
455
+ ├── client.py # search, fetch_page, extraction, HTML saving
456
+ ├── models.py # Result, ExtractedPage, SearchResponse
457
+ ├── parsing.py # count/rank parsing and text helpers
458
+ └── py.typed
459
+ ```
460
+
461
+ Tests under `tests/` are offline. They cover parsing, HTML saving, CLI output,
462
+ and the agent (using a fake Groq client); they do not launch a browser or call
463
+ Groq.
464
+
465
+ ## Publishing
466
+
467
+ ```bash
468
+ python -m pip install --upgrade build twine
469
+ python -m build
470
+ python -m twine check dist/*
471
+
472
+ # Verify on TestPyPI first
473
+ python -m twine upload --repository testpypi dist/*
474
+ python -m pip install --index-url https://test.pypi.org/simple/ usagov-search
475
+
476
+ # Production
477
+ python -m twine upload dist/*
478
+ ```
479
+
480
+ Use a PyPI API token with username `__token__` and an environment variable;
481
+ never commit the token. Bump `__version__` in `src/usagov_search/__init__.py`
482
+ before each release.
483
+
484
+ ## License
485
+
486
+ MIT. USA.gov content itself is subject to its own terms; this package only
487
+ provides a client for the public search interface.