diff --git a/offsets_db_api/geo.py b/offsets_db_api/geo.py index d2ea101..49b3dc9 100644 --- a/offsets_db_api/geo.py +++ b/offsets_db_api/geo.py @@ -89,6 +89,19 @@ def get_bboxes_for_projects(project_ids: list[str]) -> dict[str, dict[str, float return {pid: bbox_lookup[pid] for pid in project_ids if pid in bbox_lookup} +def get_projects_with_geometry() -> set[str]: + """ + Get the set of project IDs that have geographic boundaries. + + Returns + ------- + set + Set of project IDs that have boundaries + """ + bbox_lookup = load_project_bboxes() + return set(bbox_lookup.keys()) + + def clear_bbox_cache(): """Clear the cached bbox data to force a reload.""" load_project_bboxes.cache_clear() diff --git a/offsets_db_api/routers/credits.py b/offsets_db_api/routers/credits.py index aec8329..2605dbf 100644 --- a/offsets_db_api/routers/credits.py +++ b/offsets_db_api/routers/credits.py @@ -5,6 +5,7 @@ from offsets_db_api.cache import CACHE_NAMESPACE from offsets_db_api.common import build_filters from offsets_db_api.database import get_session +from offsets_db_api.geo import get_projects_with_geometry from offsets_db_api.log import get_logger from offsets_db_api.models import Credit, PaginatedCredits, Project from offsets_db_api.schemas import ( @@ -36,6 +37,10 @@ async def get_credits( project_filters: ProjectFilters = Depends(get_project_filters), credit_filters: CreditFilters = Depends(get_credit_filters), beneficiary_filters: BeneficiaryFilters = Depends(get_beneficiary_filters), + geography: bool | None = Query( + None, + description='Filter by geographic boundaries. True = only credits from projects with boundaries, False = only credits from projects without boundaries, None = no filter.', + ), sort: list[str] = Query( default=['project_id'], description='List of sorting parameters in the format `field_name` or `+field_name` for ascending order or `-field_name` for descending order.', @@ -59,6 +64,16 @@ async def get_credits( if project_id: filters.insert(0, ('project_id', project_id, '==', Project)) + # Filter by geographic boundaries + if geography is not None: + projects_with_geo = get_projects_with_geometry() + if geography: + # Only credits from projects WITH boundaries + statement = statement.where(col(Project.project_id).in_(projects_with_geo)) + else: + # Only credits from projects WITHOUT boundaries + statement = statement.where(~col(Project.project_id).in_(projects_with_geo)) + for attribute, values, operation, model in filters: statement = apply_filters( statement=statement, diff --git a/offsets_db_api/routers/projects.py b/offsets_db_api/routers/projects.py index 287c718..cf90284 100644 --- a/offsets_db_api/routers/projects.py +++ b/offsets_db_api/routers/projects.py @@ -9,7 +9,11 @@ from offsets_db_api.cache import CACHE_NAMESPACE from offsets_db_api.common import build_filters from offsets_db_api.database import get_session -from offsets_db_api.geo import get_bbox_for_project, get_bboxes_for_projects +from offsets_db_api.geo import ( + get_bbox_for_project, + get_bboxes_for_projects, + get_projects_with_geometry, +) from offsets_db_api.log import get_logger from offsets_db_api.models import ( Clip, @@ -65,6 +69,10 @@ async def get_projects( description='Case insensitive search string. Currently searches on `project_id` and `name` fields only.', ), beneficiary_filters: BeneficiaryFilters = Depends(get_beneficiary_filters), + geography: bool | None = Query( + None, + description='Filter by geographic boundaries. True = only projects with boundaries, False = only projects without boundaries, None = no filter.', + ), current_page: int = Query(1, description='Page number', ge=1), per_page: int = Query(100, description='Items per page', le=200, ge=1), sort: list[str] = Query( @@ -84,6 +92,20 @@ async def get_projects( filters = build_filters(project_filters=project_filters) + # Filter by geographic boundaries + if geography is not None: + projects_with_geo = get_projects_with_geometry() + if geography: + # Only projects WITH boundaries + matching_projects = matching_projects.where( + col(Project.project_id).in_(projects_with_geo) + ) + else: + # Only projects WITHOUT boundaries + matching_projects = matching_projects.where( + ~col(Project.project_id).in_(projects_with_geo) + ) + if search: search_pattern = f'%{search}%' matching_projects = matching_projects.where(