Building a Python API
Learn how to implement a CommonGrants API in Python using the FastAPI web framework.
Quickstart
Section titled “Quickstart”This guide will walk you through the process of setting up a new project using the CommonGrants FastAPI template, and then outline steps for extending this template to meet your specific needs.
Prerequisites
Section titled “Prerequisites”To follow this guide, you’ll need to have the following installed on your machine:
- Python 3.11+ and Poetry 1.8+
- Node.js 20+ and npm 10+
- CommonGrants and TypeSpec CLIs
Check your versions by running:
python --versionpoetry --versionnpm --versioncg --versiontsp --version
First steps
Section titled “First steps”Get a FastAPI project up and running with the following steps:
-
Create a new directory for your project:
Terminal window mkdir common-grants-apicd common-grants-api -
Set up your project using the CommonGrants CLI:
Terminal window cg init --template fast-api -
Install the dependencies:
Terminal window make install -
Run the project:
Terminal window make dev -
Open the API docs:
Terminal window open http://localhost:8000/docs
Project structure
Section titled “Project structure”The boilerplate template includes the following files and directories:
- pyproject.toml # Python project configuration
- poetry.lock # Locked versions of dependencies
- Makefile
- README.md
Directorysrc/
Directorycommon_grants/
- api.py # FastAPI application setup and config
Directoryroutes/ # API route handlers and endpoints
- …
Directoryschemas/ # Schemas for (de)serialization
- …
Directoryservices/ # Business logic and data operations
- …
Directorytests/
Directorycommon_grants/
Directoryschemas/ # Schema-related tests
- …
Directoryservices/ # Service-related tests
- …
Directoryroutes/ # Route-related tests
- …
Next steps
Section titled “Next steps”Once you’ve set up your initial project structure, you can start implementing the API routes and services.
Implementing services
Section titled “Implementing services”The services layer is responsible for implementing the business logic and data operations for the API. It includes the following files:
Directorysrc/common_grants/services/
- opportunity.py # Opportunity service
- utils.py # Utility functions
In particular, you should focus on updating the opportunity.py
file. This file contains the implementation of the OpportunityService
class, which is responsible for fetching and processing opportunities for the following CommonGrants API endpoints:
GET /common-grants/opportunities
GET /common-grants/opportunities/{id}
POST /common-grants/opportunities/search
Some specific changes you should make to this file are:
- Replacing the mock data with a real data source, e.g. a database query or a remote API call.
- Adding the sorting and filtering logic to the the
OpportunityService.search_opportunities
method.
Adding custom fields
Section titled “Adding custom fields”When adopting the CommonGrants protocol, you may need to include information about a funding opportunity that is not explicitly defined by the CommonGrants model for opportunities. The protocol defines a pattern for supporting these kinds of custom fields through the custom_fields
property on the OpportunityBase
model.
For example, let’s say you need to add a legacyId
field to map opportunities to an existing ID system. Here’s how to do it:
Define the custom field
Section titled “Define the custom field”from common_grants_sdk.schemas.fields import CustomField, CustomFieldTypefrom pydantic import BaseModel, Fieldfrom typing import Optional
class LegacyId(CustomField): """Custom field for a legacy opportunity_id."""
name: str = "legacyId" type: CustomFieldType = CustomFieldType.NUMBER value: Optional[int] = None description: Optional[str] = "Maps to the opportunity_id in the legacy system"
class OppCustomFields(BaseModel): """Custom fields for a funding opportunity."""
legacy_id: Optional[LegacyId] = Field( default=None, alias="legacyId", description="Maps to the opportunity_id in the legacy system", )
Extend the SDK’s OpportunityBase model
Section titled “Extend the SDK’s OpportunityBase model”Create a subclass that extends the SDK’s OpportunityBase
model with your custom fields:
from typing import Optionalfrom uuid import UUID
from pydantic import Field, HttpUrl
from common_grants_sdk.schemas.models.opp_base import OpportunityBase as SDKOpportunityBasefrom common_grants_sdk.schemas.models.opp_funding import OppFundingfrom common_grants_sdk.schemas.models.opp_status import OppStatusfrom common_grants_sdk.schemas.models.opp_timeline import OppTimelinefrom .opp_custom_fields import OppCustomFields
class OpportunityBase(SDKOpportunityBase): """Extended opportunity model with custom fields."""
# Override the custom_fields property to use our custom fields model custom_fields: Optional[OppCustomFields] = Field( default=None, alias="customFields", description="Additional custom fields specific to this opportunity", )
Full example
Section titled “Full example”Here’s a complete example of defining custom fields and extending the SDK’s OpportunityBase
:
"""Extended models for funding opportunities."""
from typing import Optionalfrom uuid import UUID
from pydantic import Field, HttpUrl
from common_grants_sdk.schemas.fields import CustomField, CustomFieldTypefrom common_grants_sdk.schemas.models.opp_base import OpportunityBase as SDKOpportunityBasefrom common_grants_sdk.schemas.models.opp_funding import OppFundingfrom common_grants_sdk.schemas.models.opp_status import OppStatusfrom common_grants_sdk.schemas.models.opp_timeline import OppTimeline
class LegacyId(CustomField): """Custom field for a legacy opportunity_id."""
name: str = "legacyId" type: CustomFieldType = CustomFieldType.NUMBER value: Optional[int] = None description: Optional[str] = "Maps to the opportunity_id in the legacy system"
class OppCustomFields(BaseModel): """Custom fields for a funding opportunity."""
legacy_id: Optional[LegacyId] = Field( default=None, alias="legacyId", description="Maps to the opportunity_id in the legacy system", )
class OpportunityBase(SDKOpportunityBase): """Extended opportunity model with custom fields."""
# Override the custom_fields property to use our custom fields model custom_fields: Optional[OppCustomFields] = Field( default=None, alias="customFields", description="Additional custom fields specific to this opportunity", )
This approach allows you to extend the SDK’s models with your custom fields while maintaining compatibility with the CommonGrants protocol. Your API can use this extended model for serialization and deserialization, while still being able to convert to and from the standard SDK models when needed.