A Python SDK for interacting with the CommonGrants protocol, providing a type-safe interface for managing grant opportunities.
- Type-Safe Models: Built with Pydantic v2 for robust data validation and serialization
- Comprehensive Schema Support: Full implementation of the CommonGrants protocol schemas
- Modern Python: Requires Python 3.11+ for optimal performance and type safety
- Extensible: Easy to extend with custom fields and validation
# Using pip
pip install common-grants-sdk
# Using Poetry
poetry add common-grants-sdkfrom datetime import datetime, date, UTC
from uuid import uuid4
from common_grants_sdk.schemas.pydantic import (
Event,
Money,
OpportunityBase,
OppFunding,
OppStatus,
OppStatusOptions,
OppTimeline,
)
# Create a new opportunity
opportunity = OpportunityBase(
id=uuid4(),
title="Research Grant 2024",
description="Funding for innovative research projects",
status=OppStatus(
value=OppStatusOptions.OPEN,
description="This opportunity is currently accepting applications"
),
created_at=datetime.now(UTC),
last_modified_at=datetime.now(UTC),
funding=OppFunding(
total_amount_available=Money(amount="100000.00", currency="USD"),
min_award_amount=Money(amount="10000.00", currency="USD"),
max_award_amount=Money(amount="50000.00", currency="USD"),
estimated_award_count=5
),
key_dates=OppTimeline(
app_opens=Event(
name="Application Opens",
date=date(2024, 1, 1),
description="Applications open"
),
app_deadline=Event(
name="Application Deadline",
date=date(2024, 3, 31),
description="Applications close"
)
)
)
# Serialize to JSON
json_data = opportunity.dump_json()
# Deserialize from JSON
loaded_opportunity = OpportunityBase.from_json(json_data)CommonGrantsBaseModel: Base class for all models, provides common serialization and validation methodsSystemMetadata: Tracks creation and modification timestamps for records
OpportunityBase: Core opportunity modelOppFunding: Funding details and constraintsOppStatus&OppStatusOptions: Opportunity status trackingOppTimeline: Key dates and milestones
Money: Represents monetary amounts with currencyDecimalString: Validated string representing a decimal numberEvent: Union of event typesEventType: Enum for event type discriminationSingleDateEvent: Event with a single dateDateRangeEvent: Event with a start and end dateOtherEvent: Event with a custom description or recurrenceCustomField: Flexible field type for custom dataCustomFieldType: Enum for custom field value typesISODate: Alias fordatetime.date(ISO 8601 date)ISOTime: Alias fordatetime.time(ISO 8601 time)UTCDateTime: Alias fordatetime.datetime(UTC timestamp)
The SDK includes a utility for transforming data according to a mapping specification:
transform_from_mapping()supports extracting fields, switching on values, and reshaping data dictionaries
from common_grants_sdk.utils.transformation import transform_from_mapping
source_data = {
"opportunity_id": 12345,
"opportunity_title": "Research into ABC",
"opportunity_status": "posted",
"summary": {
"award_ceiling": 100000,
"award_floor": 10000,
"forecasted_close_date": "2025-07-15",
"forecasted_post_date": "2025-05-01",
},
}
mapping = {
"id": { "field": "opportunity_id" },
"title": { "field": "opportunity_title" },
"status": {
"switch": {
"field": "opportunity_status",
"case": {
"posted": "open",
"closed": "closed",
},
"default": "custom",
}
},
"funding": {
"minAwardAmount": {
"amount": { "field": "summary.award_floor" },
"currency": "USD",
},
"maxAwardAmount": {
"amount": { "field": "summary.award_ceiling" },
"currency": "USD",
},
},
"keyDates": {
"appOpens": { "field": "summary.forecasted_post_date" },
"appDeadline": { "field": "summary.forecasted_close_date" },
},
}
transformed_data = transform_from_mapping(source_data, mapping)
assert transformed_data == {
"id": uuid4(),
"title": "Research into ABC",
"status": "open",
"funding": {
"minAwardAmount": { "amount": 10000, "currency": "USD" },
"maxAwardAmount": { "amount": 100000, "currency": "USD" },
},
"keyDates": {
"appOpens": "2025-05-01",
"appDeadline": "2025-07-15",
},
}The SDK includes a type-safe HTTP client for interacting with CommonGrants Protocol-compliant APIs. The client provides a Pythonic interface with automatic authentication, request/response parsing, and pagination support.
from common_grants_sdk.client import Client, Auth
from common_grants_sdk.client.config import Config
# Initialize client
config = Config(base_url="https://api.example.org")
client = Client(config=config, auth=Auth.api_key("YOUR_API_KEY"))
# Get a specific opportunity
opportunity = client.opportunity.get("<opportunity_id>")
print(opportunity.title)
# List opportunities
response = client.opportunity.list(page=1)
for opp in response.items:
print(opp.id, opp.title)For detailed documentation, examples, and configuration options, see the HTTP Client README.
See LICENSE
The SDK includes a plugin framework for defining typed custom fields, combining extensions from multiple sources, and generating static Pydantic models.
from common_grants_sdk import define_plugin, compose
from common_grants_sdk.extensions import SchemaExtensions, CustomFieldSpec
local_extensions: SchemaExtensions = {
"Opportunity": {
"program_area": CustomFieldSpec(field_type="string", description="Grant program area"),
"eligibility_type": CustomFieldSpec(field_type="array", description="Eligible org types"),
},
}
# Wrap extensions in a plugin config for the generator
config = define_plugin(
[local_extensions], on_conflict="error")Place this in a cg_config.py file and run the generator to emit typed Pydantic models:
python -m common_grants_sdk.extensions.generate --plugin ./plugins/my_pluginAfter generation, import the plugin and validate payloads with full type safety:
from plugins.my_plugin import my_plugin
opp = my_plugin.schemas.Opportunity.model_validate(api_response)
opp.custom_fields.program_area.value # str
opp.custom_fields.eligibility_type.value # list[str]The SDK provides utilities for extending schemas with typed custom fields, allowing developers to add domain-specific fields while maintaining type safety.
from datetime import datetime
from uuid import uuid4
from common_grants_sdk.schemas.pydantic import (
OpportunityBase,
CustomFieldType,
OppStatus,
OppStatusOptions,
)
from common_grants_sdk.extensions.specs import CustomFieldSpec
fields = {
"legacyId": CustomFieldSpec(field_type=CustomFieldType.INTEGER, value=int),
"groupName": CustomFieldSpec(field_type=CustomFieldType.STRING, value=str),
}
Opportunity = OpportunityBase.with_custom_fields(
custom_fields=fields, model_name="Opportunity"
)
opp_data = {
"id": uuid4(),
"title": "Foo bar",
"status": OppStatus(value=OppStatusOptions.OPEN),
"description": "Example opportunity",
"createdAt": datetime.fromisoformat("2024-01-01T00:00:00+00:00"),
"lastModifiedAt": datetime.fromisoformat("2024-01-01T00:00:00+00:00"),
"customFields": {
"legacyId": {
"name": "legacyId",
"fieldType": "integer",
"value": 12345,
},
"groupName": {
"name": "groupName",
"fieldType": "string",
"value": "TEST_GROUP",
},
"ignoredForNow": {"type": "string", "value": "noop"},
},
}
opp = Opportunity.model_validate(opp_data)
print(opp.custom_fields.legacy_id.value)Because retrieving custom field values can be cumbersome there is a get_custom_field_value function inside of the utils folder. Simply add this utility function to any existing pydantic object by using a wrapper function.
def get_custom_field_value(self, key: str, value_type: type[V]) -> Optional[V]:
"""Returns custom field object specified by key"""
return get_custom_field_value(self, key=key, value_type=value_type)Developers can then call the wrapper function like so.
from pydantic import BaseModel
from datetime import datetime
from uuid import uuid4
from common_grants_sdk.schemas.pydantic import (
OpportunityBase,
CustomFieldType,
OppStatus,
OppStatusOptions,
)
class LegacyIdValue(BaseModel):
system: str
id: int
opp_data = {
"id": uuid4(),
"title": "Foo bar",
"status": OppStatus(value=OppStatusOptions.OPEN),
"description": "Example opportunity",
"createdAt": datetime.fromisoformat("2024-01-01T00:00:00+00:00"),
"lastModifiedAt": datetime.fromisoformat("2024-01-01T00:00:00+00:00"),
"customFields": {
"legacyId": {
"name": "legacyId",
"fieldType": CustomFieldType.OBJECT,
"value": {"system": "legacy", "id": 123},
},
"groupName": {
"name": "groupName",
"fieldType": CustomFieldType.STRING,
"value": "test group",
},
},
}
opp = OpportunityBase.model_validate(opp_data)
print(opp.custom_fields["legacyId"])
legacy = opp.get_custom_field_value("legacyId", LegacyIdValue)
print(legacy.id)