Skip to content

Commit 3921563

Browse files
authored
Merge pull request #441 from Police-Data-Accessibility-Project/mc_425_agency_search_endpoint
Create /search/agency endpoint with test
2 parents bfd88a7 + c13f9ce commit 3921563

File tree

13 files changed

+178
-6
lines changed

13 files changed

+178
-6
lines changed

src/api/endpoints/search/agency/__init__.py

Whitespace-only changes.

src/api/endpoints/search/agency/models/__init__.py

Whitespace-only changes.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from pydantic import BaseModel
2+
3+
4+
class AgencySearchResponse(BaseModel):
5+
agency_id: int
6+
agency_name: str
7+
location_display_name: str
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from typing import Sequence
2+
3+
from sqlalchemy import select, func, RowMapping
4+
from sqlalchemy.ext.asyncio import AsyncSession
5+
6+
from src.api.endpoints.search.agency.models.response import AgencySearchResponse
7+
from src.db.helpers.session import session_helper as sh
8+
from src.db.models.impl.agency.sqlalchemy import Agency
9+
from src.db.models.impl.link.agency_location.sqlalchemy import LinkAgencyLocation
10+
from src.db.models.views.location_expanded import LocationExpandedView
11+
from src.db.queries.base.builder import QueryBuilderBase
12+
13+
14+
class SearchAgencyQueryBuilder(QueryBuilderBase):
15+
16+
def __init__(
17+
self,
18+
location_id: int | None,
19+
query: str | None
20+
):
21+
super().__init__()
22+
self.location_id = location_id
23+
self.query = query
24+
25+
async def run(self, session: AsyncSession) -> list[AgencySearchResponse]:
26+
27+
query = (
28+
select(
29+
Agency.agency_id,
30+
Agency.name.label("agency_name"),
31+
LocationExpandedView.display_name.label("location_display_name")
32+
)
33+
.join(
34+
LinkAgencyLocation,
35+
LinkAgencyLocation.agency_id == Agency.agency_id
36+
)
37+
.join(
38+
LocationExpandedView,
39+
LocationExpandedView.id == LinkAgencyLocation.location_id
40+
)
41+
)
42+
43+
if self.location_id is not None:
44+
query = query.where(
45+
LocationExpandedView.id == self.location_id
46+
)
47+
if self.query is not None:
48+
query = query.order_by(
49+
func.similarity(
50+
Agency.name,
51+
self.query
52+
).desc()
53+
)
54+
55+
mappings: Sequence[RowMapping] = await sh.mappings(session, query)
56+
57+
return [
58+
AgencySearchResponse(
59+
**mapping
60+
)
61+
for mapping in mappings
62+
]
63+
64+
65+
66+

src/api/endpoints/search/routes.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
from fastapi import APIRouter, Query, Depends
1+
2+
from fastapi import APIRouter, Query, Depends, HTTPException
3+
from starlette import status
24

35
from src.api.dependencies import get_async_core
6+
from src.api.endpoints.search.agency.models.response import AgencySearchResponse
7+
from src.api.endpoints.search.agency.query import SearchAgencyQueryBuilder
48
from src.api.endpoints.search.dtos.response import SearchURLResponse
59
from src.core.core import AsyncCore
610
from src.security.manager import get_access_info
@@ -18,4 +22,31 @@ async def search_url(
1822
"""
1923
Search for a URL in the database
2024
"""
21-
return await async_core.search_for_url(url)
25+
return await async_core.search_for_url(url)
26+
27+
28+
@search_router.get("/agency")
29+
async def search_agency(
30+
location_id: int | None = Query(
31+
description="The location id to search for",
32+
default=None
33+
),
34+
query: str | None = Query(
35+
description="The query to search for",
36+
default=None
37+
),
38+
access_info: AccessInfo = Depends(get_access_info),
39+
async_core: AsyncCore = Depends(get_async_core),
40+
) -> list[AgencySearchResponse]:
41+
if query is None and location_id is None:
42+
raise HTTPException(
43+
status_code=status.HTTP_400_BAD_REQUEST,
44+
detail="At least one of query or location_id must be provided"
45+
)
46+
47+
return await async_core.adb_client.run_query_builder(
48+
SearchAgencyQueryBuilder(
49+
location_id=location_id,
50+
query=query
51+
)
52+
)

tests/automated/integration/api/search/__init__.py

Whitespace-only changes.

tests/automated/integration/api/search/agency/__init__.py

Whitespace-only changes.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import pytest
2+
3+
from tests.helpers.api_test_helper import APITestHelper
4+
from tests.helpers.data_creator.core import DBDataCreator
5+
from tests.helpers.data_creator.models.creation_info.county import CountyCreationInfo
6+
from tests.helpers.data_creator.models.creation_info.locality import LocalityCreationInfo
7+
8+
9+
@pytest.mark.asyncio
10+
async def test_search_agency(
11+
api_test_helper: APITestHelper,
12+
db_data_creator: DBDataCreator,
13+
pittsburgh_locality: LocalityCreationInfo,
14+
allegheny_county: CountyCreationInfo
15+
):
16+
17+
agency_a_id: int = await db_data_creator.agency("A Agency")
18+
agency_b_id: int = await db_data_creator.agency("AB Agency")
19+
agency_c_id: int = await db_data_creator.agency("ABC Agency")
20+
21+
await db_data_creator.link_agencies_to_location(
22+
agency_ids=[agency_a_id, agency_c_id],
23+
location_id=pittsburgh_locality.location_id
24+
)
25+
await db_data_creator.link_agencies_to_location(
26+
agency_ids=[agency_b_id],
27+
location_id=allegheny_county.location_id
28+
)
29+
30+
responses: list[dict] = api_test_helper.request_validator.get_v2(
31+
url="/search/agency",
32+
params={
33+
"query": "A Agency",
34+
}
35+
)
36+
assert len(responses) == 3
37+
assert responses[0]["agency_id"] == agency_a_id
38+
assert responses[1]["agency_id"] == agency_b_id
39+
assert responses[2]["agency_id"] == agency_c_id
40+
41+
responses = api_test_helper.request_validator.get_v2(
42+
url="/search/agency",
43+
params={
44+
"query": "A Agency",
45+
"location_id": pittsburgh_locality.location_id
46+
}
47+
)
48+
49+
assert len(responses) == 2
50+
assert responses[0]["agency_id"] == agency_a_id
51+
assert responses[1]["agency_id"] == agency_c_id
52+
53+

tests/automated/integration/api/search/url/__init__.py

Whitespace-only changes.
File renamed without changes.

0 commit comments

Comments
 (0)