Skip to content

Commit a6eec77

Browse files
add a way to serve global pre-computed results
1 parent 98ed1ac commit a6eec77

File tree

3 files changed

+117
-30
lines changed

3 files changed

+117
-30
lines changed

app/models/pydantic/datamart.py

+6-9
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from enum import Enum
22
from typing import Dict, Literal, Optional, Union
33
from uuid import UUID
4-
from abc import ABC, abstractmethod
54

65
from pydantic import Field, root_validator, validator
76

@@ -11,16 +10,15 @@
1110
from ...crud.geostore import get_gadm_geostore_id
1211

1312

14-
class AreaOfInterest(StrictBaseModel, ABC):
15-
@abstractmethod
13+
class AreaOfInterest(StrictBaseModel):
1614
async def get_geostore_id(self) -> UUID:
1715
"""Return the unique identifier for the area of interest."""
1816
pass
1917

2018

2119
class GeostoreAreaOfInterest(AreaOfInterest):
2220
type: Literal['geostore'] = 'geostore'
23-
geostore_id:UUID = Field(..., title="Geostore ID")
21+
geostore_id: UUID = Field(..., title="Geostore ID")
2422

2523
async def get_geostore_id(self) -> UUID:
2624
return self.geostore_id
@@ -34,7 +32,6 @@ class AdminAreaOfInterest(AreaOfInterest):
3432
provider: str = Field('gadm', title="Administrative Boundary Provider")
3533
version: str = Field('4.1', title="Administrative Boundary Version")
3634

37-
3835
async def get_geostore_id(self) -> UUID:
3936
admin_level = sum(1 for field in (self.country, self.region, self.subregion) if field is not None) - 1
4037
geostore_id = await get_gadm_geostore_id(
@@ -64,6 +61,9 @@ def set_version_default(cls, v):
6461
return v or '4.1'
6562

6663

64+
class Global(AreaOfInterest):
65+
type: Literal["global"] = "global"
66+
6767

6868
class AnalysisStatus(str, Enum):
6969
saved = "saved"
@@ -99,7 +99,7 @@ class DataMartResourceLinkResponse(Response):
9999

100100

101101
class TreeCoverLossByDriverIn(StrictBaseModel):
102-
aoi: Union[GeostoreAreaOfInterest, AdminAreaOfInterest] = Field(..., discriminator='type')
102+
aoi: Union[GeostoreAreaOfInterest, AdminAreaOfInterest, Global] = Field(..., discriminator='type')
103103
canopy_cover: int = 30
104104
dataset_version: Dict[str, str] = {}
105105

@@ -132,6 +132,3 @@ class Config:
132132

133133
class TreeCoverLossByDriverResponse(Response):
134134
data: TreeCoverLossByDriver
135-
136-
137-

app/routes/datamart/land.py

+41-21
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import re
44
import uuid
5-
from typing import Dict, List, Optional
5+
from typing import Dict, Optional
66
from uuid import UUID
77

88
from fastapi import (
@@ -18,6 +18,8 @@
1818
from fastapi.openapi.models import APIKey
1919
from fastapi.responses import ORJSONResponse
2020
from pydantic import ValidationError
21+
from starlette.status import HTTP_400_BAD_REQUEST
22+
2123

2224
from app.crud import datamart as datamart_crud
2325
from app.errors import RecordNotFoundError
@@ -33,6 +35,7 @@
3335
AreaOfInterest,
3436
GeostoreAreaOfInterest,
3537
AdminAreaOfInterest,
38+
Global,
3639
)
3740
from app.settings.globals import API_URL
3841
from app.tasks.datamart.land import (
@@ -45,6 +48,7 @@
4548

4649
router = APIRouter()
4750

51+
4852
def _parse_dataset_versions(request: Request) -> Dict[str, str]:
4953
dataset_versions = {}
5054
errors = []
@@ -83,6 +87,9 @@ def _parse_area_of_interest(request: Request) -> AreaOfInterest:
8387
version=params.get('aoi[version]', None),
8488
)
8589

90+
if aoi_type == "global":
91+
return Global()
92+
8693
# If neither type is provided, raise an error
8794
raise HTTPException(status_code=422, detail="Invalid Area of Interest parameters")
8895
except ValidationError as e:
@@ -111,6 +118,7 @@ def _parse_area_of_interest(request: Request) -> AreaOfInterest:
111118
"oneOf": [
112119
{"$ref": "#/components/schemas/GeostoreAreaOfInterest"},
113120
{"$ref": "#/components/schemas/AdminAreaOfInterest"},
121+
{"$ref": "#/components/schemas/Global"},
114122
]
115123
}
116124
},
@@ -129,7 +137,7 @@ def _parse_area_of_interest(request: Request) -> AreaOfInterest:
129137
"tsc_tree_cover_loss_drivers": "v2023",
130138
},
131139
"description": (
132-
"Pass dataset version overrides as bracketed query parameters.",
140+
"Pass dataset version overrides as bracketed query parameters.",
133141
)
134142
}
135143
]
@@ -143,9 +151,9 @@ async def tree_cover_loss_by_driver_search(
143151
api_key: APIKey = Depends(get_api_key),
144152
):
145153
"""Search if a resource exists for a given geostore and canopy cover."""
146-
geostore_id = await aoi.get_geostore_id()
154+
area_id = "global" if aoi.type == "global" else await aoi.get_geostore_id()
147155
resource_id = _get_resource_id(
148-
"tree_cover_loss_by_driver", geostore_id, canopy_cover, dataset_versions
156+
"tree_cover_loss_by_driver", area_id, canopy_cover, dataset_versions
149157
)
150158

