A CommonGrants SDK plugin that extends the Opportunity model with custom fields for HHS/simpler-grants-gov opportunity data.
This plugin registers grants.gov-specific fields on the CommonGrants Opportunity schema, provides bidirectional transforms between the grants.gov v1 API format (GrantsGovOpportunitySchema) and the CommonGrants OpportunityBase format, and registers the custom search filters that Simpler.Grants.gov accepts on opportunities.search.
Capabilities: customFields, transforms, customFilters
See TRANSFORMS.md for the complete field mapping reference, custom fields table, status and applicant-type mappings, and a guide to writing your own plugin.
pip install cg-grants-govOr with Poetry:
poetry add cg-grants-govParse an opportunity already in CommonGrants format (e.g. from the API) into the typed model, including all grants.gov custom fields:
from cg_grants_gov import grants_gov
opp_raw = {
"id": "573525f2-8e15-4405-83fb-e6523511d893",
"title": "STEM Education Grant Program",
"status": {"value": "open"},
"description": "A grant program focused on STEM education.",
"createdAt": "2025-01-01T00:00:00Z",
"lastModifiedAt": "2025-01-15T00:00:00Z",
"customFields": {
"agency": {
"name": "agency",
"fieldType": "object",
"value": {"code": "HHS", "name": "Department of Health and Human Services"},
},
"fiscalYear": {"name": "fiscalYear", "fieldType": "integer", "value": 2025},
},
}
opp = grants_gov.schemas.Opportunity.model_validate(opp_raw)
print(opp.title) # "STEM Education Grant Program"
print(opp.custom_fields.agency.value.name) # "Department of Health and Human Services"
print(opp.custom_fields.fiscal_year.value) # 2025The plugin registers four custom search filters on opportunities.search, in
addition to the standard CommonGrants filters:
| Filter | Family |
|---|---|
agency |
StringArray |
applicantType |
StringArray |
fundingInstrument |
StringArray |
costSharing |
BooleanComparison |
A client from grants_gov.get_client(...) binds these filters, so
search(filters=...) validates each value against its declared filter model and
raises FilterError fail-fast for a wrong-typed value before any request is sent:
from common_grants_sdk.client import Config
from common_grants_sdk.extensions import f
from cg_grants_gov import grants_gov
client = grants_gov.get_client(
Config(base_url="https://api.simpler.grants.gov", api_key="your-api-key"),
)
result = client.opportunities.search(
filters={
"status": f.in_(["open"]),
"agency": f.in_(["NSF"]),
"applicantType": f.in_(["state_governments"]),
"fundingInstrument": f.in_(["grant"]),
"costSharing": f.eq(False),
},
page=1,
)
print(result.pagination_info.total_items)
for opp in result.items:
print(opp.title, opp.custom_fields.agency.value.code)For a runnable end-to-end version, see
examples/search_with_filters.py. It calls
the live Simpler.Grants.gov API, so it requires an SGG_API_KEY; SGG_BASE_URL
is optional and defaults to the production API. Run it with:
poetry run python examples/search_with_filters.pyConvert a raw grants.gov v1 API response to CommonGrants format:
from cg_grants_gov import grants_gov
from cg_grants_gov.transforms import to_common
source = {
"opportunity_id": "573525f2-8e15-4405-83fb-e6523511d893",
"opportunity_title": "STEM Education Grant Program",
"opportunity_status": "posted",
"agency_code": "HHS",
"agency_name": "Department of Health and Human Services",
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-15T00:00:00Z",
"summary": {
"summary_description": "A grant program focused on STEM education.",
"is_forecast": False,
"fiscal_year": 2025,
"award_ceiling": 500000,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-15T00:00:00Z",
},
}
result = to_common(source)
if result.errors:
for err in result.errors:
print(f"Transform error at {err.path}: {err.message}")
else:
opp = result.result
print(opp.title) # "STEM Education Grant Program"
print(opp.status.value) # "open" (posted → open)
print(opp.custom_fields.fiscal_year.value) # 2025
print(opp.custom_fields.agency.value.code) # "HHS"Convert a CommonGrants opportunity back to grants.gov format:
from cg_grants_gov.transforms import from_common
result = from_common(opp)
if not result.errors:
native = result.result
print(native.opportunity_status) # "posted"
print(native.agency_code) # "HHS"
print(native.summary.fiscal_year) # 2025The plugin is assembled in cg_grants_gov/__init__.py using these components:
| Component | What it is | File |
|---|---|---|
GrantsGovOpportunitySchema |
Pydantic model for the grants.gov v1 API response | models.py |
OpportunityFields |
CustomFieldSet subclass declaring all 21 custom fields |
models.py |
to_common |
Transforms GrantsGovOpportunitySchema → OpportunityBase[OpportunityFields] |
transforms.py |
from_common |
Transforms OpportunityBase[OpportunityFields] → GrantsGovOpportunitySchema |
transforms.py |
OppSearchFilters |
OpportunityFilters subclass declaring the custom search filters |
__init__.py |
OppSearchFilters extends the SDK's OpportunityFilters with the four custom
filters documented under Search with custom filters,
each annotated with its filter family so the SDK validates call-site values and
recovers their type when classifying the request.
The schema, transforms, and filter routes are wired together via define_plugin.
Passing OppSearchFilters through PluginRoutes is what registers the custom
filters on opportunities.search and backs the customFilters capability:
from common_grants_sdk import PluginSchemas, define_plugin, schema
from common_grants_sdk.extensions import PluginMeta, PluginRoutes, ResourceRoutes
grants_gov = define_plugin(
PluginSchemas(
Opportunity=schema(
source_schema=GrantsGovOpportunitySchema,
common_schema=OpportunityBase[OpportunityFields],
to_common=to_common,
from_common=from_common,
)
),
routes=PluginRoutes(opportunities=ResourceRoutes(search=OppSearchFilters)),
meta=PluginMeta(
name="grants.gov",
source_system="Simpler.Grants.gov",
capabilities=["customFields", "transforms", "customFilters"],
),
)See TRANSFORMS.md for a detailed walkthrough of how to write your own plugin modelled on this one.
See DEVELOPMENT.md for setup instructions, available commands, and the release runbook.
See CONTRIBUTING.md for guidelines.
This project is in the public domain. See LICENSE.md for details.