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

[Issue #12] Setup the opportunity v1 endpoint which will be backed by the index #44

Merged
merged 5 commits into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading