Skip to content

Commit

Permalink
More error handling and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
dmannarino committed Dec 20, 2024
1 parent 9c2e0ca commit 899e772
Show file tree
Hide file tree
Showing 2 changed files with 152 additions and 38 deletions.
58 changes: 48 additions & 10 deletions app/routes/thematic/geoencoder.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import re
from typing import Optional, Any, Dict, List

from fastapi import APIRouter, HTTPException, Query

from app.crud.versions import get_version, get_version_names
from app.errors import RecordNotFoundError
from app.models.pydantic.responses import Response
from app.routes import VERSION_REGEX
from app.routes.datasets.queries import _query_dataset_json


Expand All @@ -20,7 +25,7 @@ async def geoencode(
description="The source of administrative boundaries to use."
),
admin_version: str = Query(
None,
...,
description="Version of the administrative boundaries dataset to use.",
),
country: str = Query(
Expand All @@ -46,13 +51,15 @@ async def geoencode(
dataset = admin_source_to_dataset[admin_source.upper()]
except KeyError:
raise HTTPException(
status_code=404,
status_code=400,
detail=f"Invalid admin boundary source. Valid sources: {admin_source_to_dataset.keys()}"
)

version_str = "v" + str(admin_version).lstrip("v")

sql: str = await admin_boundary_lookup_sql(
await version_is_valid(dataset, version_str)

sql: str = _admin_boundary_lookup_sql(
admin_source,
country,
region,
Expand All @@ -63,14 +70,16 @@ async def geoencode(
dataset, version_str, sql, None
)

return {
"adminSource": admin_source,
"adminVersion": admin_version,
"matches": json_data
}
return Response(
data={
"adminSource": admin_source,
"adminVersion": admin_version,
"matches": json_data
}
)


async def admin_boundary_lookup_sql(
def _admin_boundary_lookup_sql(
dataset: str,
country_name: str,
region_name: Optional[str],
Expand All @@ -81,11 +90,40 @@ async def admin_boundary_lookup_sql(
"""
sql = (
f"SELECT gid_0, gid_1, gid_2, country, name_1, name_2 FROM {dataset}"
f" AND WHERE country='{country_name}'"
f" WHERE country='{country_name}'"
)
if region_name is not None:
sql += f" AND WHERE region='{region_name}'"
if subregion_name is not None:
sql += f" AND WHERE subregion='{subregion_name}'"

return sql


async def version_is_valid(
dataset: str,
version: str,
) -> None:
"""
"""
if re.match(VERSION_REGEX, version) is None:
raise HTTPException(
status_code=400,
detail=(
"Invalid version name. Version names begin with a 'v' and "
"consist of one to three integers separated by periods. "
"eg. 'v1', 'v7.1', 'v4.1.0', 'v20240801'"
)
)

try:
_ = await get_version(dataset, version)
except RecordNotFoundError:
raise HTTPException(
status_code=400,
detail=(
"Version not found. Existing versions for this dataset "
f"include {await get_version_names(dataset)}"
)
)
132 changes: 104 additions & 28 deletions tests_v2/unit/app/routes/thematic/geoencoder/test_geoencoder.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,104 @@
# import pytest
# from httpx import AsyncClient
#
#
# @pytest.mark.asyncio
# async def test_geoencoder_no_version(async_client: AsyncClient) -> None:
# params = {"country": "Canada"}
#
# resp = await async_client.get("/thematic/geoencode", params=params)
#
# assert resp.status_code == 400
#
#
# @pytest.mark.asyncio
# async def test_geoencoder_fake_country_no_matches(async_client: AsyncClient) -> None:
#
# params = {"admin_version": "4.1", "country": "Canadiastan"}
#
# resp = await async_client.get("/thematic/geoencode", params=params)
#
# assert resp.json() == {
# "status": "success",
# "data": {
# "adminVersion": "4.1",
# "matches": []
# }
# }
# assert resp.status_code == 200
from typing import Optional, Any, Dict, List

import pytest
from httpx import AsyncClient

from app.models.pydantic.geostore import GeostoreCommon
from app.routes.thematic import geoencoder
from app.routes.thematic.geoencoder import _admin_boundary_lookup_sql


@pytest.mark.asyncio
async def test__admin_boundary_lookup_sql() -> None:
sql = _admin_boundary_lookup_sql(
"some_dataset", "some_country", "some_region", "some_subregion"
)
assert sql == (
"SELECT gid_0, gid_1, gid_2, country, name_1, name_2 FROM some_dataset "
"WHERE country='some_country' "
"AND WHERE region='some_region' "
"AND WHERE subregion='some_subregion'"
)


@pytest.mark.asyncio
async def test_geoencoder_no_admin_version(async_client: AsyncClient) -> None:
params = {"country": "Canada"}

resp = await async_client.get("/thematic/geoencode", params=params)

assert resp.status_code == 422


@pytest.mark.asyncio
async def test_geoencoder_invalid_version_pattern(async_client: AsyncClient) -> None:
params = {"country": "Canada", "admin_version": "fails_regex"}

resp = await async_client.get("/thematic/geoencode", params=params)

assert resp.json().get("message", {}).startswith("Invalid version")
assert resp.status_code == 400


@pytest.mark.asyncio
async def test_geoencoder_nonexistant_version(async_client: AsyncClient) -> None:
params = {"country": "Canada", "admin_version": "v4.0"}

resp = await async_client.get("/thematic/geoencode", params=params)

assert resp.json().get("message", {}).startswith("Version not found")
assert resp.status_code == 400


@pytest.mark.asyncio
async def test_geoencoder_bad_boundary_source(async_client: AsyncClient) -> None:
params = {
"admin_source": "bobs_boundaries",
"admin_version": "4.1",
"country": "Canadiastan"
}

resp = await async_client.get("/thematic/geoencode", params=params)

assert resp.json().get("message", {}).startswith("Invalid admin boundary source")
assert resp.status_code == 400


@pytest.mark.asyncio
async def test_geoencoder_no_matches(
async_client: AsyncClient,
monkeypatch: pytest.MonkeyPatch
) -> None:
admin_source = "gadm"
admin_version = "v4.1"

params = {
"admin_source": admin_source,
"admin_version": admin_version,
"country": "Canadiastan"
}

async def mock_version_is_valid(dataset: str, version: str): return None
monkeypatch.setattr(geoencoder, "version_is_valid", mock_version_is_valid)
monkeypatch.setattr(geoencoder, "_query_dataset_json", _query_dataset_json_mocked_results)

resp = await async_client.get("/thematic/geoencode", params=params)

assert resp.json() == {
"status": "success",
"data": {
"adminSource": admin_source,
"adminVersion": admin_version,
"matches": []
}
}
assert resp.status_code == 200


async def _query_dataset_json_mocked_results(
dataset: str,
version: str,
sql: str,
geostore: Optional[GeostoreCommon],
) -> List[Dict[str, Any]]:
return []

0 comments on commit 899e772

Please sign in to comment.