Skip to content

Commit

Permalink
Merge pull request #253 from hotosm/fix/stats-limit-admin
Browse files Browse the repository at this point in the history
Feature : Admin to bypass area threshold in stats endpoint
  • Loading branch information
kshitijrajsharma authored Jun 6, 2024
2 parents b3d2a45 + fccf789 commit ef5e335
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 55 deletions.
22 changes: 21 additions & 1 deletion API/stats.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
# Standard library imports
import json

from fastapi import APIRouter, Body, Request
# Third party imports
from area import area
from fastapi import APIRouter, Body, Depends, HTTPException, Request
from fastapi_versioning import version

# Reader imports
from src.app import PolygonStats
from src.config import LIMITER as limiter
from src.config import POLYGON_STATISTICS_API_RATE_LIMIT
from src.validation.models import StatsRequestParams

router = APIRouter(prefix="/stats", tags=["Stats"])
from .auth import AuthUser, UserRole, get_optional_user


@router.post("/polygon/")
Expand Down Expand Up @@ -45,6 +50,7 @@ async def get_polygon_stats(
},
},
),
user: AuthUser = Depends(get_optional_user),
):
"""Get statistics for the specified polygon.
Expand All @@ -55,6 +61,20 @@ async def get_polygon_stats(
Returns:
dict: A dictionary containing statistics for the specified polygon.
"""
if not (user.role is UserRole.STAFF.value or user.role is UserRole.ADMIN.value):
if params.geometry:
area_m2 = area(json.loads(params.geometry.model_dump_json()))
area_km2 = area_m2 * 1e-6
limit = 10000
if area_km2 > limit:
raise HTTPException(
status_code=400,
detail=[
{
"msg": f"""Polygon Area {int(area_km2)} Sq.KM is higher than Threshold : {limit} Sq.KM"""
}
],
)
feature = None
if params.geometry:
feature = {
Expand Down
36 changes: 27 additions & 9 deletions src/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import json
import os
import pathlib
import random
import re
import shutil
import subprocess
Expand Down Expand Up @@ -1050,15 +1051,32 @@ def get_osm_analytics_meta_stats(self):
Returns:
dict: Raw statistics translated into JSON.
"""
try:
query = generate_polygon_stats_graphql_query(self.INPUT_GEOM)
payload = {"query": query}
response = requests.post(self.API_URL, json=payload, timeout=30)
response.raise_for_status() # Raise an HTTPError for bad responses
return response.json()
except Exception as e:
print(f"Request failed: {e}")
return None
MAX_RETRIES = 2 # Maximum number of retries
INITIAL_DELAY = 1 # Initial delay in seconds
MAX_DELAY = 8

retries = 0
delay = INITIAL_DELAY

while retries < MAX_RETRIES:
try:
query = generate_polygon_stats_graphql_query(self.INPUT_GEOM)
payload = {"query": query}
response = requests.post(self.API_URL, json=payload, timeout=20)
response.raise_for_status()
return response.json()
except Exception as e:
print(f"Request failed: {e}")
retries += 1
delay = min(delay * 0.5, MAX_DELAY) # Exponential backoff
jitter = random.uniform(0, 1) # jitter to avoid simultaneous retries
sleep_time = delay * (1 + jitter)
print(f"Retrying in {sleep_time} seconds...")
time.sleep(sleep_time)

# If all retries failed, return None
print("Maximum retries exceeded. Unable to fetch data.")
return None

def get_summary_stats(self):
"""
Expand Down
77 changes: 32 additions & 45 deletions src/validation/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,22 +301,22 @@ class StatsRequestParams(BaseModel, GeometryValidatorMixin):
max_length=3,
example="NPL",
)
geometry: Optional[
Union[Polygon, MultiPolygon, Feature, FeatureCollection]
] = Field(
default=None,
example={
"type": "Polygon",
"coordinates": [
[
[83.96919250488281, 28.194446860487773],
[83.99751663208006, 28.194446860487773],
[83.99751663208006, 28.214869548073377],
[83.96919250488281, 28.214869548073377],
[83.96919250488281, 28.194446860487773],
]
],
},
geometry: Optional[Union[Polygon, MultiPolygon, Feature, FeatureCollection]] = (
Field(
default=None,
example={
"type": "Polygon",
"coordinates": [
[
[83.96919250488281, 28.194446860487773],
[83.99751663208006, 28.194446860487773],
[83.99751663208006, 28.214869548073377],
[83.96919250488281, 28.214869548073377],
[83.96919250488281, 28.194446860487773],
]
],
},
)
)

@validator("geometry", pre=True, always=True)
Expand All @@ -328,19 +328,6 @@ def set_geometry_or_iso3(cls, value, values):
raise ValueError("Either geometry or iso3 should be supplied.")
return value

@validator("geometry", pre=True, always=True)
def validate_geometry_area(cls, value):
"""Validate that the geometry area does not exceed threshold."""
if value is not None:
geometry_json = value
area_m2 = area(geometry_json)
max_area = 10000
if area_m2 * 1e-6 > max_area:
raise ValueError(
f"The area {area_m2 * 1e-6} sqkm of the geometry should not exceed {max_area} square km."
)
return value


### HDX BLock

Expand Down Expand Up @@ -626,22 +613,22 @@ class DynamicCategoriesModel(BaseModel, GeometryValidatorMixin):
}
],
)
geometry: Optional[
Union[Polygon, MultiPolygon, Feature, FeatureCollection]
] = Field(
default=None,
example={
"type": "Polygon",
"coordinates": [
[
[83.96919250488281, 28.194446860487773],
[83.99751663208006, 28.194446860487773],
[83.99751663208006, 28.214869548073377],
[83.96919250488281, 28.214869548073377],
[83.96919250488281, 28.194446860487773],
]
],
},
geometry: Optional[Union[Polygon, MultiPolygon, Feature, FeatureCollection]] = (
Field(
default=None,
example={
"type": "Polygon",
"coordinates": [
[
[83.96919250488281, 28.194446860487773],
[83.99751663208006, 28.194446860487773],
[83.99751663208006, 28.214869548073377],
[83.96919250488281, 28.214869548073377],
[83.96919250488281, 28.194446860487773],
]
],
},
)
)

@validator("geometry", pre=True, always=True)
Expand Down

0 comments on commit ef5e335

Please sign in to comment.