Skip to content

Building a Python API

Learn how to implement a CommonGrants API in Python using the FastAPI web framework.

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

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:

Terminal window
python --version
poetry --version
npm --version
cg --version
tsp --version

First steps

Get a FastAPI project up and running with the following steps:

  1. Create a new directory for your project:

    Terminal window
    mkdir common-grants-api
    cd common-grants-api
  2. Set up your project using the CommonGrants CLI:

    Terminal window
    cg init --template fast-api
  3. Install the dependencies:

    Terminal window
    make install
  4. Run the project:

    Terminal window
    make dev
  5. Open the API docs:

    Terminal window
    open http://localhost:8000/docs

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

Once you’ve set up your initial project structure, you can start implementing the API routes and services.

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

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

from common_grants.schemas.fields import CustomField, CustomFieldType
from pydantic import BaseModel, Field
from typing import Optional
class LegacyId(CustomField):
"""Custom field for a legacy opportunity_id."""
name: str = "legacyId"
field_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",
)

Update the OpportunityBase model

Update the OpportunityBase model to include the new custom field in the custom_fields property.

from common_grants.schemas.models.opp_custom_fields import OppCustomFields
class OpportunityBase(SystemMetadata):
# other fields omitted for brevity
custom_fields: Optional[OppCustomFields] = Field(
default=None,
alias="customFields",
description="Additional custom fields specific to this opportunity",
)

Full example

Here’s a full example of defining a custom field within the opp_base.py file.

"""Base models for funding opportunities."""
from typing import Optional
from uuid import UUID
from pydantic import BaseModel, Field, HttpUrl
from common_grants.schemas.fields import CustomField, CustomFieldType, SystemMetadata
from common_grants.schemas.models.opp_funding import OppFunding
from common_grants.schemas.models.opp_status import OppStatus
from common_grants.schemas.models.opp_timeline import OppTimeline
class LegacyId(CustomField):
"""Custom field for a legacy opportunity_id."""
name: str = "legacyId"
field_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(SystemMetadata):
"""Base model for a funding opportunity with all core fields."""
id: UUID = Field(..., description="Globally unique id for the opportunity")
title: str = Field(..., description="Title or name of the funding opportunity")
status: OppStatus = Field(..., description="Status of the opportunity")
description: str = Field(
...,
description="Description of the opportunity's purpose and scope",
)
funding: OppFunding = Field(..., description="Details about the funding available")
key_dates: OppTimeline = Field(
...,
description="Key dates for the opportunity, such as when the application opens and closes",
)
source: Optional[HttpUrl] = Field(
default=None,
description="URL for the original source of the opportunity",
)
custom_fields: Optional[OppCustomFields] = Field(
default=None,
description="Additional custom fields specific to this opportunity",
)

Additional resources