Skip to content
This repository has been archived by the owner on Sep 18, 2024. It is now read-only.

Commit

Permalink
[Issue #12] Setup the opportunity v1 endpoint which will be backed by…
Browse files Browse the repository at this point in the history
… the index (#44)

## Summary
Fixes #12

### Time to review: __5 mins__

## Changes proposed
Made a new set of v1 endpoints that are basically copy-pastes of the
v0.1 opportunity endpoints

## Context for reviewers
Some changes I want to make to the schemas wouldn't make sense without
the search index (eg. adding the filter counts to the response). As we
have no idea what the actual launch of the v0.1 endpoint is going to
look like, I don't want to mess with any of that code or try to make a
weird hacky approach that needs to account for both the DB
implementation and the search index one.

Also, I think we've heard that with the launch of the search index,
we'll be "officially" launched, so might as well call in v1 at the same
time.

Other than adjusting the names of a few schemas in v0.1, I left that
implementation alone and just copied the boilerplate that I'll fill out
in subsequent tickets.

## Additional information
The endpoint appears locally:
![Screenshot 2024-05-20 at 12 18 32
PM](https://github.com/navapbc/simpler-grants-gov/assets/46358556/86231ec1-417a-41c6-ad88-3d06bb6214e5)

---------

Co-authored-by: nava-platform-bot <platform-admins@navapbc.com>
  • Loading branch information
chouinar and nava-platform-bot authored May 22, 2024
1 parent 25b0295 commit b40344d
Show file tree
Hide file tree
Showing 17 changed files with 1,488 additions and 51 deletions.
740 changes: 706 additions & 34 deletions api/openapi.generated.yml

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions api/src/api/opportunities_v0_1/opportunity_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,12 @@

@opportunity_blueprint.post("/opportunities/search")
@opportunity_blueprint.input(
opportunity_schemas.OpportunitySearchRequestSchema, arg_name="search_params", examples=examples
opportunity_schemas.OpportunitySearchRequestV01Schema,
arg_name="search_params",
examples=examples,
)
# many=True allows us to return a list of opportunity objects
@opportunity_blueprint.output(opportunity_schemas.OpportunitySchema(many=True))
@opportunity_blueprint.output(opportunity_schemas.OpportunityV01Schema(many=True))
@opportunity_blueprint.auth_required(api_key_auth)
@opportunity_blueprint.doc(description=SHARED_ALPHA_DESCRIPTION)
@flask_db.with_db_session()
Expand All @@ -90,7 +92,7 @@ def opportunity_search(db_session: db.Session, search_params: dict) -> response.


@opportunity_blueprint.get("/opportunities/<int:opportunity_id>")
@opportunity_blueprint.output(opportunity_schemas.OpportunitySchema)
@opportunity_blueprint.output(opportunity_schemas.OpportunityV01Schema)
@opportunity_blueprint.auth_required(api_key_auth)
@opportunity_blueprint.doc(description=SHARED_ALPHA_DESCRIPTION)
@flask_db.with_db_session()
Expand Down
26 changes: 13 additions & 13 deletions api/src/api/opportunities_v0_1/opportunity_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from src.pagination.pagination_schema import generate_pagination_schema


class OpportunitySummarySchema(Schema):
class OpportunitySummaryV01Schema(Schema):
summary_description = fields.String(
metadata={
"description": "The summary of the opportunity",
Expand Down Expand Up @@ -178,7 +178,7 @@ class OpportunitySummarySchema(Schema):
applicant_types = fields.List(fields.Enum(ApplicantType))


class OpportunityAssistanceListingSchema(Schema):
class OpportunityAssistanceListingV01Schema(Schema):
program_title = fields.String(
metadata={
"description": "The name of the program, see https://sam.gov/content/assistance-listings for more detail",
Expand All @@ -193,7 +193,7 @@ class OpportunityAssistanceListingSchema(Schema):
)


class OpportunitySchema(Schema):
class OpportunityV01Schema(Schema):
opportunity_id = fields.Integer(
dump_only=True,
metadata={"description": "The internal ID of the opportunity", "example": 12345},
Expand Down Expand Up @@ -227,9 +227,9 @@ class OpportunitySchema(Schema):
)

opportunity_assistance_listings = fields.List(
fields.Nested(OpportunityAssistanceListingSchema())
fields.Nested(OpportunityAssistanceListingV01Schema())
)
summary = fields.Nested(OpportunitySummarySchema())
summary = fields.Nested(OpportunitySummaryV01Schema())

opportunity_status = fields.Enum(
OpportunityStatus,
Expand All @@ -243,35 +243,35 @@ class OpportunitySchema(Schema):
updated_at = fields.DateTime(dump_only=True)


class OpportunitySearchFilterSchema(Schema):
class OpportunitySearchFilterV01Schema(Schema):
funding_instrument = fields.Nested(
StrSearchSchemaBuilder("FundingInstrumentFilterSchema")
StrSearchSchemaBuilder("FundingInstrumentFilterV01Schema")
.with_one_of(allowed_values=FundingInstrument)
.build()
)
funding_category = fields.Nested(
StrSearchSchemaBuilder("FundingCategoryFilterSchema")
StrSearchSchemaBuilder("FundingCategoryFilterV01Schema")
.with_one_of(allowed_values=FundingCategory)
.build()
)
applicant_type = fields.Nested(
StrSearchSchemaBuilder("ApplicantTypeFilterSchema")
StrSearchSchemaBuilder("ApplicantTypeFilterV01Schema")
.with_one_of(allowed_values=ApplicantType)
.build()
)
opportunity_status = fields.Nested(
StrSearchSchemaBuilder("OpportunityStatusFilterSchema")
StrSearchSchemaBuilder("OpportunityStatusFilterV01Schema")
.with_one_of(allowed_values=OpportunityStatus)
.build()
)
agency = fields.Nested(
StrSearchSchemaBuilder("AgencyFilterSchema")
StrSearchSchemaBuilder("AgencyFilterV01Schema")
.with_one_of(example="US-ABC", minimum_length=2)
.build()
)


class OpportunitySearchRequestSchema(Schema):
class OpportunitySearchRequestV01Schema(Schema):
query = fields.String(
metadata={
"description": "Query string which searches against several text fields",
Expand All @@ -280,7 +280,7 @@ class OpportunitySearchRequestSchema(Schema):
validate=[validators.Length(min=1, max=100)],
)

filters = fields.Nested(OpportunitySearchFilterSchema())
filters = fields.Nested(OpportunitySearchFilterV01Schema())

pagination = fields.Nested(
generate_pagination_schema(
Expand Down
6 changes: 6 additions & 0 deletions api/src/api/opportunities_v1/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from src.api.opportunities_v1.opportunity_blueprint import opportunity_blueprint

# import opportunity_routes module to register the API routes on the blueprint
import src.api.opportunities_v1.opportunity_routes # noqa: F401 E402 isort:skip

__all__ = ["opportunity_blueprint"]
9 changes: 9 additions & 0 deletions api/src/api/opportunities_v1/opportunity_blueprint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from apiflask import APIBlueprint

opportunity_blueprint = APIBlueprint(
"opportunity_v1",
__name__,
tag="Opportunity v1",
cli_group="opportunity_v1",
url_prefix="/v1",
)
66 changes: 66 additions & 0 deletions api/src/api/opportunities_v1/opportunity_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import logging

import src.adapters.db as db
import src.adapters.db.flask_db as flask_db
import src.api.opportunities_v1.opportunity_schemas as opportunity_schemas
import src.api.response as response
from src.api.opportunities_v1.opportunity_blueprint import opportunity_blueprint
from src.auth.api_key_auth import api_key_auth
from src.logging.flask_logger import add_extra_data_to_current_request_logs
from src.services.opportunities_v1.get_opportunity import get_opportunity
from src.services.opportunities_v1.search_opportunities import search_opportunities
from src.util.dict_util import flatten_dict

logger = logging.getLogger(__name__)

# Descriptions in OpenAPI support markdown https://swagger.io/specification/
SHARED_ALPHA_DESCRIPTION = """
__ALPHA VERSION__
This endpoint in its current form is primarily for testing and feedback.
Features in this endpoint are still under heavy development, and subject to change. Not for production use.
See [Release Phases](https://github.com/github/roadmap?tab=readme-ov-file#release-phases) for further details.
"""


@opportunity_blueprint.post("/opportunities/search")
@opportunity_blueprint.input(
opportunity_schemas.OpportunitySearchRequestV1Schema, arg_name="search_params"
)
# many=True allows us to return a list of opportunity objects
@opportunity_blueprint.output(opportunity_schemas.OpportunityV1Schema(many=True))
@opportunity_blueprint.auth_required(api_key_auth)
@opportunity_blueprint.doc(description=SHARED_ALPHA_DESCRIPTION)
def opportunity_search(search_params: dict) -> response.ApiResponse:
add_extra_data_to_current_request_logs(flatten_dict(search_params, prefix="request.body"))
logger.info("POST /v1/opportunities/search")

opportunities, pagination_info = search_opportunities(search_params)

add_extra_data_to_current_request_logs(
{
"response.pagination.total_pages": pagination_info.total_pages,
"response.pagination.total_records": pagination_info.total_records,
}
)
logger.info("Successfully fetched opportunities")

return response.ApiResponse(
message="Success", data=opportunities, pagination_info=pagination_info
)


@opportunity_blueprint.get("/opportunities/<int:opportunity_id>")
@opportunity_blueprint.output(opportunity_schemas.OpportunityV1Schema)
@opportunity_blueprint.auth_required(api_key_auth)
@opportunity_blueprint.doc(description=SHARED_ALPHA_DESCRIPTION)
@flask_db.with_db_session()
def opportunity_get(db_session: db.Session, opportunity_id: int) -> response.ApiResponse:
add_extra_data_to_current_request_logs({"opportunity.opportunity_id": opportunity_id})
logger.info("GET /v1/opportunities/:opportunity_id")
with db_session.begin():
opportunity = get_opportunity(db_session, opportunity_id)

return response.ApiResponse(message="Success", data=opportunity)
Loading

0 comments on commit b40344d

Please sign in to comment.