-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #214 from Police-Data-Accessibility-Project/mc_550…
…_agencies_matching_endpoint Mc 550 agencies matching endpoint
- Loading branch information
Showing
30 changed files
with
466 additions
and
171 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
from enum import Enum | ||
from typing import Optional, List | ||
|
||
from flask import Response | ||
|
||
from database_client.database_client import DatabaseClient | ||
from database_client.db_client_dataclasses import WhereMapping | ||
from middleware.flask_response_manager import FlaskResponseManager | ||
from middleware.schema_and_dto_logic.primary_resource_dtos.match_dtos import ( | ||
AgencyMatchOuterDTO, | ||
AgencyMatchDTO, | ||
) | ||
from rapidfuzz import fuzz | ||
|
||
from middleware.util import update_if_not_none | ||
|
||
|
||
class AgencyMatchStatus(Enum): | ||
EXACT = "Exact Match" | ||
PARTIAL = "Partial Matches" | ||
NO_MATCH = "No Match" | ||
|
||
|
||
SIMILARITY_THRESHOLD = 80 | ||
|
||
|
||
def get_agency_match_message(status: AgencyMatchStatus): | ||
match status: | ||
case AgencyMatchStatus.EXACT: | ||
return "Exact match found." | ||
case AgencyMatchStatus.PARTIAL: | ||
return "Partial matches found." | ||
case AgencyMatchStatus.NO_MATCH: | ||
return "No matches found." | ||
|
||
|
||
class AgencyMatchResponse: | ||
|
||
def __init__(self, status: AgencyMatchStatus, agencies: Optional[list] = None): | ||
self.status = status | ||
self.agencies = agencies | ||
self.message = get_agency_match_message(status) | ||
|
||
|
||
def match_agencies(db_client: DatabaseClient, dto: AgencyMatchOuterDTO): | ||
amrs: List[AgencyMatchResponse] = [] | ||
for entry in dto.entries: | ||
amr: AgencyMatchResponse = try_matching_agency(db_client=db_client, dto=entry) | ||
amrs.append(amr) | ||
|
||
|
||
def try_getting_exact_match_agency(dto: AgencyMatchDTO, agencies: list[dict]): | ||
for agency in agencies: | ||
if agency["submitted_name"] == dto.name: | ||
return agency | ||
|
||
|
||
def try_getting_partial_match_agencies(dto: AgencyMatchDTO, agencies: list[dict]): | ||
partial_matches = [] | ||
for agency in agencies: | ||
if fuzz.ratio(dto.name, agency["submitted_name"]) >= SIMILARITY_THRESHOLD: | ||
partial_matches.append(agency) | ||
|
||
return partial_matches | ||
|
||
def format_response(amr: AgencyMatchResponse) -> Response: | ||
data = { | ||
"status": amr.status.value, | ||
"message": amr.message, | ||
} | ||
update_if_not_none(dict_to_update=data, secondary_dict={"agencies": amr.agencies}) | ||
return FlaskResponseManager.make_response( | ||
data=data, | ||
) | ||
|
||
def match_agency_wrapper(db_client: DatabaseClient, dto: AgencyMatchOuterDTO): | ||
result = try_matching_agency(db_client=db_client, dto=dto) | ||
return format_response(result) | ||
|
||
|
||
def try_matching_agency( | ||
db_client: DatabaseClient, dto: AgencyMatchDTO | ||
) -> AgencyMatchResponse: | ||
|
||
location_id = _get_location_id(db_client, dto) | ||
if location_id is None: | ||
return _no_match_response() | ||
|
||
agencies = _get_agencies(db_client, location_id) | ||
if len(agencies) == 0: | ||
return _no_match_response() | ||
|
||
exact_match_agency = try_getting_exact_match_agency(dto=dto, agencies=agencies) | ||
if exact_match_agency is not None: | ||
return _exact_match_response(exact_match_agency) | ||
|
||
partial_match_agencies = try_getting_partial_match_agencies( | ||
dto=dto, agencies=agencies | ||
) | ||
if len(partial_match_agencies) > 0: | ||
return _partial_match_response(partial_match_agencies) | ||
|
||
return _no_match_response() | ||
|
||
|
||
def _partial_match_response(partial_match_agencies): | ||
return AgencyMatchResponse( | ||
status=AgencyMatchStatus.PARTIAL, agencies=partial_match_agencies | ||
) | ||
|
||
|
||
def _exact_match_response(exact_match_agency): | ||
return AgencyMatchResponse( | ||
status=AgencyMatchStatus.EXACT, agencies=[exact_match_agency] | ||
) | ||
|
||
|
||
def _no_match_response(): | ||
return AgencyMatchResponse( | ||
status=AgencyMatchStatus.NO_MATCH, | ||
) | ||
|
||
|
||
def _get_agencies(db_client, location_id): | ||
return db_client.get_agencies( | ||
columns=["id", "submitted_name"], | ||
where_mappings=WhereMapping.from_dict({"location_id": location_id}), | ||
) | ||
|
||
|
||
def _get_location_id(db_client, dto): | ||
return db_client.get_location_id( | ||
where_mappings=WhereMapping.from_dict( | ||
{ | ||
"state_name": dto.state, | ||
"county_name": dto.county, | ||
"locality_name": dto.locality, | ||
} | ||
) | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
14 changes: 14 additions & 0 deletions
14
middleware/schema_and_dto_logic/primary_resource_dtos/match_dtos.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
from typing import Optional | ||
|
||
from pydantic import BaseModel | ||
|
||
|
||
class AgencyMatchDTO(BaseModel): | ||
name: str | ||
state: str | ||
county: Optional[str] | ||
locality: Optional[str] | ||
|
||
|
||
class AgencyMatchOuterDTO(BaseModel): | ||
entries: list[AgencyMatchDTO] |
33 changes: 33 additions & 0 deletions
33
middleware/schema_and_dto_logic/primary_resource_schemas/match_schemas.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
from marshmallow import Schema, fields | ||
|
||
from middleware.primary_resource_logic.match_logic import AgencyMatchStatus | ||
from middleware.schema_and_dto_logic.common_response_schemas import MessageSchema | ||
from middleware.schema_and_dto_logic.util import get_json_metadata | ||
|
||
|
||
class AgencyMatchSchema(Schema): | ||
name = fields.String(metadata=get_json_metadata("The name of the agency")) | ||
state = fields.String(metadata=get_json_metadata("The state of the agency")) | ||
county = fields.String(metadata=get_json_metadata("The county of the agency")) | ||
locality = fields.String(metadata=get_json_metadata("The locality of the agency")) | ||
|
||
class MatchAgenciesResultSchema(Schema): | ||
submitted_name = fields.String(metadata=get_json_metadata("The name of the agency")) | ||
id = fields.Integer(metadata=get_json_metadata("The id of the agency")) | ||
|
||
class MatchAgencyResponseSchema(MessageSchema): | ||
|
||
status = fields.Enum( | ||
enum=AgencyMatchStatus, | ||
by_value=fields.Str, | ||
required=True, | ||
metadata=get_json_metadata("The status of the match") | ||
) | ||
agencies = fields.List( | ||
fields.Nested( | ||
MatchAgenciesResultSchema(), | ||
metadata=get_json_metadata("The list of results, if any") | ||
), | ||
required=False, | ||
metadata=get_json_metadata("The list of results, if any") | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
from flask import Response | ||
|
||
from middleware.access_logic import STANDARD_JWT_AUTH_INFO, AccessInfoPrimary | ||
from middleware.decorators import endpoint_info | ||
from middleware.primary_resource_logic.match_logic import try_matching_agency, match_agency_wrapper | ||
from resources.PsycopgResource import PsycopgResource | ||
from resources.endpoint_schema_config import SchemaConfigs | ||
from resources.resource_helpers import ResponseInfo | ||
from utilities.namespace import create_namespace, AppNamespaces | ||
|
||
namespace_match = create_namespace(AppNamespaces.MATCH) | ||
|
||
|
||
@namespace_match.route("/agency") | ||
class MatchAgencies(PsycopgResource): | ||
|
||
@endpoint_info( | ||
namespace=namespace_match, | ||
auth_info=STANDARD_JWT_AUTH_INFO, | ||
schema_config=SchemaConfigs.MATCH_AGENCY, | ||
response_info=ResponseInfo( | ||
success_message="Found any possible matches for the search criteria." | ||
), | ||
description="Returns agencies, if any, that match or partially match the search criteria", | ||
) | ||
def post(self, access_info: AccessInfoPrimary) -> Response: | ||
return self.run_endpoint( | ||
wrapper_function=match_agency_wrapper, | ||
schema_populate_parameters=SchemaConfigs.MATCH_AGENCY.value.get_schema_populate_parameters(), | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.