agroplan-ai-cli 1.0.35 → 1.0.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
- "cli_version": "1.0.35",
3
- "backend_template_version": "1.0.35",
2
+ "cli_version": "1.0.37",
3
+ "backend_template_version": "1.0.37",
4
4
  "zarc_index_version": "2025-2026-fast-index-v2",
5
5
  "price_index_version": "2025-05-reference-v1",
6
6
  "features": [
@@ -24,7 +24,9 @@
24
24
  "expanded_crop_calendar_10_cultures",
25
25
  "calendar_date_safety",
26
26
  "calendar_weather_integration",
27
- "calendar_weather_dependency_fix"
27
+ "calendar_weather_dependency_fix",
28
+ "nasa_power_climatology",
29
+ "nasa_power_parser_fix"
28
30
  ],
29
- "generated_at": "2026-05-10T22:00:00Z"
31
+ "generated_at": "2026-05-10T23:30:00Z"
30
32
  }
@@ -372,6 +372,82 @@ def get_clima(
372
372
  except Exception as e:
373
373
  raise HTTPException(status_code=500, detail=str(e))
374
374
 
375
+ @app.get("/dados/clima/nasa-power")
376
+ def get_clima_nasa_power(
377
+ lat: Optional[float] = Query(None, description="Latitude"),
378
+ lon: Optional[float] = Query(None, description="Longitude"),
379
+ month: int = Query(None, description="Mês (1-12)")
380
+ ):
381
+ """Obtém climatologia NASA POWER para um mês específico"""
382
+ try:
383
+ from providers.nasa_power_provider import buscar_climatologia_nasa_power, get_nasa_power_status
384
+
385
+ # Se lat ou lon não foram fornecidos, retornar informações
386
+ if lat is None or lon is None or month is None:
387
+ return {
388
+ "message": "Informe latitude, longitude e mês para consultar climatologia NASA POWER.",
389
+ "exemplo": "/dados/clima/nasa-power?lat=-21.56&lon=-50.45&month=5",
390
+ "parametros": {
391
+ "lat": "Latitude da localização",
392
+ "lon": "Longitude da localização",
393
+ "month": "Mês (1-12)"
394
+ },
395
+ "provider_info": get_nasa_power_status()
396
+ }
397
+
398
+ if month < 1 or month > 12:
399
+ raise HTTPException(status_code=400, detail="Month deve estar entre 1 e 12")
400
+
401
+ # Buscar climatologia
402
+ climatology = buscar_climatologia_nasa_power(lat, lon, month)
403
+
404
+ if climatology:
405
+ return climatology
406
+ else:
407
+ return {
408
+ "message": "Não foi possível obter dados NASA POWER para esta localização.",
409
+ "lat": lat,
410
+ "lon": lon,
411
+ "month": month,
412
+ "note": "NASA POWER pode estar temporariamente indisponível ou a localização pode estar fora da cobertura."
413
+ }
414
+
415
+ except Exception as e:
416
+ raise HTTPException(status_code=500, detail=str(e))
417
+
418
+ @app.get("/dados/clima/nasa-power/debug")
419
+ def get_clima_nasa_power_debug(
420
+ lat: Optional[float] = Query(None, description="Latitude"),
421
+ lon: Optional[float] = Query(None, description="Longitude"),
422
+ month: int = Query(None, description="Mês (1-12)")
423
+ ):
424
+ """Debug endpoint - mostra resposta bruta da NASA POWER para diagnóstico"""
425
+ try:
426
+ from providers.nasa_power_provider import buscar_climatologia_nasa_power_debug
427
+
428
+ # Se lat ou lon não foram fornecidos, retornar informações
429
+ if lat is None or lon is None or month is None:
430
+ return {
431
+ "message": "Informe latitude, longitude e mês para debug NASA POWER.",
432
+ "exemplo": "/dados/clima/nasa-power/debug?lat=-21.56&lon=-50.45&month=5",
433
+ "parametros": {
434
+ "lat": "Latitude da localização",
435
+ "lon": "Longitude da localização",
436
+ "month": "Mês (1-12)"
437
+ }
438
+ }
439
+
440
+ if month < 1 or month > 12:
441
+ raise HTTPException(status_code=400, detail="Month deve estar entre 1 e 12")
442
+
443
+ # Buscar debug info
444
+ debug_info = buscar_climatologia_nasa_power_debug(lat, lon, month)
445
+
446
+ return debug_info
447
+
448
+ except Exception as e:
449
+ raise HTTPException(status_code=500, detail=str(e))
450
+
375
451
  @app.get("/dados/zarc")
376
452
  def get_zarc(
377
453
  cultura: Optional[str] = Query(None, description="Nome da cultura"),
@@ -121,8 +121,12 @@ def enriquecer_calendario_com_clima(
121
121
 
122
122
  # Gerar resumo
123
123
  forecast_type = weather_data.get("forecast_type", "climatology")
124
+ source = weather_data.get("source", "unknown")
125
+
124
126
  if forecast_type == "forecast":
125
127
  summary = f"Previsão: {weather_data.get('precipitation_sum', 0):.1f}mm de chuva, {weather_data.get('temperature_min', 0):.0f}°C a {weather_data.get('temperature_max', 0):.0f}°C"
128
+ elif source == "nasa-power":
129
+ summary = f"Climatologia NASA POWER: temperatura média {weather_data.get('temperature_avg', 0):.0f}°C, {weather_data.get('precipitation_expected', 'condições típicas')}"
126
130
  else:
127
131
  summary = f"Climatologia: {weather_data.get('precipitation_expected', 'Condições típicas')}"
128
132
 
@@ -132,7 +136,7 @@ def enriquecer_calendario_com_clima(
132
136
  "source": weather_data.get("source", "unknown"),
133
137
  "forecast_type": forecast_type,
134
138
  "summary": summary,
135
- "precipitation_mm": weather_data.get("precipitation_sum") or weather_data.get("precipitation_mm_avg"),
139
+ "precipitation_mm": weather_data.get("precipitation_sum") or weather_data.get("precipitation_monthly_total") or weather_data.get("precipitation_daily_avg"),
136
140
  "precipitation_probability": weather_data.get("precipitation_probability"),
137
141
  "temperature_min": weather_data.get("temperature_min"),
138
142
  "temperature_max": weather_data.get("temperature_max"),
@@ -99,8 +99,7 @@ def buscar_climatologia_longo_prazo(
99
99
  """
100
100
  Busca climatologia/histórico para períodos longos (17+ dias).
101
101
 
102
- Inicialmente usa fallback local mensal.
103
- Futuramente pode integrar NASA POWER.
102
+ Tenta usar NASA POWER primeiro, fallback local se falhar.
104
103
 
105
104
  Args:
106
105
  lat: Latitude
@@ -112,28 +111,53 @@ def buscar_climatologia_longo_prazo(
112
111
  Lista de dicionários com climatologia por data/mês
113
112
  """
114
113
 
115
- # Por enquanto, usar fallback local baseado em médias mensais
116
- # Futuramente: integrar NASA POWER Climatology API
114
+ from .nasa_power_provider import buscar_climatologia_nasa_power
117
115
 
118
116
  result = []
119
117
  current = start_date
120
118
 
119
+ # Cache de dados NASA POWER por mês
120
+ nasa_cache = {}
121
+
121
122
  while current <= end_date:
122
123
  month = current.month
123
124
 
124
- # Climatologia simplificada por mês (Brasil)
125
- climatology = _get_monthly_climatology(month, lat, lon)
125
+ # Tentar NASA POWER primeiro
126
+ if month not in nasa_cache:
127
+ nasa_data = buscar_climatologia_nasa_power(lat, lon, month)
128
+ nasa_cache[month] = nasa_data
129
+
130
+ nasa_data = nasa_cache[month]
126
131
 
127
- result.append({
128
- "date": current.isoformat(),
129
- "source": "climate-fallback",
130
- "forecast_type": "climatology",
131
- "temperature_max": climatology["temp_max"],
132
- "temperature_min": climatology["temp_min"],
133
- "precipitation_expected": climatology["precip_desc"],
134
- "precipitation_mm_avg": climatology["precip_mm"],
135
- "confidence": "media"
136
- })
132
+ if nasa_data:
133
+ # Usar dados NASA POWER
134
+ result.append({
135
+ "date": current.isoformat(),
136
+ "source": "nasa-power",
137
+ "forecast_type": "climatology",
138
+ "temperature_max": nasa_data["temperature_max"],
139
+ "temperature_min": nasa_data["temperature_min"],
140
+ "temperature_avg": nasa_data["temperature_avg"],
141
+ "precipitation_expected": nasa_data["precipitation_expected"],
142
+ "precipitation_daily_avg": nasa_data["precipitation_daily_avg"],
143
+ "precipitation_monthly_total": nasa_data["precipitation_monthly_total"],
144
+ "confidence": "media",
145
+ "note": "Climatologia NASA POWER, não previsão exata"
146
+ })
147
+ else:
148
+ # Fallback local se NASA POWER falhar
149
+ climatology = _get_monthly_climatology(month, lat, lon)
150
+ result.append({
151
+ "date": current.isoformat(),
152
+ "source": "climate-fallback",
153
+ "forecast_type": "climatology",
154
+ "temperature_max": climatology["temp_max"],
155
+ "temperature_min": climatology["temp_min"],
156
+ "precipitation_expected": climatology["precip_desc"],
157
+ "precipitation_daily_avg": climatology["precip_mm"],
158
+ "confidence": "baixa",
159
+ "note": "Climatologia simplificada, NASA POWER indisponível"
160
+ })
137
161
 
138
162
  current += timedelta(days=1)
139
163
 
@@ -228,7 +252,7 @@ def gerar_recomendacao_climatica(
228
252
  """
229
253
 
230
254
  forecast_type = weather_data.get("forecast_type", "climatology")
231
- precip = weather_data.get("precipitation_sum") or weather_data.get("precipitation_mm_avg", 0)
255
+ precip = weather_data.get("precipitation_sum") or weather_data.get("precipitation_monthly_total") or weather_data.get("precipitation_daily_avg", 0)
232
256
  precip_prob = weather_data.get("precipitation_probability", 0)
233
257
  temp_max = weather_data.get("temperature_max", 25)
234
258
  temp_min = weather_data.get("temperature_min", 15)
@@ -0,0 +1,300 @@
1
+ """
2
+ Provider NASA POWER para Climatologia
3
+
4
+ Usa NASA POWER Climatology API para obter dados históricos/climatológicos
5
+ para períodos além da janela de previsão meteorológica (17+ dias).
6
+
7
+ NASA POWER não é previsão exata, é climatologia/histórico.
8
+ """
9
+
10
+ from typing import Dict, Optional
11
+ import requests
12
+ from .cache import get_cache, set_cache
13
+
14
+
15
+ # Mapeamento de mês numérico para chave NASA POWER
16
+ MONTH_KEYS = {
17
+ 1: "JAN",
18
+ 2: "FEB",
19
+ 3: "MAR",
20
+ 4: "APR",
21
+ 5: "MAY",
22
+ 6: "JUN",
23
+ 7: "JUL",
24
+ 8: "AUG",
25
+ 9: "SEP",
26
+ 10: "OCT",
27
+ 11: "NOV",
28
+ 12: "DEC",
29
+ }
30
+
31
+
32
+ def get_month_value(parameter_data: Dict, month: int) -> Optional[float]:
33
+ """
34
+ Extrai valor mensal de parâmetro NASA POWER.
35
+
36
+ Tenta múltiplos formatos de chave:
37
+ 1. Chave alfabética (MAY, JUN, etc.) - formato padrão NASA POWER
38
+ 2. Chave numérica string ("5", "6", etc.)
39
+ 3. Chave numérica com zero ("05", "06", etc.)
40
+
41
+ Args:
42
+ parameter_data: Dicionário com dados do parâmetro
43
+ month: Mês numérico (1-12)
44
+
45
+ Returns:
46
+ Valor do parâmetro ou None se não encontrado
47
+ """
48
+ if not parameter_data:
49
+ return None
50
+
51
+ # Tentativa 1: Chave alfabética (formato padrão NASA POWER)
52
+ month_key = MONTH_KEYS.get(month)
53
+ if month_key and month_key in parameter_data:
54
+ return parameter_data.get(month_key)
55
+
56
+ # Tentativa 2: Chave numérica string
57
+ if str(month) in parameter_data:
58
+ return parameter_data.get(str(month))
59
+
60
+ # Tentativa 3: Chave numérica com zero
61
+ if f"{month:02d}" in parameter_data:
62
+ return parameter_data.get(f"{month:02d}")
63
+
64
+ return None
65
+
66
+
67
+ def buscar_climatologia_nasa_power(
68
+ lat: float,
69
+ lon: float,
70
+ month: int
71
+ ) -> Optional[Dict]:
72
+ """
73
+ Busca climatologia NASA POWER para um mês específico.
74
+
75
+ Args:
76
+ lat: Latitude
77
+ lon: Longitude
78
+ month: Mês (1-12)
79
+
80
+ Returns:
81
+ Dicionário com dados climatológicos ou None se falhar
82
+ """
83
+
84
+ # Cache key
85
+ cache_key = f"nasa_power:{lat}:{lon}:{month}"
86
+ cached = get_cache(cache_key)
87
+ if cached:
88
+ return cached
89
+
90
+ try:
91
+ # NASA POWER Climatology API
92
+ # Documentação: https://power.larc.nasa.gov/docs/services/api/
93
+
94
+ # Parâmetros climatológicos
95
+ parameters = [
96
+ "T2M", # Temperatura média a 2m
97
+ "T2M_MAX", # Temperatura máxima a 2m
98
+ "T2M_MIN", # Temperatura mínima a 2m
99
+ "PRECTOTCORR", # Precipitação diária média corrigida
100
+ "PRECTOTCORR_SUM", # Precipitação total mensal corrigida
101
+ ]
102
+
103
+ # Endpoint NASA POWER Climatology
104
+ url = "https://power.larc.nasa.gov/api/temporal/climatology/point"
105
+
106
+ params = {
107
+ "parameters": ",".join(parameters),
108
+ "community": "AG", # Agricultural community
109
+ "longitude": lon,
110
+ "latitude": lat,
111
+ "format": "JSON"
112
+ }
113
+
114
+ response = requests.get(url, params=params, timeout=30)
115
+ response.raise_for_status()
116
+ data = response.json()
117
+
118
+ # Extrair dados do mês específico
119
+ properties = data.get("properties", {}).get("parameter", {})
120
+
121
+ # Usar helper para extrair valores com fallback robusto
122
+ month_key = MONTH_KEYS.get(month)
123
+
124
+ temp_avg = get_month_value(properties.get("T2M"), month)
125
+ temp_max = get_month_value(properties.get("T2M_MAX"), month)
126
+ temp_min = get_month_value(properties.get("T2M_MIN"), month)
127
+
128
+ # Precipitação: priorizar PRECTOTCORR_SUM (total mensal), fallback para PRECTOTCORR (média diária)
129
+ precip_sum = get_month_value(properties.get("PRECTOTCORR_SUM"), month)
130
+ precip_daily = get_month_value(properties.get("PRECTOTCORR"), month)
131
+
132
+ # Validar dados essenciais
133
+ if temp_avg is None:
134
+ return None
135
+
136
+ # Calcular precipitação mensal
137
+ if precip_sum is not None:
138
+ precip_monthly = precip_sum
139
+ precip_daily_avg = precip_sum / 30 # Estimativa
140
+ elif precip_daily is not None:
141
+ precip_daily_avg = precip_daily
142
+ precip_monthly = precip_daily * 30 # Estimativa
143
+ else:
144
+ return None
145
+
146
+ # Classificar precipitação
147
+ if precip_monthly < 50:
148
+ precip_desc = "Período seco"
149
+ elif precip_monthly < 100:
150
+ precip_desc = "Chuvas ocasionais"
151
+ elif precip_monthly < 150:
152
+ precip_desc = "Chuvas moderadas"
153
+ elif precip_monthly < 200:
154
+ precip_desc = "Chuvas frequentes"
155
+ else:
156
+ precip_desc = "Estação chuvosa"
157
+
158
+ result = {
159
+ "source": "nasa-power",
160
+ "forecast_type": "climatology",
161
+ "month": month,
162
+ "month_key": month_key,
163
+ "temperature_avg": round(temp_avg, 1),
164
+ "temperature_max": round(temp_max, 1) if temp_max else None,
165
+ "temperature_min": round(temp_min, 1) if temp_min else None,
166
+ "precipitation_expected": precip_desc,
167
+ "precipitation_daily_avg": round(precip_daily_avg, 1),
168
+ "precipitation_monthly_total": round(precip_monthly, 1),
169
+ "confidence": "media",
170
+ "note": "Dados climatológicos/históricos NASA POWER, não previsão exata."
171
+ }
172
+
173
+ # Cache por 7 dias (climatologia muda pouco)
174
+ set_cache(cache_key, result, ttl_seconds=604800)
175
+
176
+ return result
177
+
178
+ except requests.exceptions.Timeout:
179
+ print(f"[NASA POWER] Timeout ao buscar dados para lat={lat}, lon={lon}, month={month}")
180
+ return None
181
+ except requests.exceptions.RequestException as e:
182
+ print(f"[NASA POWER] Erro de requisição: {e}")
183
+ return None
184
+ except KeyError as e:
185
+ print(f"[NASA POWER] Erro ao parsear resposta - chave ausente: {e}")
186
+ return None
187
+ except Exception as e:
188
+ print(f"[NASA POWER] Erro inesperado: {e}")
189
+ return None
190
+
191
+
192
+ def get_nasa_power_status() -> Dict:
193
+ """
194
+ Retorna status do provider NASA POWER.
195
+
196
+ Returns:
197
+ Dicionário com informações de status
198
+ """
199
+ return {
200
+ "provider": "NASA POWER",
201
+ "type": "climatology",
202
+ "source": "https://power.larc.nasa.gov/",
203
+ "parameters": ["T2M", "T2M_MAX", "T2M_MIN", "PRECTOTCORR", "PRECTOTCORR_SUM"],
204
+ "community": "AG (Agricultural)",
205
+ "cache_ttl": "7 days",
206
+ "note": "Climatologia/histórico, não previsão exata"
207
+ }
208
+
209
+
210
+ def buscar_climatologia_nasa_power_debug(
211
+ lat: float,
212
+ lon: float,
213
+ month: int
214
+ ) -> Dict:
215
+ """
216
+ Versão debug que retorna resposta bruta da NASA POWER para diagnóstico.
217
+
218
+ Args:
219
+ lat: Latitude
220
+ lon: Longitude
221
+ month: Mês (1-12)
222
+
223
+ Returns:
224
+ Dicionário com resposta bruta e diagnóstico
225
+ """
226
+ try:
227
+ # Parâmetros climatológicos
228
+ parameters = [
229
+ "T2M",
230
+ "T2M_MAX",
231
+ "T2M_MIN",
232
+ "PRECTOTCORR",
233
+ "PRECTOTCORR_SUM",
234
+ ]
235
+
236
+ # Endpoint NASA POWER Climatology
237
+ url = "https://power.larc.nasa.gov/api/temporal/climatology/point"
238
+
239
+ params = {
240
+ "parameters": ",".join(parameters),
241
+ "community": "AG",
242
+ "longitude": lon,
243
+ "latitude": lat,
244
+ "format": "JSON"
245
+ }
246
+
247
+ response = requests.get(url, params=params, timeout=30)
248
+ response.raise_for_status()
249
+ data = response.json()
250
+
251
+ # Extrair informações para diagnóstico
252
+ properties = data.get("properties", {}).get("parameter", {})
253
+
254
+ # Coletar chaves de cada parâmetro
255
+ parameter_keys = {}
256
+ for param in parameters:
257
+ param_data = properties.get(param, {})
258
+ if isinstance(param_data, dict):
259
+ parameter_keys[param] = list(param_data.keys())
260
+ else:
261
+ parameter_keys[param] = f"Not a dict: {type(param_data)}"
262
+
263
+ # Tentar extrair valores para o mês solicitado
264
+ month_key = MONTH_KEYS.get(month)
265
+ extracted_values = {}
266
+ for param in parameters:
267
+ extracted_values[param] = get_month_value(properties.get(param), month)
268
+
269
+ return {
270
+ "status": "success",
271
+ "url": url,
272
+ "params": params,
273
+ "status_code": response.status_code,
274
+ "month_requested": month,
275
+ "month_key": month_key,
276
+ "parameter_keys": parameter_keys,
277
+ "extracted_values": extracted_values,
278
+ "raw_response_keys": list(data.keys()),
279
+ "properties_keys": list(data.get("properties", {}).keys()),
280
+ "note": "Debug endpoint - mostra estrutura bruta da resposta NASA POWER"
281
+ }
282
+
283
+ except requests.exceptions.Timeout:
284
+ return {
285
+ "status": "error",
286
+ "error_type": "timeout",
287
+ "message": "Timeout ao buscar NASA POWER"
288
+ }
289
+ except requests.exceptions.RequestException as e:
290
+ return {
291
+ "status": "error",
292
+ "error_type": "request_error",
293
+ "message": str(e)
294
+ }
295
+ except Exception as e:
296
+ return {
297
+ "status": "error",
298
+ "error_type": "unexpected_error",
299
+ "message": str(e)
300
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agroplan-ai-cli",
3
- "version": "1.0.35",
3
+ "version": "1.0.37",
4
4
  "description": "CLI global para AgroPlan AI - modo local acelerado",
5
5
  "type": "module",
6
6
  "bin": {