agroplan-ai-cli 1.0.15 → 1.0.18
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.
- package/backend-template/.env.example +19 -0
- package/backend-template/api.py +128 -65
- package/backend-template/core/zarc_adapter.py +290 -0
- package/backend-template/data/zarc/zarc_index_2025-2026.json +1612 -0
- package/backend-template/providers/zarc_provider.py +294 -130
- package/backend-template/scripts/build_zarc_index.py +256 -0
- package/package.json +1 -1
|
@@ -16,6 +16,11 @@ ZARC_CACHE_DIR = os.path.join(os.path.dirname(__file__), '..', 'data', 'zarc')
|
|
|
16
16
|
ZARC_CACHE_TTL = int(os.getenv("ZARC_CACHE_TTL", "86400")) # 24 horas
|
|
17
17
|
ZARC_SOURCE = os.getenv("ZARC_SOURCE", "official") # official, fallback
|
|
18
18
|
ZARC_SAFRA_DEFAULT = os.getenv("ZARC_SAFRA", "2025/2026")
|
|
19
|
+
ZARC_FAST_INDEX_ENABLED = os.getenv("ZARC_FAST_INDEX_ENABLED", "true").lower() == "true"
|
|
20
|
+
ZARC_ALLOW_FULL_SCAN = os.getenv("ZARC_ALLOW_FULL_SCAN", "false").lower() == "true"
|
|
21
|
+
|
|
22
|
+
# Cache do índice em memória (pequeno, pode ficar em RAM)
|
|
23
|
+
_zarc_index_cache = {}
|
|
19
24
|
|
|
20
25
|
# URLs oficiais do Portal de Dados Abertos do Ministério da Agricultura
|
|
21
26
|
ZARC_URLS = {
|
|
@@ -314,163 +319,275 @@ def is_cache_valid(cache_path: str) -> bool:
|
|
|
314
319
|
file_age = datetime.now() - datetime.fromtimestamp(os.path.getmtime(cache_path))
|
|
315
320
|
return file_age.total_seconds() < ZARC_CACHE_TTL
|
|
316
321
|
|
|
317
|
-
def
|
|
322
|
+
def get_zarc_status(safra: str = ZARC_SAFRA_DEFAULT) -> Dict[str, Any]:
|
|
318
323
|
"""
|
|
319
|
-
|
|
324
|
+
Retorna status do ZARC sem carregar dados
|
|
325
|
+
|
|
326
|
+
MEMORY SAFE: Não carrega CSV, apenas verifica arquivos
|
|
320
327
|
|
|
321
328
|
Returns:
|
|
322
|
-
|
|
329
|
+
Status do ZARC (configuração, cache, etc)
|
|
323
330
|
"""
|
|
331
|
+
cache_path = get_cache_path(safra)
|
|
332
|
+
|
|
333
|
+
# Verificar índice
|
|
334
|
+
safra_filename = safra.replace("/", "-")
|
|
335
|
+
index_path = os.path.join(ZARC_CACHE_DIR, f"zarc_index_{safra_filename}.json")
|
|
336
|
+
|
|
337
|
+
status = {
|
|
338
|
+
"status": "configured",
|
|
339
|
+
"safra": safra,
|
|
340
|
+
"source": ZARC_SOURCE,
|
|
341
|
+
"fast_index": ZARC_FAST_INDEX_ENABLED,
|
|
342
|
+
"full_scan": ZARC_ALLOW_FULL_SCAN,
|
|
343
|
+
"index_exists": os.path.exists(index_path),
|
|
344
|
+
"cache_exists": os.path.exists(cache_path),
|
|
345
|
+
"cache_valid": False,
|
|
346
|
+
"cache_size_mb": 0
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if os.path.exists(cache_path):
|
|
350
|
+
try:
|
|
351
|
+
# Tamanho do arquivo em MB
|
|
352
|
+
size_bytes = os.path.getsize(cache_path)
|
|
353
|
+
status["cache_size_mb"] = round(size_bytes / (1024 * 1024), 2)
|
|
354
|
+
|
|
355
|
+
# Verificar se cache é válido
|
|
356
|
+
status["cache_valid"] = is_cache_valid(cache_path)
|
|
357
|
+
except Exception:
|
|
358
|
+
pass
|
|
359
|
+
|
|
360
|
+
return status
|
|
361
|
+
|
|
362
|
+
def load_zarc_index(safra: str = ZARC_SAFRA_DEFAULT) -> Optional[Dict[str, Any]]:
|
|
363
|
+
"""
|
|
364
|
+
Carrega índice ZARC compacto em memória
|
|
365
|
+
|
|
366
|
+
MEMORY SAFE: Índice é pequeno (~35KB), pode ficar em RAM
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
Índice ZARC ou None se não existir
|
|
370
|
+
"""
|
|
371
|
+
global _zarc_index_cache
|
|
372
|
+
|
|
373
|
+
# Verificar cache em memória
|
|
374
|
+
if safra in _zarc_index_cache:
|
|
375
|
+
return _zarc_index_cache[safra]
|
|
376
|
+
|
|
377
|
+
# Carregar do arquivo
|
|
378
|
+
safra_filename = safra.replace("/", "-")
|
|
379
|
+
index_path = os.path.join(ZARC_CACHE_DIR, f"zarc_index_{safra_filename}.json")
|
|
380
|
+
|
|
381
|
+
if not os.path.exists(index_path):
|
|
382
|
+
return None
|
|
383
|
+
|
|
324
384
|
try:
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
print(f"URL não disponível para safra {safra}")
|
|
328
|
-
return None
|
|
385
|
+
with open(index_path, 'r', encoding='utf-8') as f:
|
|
386
|
+
index = json.load(f)
|
|
329
387
|
|
|
330
|
-
|
|
388
|
+
# Cachear em memória
|
|
389
|
+
_zarc_index_cache[safra] = index
|
|
331
390
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
391
|
+
return index
|
|
392
|
+
except Exception as e:
|
|
393
|
+
print(f"Erro ao carregar índice ZARC: {e}")
|
|
394
|
+
return None
|
|
395
|
+
|
|
396
|
+
def buscar_zarc_indexado(
|
|
397
|
+
cultura: str,
|
|
398
|
+
uf: Optional[str] = None,
|
|
399
|
+
municipio: Optional[str] = None,
|
|
400
|
+
solo: Optional[str] = None,
|
|
401
|
+
safra: str = ZARC_SAFRA_DEFAULT
|
|
402
|
+
) -> Optional[Dict[str, Any]]:
|
|
403
|
+
"""
|
|
404
|
+
Busca ZARC no índice compacto (rápido)
|
|
405
|
+
|
|
406
|
+
PERFORMANCE: Lookup O(1) em vez de O(n) no CSV
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
Dados ZARC ou None se não encontrar no índice
|
|
410
|
+
"""
|
|
411
|
+
if not ZARC_FAST_INDEX_ENABLED:
|
|
412
|
+
return None
|
|
413
|
+
|
|
414
|
+
index = load_zarc_index(safra)
|
|
415
|
+
if not index:
|
|
416
|
+
return None
|
|
417
|
+
|
|
418
|
+
# Normalizar parâmetros
|
|
419
|
+
cultura_norm = normalizar_cultura(cultura)
|
|
420
|
+
|
|
421
|
+
if not uf or not municipio:
|
|
422
|
+
return None
|
|
423
|
+
|
|
424
|
+
uf_norm = normalizar_uf(uf)
|
|
425
|
+
municipio_norm = normalizar_municipio(municipio)
|
|
426
|
+
|
|
427
|
+
# Tentar diferentes combinações de solo
|
|
428
|
+
solos_tentar = []
|
|
429
|
+
if solo:
|
|
430
|
+
solo_norm = normalizar_solo(solo)
|
|
431
|
+
# Tentar o solo especificado primeiro, depois outros como fallback
|
|
432
|
+
solos_tentar = [solo_norm, "medio", "arenoso", "argiloso", "misto"]
|
|
433
|
+
# Remover duplicatas mantendo ordem
|
|
434
|
+
seen = set()
|
|
435
|
+
solos_tentar = [s for s in solos_tentar if not (s in seen or seen.add(s))]
|
|
436
|
+
else:
|
|
437
|
+
# Se não especificou solo, tentar todos (preferir medio/argiloso)
|
|
438
|
+
solos_tentar = ["medio", "argiloso", "arenoso", "misto"]
|
|
439
|
+
|
|
440
|
+
# Buscar no índice
|
|
441
|
+
for solo_test in solos_tentar:
|
|
442
|
+
# Chave: UF|municipio|cultura|solo
|
|
443
|
+
chave = f"{uf_norm}|{municipio_norm}|{cultura_norm}|{solo_test}"
|
|
341
444
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
445
|
+
if chave in index["records"]:
|
|
446
|
+
return index["records"][chave]
|
|
447
|
+
|
|
448
|
+
return None
|
|
449
|
+
|
|
450
|
+
def iter_zarc_records(file_path: str):
|
|
451
|
+
"""
|
|
452
|
+
Itera sobre registros ZARC em streaming
|
|
453
|
+
|
|
454
|
+
MEMORY SAFE: Usa yield para processar linha por linha
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
file_path: Caminho do arquivo CSV
|
|
458
|
+
|
|
459
|
+
Yields:
|
|
460
|
+
Dicionário com dados de cada linha
|
|
461
|
+
"""
|
|
462
|
+
with open(file_path, 'r', encoding='utf-8-sig', newline='') as f:
|
|
463
|
+
# Detectar delimitador
|
|
464
|
+
primeira_linha = f.readline()
|
|
465
|
+
f.seek(0)
|
|
345
466
|
|
|
346
|
-
|
|
347
|
-
with open(cache_path, 'w', encoding='utf-8') as f:
|
|
348
|
-
f.write(content)
|
|
467
|
+
delimiter = ';' if ';' in primeira_linha else ','
|
|
349
468
|
|
|
350
|
-
|
|
351
|
-
return cache_path
|
|
469
|
+
reader = csv.DictReader(f, delimiter=delimiter)
|
|
352
470
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
return None
|
|
471
|
+
for row in reader:
|
|
472
|
+
yield row
|
|
356
473
|
|
|
357
|
-
def
|
|
474
|
+
def ensure_zarc_file(safra: str = ZARC_SAFRA_DEFAULT) -> Optional[Dict[str, Any]]:
|
|
358
475
|
"""
|
|
359
|
-
|
|
476
|
+
Garante que arquivo ZARC existe, baixando se necessário
|
|
477
|
+
|
|
478
|
+
MEMORY SAFE: Não carrega registros, apenas gerencia arquivo
|
|
360
479
|
|
|
361
480
|
Returns:
|
|
362
|
-
|
|
363
|
-
- records: Lista de registros ZARC
|
|
364
|
-
- source: "zarc-oficial" | "zarc-cache" | "zarc-fallback"
|
|
365
|
-
- fallback: bool
|
|
366
|
-
- cache_path: str ou None
|
|
367
|
-
- error: str ou None
|
|
481
|
+
Metadata do arquivo ou None se não disponível
|
|
368
482
|
"""
|
|
369
483
|
cache_path = get_cache_path(safra)
|
|
370
484
|
|
|
371
485
|
# Verificar cache válido
|
|
372
486
|
if is_cache_valid(cache_path):
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
"cache_path": cache_path,
|
|
380
|
-
"error": None
|
|
381
|
-
}
|
|
382
|
-
except Exception as e:
|
|
383
|
-
print(f"Erro ao carregar cache ZARC: {e}")
|
|
487
|
+
return {
|
|
488
|
+
"file_path": cache_path,
|
|
489
|
+
"source": "zarc-cache",
|
|
490
|
+
"fallback": False,
|
|
491
|
+
"error": None
|
|
492
|
+
}
|
|
384
493
|
|
|
385
494
|
# Tentar download se source for official
|
|
386
495
|
if ZARC_SOURCE == "official":
|
|
387
|
-
|
|
388
|
-
if
|
|
496
|
+
url = ZARC_URLS.get(safra)
|
|
497
|
+
if url:
|
|
389
498
|
try:
|
|
390
|
-
|
|
499
|
+
# Criar request com User-Agent
|
|
500
|
+
req = urllib.request.Request(
|
|
501
|
+
url,
|
|
502
|
+
headers={
|
|
503
|
+
'User-Agent': 'AgroPlan-AI/1.0 (https://github.com/Kuuhaku-Allan/agroplan-ai)'
|
|
504
|
+
}
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
# Download
|
|
508
|
+
with urllib.request.urlopen(req, timeout=30) as response:
|
|
509
|
+
content = response.read().decode('utf-8')
|
|
510
|
+
|
|
511
|
+
# Salvar
|
|
512
|
+
with open(cache_path, 'w', encoding='utf-8') as f:
|
|
513
|
+
f.write(content)
|
|
514
|
+
|
|
391
515
|
return {
|
|
392
|
-
"
|
|
516
|
+
"file_path": cache_path,
|
|
393
517
|
"source": "zarc-oficial",
|
|
394
518
|
"fallback": False,
|
|
395
|
-
"cache_path": downloaded_path,
|
|
396
519
|
"error": None
|
|
397
520
|
}
|
|
398
521
|
except Exception as e:
|
|
399
|
-
|
|
522
|
+
# Se download falhar, tentar usar cache antigo
|
|
523
|
+
if os.path.exists(cache_path):
|
|
524
|
+
return {
|
|
525
|
+
"file_path": cache_path,
|
|
526
|
+
"source": "zarc-cache",
|
|
527
|
+
"fallback": False,
|
|
528
|
+
"error": f"Download falhou, usando cache antigo: {str(e)}"
|
|
529
|
+
}
|
|
400
530
|
|
|
401
531
|
# Usar cache antigo se existir (mesmo expirado)
|
|
402
532
|
if os.path.exists(cache_path):
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
"cache_path": cache_path,
|
|
410
|
-
"error": "Cache expirado mas usado"
|
|
411
|
-
}
|
|
412
|
-
except Exception as e:
|
|
413
|
-
print(f"Erro ao carregar cache antigo: {e}")
|
|
533
|
+
return {
|
|
534
|
+
"file_path": cache_path,
|
|
535
|
+
"source": "zarc-cache",
|
|
536
|
+
"fallback": False,
|
|
537
|
+
"error": "Cache expirado mas usado"
|
|
538
|
+
}
|
|
414
539
|
|
|
415
|
-
#
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
"cache_path": None,
|
|
422
|
-
"error": "CSV oficial não disponível, usando dados simplificados"
|
|
423
|
-
}
|
|
540
|
+
# Nenhum arquivo disponível
|
|
541
|
+
return None
|
|
542
|
+
|
|
543
|
+
# REMOVIDO: Funções obsoletas que carregavam CSV inteiro em memória
|
|
544
|
+
# Essas funções foram removidas para evitar problemas de memória
|
|
545
|
+
# Use: ensure_zarc_file() + iter_zarc_records() + buscar_zarc_indexado()
|
|
424
546
|
|
|
425
|
-
def
|
|
426
|
-
"""
|
|
427
|
-
|
|
547
|
+
def download_zarc_dataset(*args, **kwargs):
|
|
548
|
+
"""
|
|
549
|
+
REMOVIDO: Esta função carregava o CSV inteiro em memória (214 MB).
|
|
428
550
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
if reader.fieldnames:
|
|
440
|
-
print(f"Colunas ZARC encontradas ({len(reader.fieldnames)} colunas, delimiter='{delimiter}')")
|
|
441
|
-
|
|
442
|
-
for row in reader:
|
|
443
|
-
registros.append(row)
|
|
551
|
+
Use: ensure_zarc_file() para gerenciar o arquivo sem carregar dados
|
|
552
|
+
"""
|
|
553
|
+
raise RuntimeError(
|
|
554
|
+
"download_zarc_dataset() foi removido por causar problemas de memória. "
|
|
555
|
+
"Use ensure_zarc_file() para gerenciar o arquivo ZARC."
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
def get_zarc_dataset(*args, **kwargs):
|
|
559
|
+
"""
|
|
560
|
+
REMOVIDO: Esta função carregava 1M+ registros em memória.
|
|
444
561
|
|
|
445
|
-
|
|
562
|
+
Use: buscar_zarc_indexado() para lookup rápido O(1)
|
|
563
|
+
Use: iter_zarc_records() para processar em streaming
|
|
564
|
+
"""
|
|
565
|
+
raise RuntimeError(
|
|
566
|
+
"get_zarc_dataset() foi removido por carregar 1M+ registros em memória. "
|
|
567
|
+
"Use buscar_zarc_indexado() para lookup rápido ou iter_zarc_records() para streaming."
|
|
568
|
+
)
|
|
446
569
|
|
|
447
|
-
def
|
|
570
|
+
def load_zarc_from_file(*args, **kwargs):
|
|
448
571
|
"""
|
|
449
|
-
|
|
572
|
+
REMOVIDO: Esta função carregava o CSV inteiro em uma lista.
|
|
450
573
|
|
|
451
|
-
|
|
452
|
-
Lista de nomes de colunas ou None se falhar
|
|
574
|
+
Use: iter_zarc_records() para processar linha por linha
|
|
453
575
|
"""
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
return None
|
|
470
|
-
|
|
471
|
-
except Exception as e:
|
|
472
|
-
print(f"Erro ao inspecionar colunas ZARC: {e}")
|
|
473
|
-
return None
|
|
576
|
+
raise RuntimeError(
|
|
577
|
+
"load_zarc_from_file() foi removido por carregar CSV inteiro em lista. "
|
|
578
|
+
"Use iter_zarc_records() para processar linha por linha."
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
def inspect_zarc_columns(*args, **kwargs):
|
|
582
|
+
"""
|
|
583
|
+
REMOVIDO: Esta função dependia de get_zarc_dataset().
|
|
584
|
+
|
|
585
|
+
Use: iter_zarc_records() e inspecione a primeira linha
|
|
586
|
+
"""
|
|
587
|
+
raise RuntimeError(
|
|
588
|
+
"inspect_zarc_columns() foi removido por depender de get_zarc_dataset(). "
|
|
589
|
+
"Use iter_zarc_records() e inspecione a primeira linha."
|
|
590
|
+
)
|
|
474
591
|
|
|
475
592
|
def get_zarc_fallback() -> List[Dict[str, Any]]:
|
|
476
593
|
"""
|
|
@@ -576,6 +693,8 @@ def buscar_zarc(
|
|
|
576
693
|
"""
|
|
577
694
|
Busca dados ZARC para cultura/região específica
|
|
578
695
|
|
|
696
|
+
PERFORMANCE: Tenta índice primeiro (rápido), depois streaming (lento)
|
|
697
|
+
|
|
579
698
|
Args:
|
|
580
699
|
cultura: Nome da cultura
|
|
581
700
|
uf: Unidade Federativa (opcional)
|
|
@@ -586,27 +705,51 @@ def buscar_zarc(
|
|
|
586
705
|
Returns:
|
|
587
706
|
Dicionário com dados ZARC ou None se não encontrar
|
|
588
707
|
"""
|
|
589
|
-
|
|
590
|
-
if
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
708
|
+
# FAST PATH: Tentar índice primeiro (O(1) lookup)
|
|
709
|
+
if ZARC_FAST_INDEX_ENABLED:
|
|
710
|
+
resultado_indexado = buscar_zarc_indexado(cultura, uf, municipio, solo, safra)
|
|
711
|
+
if resultado_indexado:
|
|
712
|
+
return resultado_indexado
|
|
713
|
+
|
|
714
|
+
# SLOW PATH: Full scan no CSV (apenas se permitido)
|
|
715
|
+
if not ZARC_ALLOW_FULL_SCAN:
|
|
716
|
+
# Não encontrou no índice e full scan não é permitido
|
|
717
|
+
# Tentar fallback
|
|
718
|
+
return buscar_zarc_fallback(cultura, uf, municipio, solo, safra)
|
|
719
|
+
|
|
720
|
+
# Full scan permitido (desenvolvimento local)
|
|
721
|
+
return buscar_zarc_streaming(cultura, uf, municipio, solo, safra)
|
|
722
|
+
|
|
723
|
+
def buscar_zarc_streaming(
|
|
724
|
+
cultura: str,
|
|
725
|
+
uf: Optional[str] = None,
|
|
726
|
+
municipio: Optional[str] = None,
|
|
727
|
+
solo: Optional[str] = None,
|
|
728
|
+
safra: str = ZARC_SAFRA_DEFAULT
|
|
729
|
+
) -> Optional[Dict[str, Any]]:
|
|
730
|
+
"""
|
|
731
|
+
Busca ZARC usando streaming no CSV (LENTO mas memory-safe)
|
|
596
732
|
|
|
733
|
+
PERFORMANCE: O(n) - varre todo o CSV
|
|
734
|
+
Usar apenas em desenvolvimento ou quando índice não disponível
|
|
735
|
+
"""
|
|
597
736
|
# Normalizar parâmetros de busca
|
|
598
737
|
cultura_norm = normalizar_cultura(cultura)
|
|
599
738
|
uf_norm = normalizar_uf(uf) if uf else None
|
|
600
739
|
municipio_norm = normalizar_municipio(municipio) if municipio else None
|
|
601
740
|
solo_norm = normalizar_solo(solo) if solo else None
|
|
602
741
|
|
|
603
|
-
#
|
|
604
|
-
|
|
605
|
-
|
|
742
|
+
# Tentar obter arquivo ZARC
|
|
743
|
+
file_info = ensure_zarc_file(safra)
|
|
744
|
+
|
|
745
|
+
if file_info:
|
|
746
|
+
# Usar arquivo oficial/cache com streaming
|
|
747
|
+
source = file_info["source"]
|
|
606
748
|
melhor_match = None
|
|
607
749
|
melhor_score = 0
|
|
608
750
|
|
|
609
|
-
|
|
751
|
+
# Processar CSV em streaming (linha por linha)
|
|
752
|
+
for registro in iter_zarc_records(file_info["file_path"]):
|
|
610
753
|
score = 0
|
|
611
754
|
|
|
612
755
|
# Cultura deve bater
|
|
@@ -628,9 +771,10 @@ def buscar_zarc(
|
|
|
628
771
|
if normalizar_solo(solo_registro) == solo_norm:
|
|
629
772
|
score += 2
|
|
630
773
|
|
|
774
|
+
# Manter apenas o melhor match (não acumula lista)
|
|
631
775
|
if score > melhor_score:
|
|
632
776
|
melhor_score = score
|
|
633
|
-
melhor_match = registro
|
|
777
|
+
melhor_match = registro.copy() # Copia apenas este registro
|
|
634
778
|
|
|
635
779
|
if melhor_match:
|
|
636
780
|
# Extrair janelas de plantio dos decêndios
|
|
@@ -684,11 +828,31 @@ def buscar_zarc(
|
|
|
684
828
|
"message": "Nenhuma recomendação ZARC encontrada para os parâmetros informados."
|
|
685
829
|
}
|
|
686
830
|
|
|
687
|
-
#
|
|
831
|
+
# Arquivo não disponível, usar fallback
|
|
832
|
+
return buscar_zarc_fallback(cultura, uf, municipio, solo, safra)
|
|
833
|
+
|
|
834
|
+
def buscar_zarc_fallback(
|
|
835
|
+
cultura: str,
|
|
836
|
+
uf: Optional[str] = None,
|
|
837
|
+
municipio: Optional[str] = None,
|
|
838
|
+
solo: Optional[str] = None,
|
|
839
|
+
safra: str = ZARC_SAFRA_DEFAULT
|
|
840
|
+
) -> Optional[Dict[str, Any]]:
|
|
841
|
+
"""
|
|
842
|
+
Busca ZARC em dados simplificados (fallback)
|
|
843
|
+
"""
|
|
844
|
+
# Normalizar parâmetros
|
|
845
|
+
cultura_norm = normalizar_cultura(cultura)
|
|
846
|
+
uf_norm = normalizar_uf(uf) if uf else None
|
|
847
|
+
municipio_norm = normalizar_municipio(municipio) if municipio else None
|
|
848
|
+
solo_norm = normalizar_solo(solo) if solo else None
|
|
849
|
+
|
|
850
|
+
# Fallback: usar dados simplificados (lista pequena em memória)
|
|
851
|
+
fallback_data = get_zarc_fallback()
|
|
688
852
|
melhor_match = None
|
|
689
853
|
melhor_score = 0
|
|
690
854
|
|
|
691
|
-
for registro in
|
|
855
|
+
for registro in fallback_data:
|
|
692
856
|
score = 0
|
|
693
857
|
|
|
694
858
|
# Cultura deve bater
|