Skip to content

Commit be15c5b

Browse files
committed
Merge branch 'develop' into bump_crypto_for_security
2 parents 837dc8f + 6de227d commit be15c5b

File tree

25 files changed

+1674
-389
lines changed

25 files changed

+1674
-389
lines changed

app/crud/geostore.py

+273-6
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,33 @@
1-
from typing import List
1+
import json
2+
from typing import Dict, List, Optional, Tuple
23
from uuid import UUID
34

45
from asyncpg.exceptions import UniqueViolationError
56
from fastapi.logger import logger
6-
from sqlalchemy import Column, Table
7-
from sqlalchemy.sql import Select
8-
from sqlalchemy.sql.elements import TextClause
7+
from sqlalchemy import Column, Table, func
8+
from sqlalchemy.sql import Select, label
9+
from sqlalchemy.sql.elements import Label, TextClause
910

1011
from app.application import db
11-
from app.errors import RecordNotFoundError
12+
from app.errors import (
13+
BadAdminSourceException,
14+
BadAdminVersionException,
15+
GeometryIsNullError,
16+
RecordNotFoundError,
17+
)
1218
from app.models.orm.user_areas import UserArea as ORMUserArea
13-
from app.models.pydantic.geostore import Geometry, Geostore
19+
from app.models.pydantic.geostore import (
20+
Adm0BoundaryInfo,
21+
Adm1BoundaryInfo,
22+
Adm2BoundaryInfo,
23+
AdminGeostore,
24+
AdminGeostoreResponse,
25+
AdminListResponse,
26+
Geometry,
27+
Geostore,
28+
)
29+
from app.settings.globals import ENV, per_env_admin_boundary_versions
30+
from app.utils.gadm import extract_level_id, fix_id_pattern
1431

