diff --git a/API/stats.py b/API/stats.py index b2ca5414..bb1522ec 100644 --- a/API/stats.py +++ b/API/stats.py @@ -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/") @@ -45,6 +50,7 @@ async def get_polygon_stats( }, }, ), + user: AuthUser = Depends(get_optional_user), ): """Get statistics for the specified polygon. @@ -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 = { diff --git a/src/app.py b/src/app.py index 0c9227ab..8c29ee2f 100644 --- a/src/app.py +++ b/src/app.py @@ -22,6 +22,7 @@ import json import os import pathlib +import random import re import shutil import subprocess @@ -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): """ diff --git a/src/validation/models.py b/src/validation/models.py index a1f1ef43..69f34212 100644 --- a/src/validation/models.py +++ b/src/validation/models.py @@ -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) @@ -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 @@ -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)