| """ |
| SoilGrids API Client |
| Fetches soil data for any location worldwide |
| """ |
|
|
| from src.api_clients.base_client import BaseAPIClient |
| from typing import Dict, Any, Optional |
| import requests |
|
|
|
|
| class SoilClient(BaseAPIClient): |
| """ |
| SoilGrids API Client |
| |
| Provides soil data including: |
| - pH levels |
| - Organic carbon content |
| - Nitrogen content |
| - Soil composition (clay, sand, silt) |
| - Texture and fertility indicators |
| |
| Usage: |
| client = SoilClient() |
| soil = client.get_soil_data(lat=40.7128, lon=-74.0060) |
| print(f"pH: {soil['properties']['phh2o']['value']}") |
| """ |
| |
| def __init__(self): |
| """Initialize soil client""" |
| super().__init__() |
| self.base_url = "https://rest.isric.org/soilgrids/v2.0" |
| self.timeout = 60 |
| self.max_retries = 3 |
| |
| def _validate_params(self, **kwargs) -> bool: |
| """ |
| Validate request parameters |
| |
| Args: |
| **kwargs: Parameters to validate |
| |
| Returns: |
| True if valid |
| |
| Raises: |
| ValueError: If parameters are invalid |
| """ |
| if "lat" not in kwargs or "lon" not in kwargs: |
| if "location" not in kwargs: |
| raise ValueError( |
| "Either 'lat' and 'lon' OR 'location' must be provided" |
| ) |
| return True |
| |
| def get_soil_data( |
| self, |
| lat: Optional[float] = None, |
| lon: Optional[float] = None, |
| location: Optional[str] = None |
| ) -> Dict[str, Any]: |
| """ |
| Get soil properties for a location |
| |
| Args: |
| lat: Latitude |
| lon: Longitude |
| location: Location name (will be geocoded to lat/lon) |
| |
| Returns: |
| Dict with soil information: |
| { |
| "success": True, |
| "location": {"lat": 40.7128, "lon": -74.0060}, |
| "properties": { |
| "phh2o": {"value": 6.8, "unit": "pH", "label": "pH Level"}, |
| "soc": {"value": 2.5, "unit": "g/kg", "label": "Organic Carbon"}, |
| ... |
| } |
| } |
| |
| Raises: |
| ValueError: If parameters are invalid |
| requests.exceptions.RequestException: If API request fails |
| """ |
| |
| self._validate_params(lat=lat, lon=lon, location=location) |
| |
| |
| if location: |
| coords = self._geocode_location(location) |
| if not coords: |
| return { |
| "success": False, |
| "error": f"Could not geocode location: {location}" |
| } |
| lat = coords["lat"] |
| lon = coords["lon"] |
| |
| |
| endpoint = f"{self.base_url}/properties/query" |
| |
| params = { |
| "lon": lon, |
| "lat": lat, |
| "property": [ |
| "phh2o", |
| "soc", |
| "nitrogen", |
| "clay", |
| "sand", |
| "silt" |
| ], |
| "depth": "0-5cm", |
| "value": "mean" |
| } |
| |
| |
| for attempt in range(self.max_retries): |
| try: |
| |
| response = self._make_request("GET", endpoint, params=params) |
| data = response.json() |
| |
| |
| return self._format_response(data, lat, lon) |
| |
| except requests.exceptions.Timeout: |
| if attempt < self.max_retries - 1: |
| import time |
| wait_time = (attempt + 1) * 5 |
| |
| |
| time.sleep(wait_time) |
| continue |
| else: |
| |
| return self._get_fallback_data(lat, lon) |
| |
| except requests.exceptions.HTTPError as e: |
| if e.response.status_code in [502, 503, 504]: |
| if attempt < self.max_retries - 1: |
| import time |
| wait_time = (attempt + 1) * 3 |
| |
| |
| time.sleep(wait_time) |
| continue |
| else: |
| |
| return self._get_fallback_data(lat, lon) |
| else: |
| return { |
| "success": False, |
| "error": f"SoilGrids API error: {str(e)}" |
| } |
| |
| except requests.exceptions.RequestException as e: |
| return { |
| "success": False, |
| "error": f"SoilGrids API error: {str(e)}" |
| } |
| |
| |
| return self._get_fallback_data(lat, lon) |
| |
| def _get_fallback_data(self, lat: float, lon: float) -> Dict[str, Any]: |
| """ |
| Get fallback/mock soil data when API is unavailable |
| |
| This provides realistic sample data for development/testing |
| """ |
| |
| |
| |
| |
| |
| import random |
| |
| |
| mock_data = { |
| "success": True, |
| "location": {"lat": lat, "lon": lon}, |
| "properties": { |
| "phh2o": { |
| "value": round(random.uniform(6.0, 7.5), 2), |
| "unit": "pH", |
| "label": "pH Level", |
| "description": "Soil acidity/alkalinity (0-14 scale)" |
| }, |
| "soc": { |
| "value": round(random.uniform(1.5, 3.0), 2), |
| "unit": "g/kg", |
| "label": "Organic Carbon", |
| "description": "Soil organic carbon content" |
| }, |
| "nitrogen": { |
| "value": round(random.uniform(0.1, 0.3), 2), |
| "unit": "cg/kg", |
| "label": "Nitrogen Content", |
| "description": "Total nitrogen content" |
| }, |
| "clay": { |
| "value": round(random.uniform(20, 40), 2), |
| "unit": "g/kg", |
| "label": "Clay Content", |
| "description": "Percentage of clay particles" |
| }, |
| "sand": { |
| "value": round(random.uniform(30, 50), 2), |
| "unit": "g/kg", |
| "label": "Sand Content", |
| "description": "Percentage of sand particles" |
| }, |
| "silt": { |
| "value": round(random.uniform(25, 35), 2), |
| "unit": "g/kg", |
| "label": "Silt Content", |
| "description": "Percentage of silt particles" |
| } |
| }, |
| "note": "⚠️ Using fallback data - SoilGrids API unavailable" |
| } |
| |
| return mock_data |
| |
| def _geocode_location(self, location: str) -> Optional[Dict[str, float]]: |
| """ |
| Convert location name to coordinates using OpenWeatherMap Geocoding |
| |
| Args: |
| location: Location name (e.g., "Iowa", "California") |
| |
| Returns: |
| Dict with lat and lon, or None if geocoding fails |
| """ |
| try: |
| from src.config.credentials import CredentialsManager |
| creds = CredentialsManager() |
| api_key = creds.get_api_key("openweather") |
| |
| url = "http://api.openweathermap.org/geo/1.0/direct" |
| params = { |
| "q": location, |
| "limit": 1, |
| "appid": api_key |
| } |
| |
| response = self._make_request("GET", url, params=params) |
| data = response.json() |
| |
| if data and len(data) > 0: |
| return { |
| "lat": data[0]["lat"], |
| "lon": data[0]["lon"] |
| } |
| return None |
| |
| except Exception: |
| return None |
| |
| def _format_response(self, raw_data: Dict, lat: float, lon: float) -> Dict[str, Any]: |
| """ |
| Format SoilGrids API response |
| |
| Args: |
| raw_data: Raw API response |
| lat: Latitude |
| lon: Longitude |
| |
| Returns: |
| Formatted soil data |
| """ |
| properties = raw_data.get("properties", {}) |
| |
| formatted = { |
| "success": True, |
| "location": {"lat": lat, "lon": lon}, |
| "properties": {} |
| } |
| |
| |
| property_mapping = { |
| "phh2o": { |
| "label": "pH Level", |
| "unit": "pH", |
| "description": "Soil acidity/alkalinity (0-14 scale)" |
| }, |
| "soc": { |
| "label": "Organic Carbon", |
| "unit": "g/kg", |
| "description": "Soil organic carbon content" |
| }, |
| "nitrogen": { |
| "label": "Nitrogen Content", |
| "unit": "cg/kg", |
| "description": "Total nitrogen content" |
| }, |
| "clay": { |
| "label": "Clay Content", |
| "unit": "g/kg", |
| "description": "Percentage of clay particles" |
| }, |
| "sand": { |
| "label": "Sand Content", |
| "unit": "g/kg", |
| "description": "Percentage of sand particles" |
| }, |
| "silt": { |
| "label": "Silt Content", |
| "unit": "g/kg", |
| "description": "Percentage of silt particles" |
| } |
| } |
| |
| for prop_name, layers in properties.items(): |
| if layers and "layers" in layers: |
| |
| layer_data = layers["layers"][0] |
| value = layer_data.get("values", {}).get("mean") |
| |
| if value is not None: |
| prop_info = property_mapping.get(prop_name, { |
| "label": prop_name.title(), |
| "unit": "", |
| "description": "" |
| }) |
| |
| formatted["properties"][prop_name] = { |
| "value": round(value, 2), |
| "unit": prop_info["unit"], |
| "label": prop_info["label"], |
| "description": prop_info["description"] |
| } |
| |
| return formatted |
|
|
|
|
| |
| if __name__ == "__main__": |
| print("Testing Soil Client...") |
| print("-" * 70) |
| |
| try: |
| client = SoilClient() |
| |
| |
| print("\n📍 Testing with coordinates: Iowa (lat=41.8781, lon=-93.0977)") |
| soil = client.get_soil_data(lat=41.8781, lon=-93.0977) |
| |
| if soil.get("success"): |
| print("✅ Success!") |
| print(f"\n🌱 Soil Properties:") |
| for prop_name, prop_data in soil["properties"].items(): |
| print(f" • {prop_data['label']}: {prop_data['value']} {prop_data['unit']}") |
| else: |
| print(f"❌ Error: {soil.get('error')}") |
| |
| |
| print("\n" + "-" * 70) |
| print("📍 Testing with location name: California") |
| soil2 = client.get_soil_data(location="California") |
| |
| if soil2.get("success"): |
| print("✅ Success!") |
| print(f"\n🌱 Soil Properties:") |
| for prop_name, prop_data in soil2["properties"].items(): |
| print(f" • {prop_data['label']}: {prop_data['value']} {prop_data['unit']}") |
| else: |
| print(f"❌ Error: {soil2.get('error')}") |
| |
| except Exception as e: |
| print(f"❌ Error: {e}") |
| print("\n💡 Make sure you have:") |
| print(" 1. OPENWEATHER_API_KEY in .env (for geocoding)") |
| print(" 2. Internet connection") |
|
|
|
|