1532
GEOSTORE_COLUMNS: List[Column] = [
1633
db.column("gfw_geostore_id"),
@@ -115,3 +132,253 @@ async def create_user_area(geometry: Geometry) -> Geostore:
115132
geostore = await get_gfw_geostore_from_any_dataset(geo_id)
116133

117134
return geostore
135+
136+
137+
async def get_admin_boundary_list(
138+
admin_provider: str, admin_version: str
139+
) -> AdminListResponse:
140+
dv: Tuple[str, str] = await admin_params_to_dataset_version(
141+
admin_provider, admin_version
142+
)
143+
dataset, version = dv
144+
145+
src_table: Table = db.table(version)
146+
src_table.schema = dataset
147+
148+
# What exactly is served-up by RW? It looks like it INTENDS to just
149+
# serve admin 0s, but the response contains much more
150+
where_clause: TextClause = db.text("adm_level=:adm_level").bindparams(adm_level="0")
151+
152+
gadm_admin_list_columns: List[Column] = [
153+
db.column("adm_level"),
154+
db.column("gfw_geostore_id"),
155+
db.column("gid_0"),
156+
db.column("country"),
157+
]
158+
sql: Select = (
159+
db.select(gadm_admin_list_columns)
160+
.select_from(src_table)
161+
.where(where_clause)
162+
.order_by("gid_0")
163+
)
164+
165+
rows = await get_all_rows(sql)
166+
167+
return AdminListResponse.parse_obj(
168+
{
169+
"data": [
170+
{
171+
"geostoreId": str(row.gfw_geostore_id),
172+
"iso": str(row.gid_0),
173+
"name": str(row.country),
174+
}
175+
for row in rows
176+
],
177+
}
178+
)
179+
180+
181+
async def get_all_rows(sql: Select):
182+
rows = await db.all(sql)
183+
184+
return rows
185+
186+
187+
async def get_first_row(sql: Select):
188+
row = await db.first(sql)
189+
190+
return row
191+
192+
193+
async def get_gadm_geostore(
194+
admin_provider: str,
195+
admin_version: str,
196+
adm_level: int,
197+
simplify: float | None,
198+
country_id: str,
199+
region_id: str | None = None,
200+
subregion_id: str | None = None,
201+
) -> AdminGeostoreResponse:
202+
dv: Tuple[str, str] = await admin_params_to_dataset_version(
203+
admin_provider, admin_version
204+
)
205+
dataset, version = dv
206+
207+
src_table: Table = db.table(version)
208+
src_table.schema = dataset
209+
210+
columns_etc: List[Column | Label] = [
211+
db.column("adm_level"),
212+
db.column("gfw_area__ha"),
213+
db.column("gfw_bbox"),
214+
db.column("gfw_geostore_id"),
215+
label("level_id", db.column(f"gid_{adm_level}")),
216+
]
217+
218+
if adm_level == 0:
219+
columns_etc.append(label("name", db.column("country")))
220+
else:
221+
columns_etc.append(label("name", db.column(f"name_{adm_level}")))
222+
223+
if simplify is None:
224+
columns_etc.append(label("geojson", func.ST_AsGeoJSON(db.column("geom"))))
225+
else:
226+
columns_etc.append(
227+
label(
228+
"geojson",
229+
func.ST_AsGeoJSON(func.ST_Simplify(db.column("geom"), simplify)),
230+
)
231+
)
232+
233+
where_clauses: List[TextClause] = [
234+
db.text("adm_level=:adm_level").bindparams(adm_level=str(adm_level))
235+
]
236+
237+
# gid_0 is just a three-character value, but all more specific ids are
238+
# followed by an underscore (which has to be escaped because normally in
239+
# SQL an underscore is a wildcard) and a revision number (for which we
240+
# use an UN-escaped underscore).
241+
level_id_pattern: str = country_id
242+
243+
if adm_level == 0: # Special-case to avoid slow LIKE
244+
where_clauses.append(
245+
db.text("gid_0=:level_id_pattern").bindparams(
246+
level_id_pattern=level_id_pattern
247+
)
248+
)
249+
else:
250+
assert region_id is not None
251+
level_id_pattern = ".".join((level_id_pattern, region_id))
252+
if adm_level >= 2:
253+
assert subregion_id is not None
254+
level_id_pattern = ".".join((level_id_pattern, subregion_id))
255+
level_id_pattern += r"\__"
256+
257+
# Adjust for any errata
258+
level_id_pattern = fix_id_pattern(
259+
adm_level, level_id_pattern, admin_provider, admin_version
260+
)
261+
262+
where_clauses.append(
263+
db.text(f"gid_{adm_level} LIKE :level_id_pattern").bindparams(
264+
level_id_pattern=level_id_pattern
265+
)
266+
)
267+
268+
sql: Select = db.select(columns_etc).select_from(src_table)
269+
270+
for clause in where_clauses:
271+
sql = sql.where(clause)
272+
273+
row = await get_first_row(sql)
274+
if row is None:
275+
raise RecordNotFoundError(
276+
f"Admin boundary not found in {admin_provider} version {admin_version}"
277+
)
278+
279+
if row.geojson is None:
280+
raise GeometryIsNullError(
281+
"GeoJSON is None, try reducing or eliminating simplification."
282+
)
283+
284+
geostore: AdminGeostore = await form_admin_geostore(
285+
adm_level=adm_level,
286+
admin_version=admin_version,
287+
area=float(row.gfw_area__ha),
288+
bbox=[float(val) for val in row.gfw_bbox],
289+
name=str(row.name),
290+
geojson=json.loads(row.geojson),
291+
geostore_id=str(row.gfw_geostore_id),
292+
level_id=str(row.level_id),
293+
simplify=simplify,
294+
)
295+
296+
return AdminGeostoreResponse(data=geostore)
297+
298+
299+
async def admin_params_to_dataset_version(
300+
source_provider: str, source_version: str
301+
) -> Tuple[str, str]:
302+
admin_source_to_dataset: Dict[str, str] = {"GADM": "gadm_administrative_boundaries"}
303+
304+
try:
305+
dataset: str = admin_source_to_dataset[source_provider.upper()]
306+
except KeyError:
307+
raise BadAdminSourceException(
308+
(
309+
"Invalid admin boundary source. Valid sources:"
310+
f" {[source.lower() for source in admin_source_to_dataset.keys()]}"
311+
)
312+
)
313+
314+
try:
315+
version: str = per_env_admin_boundary_versions[ENV][source_provider.upper()][
316+
source_version
317+
]
318+
except KeyError:
319+
raise BadAdminVersionException(
320+
(
321+
"Invalid admin boundary version. Valid versions:"
322+
f" {[v for v in per_env_admin_boundary_versions[ENV][source_provider.upper()].keys()]}"
323+
)
324+
)
325+
326+
return dataset, version
327+
328+
329+
async def form_admin_geostore(
330+
adm_level: int,
331+
bbox: List[float],
332+
area: float,
333+
geostore_id: str,
334+
level_id: str,
335+
simplify: Optional[float],
336+
admin_version: str,
337+
geojson: Dict,
338+
name: str,
339+
) -> AdminGeostore:
340+
info = Adm0BoundaryInfo.parse_obj(
341+
{
342+
"use": {},
343+
"simplifyThresh": simplify,
344+
"gadm": admin_version,
345+
"name": name,
346+
"iso": extract_level_id(0, level_id),
347+
}
348+
)
349+
if adm_level >= 1:
350+
info = Adm1BoundaryInfo(
351+
**info.dict(),
352+
id1=int(extract_level_id(1, level_id)),
353+
)
354+
if adm_level == 2:
355+
info = Adm2BoundaryInfo(
356+
**info.dict(),
357+
id2=int(extract_level_id(2, level_id)),
358+
)
359+
360+
return AdminGeostore.parse_obj(
361+
{
362+
"type": "geoStore",
363+
"id": geostore_id,
364+
"attributes": {
365+
"geojson": {
366+
"crs": {},
367+
"type": "FeatureCollection",
368+
"features": [
369+
{
370+
"geometry": geojson,
371+
"properties": None,
372+
"type": "Feature",
373+
}
374+
],
375+
},
376+
"hash": geostore_id,
377+
"provider": {},
378+
"areaHa": area,
379+
"bbox": bbox,
380+
"lock": False,
381+
"info": info.dict(),
382+
},
383+
}
384+
)

app/errors.py

+12
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,15 @@ def http_error_handler(exc: HTTPException) -> ORJSONResponse:
5151
return ORJSONResponse(
5252
status_code=exc.status_code, content={"status": status, "message": message}
5353
)
54+
55+
56+
class BadAdminSourceException(Exception):
57+
pass
58+
59+
60+
class BadAdminVersionException(Exception):
61+
pass
62+
63+
64+
class GeometryIsNullError(Exception):
65+
pass
+2-9
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
from ....application import db
2-
from ...enum.pixetl import Grid
3-
4-
sql = """
1+
data_environment_raster_tile_sets = """
52
SELECT
63
assets.asset_id,
74
assets.dataset,
@@ -18,11 +15,7 @@
1815
AND versions.version = assets.version
1916
LEFT JOIN raster_band_metadata rb
2017
ON rb.asset_metadata_id = am.id
21-
WHERE versions.is_latest = true
22-
AND assets.asset_type = 'Raster tile set'
18+
WHERE assets.asset_type = 'Raster tile set'
2319
AND assets.creation_options->>'pixel_meaning' NOT LIKE '%tcd%'
2420
AND assets.creation_options->>'grid' = :grid
2521
"""
26-
27-
28-
latest_raster_tile_sets = db.text(sql)

app/models/pydantic/datamart.py

+41
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,49 @@
1+
from typing import Dict, Optional
12
from uuid import UUID
23

4+
from app.models.pydantic.responses import Response
5+
36
from .base import StrictBaseModel
47

58

9+
class DataMartSource(StrictBaseModel):
10+
dataset: str
11+
version: str
12+
13+
14+
class DataMartMetadata(StrictBaseModel):
15+
geostore_id: UUID
16+
sources: list[DataMartSource]
17+
18+
19+
class DataMartResource(StrictBaseModel):
20+
status: str = "saved"
21+
details: Optional[str] = None
22+
metadata: Optional[DataMartMetadata] = None
23+
24+
25+
class DataMartResourceLink(StrictBaseModel):
26+
link: str
27+
28+
29+
class DataMartResourceLinkResponse(Response):
30+
data: DataMartResourceLink
31+
32+
633
class TreeCoverLossByDriverIn(StrictBaseModel):
734
geostore_id: UUID
835
canopy_cover: int = 30
36+
dataset_version: Dict[str, str] = {}
37+
38+
39+
class TreeCoverLossByDriverMetadata(DataMartMetadata):
40+
canopy_cover: int
41+
42+
43+
class TreeCoverLossByDriver(DataMartResource):
44+
tree_cover_loss_by_driver: Optional[Dict[str, float]] = None
45+
metadata: Optional[TreeCoverLossByDriverMetadata] = None
46+
47+
48+
class TreeCoverLossByDriverResponse(Response):
49+
data: TreeCoverLossByDriver

0 commit comments

Comments
 (0)