docs(sdk): add grants.gov custom-filters search examples (Py + TS)#1003
docs(sdk): add grants.gov custom-filters search examples (Py + TS)#1003SnowboardTechie wants to merge 6 commits into
Conversation
Add examples/grants_gov_custom_filters.py demonstrating a Simpler.Grants.gov opportunity search with the cg-grants-gov plugin's custom filters (agency, applicantType, fundingInstrument, costSharing) via get_client + search(filters=).
…example classify_filters now returns the OppFilters request body directly (ClassifyResult retired) and raises FilterError on a bad value. Drop the .result unwrap and the collected-errors check.
Add examples/grants-gov-custom-filters.ts + example:grants-gov-custom-filters script, mirroring the Python grants_gov_custom_filters.py: import the cg-grants-gov plugin, show the classified customFilters request body, then getClient + search. Same 4 SGG filters as the Python side (contract parity).
The TS example passed the text query as 'search', but SearchOptions names it 'query' (the client maps it to the wire body's 'search' field). tsx does not typecheck, so the example ran with the term silently dropped; tsc catches it as TS2353. Also apply black to the Python example. The remaining red on the Python type check is release-ordering: the installed cg-grants-gov predates the filters release, so get_client still types filters as the core OpportunityFilters. It clears when the plugin publishes and the pin bumps.
The client's search(filters=...) is typed by the plugin's registered route TypedDict, so the flat dict literal needs the OppSearchFilters annotation to typecheck. Annotation-only; no runtime change.
| # The request body the client will POST. Shown here (via classify_filters, | ||
| # which get_client runs internally) so the customFilters split is visible | ||
| # without a live endpoint: status stays top-level, the four customs land in | ||
| # customFilters. classify_filters returns the body directly and raises | ||
| # FilterError on a bad value (fail-fast) — these filters are all valid. | ||
| body = classify_filters( | ||
| grants_gov.routes, "opportunities", "search", filters | ||
| ).model_dump(by_alias=True, exclude_none=True, mode="json") | ||
| print("Request body (default fields + customFilters):") | ||
| print(json.dumps(body, indent=2), "\n") |
There was a problem hiding this comment.
I know this is mostly included to demonstrate what's happening behind the scenes, but I'd avoid any direct invocation of classify_filters() in examples, since the ultimate goal would be to make this a private function or method.
| search_term = sys.argv[1] if len(sys.argv) > 1 else "education" | ||
| api_key = "two_org_user_key" | ||
| base_url = "http://localhost:8080" | ||
| config = Config(base_url=base_url, api_key=api_key, timeout=5.0) |
There was a problem hiding this comment.
Rather than using the locally hosted version of the SGG API, I'd prefer to have this run against the production API and prompt users to set their API key as an environment variable if not already set.
| // status stays top-level, the four customs land under customFilters. | ||
| const routes = plugin.routes; | ||
| if (!routes) throw new Error("plugin registered no custom filter routes"); | ||
| const body = classifyFilters(routes, "opportunities", "search", filters); |
There was a problem hiding this comment.
Same note as the Python example. Let's not demonstrate any public use of this function, since the goal is to make this private and invisible to the plugin consumer
| import { classifyFilters, F } from "@common-grants/sdk/extensions"; | ||
|
|
||
| const searchTerm = process.argv[2] ?? "education"; | ||
| const baseUrl = "http://localhost:8080"; |
There was a problem hiding this comment.
Also same note as python, let's actually test this against the production SGG API, rather than the locally hosted version.
|
Closing per the review discussion in Slack: plugin-consuming examples belong in the plugin repos rather than the SDK codebases, which also removes the publish-ordering dependency that kept this PR's CI red. The live filters examples are in py-cg-grants-gov PR 11 and ts-cg-grants-gov PR 18; moving the transforms examples out of the SDKs is a follow-up. |
Summary
Changes proposed
lib/python-sdk/examples/grants_gov_custom_filters.py— imports thegrants.govplugin, classifies a filter set through its registered routes into the three-bucket wire body, and runs a search via the SDK client.lib/ts-sdk/examples/grants-gov-custom-filters.ts— the TypeScript counterpart, plus anexample:grants-gov-custom-filterspnpm script.Context for reviewers
Mirrors the existing
grants_gov_transforms.py/grants-gov-transforms.tsexamples (#900) one-for-one — same structure, same "consume the real plugin" shape. The Python example uses the SDK's fail-fastclassify_filters(returns the classified body directly, raisesFilterErroron a bad value).Targets
HOLD-filtersbecause the examples need the SDK's filters API, which rides #918 to main.Verified locally against the
HOLD-filtersSDK build and the plugin's registered filter routes (#901 / #903).Release ordering: the examples import the published plugin, so the
cg-grants-gov/@common-grants/cg-grants-govpins bump to the plugin's filters release at merge time. CI stays red until the plugin publishes; verified locally in the meantime.Additional information
Filter set exercised:
agency,applicantType,fundingInstrument,costSharing— matching the plugin registration and the Simpler.Grants.gov deployment's honored customFilters.