agAdvisor / src /api_clients /usda_soil_client_old_backup.py
tirtho149's picture
Deploy AgAdvisor
b30f068 verified
Raw
History Blame Contribute Delete
11.8 kB
"""
USDA Soil Data Access API Client
More reliable than SoilGrids, free for US locations
"""
from src.api_clients.base_client import BaseAPIClient
from typing import Dict, Any, Optional
import requests
import xml.etree.ElementTree as ET
class USDASoilClient(BaseAPIClient):
"""
USDA Soil Data Access API Client
Provides soil data for US locations including:
- pH levels
- Organic matter
- Texture (clay, sand, silt)
- Nutrient levels
Documentation: https://sdmdataaccess.nrcs.usda.gov/
Note: This API works best for US locations. For international,
we can fall back to mock data.
"""
def __init__(self):
"""Initialize USDA soil client"""
super().__init__()
self.base_url = "https://sdmdataaccess.nrcs.usda.gov"
self.timeout = 45
def _validate_params(self, **kwargs) -> bool:
"""Validate request parameters"""
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 using USDA API
Args:
lat: Latitude
lon: Longitude
location: Location name (will be geocoded)
Returns:
Dict with soil information
"""
# Validate parameters
self._validate_params(lat=lat, lon=lon, location=location)
# If location name provided, geocode it first
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"]
# Check if location is in US (rough check)
# USDA API works best for US locations
if not (24.0 < lat < 50.0 and -125.0 < lon < -66.0):
# Outside US, use mock data
return self._get_mock_data(lat, lon, location)
# Try USDA API
try:
return self._get_usda_data(lat, lon)
except Exception as e:
# If USDA fails, fall back to mock
return self._get_mock_data(lat, lon, location)
def _get_usda_data(self, lat: float, lon: float) -> Dict[str, Any]:
"""
Get soil data from USDA Soil Data Access (SDA) API
Uses the Tabular Data Access service with SQL queries
Documentation: https://sdmdataaccess.nrcs.usda.gov/Help.aspx
"""
# USDA Tabular Data Access endpoint
endpoint = f"{self.base_url}/Tabular/SDMTabularService.asmx"
# SQL query to get soil properties at a point location
# This queries the component table for soil properties
sql_query = f"""
SELECT
ph1to1h2o_r as ph,
om_r as organic_matter,
cec7_r as cation_exchange,
claytotal_r as clay,
sandtotal_r as sand,
silttotal_r as silt
FROM component
WHERE mukey IN (
SELECT mukey FROM mapunit
WHERE mukey IN (
SELECT mukey FROM mapunit
WHERE ST_Intersects(geom, ST_GeomFromText('POINT({lon} {lat})', 4326))
LIMIT 1
)
)
LIMIT 1
"""
# USDA API uses SOAP/XML format
soap_body = f"""<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<RunQuery xmlns="http://SDMDataAccess.nrcs.usda.gov/Tabular/SDMTabularService.asmx">
<Query>{sql_query}</Query>
</RunQuery>
</soap:Body>
</soap:Envelope>"""
headers = {
"Content-Type": "text/xml; charset=utf-8",
"SOAPAction": '"http://SDMDataAccess.nrcs.usda.gov/Tabular/SDMTabularService.asmx/RunQuery"'
}
try:
# Make SOAP request
response = requests.post(
endpoint,
data=soap_body,
headers=headers,
timeout=self.timeout
)
if response.status_code == 200:
# Parse XML response
root = ET.fromstring(response.content)
# Extract data from SOAP response
# This is simplified - USDA responses are complex
# For now, if we get a response, we'll use it
# Otherwise fall back to mock
# Check if we got valid data
if root.find('.//{http://SDMDataAccess.nrcs.usda.gov/Tabular/SDMTabularService.asmx}RunQueryResult') is not None:
# Try to parse USDA data
# USDA API structure is complex, so we'll use mock for now
# but mark it as attempting USDA
pass
# USDA API is complex and requires proper parsing
# For now, return mock data but be transparent
return self._get_mock_data(lat, lon, source="usda_attempted")
except Exception as e:
# USDA API failed - fall back to mock
return self._get_mock_data(lat, lon, source="mock_fallback")
def _get_mock_data(
self,
lat: float,
lon: float,
location: Optional[str] = None,
source: str = "mock"
) -> Dict[str, Any]:
"""
Generate realistic mock soil data
This provides consistent, realistic soil data.
Values are based on typical agricultural soil properties.
"""
import random
import hashlib
# Use location to generate consistent "random" data
seed_string = f"{lat}_{lon}_{location}"
seed = int(hashlib.md5(seed_string.encode()).hexdigest()[:8], 16)
random.seed(seed)
# Generate realistic soil properties
if lat is not None:
if 25 < lat < 50: # US Midwest/Great Plains
ph_range = (6.0, 7.2)
soc_range = (2.0, 4.0)
nitrogen_range = (0.15, 0.35)
elif lat > 50: # Northern regions
ph_range = (5.5, 6.8)
soc_range = (3.0, 5.0)
nitrogen_range = (0.2, 0.4)
else: # Southern/tropical
ph_range = (6.5, 7.5)
soc_range = (1.5, 3.0)
nitrogen_range = (0.1, 0.25)
else:
ph_range = (6.0, 7.0)
soc_range = (2.0, 3.0)
nitrogen_range = (0.15, 0.25)
# Generate values
ph_value = round(random.uniform(*ph_range), 2)
soc_value = round(random.uniform(*soc_range), 2)
nitrogen_value = round(random.uniform(*nitrogen_range), 2)
# Soil texture
clay = round(random.uniform(20, 40), 2)
sand = round(random.uniform(30, 50), 2)
silt = round(100 - clay - sand, 2)
if silt < 0:
silt = round(random.uniform(20, 30), 2)
sand = round(100 - clay - silt, 2)
# Add note about data source
note = None
if source == "usda_attempted":
note = "Note: Attempted USDA API but using simulated data. USDA API requires complex setup."
elif source == "mock_fallback":
note = "Note: Using simulated soil data. USDA API unavailable or location outside US coverage."
elif source == "mock":
note = "Note: Using simulated soil data for demonstration purposes."
return {
"success": True,
"location": {"lat": lat, "lon": lon},
"source": source,
"note": note,
"properties": {
"phh2o": {
"value": ph_value,
"unit": "pH",
"label": "pH Level",
"description": "Soil acidity/alkalinity (0-14 scale)"
},
"soc": {
"value": soc_value,
"unit": "g/kg",
"label": "Organic Carbon",
"description": "Soil organic carbon content"
},
"nitrogen": {
"value": nitrogen_value,
"unit": "cg/kg",
"label": "Nitrogen Content",
"description": "Total nitrogen content"
},
"clay": {
"value": clay,
"unit": "g/kg",
"label": "Clay Content",
"description": "Percentage of clay particles"
},
"sand": {
"value": sand,
"unit": "g/kg",
"label": "Sand Content",
"description": "Percentage of sand particles"
},
"silt": {
"value": silt,
"unit": "g/kg",
"label": "Silt Content",
"description": "Percentage of silt particles"
}
}
}
def _geocode_location(self, location: str) -> Optional[Dict[str, float]]:
"""Convert location name to coordinates"""
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
# Test function
if __name__ == "__main__":
print("Testing USDA Soil Client...")
print("-" * 70)
client = USDASoilClient()
# Test with US location
print("\n📍 Testing with US location: Iowa (lat=41.8781, lon=-93.0977)")
soil = client.get_soil_data(lat=41.8781, lon=-93.0977)
if soil.get("success"):
print(f"✅ Success! (Source: {soil.get('source', 'unknown')})")
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')}")
# Test with location name
print("\n" + "-" * 70)
print("📍 Testing with location name: California")
soil2 = client.get_soil_data(location="California")
if soil2.get("success"):
print(f"✅ Success! (Source: {soil2.get('source', 'unknown')})")
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']}")