Skip to content

Commit

Permalink
Merge pull request #621 from wri/gtc-3084_new_geostore_endpts
Browse files Browse the repository at this point in the history
GTC-3084: Add new endpoints that proxy to geostore microservice (for now)
  • Loading branch information
dmannarino authored Jan 28, 2025
2 parents c4b64f0 + c61a2d0 commit 986405d
Show file tree
Hide file tree
Showing 9 changed files with 2,951 additions and 2,247 deletions.
20 changes: 10 additions & 10 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,32 @@ repos:
hooks:
- id: isort
- repo: https://github.com/myint/docformatter
rev: v1.4
rev: eb1df347edd128b30cd3368dddc3aa65edcfac38 # pragma: allowlist secret
hooks:
- id: docformatter
args: [--in-place]
- repo: https://github.com/ambv/black
rev: 22.12.0
rev: 24.10.0
hooks:
- id: black
language_version: python3.10
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
rev: v5.0.0
hooks:
- id: detect-aws-credentials
- id: detect-private-key
- id: trailing-whitespace
- repo: https://github.com/pycqa/flake8
rev: 6.0.0
rev: 7.1.1
hooks:
- id: flake8
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.971
rev: v1.14.1
hooks:
- id: mypy
- id: mypy
- repo: https://github.com/Yelp/detect-secrets
rev: v1.3.0
rev: v1.5.0
hooks:
- id: detect-secrets
args: ['--baseline', '.secrets.baseline'] # run: `pip install detect-secrets` to establish baseline
exclude: Pipfile.lock
- id: detect-secrets
args: ['--baseline', '.secrets.baseline'] # run: `pip install detect-secrets` to establish baseline
exclude: Pipfile.lock
2 changes: 1 addition & 1 deletion .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
}
]
},
"version": "1.3.0",
"version": "1.5.0",
"filters_used": [
{
"path": "detect_secrets.filters.allowlist.is_line_allowlisted"
Expand Down
2 changes: 1 addition & 1 deletion Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ google-cloud-storage = "*"
httpcore = "*"
httpx = "*"
httpx-auth = "*"
numpy = "<2.0"
numpy = "*"
orjson = "*"
packaging = "*"
pendulum = "<3"
Expand Down
4,534 changes: 2,318 additions & 2,216 deletions Pipfile.lock

Large diffs are not rendered by default.

72 changes: 70 additions & 2 deletions app/models/pydantic/geostore.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import json
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Literal, Optional
from uuid import UUID

from pydantic import validator
Expand All @@ -14,7 +14,7 @@ class Geometry(StrictBaseModel):


class Feature(StrictBaseModel):
properties: Dict[str, Any]
properties: Optional[Dict[str, Any]]
type: str
geometry: Optional[Geometry]

Expand Down Expand Up @@ -50,5 +50,73 @@ class GeostoreIn(StrictBaseModel):
geometry: Geometry


class RWGeostoreIn(StrictBaseModel):
geojson: Geometry | Feature | FeatureCollection


class GeostoreResponse(Response):
data: Geostore


class AdminBoundaryInfo(StrictBaseModel):
use: Dict
simplifyThresh: float
gadm: str # TODO: Make an enum?
name: str
id2: int
id1: int
iso: str


class CreateGeostoreResponseInfo(StrictBaseModel):
use: Dict


class RWAdminListItem(StrictBaseModel):
geostoreId: str
iso: str
name: str


class RWAdminListItemWithName(StrictBaseModel):
geostoreId: str
iso: str


class RWAdminListResponse(StrictBaseModel):
data: List[RWAdminListItem | RWAdminListItemWithName]


class WDPAInfo(StrictBaseModel):
use: Dict
wdpaid: int


class LandUseUse(StrictBaseModel):
use: str
id: int


class LandUseInfo(StrictBaseModel):
use: LandUseUse
simplify: bool


class RWGeostoreAttributes(StrictBaseModel):
geojson: FeatureCollection
hash: str
provider: Dict
areaHa: float
bbox: List[float]
lock: bool
info: AdminBoundaryInfo | CreateGeostoreResponseInfo | LandUseInfo | WDPAInfo


class RWGeostore(StrictBaseModel):
type: Literal["geoStore"]
id: str
attributes: RWGeostoreAttributes


class RWGeostoreResponse(StrictBaseModel):
data: RWGeostore
174 changes: 158 additions & 16 deletions app/routes/geostore/geostore.py
Original file line number Diff line number Diff line change
@@ -1,52 +1,194 @@
"""Retrieve a geometry using its md5 hash for a given dataset, user defined
geometries in the datastore."""

from typing import Annotated
from uuid import UUID

from fastapi import APIRouter, HTTPException, Path
from fastapi import APIRouter, Header, HTTPException, Path
from fastapi.responses import ORJSONResponse

from ...crud import geostore
from ...errors import BadRequestError, RecordNotFoundError
from ...models.pydantic.geostore import Geostore, GeostoreIn, GeostoreResponse
from ...models.pydantic.geostore import (
Geostore,
GeostoreIn,
GeostoreResponse,
RWAdminListResponse,
RWGeostoreIn,
RWGeostoreResponse,
)
from ...utils.rw_api import (
create_rw_geostore,
get_admin_list,
get_boundary_by_country_id,
get_boundary_by_region_id,
get_boundary_by_subregion_id,
get_geostore_by_land_use_and_index,
proxy_get_geostore,
)

router = APIRouter()


@router.post(
"/",
response_class=ORJSONResponse,
response_model=GeostoreResponse,
response_model=GeostoreResponse | RWGeostoreResponse,
status_code=201,
tags=["Geostore"],
)
async def add_new_geostore(
*,
request: GeostoreIn,
response: ORJSONResponse,
request: GeostoreIn | RWGeostoreIn,
x_api_key: Annotated[str | None, Header()] = None,
):
"""Add geostore feature to user area of geostore."""
"""Add geostore feature to user area of geostore.
If request follows RW style forward to RW, otherwise create in Data
API
"""
if isinstance(request, RWGeostoreIn):
result: RWGeostoreResponse = await create_rw_geostore(request, x_api_key)
return result
# Otherwise, meant for GFW Data API geostore
try:
new_user_area: Geostore = await geostore.create_user_area(request.geometry)
return GeostoreResponse(data=new_user_area)
except BadRequestError as e:
raise HTTPException(status_code=400, detail=str(e))

return GeostoreResponse(data=new_user_area)


@router.get(
"/{geostore_id}",
response_class=ORJSONResponse,
response_model=GeostoreResponse,
response_model=GeostoreResponse | RWGeostoreResponse,
tags=["Geostore"],
)
async def get_any_geostore(*, geostore_id: UUID = Path(..., title="geostore_id")):
"""Retrieve GeoJSON representation for a given geostore ID of any
dataset."""
async def get_any_geostore(
*,
geostore_id: str = Path(..., title="geostore_id"),
x_api_key: Annotated[str | None, Header()] = None,
):
"""Retrieve GeoJSON representation for a given geostore ID of any dataset.
If the provided ID is in UUID style, get from the GFW Data API.
Otherwise, forward request to RW API.
"""
try:
result: Geostore = await geostore.get_gfw_geostore_from_any_dataset(geostore_id)
except RecordNotFoundError as e:
raise HTTPException(status_code=404, detail=str(e))
geostore_uuid = UUID(geostore_id)
if str(geostore_uuid) == geostore_id:
try:
result = await geostore.get_gfw_geostore_from_any_dataset(geostore_uuid)
return GeostoreResponse(data=result)
except RecordNotFoundError as e:
raise HTTPException(status_code=404, detail=str(e))
except (AttributeError, ValueError):
pass
result = await proxy_get_geostore(geostore_id, x_api_key)
return result


@router.get(
"/admin/list",
response_class=ORJSONResponse,
response_model=RWAdminListResponse,
tags=["Geostore"],
include_in_schema=False,
)
async def rw_get_admin_list(x_api_key: Annotated[str | None, Header()] = None):
"""Get all Geostore IDs, names and country codes (proxies request to the RW
API)"""
result: RWAdminListResponse = await get_admin_list(x_api_key)

return result


@router.get(
"/admin/{country_id}",
response_class=ORJSONResponse,
response_model=RWGeostoreResponse,
tags=["Geostore"],
include_in_schema=False,
)
async def rw_get_boundary_by_country_id(
*,
country_id: str = Path(..., title="country_id"),
x_api_key: Annotated[str | None, Header()] = None,
):
"""Get a GADM boundary by country ID (proxies request to the RW API)"""

result: RWGeostoreResponse = await get_boundary_by_country_id(country_id, x_api_key)

return result


@router.get(
"/admin/{country_id}/{region_id}",
response_class=ORJSONResponse,
response_model=RWGeostoreResponse,
tags=["Geostore"],
include_in_schema=False,
)
async def rw_get_boundary_by_region_id(
*,
country_id: str = Path(..., title="country_id"),
region_id: str = Path(..., title="region_id"),
x_api_key: Annotated[str | None, Header()] = None,
):
"""Get a GADM boundary by country and region IDs (proxies request to the RW
API)"""
result: RWGeostoreResponse = await get_boundary_by_region_id(
country_id, region_id, x_api_key
)

return result


@router.get(
"/admin/{country_id}/{region_id}/{subregion_id}",
response_class=ORJSONResponse,
response_model=RWGeostoreResponse,
tags=["Geostore"],
include_in_schema=False,
)
async def rw_get_boundary_by_subregion_id(
*,
country_id: str = Path(..., title="country_id"),
region_id: str = Path(..., title="region_id"),
subregion_id: str = Path(..., title="subregion_id"),
x_api_key: Annotated[str | None, Header()] = None,
):
"""Get a GADM boundary by country, region, and subregion IDs (proxies
request to the RW API)"""

result: RWGeostoreResponse = await get_boundary_by_subregion_id(
country_id, region_id, subregion_id, x_api_key
)

return result


@router.get(
"/use/{land_use_type}/{index}",
response_class=ORJSONResponse,
response_model=RWGeostoreResponse,
tags=["Geostore"],
include_in_schema=False,
)
async def rw_get_geostore_by_land_use_and_index(
*,
land_use_type: str = Path(..., title="land_use_type"),
index: str = Path(..., title="index"),
x_api_key: Annotated[str | None, Header()] = None,
):
"""Get a geostore object by land use type name and id.
Deprecated, returns out of date info, but still used by Flagship.
Present just for completeness for now. (proxies request to the RW
API)
"""
result: RWGeostoreResponse = await get_geostore_by_land_use_and_index(
land_use_type, index, x_api_key
)

return GeostoreResponse(data=result)
return result
Loading

0 comments on commit 986405d

Please sign in to comment.