diff --git a/docs/guide/api/public_api.md b/docs/guide/api/public_api.md new file mode 100644 index 00000000..93758b8f --- /dev/null +++ b/docs/guide/api/public_api.md @@ -0,0 +1,223 @@ +# Developer Guide: Documentation for the Public APIs + +This guide provides a professional walkthrough for documenting new API endpoints in the `public_api` app using `drf-yasg`. + +--- + +## Quick Access + +| Resource | URL | +| :--- | :--- | +| **Swagger UI** | [http://127.0.0.1:8000/swagger/](http://127.0.0.1:8000/swagger/) | +| **ReDoc** | [http://127.0.0.1:8000/](http://127.0.0.1:8000/) | + +--- + + +## Implementation Workflow + +### Step 1 — Define Parameters (`swagger_schemas.py`) + +Before creating a new parameter, always check the **COMMON PARAMETERS** section first. Reusing existing parameters prevents duplication and ensures consistency across API documentation. + +If your parameter doesn't exist yet, define it using `openapi.Parameter`: + +```python +# External definition makes parameters reusable across multiple APIs +latitude_param = openapi.Parameter( + "latitude", + openapi.IN_QUERY, + description="Latitude coordinate (-90 to 90)", + type=openapi.TYPE_NUMBER, + required=True, +) + +longitude_param = openapi.Parameter( + "longitude", + openapi.IN_QUERY, + description="Longitude coordinate (-180 to 180)", + type=openapi.TYPE_NUMBER, + required=True, +) + +authorization_param = openapi.Parameter( + "X-API-Key", + openapi.IN_HEADER, + description="API key for authentication", + type=openapi.TYPE_STRING, + required=True, +) +``` + +**Parameter fields explained:** + +| Field | Description | +| :--- | :--- | +| `name` | The parameter name as it appears in the request | +| `in` | Location: `openapi.IN_QUERY`, `openapi.IN_HEADER`, `openapi.IN_PATH`, or `openapi.IN_BODY` | +| `description` | Human-readable explanation shown in Swagger UI | +| `type` | Data type: `openapi.TYPE_STRING`, `openapi.TYPE_NUMBER`, `openapi.TYPE_INTEGER`, `openapi.TYPE_BOOLEAN` | +| `required` | `True` if the API cannot function without this parameter | + +--- + +### Step 2 — Create the Schema Dictionary (`swagger_schemas.py`) + +The schema dictionary defines the **"contract"** for your API — what it accepts, what it returns, and how it behaves. The example below uses `get_admin_details_by_lat_lon` as a reference implementation. + +```python +admin_by_latlon_schema = { + "method": "get", + "operation_id": "get_admin_details_by_latlon", + "operation_summary": "Get Admin Details by Lat Lon", + "operation_description": "Retrieve administrative hierarchy (State, District, Tehsil) for a coordinate.", + "manual_parameters": [ + latitude_param, + longitude_param, + authorization_param, # Mandatory for secured APIs + ], + "responses": { + 200: openapi.Response( + description="Success", + examples={ + "application/json": { + "State": "UTTAR PRADESH", + "District": "JAUNPUR", + "Tehsil": "BADLAPUR", + } + }, + ), + 400: bad_request_response, + 401: unauthorized_response, + }, + "tags": ["Dataset APIs"], +} +``` + +**Schema dictionary fields explained:** + +| Field | Purpose | +| :--- | :--- | +| `method` | HTTP method: `"get"`, `"post"`, `"put"`, `"delete"` | +| `operation_id` | Unique identifier for this operation — must not repeat across the project | +| `operation_summary` | Short, one-line title shown in Swagger UI | +| `operation_description` | Longer explanation of what the API does | +| `manual_parameters` | List of `openapi.Parameter` objects | +| `responses` | Dictionary mapping HTTP status codes to `openapi.Response` objects | +| `tags` | List of category strings used for grouping in the UI | + +**Predefined common responses** (import from `swagger_schemas.py`): + +```python +bad_request_response = openapi.Response( + description="Bad Request — Invalid or missing parameters" +) + +unauthorized_response = openapi.Response( + description="Unauthorized — Invalid or missing API key" +) +``` + +--- + +### Step 3 — Apply to View (`api.py`) + +Import the schema dictionary and unpack it into the `@swagger_auto_schema` decorator using the `**` operator. + +```python +from drf_yasg.utils import swagger_auto_schema +from rest_framework.response import Response + +from .swagger_schemas import admin_by_latlon_schema +from utilities.auth_check_decorator import api_security_check + + +@swagger_auto_schema(**admin_by_latlon_schema) +@api_security_check(auth_type="API_key") +def get_admin_details_by_lat_lon(request): + # Use request.query_params for GET requests in DRF + lat = request.query_params.get("latitude") + lon = request.query_params.get("longitude") + + # ... fetch data ... + + return Response(data) +``` + +> **Decorator order matters.** `@swagger_auto_schema` must be placed **above** `@api_security_check` and other decorators to ensure Swagger processes the metadata correctly. + +--- + +## File Structure Reference + +``` +public_api/ +├── api.py # View functions with @swagger_auto_schema decorators +├── swagger_schemas.py # All parameter definitions and schema dictionaries +├── views.py # Heavy business logic and helper functions +└── urls.py # URL routing +``` + +--- + +## Developer Checklist + +When adding a new API, ensure you've completed all of these steps: + +- [ ] **Parameter Reuse** — Used existing parameters from `swagger_schemas.py` wherever possible +- [ ] **New Parameters** — Any new parameters are defined at the top of `swagger_schemas.py` in the COMMON PARAMETERS section +- [ ] **Authorization** — Added `authorization_param` to `manual_parameters` if the view uses `@api_security_check` +- [ ] **Unique `operation_id`** — Provided a unique `operation_id` string to avoid Swagger UI conflicts +- [ ] **Realistic Example** — Included a real-world JSON example in the `200` response +- [ ] **Error Responses** — Mapped all relevant error codes (`400`, `401`, `404`, etc.) to responses +- [ ] **Tags** — Assigned a tag to group the API correctly in the Swagger UI +- [ ] **Decorator Order** — Confirmed `@swagger_auto_schema` is above all other decorators on the view + +--- + +## Troubleshooting + +### Swagger page fails to load + +**Cause:** Syntax errors in `swagger_schemas.py` — most commonly missing commas, unclosed brackets, or incorrect dictionary nesting. + +**Fix:** Check your schema dictionary carefully for: +- Missing `,` between list items in `manual_parameters` +- Unclosed `{` or `[` in the `responses` block +- Incorrect indentation in nested `openapi.Response(...)` calls + +### `operation_id` conflict warning + +**Cause:** Two schema dictionaries share the same `operation_id` value. + +**Fix:** Ensure every `operation_id` is globally unique across the entire project. Use a naming convention such as `verb_resource_by_qualifier` (e.g., `get_admin_details_by_latlon`). + +### API key not being sent in requests + +**Cause:** The Swagger UI prompts for authorization using the header field label, and users may enter the key in the wrong field. + +**Fix:** In the Swagger UI, the API key must be provided in the `X-API-Key` **header field**, not as a query parameter. Inform API consumers of this when sharing documentation. + +### Parameters not showing up in Swagger UI + +**Cause:** The parameter was defined but not added to `manual_parameters` in the schema dictionary. + +**Fix:** Ensure the parameter variable is included in the `manual_parameters` list in the relevant schema. + +--- + +## Quick Reference: `openapi` Types + +| Constant | Value | Use Case | +| :--- | :--- | :--- | +| `openapi.TYPE_STRING` | `"string"` | Text values, UUIDs, slugs | +| `openapi.TYPE_NUMBER` | `"number"` | Floats (e.g., lat/lon) | +| `openapi.TYPE_INTEGER` | `"integer"` | Whole numbers (e.g., IDs) | +| `openapi.TYPE_BOOLEAN` | `"boolean"` | True/false flags | +| `openapi.IN_QUERY` | `"query"` | URL query string (`?key=value`) | +| `openapi.IN_HEADER` | `"header"` | HTTP request header | +| `openapi.IN_PATH` | `"path"` | URL path segment (`/resource/{id}/`) | +| `openapi.IN_BODY` | `"body"` | POST/PUT request body | + +--- + diff --git a/utilities/generate_layers_for_active_locations.py b/utilities/generate_layers_for_active_locations.py new file mode 100644 index 00000000..71fa5c57 --- /dev/null +++ b/utilities/generate_layers_for_active_locations.py @@ -0,0 +1,176 @@ +import json +import requests +import time +import os +import argparse + +""" +## How to run the script +python batch_generate_mws_layers.py --refresh --skip 80 --limit 20 gee_account_id 19 --start_year 2020 --end_year 2023 + +## Parameters +--refresh: Refresh the active locations list from the API without this parameter, the script will use the existing list. +--skip: Skip the first N locations +--limit: Limit the number of locations to process +--gee_account_id: GEE account ID to use +--start_year: Start year for analysis (optional) +--end_year: End year for analysis (optional) + +## To Add in script +-- pass your bearer token for authentication. +-- API for which layer to be generated + +""" + +bearer_token = "" +layer_api = "http://localhost:8000/api/v1/tree_health_raster/" + + +def get_active_locations(): + url = "https://geoserver.core-stack.org/api/v1/proposed_blocks/" + print(f"Fetching active locations from {url}...") + + response = requests.get(url) + response.raise_for_status() + data = response.json() + + # Flatten the JSON + flattened = [] + for state in data: + for district in state.get("district", []): + for block in district.get("blocks", []): + flattened.append( + { + "state": state["label"], + "district": district["label"], + "block": block["label"], + } + ) + + # Save to JSON file + with open("flattened_locations.json", "w", encoding="utf-8") as f: + json.dump(flattened, f, ensure_ascii=False, indent=2) + + print( + f"Flattened JSON saved to flattened_locations.json. Total active blocks: {len(flattened)}" + ) + return flattened + + +def trigger_layer_generation( + location, token, gee_account_id, start_year=None, end_year=None +): + """Hits the generate_mws_layer API for a given location.""" + + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + + # Build payload with required fields + payload = { + "state": location["state"], + "district": location["district"], + "block": location["block"], + "gee_account_id": gee_account_id, + "start_year": start_year, + "end_year": end_year, + } + + try: + response = requests.post(layer_api, json=payload, headers=headers) + + # Check if the request was successful + if response.ok: + print( + f"Successfully triggered for {location['district']} - {location['block']}. Response: {response.text}" + ) + else: + print( + f"Failed for {location['district']} - {location['block']}. Status: {response.status_code}" + ) + print(f"Error details: {response.text}") + + except Exception as e: + print( + f"Request failed for {location['district']} - {location['block']} with exception: {str(e)}" + ) + + +def main(): + parser = argparse.ArgumentParser( + description="Batch generate MWS layers for active blocks" + ) + parser.add_argument( + "--limit", + type=int, + default=None, + help="Limit the number of locations to process at a time", + ) + parser.add_argument( + "--skip", type=int, default=0, help="Number of starting locations to skip" + ) + parser.add_argument( + "--refresh", + action="store_true", + help="Force fetch from the API and refresh locations list", + ) + parser.add_argument( + "--gee_account_id", + type=int, + default=19, + help="GEE account ID to use (default: 19)", + ) + parser.add_argument( + "--start_year", + type=int, + default=None, + help="Start year for analysis (optional)", + ) + parser.add_argument( + "--end_year", type=int, default=None, help="End year for analysis (optional)" + ) + + args = parser.parse_args() + + active_locations_file = "flattened_locations.json" + + # Fetch from API if refreshing or if the file doesn't exist yet + if args.refresh or not os.path.exists(active_locations_file): + locations = get_active_locations() + else: + print(f"Reading active locations from existing '{active_locations_file}'...") + with open(active_locations_file, "r", encoding="utf-8") as f: + locations = json.load(f) + print(f"Loaded {len(locations)} active locations from file.") + + # Apply skip if specified + if args.skip and args.skip > 0: + locations = locations[args.skip :] + print( + f"\nSkipping the first {args.skip} locations. Remaining: {len(locations)}" + ) + + # Apply limit if specified + if args.limit and args.limit > 0: + locations = locations[: args.limit] + print(f"\nProcessing limited to {args.limit} locations.\n") + + # Show which parameters are being used + params_used = [f"GEE Account ID: {args.gee_account_id}"] + if args.start_year is not None: + params_used.append(f"Start Year: {args.start_year}") + if args.end_year is not None: + params_used.append(f"End Year: {args.end_year}") + + # Loop through each location and trigger the generation + for i, loc in enumerate(locations, 1): + print(f"\n--- Processing location {i} of {len(locations)} ---") + trigger_layer_generation( + loc, bearer_token, args.gee_account_id, args.start_year, args.end_year + ) + + # Small delay between requests to be gentle to the API server + if i < len(locations): # Don't sleep after the last request + time.sleep(2) + + +if __name__ == "__main__": + main()