151159
# check if it exists
@@ -198,41 +206,53 @@ async def tree_cover_loss_by_driver_post(
198206
):
199207
"""Create new tree cover loss by drivers resource for a given geostore and
200208
canopy cover."""
201-
geostore_id = await data.aoi.get_geostore_id()
202209

203-
# check geostore is valid
204-
try:
205-
await get_geostore(geostore_id, GeostoreOrigin.rw)
206-
except HTTPException:
207-
raise HTTPException(
208-
status_code=422,
209-
detail=f"Geostore {geostore_id} can't be found or is not valid.",
210-
)
210+
area_id = "global" if data.aoi.type == "global" else await data.aoi.get_geostore_id()
211211

212-
# create initial Job item as pending
213-
# trigger background task to create item
214-
# return 202 accepted
215212
dataset_version = DEFAULT_LAND_DATASET_VERSIONS | data.dataset_version
216213
resource_id = _get_resource_id(
217214
"tree_cover_loss_by_driver",
218-
geostore_id,
215+
area_id,
219216
data.canopy_cover,
220217
dataset_version,
221218
)
222219

220+
link = DataMartResourceLink(
221+
link=f"{API_URL}/v0/land/tree_cover_loss_by_driver/{resource_id}"
222+
)
223+
if data.aoi.type == "global":
224+
try:
225+
_ = await _get_resource(resource_id)
226+
except HTTPException:
227+
raise HTTPException(
228+
HTTP_400_BAD_REQUEST,
229+
detail="Global computation not supported for this dataset and pre-computed results are not available."
230+
)
231+
232+
return DataMartResourceLinkResponse(data=link)
233+
234+
# check geostore is valid
235+
try:
236+
await get_geostore(area_id, GeostoreOrigin.rw)
237+
except HTTPException:
238+
raise HTTPException(
239+
status_code=422,
240+
detail=f"Geostore {area_id} can't be found or is not valid.",
241+
)
242+
243+
# create initial Job item as pending
223244
await _save_pending_resource(resource_id, request.url.path, api_key)
224245

246+
# trigger background task to create item
247+
# return 202 accepted
225248
background_tasks.add_task(
226249
compute_tree_cover_loss_by_driver,
227250
resource_id,
228-
geostore_id,
251+
area_id,
229252
data.canopy_cover,
230253
dataset_version,
231254
)
232255

233-
link = DataMartResourceLink(
234-
link=f"{API_URL}/v0/land/tree_cover_loss_by_driver/{resource_id}"
235-
)
236256
return DataMartResourceLinkResponse(data=link)
237257

238258

tests_v2/unit/app/routes/datamart/test_land.py

+70
Original file line numberDiff line numberDiff line change
@@ -431,3 +431,73 @@ async def test_get_tree_cover_loss_by_drivers_found(
431431
in response.json()["data"]["link"]
432432
)
433433
mock_get_resources.assert_awaited_with(resource_id)
434+
435+
class TestGlobal:
436+
@pytest.mark.asyncio
437+
async def test_get_tree_cover_loss_by_drivers_found(
438+
self,
439+
apikey,
440+
async_client: AsyncClient,
441+
):
442+
with (
443+
patch("app.routes.datamart.land._get_resource", return_value=None) as mock_get_resources,
444+
):
445+
api_key, payload = apikey
446+
origin = payload["domains"][0]
447+
448+
headers = {"origin": origin}
449+
params = {"x-api-key": api_key, "aoi[type]": "global", "canopy_cover": 30}
450+
resource_id = _get_resource_id(
451+
"tree_cover_loss_by_driver", "global", 30, DEFAULT_LAND_DATASET_VERSIONS
452+
)
453+
454+
response = await async_client.get(
455+
"/v0/land/tree_cover_loss_by_driver", headers=headers, params=params
456+
)
457+
458+
assert response.status_code == 200
459+
assert (
460+
f"/v0/land/tree_cover_loss_by_driver/{resource_id}"
461+
in response.json()["data"]["link"]
462+
)
463+
mock_get_resources.assert_awaited_with(resource_id)
464+
465+
466+
@pytest.mark.asyncio
467+
async def test_post_tree_cover_loss_by_drivers(
468+
self,
469+
apikey,
470+
async_client: AsyncClient,
471+
):
472+
api_key, payload = apikey
473+
origin = payload["domains"][0]
474+
475+
headers = {"origin": origin, "x-api-key": api_key}
476+
payload = {
477+
"aoi": {
478+
"type": "global",
479+
},
480+
"canopy_cover": 30,
481+
"dataset_version": {"umd_tree_cover_loss": "v1.8"},
482+
}
483+
with (
484+
patch("app.routes.datamart.land._get_resource", return_value=None) as mock_get_resources,
485+
):
486+
response = await async_client.post(
487+
"/v0/land/tree_cover_loss_by_driver", headers=headers, json=payload
488+
)
489+
490+
assert response.status_code == 202
491+
492+
body = response.json()
493+
assert body["status"] == "success"
494+
assert "/v0/land/tree_cover_loss_by_driver/" in body["data"]["link"]
495+
496+
resource_id = body["data"]["link"].split("/")[-1]
497+
try:
498+
resource_id = uuid.UUID(resource_id)
499+
assert True
500+
except ValueError:
501+
assert False
502+
503+
mock_get_resources.assert_awaited_with(resource_id)

0 commit comments

Comments
 (0)