agroplan-ai-cli 1.0.15 → 1.0.17
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 +302 -15
- 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,7 +319,242 @@ 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]:
|
|
323
|
+
"""
|
|
324
|
+
Retorna status do ZARC sem carregar dados
|
|
325
|
+
|
|
326
|
+
MEMORY SAFE: Não carrega CSV, apenas verifica arquivos
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
Status do ZARC (configuração, cache, etc)
|
|
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
|
+
|
|
384
|
+
try:
|
|
385
|
+
with open(index_path, 'r', encoding='utf-8') as f:
|
|
386
|
+
index = json.load(f)
|
|
387
|
+
|
|
388
|
+
# Cachear em memória
|
|
389
|
+
_zarc_index_cache[safra] = index
|
|
390
|
+
|
|
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}"
|
|
444
|
+
|
|
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)
|
|
466
|
+
|
|
467
|
+
delimiter = ';' if ';' in primeira_linha else ','
|
|
468
|
+
|
|
469
|
+
reader = csv.DictReader(f, delimiter=delimiter)
|
|
470
|
+
|
|
471
|
+
for row in reader:
|
|
472
|
+
yield row
|
|
473
|
+
|
|
474
|
+
def ensure_zarc_file(safra: str = ZARC_SAFRA_DEFAULT) -> Optional[Dict[str, Any]]:
|
|
475
|
+
"""
|
|
476
|
+
Garante que arquivo ZARC existe, baixando se necessário
|
|
477
|
+
|
|
478
|
+
MEMORY SAFE: Não carrega registros, apenas gerencia arquivo
|
|
479
|
+
|
|
480
|
+
Returns:
|
|
481
|
+
Metadata do arquivo ou None se não disponível
|
|
482
|
+
"""
|
|
483
|
+
cache_path = get_cache_path(safra)
|
|
484
|
+
|
|
485
|
+
# Verificar cache válido
|
|
486
|
+
if is_cache_valid(cache_path):
|
|
487
|
+
return {
|
|
488
|
+
"file_path": cache_path,
|
|
489
|
+
"source": "zarc-cache",
|
|
490
|
+
"fallback": False,
|
|
491
|
+
"error": None
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
# Tentar download se source for official
|
|
495
|
+
if ZARC_SOURCE == "official":
|
|
496
|
+
url = ZARC_URLS.get(safra)
|
|
497
|
+
if url:
|
|
498
|
+
try:
|
|
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
|
+
|
|
515
|
+
return {
|
|
516
|
+
"file_path": cache_path,
|
|
517
|
+
"source": "zarc-oficial",
|
|
518
|
+
"fallback": False,
|
|
519
|
+
"error": None
|
|
520
|
+
}
|
|
521
|
+
except Exception as e:
|
|
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
|
+
}
|
|
530
|
+
|
|
531
|
+
# Usar cache antigo se existir (mesmo expirado)
|
|
532
|
+
if os.path.exists(cache_path):
|
|
533
|
+
return {
|
|
534
|
+
"file_path": cache_path,
|
|
535
|
+
"source": "zarc-cache",
|
|
536
|
+
"fallback": False,
|
|
537
|
+
"error": "Cache expirado mas usado"
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
# Nenhum arquivo disponível
|
|
541
|
+
return None
|
|
542
|
+
|
|
543
|
+
# OBSOLETO: Funções antigas que carregavam CSV inteiro em memória
|
|
544
|
+
# Mantidas apenas para referência, não devem ser usadas
|
|
545
|
+
# Use: ensure_zarc_file() + iter_zarc_records() + buscar_zarc()
|
|
546
|
+
|
|
547
|
+
# def download_zarc_dataset(safra: str) -> Optional[str]:
|
|
548
|
+
# """OBSOLETO - Não usar, causa problemas de memória"""
|
|
549
|
+
# pass
|
|
550
|
+
|
|
551
|
+
# def get_zarc_dataset(safra: str = ZARC_SAFRA_DEFAULT) -> Dict[str, Any]:
|
|
552
|
+
# """OBSOLETO - Não usar, carrega 1M+ registros em memória"""
|
|
553
|
+
# pass
|
|
554
|
+
|
|
555
|
+
# def load_zarc_from_file(file_path: str) -> List[Dict[str, Any]]:
|
|
556
|
+
# """OBSOLETO - Não usar, carrega CSV inteiro em lista"""
|
|
557
|
+
# pass
|
|
318
558
|
"""
|
|
319
559
|
Baixa dataset ZARC oficial
|
|
320
560
|
|
|
@@ -576,6 +816,8 @@ def buscar_zarc(
|
|
|
576
816
|
"""
|
|
577
817
|
Busca dados ZARC para cultura/região específica
|
|
578
818
|
|
|
819
|
+
PERFORMANCE: Tenta índice primeiro (rápido), depois streaming (lento)
|
|
820
|
+
|
|
579
821
|
Args:
|
|
580
822
|
cultura: Nome da cultura
|
|
581
823
|
uf: Unidade Federativa (opcional)
|
|
@@ -586,27 +828,51 @@ def buscar_zarc(
|
|
|
586
828
|
Returns:
|
|
587
829
|
Dicionário com dados ZARC ou None se não encontrar
|
|
588
830
|
"""
|
|
589
|
-
|
|
590
|
-
if
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
831
|
+
# FAST PATH: Tentar índice primeiro (O(1) lookup)
|
|
832
|
+
if ZARC_FAST_INDEX_ENABLED:
|
|
833
|
+
resultado_indexado = buscar_zarc_indexado(cultura, uf, municipio, solo, safra)
|
|
834
|
+
if resultado_indexado:
|
|
835
|
+
return resultado_indexado
|
|
836
|
+
|
|
837
|
+
# SLOW PATH: Full scan no CSV (apenas se permitido)
|
|
838
|
+
if not ZARC_ALLOW_FULL_SCAN:
|
|
839
|
+
# Não encontrou no índice e full scan não é permitido
|
|
840
|
+
# Tentar fallback
|
|
841
|
+
return buscar_zarc_fallback(cultura, uf, municipio, solo, safra)
|
|
842
|
+
|
|
843
|
+
# Full scan permitido (desenvolvimento local)
|
|
844
|
+
return buscar_zarc_streaming(cultura, uf, municipio, solo, safra)
|
|
845
|
+
|
|
846
|
+
def buscar_zarc_streaming(
|
|
847
|
+
cultura: str,
|
|
848
|
+
uf: Optional[str] = None,
|
|
849
|
+
municipio: Optional[str] = None,
|
|
850
|
+
solo: Optional[str] = None,
|
|
851
|
+
safra: str = ZARC_SAFRA_DEFAULT
|
|
852
|
+
) -> Optional[Dict[str, Any]]:
|
|
853
|
+
"""
|
|
854
|
+
Busca ZARC usando streaming no CSV (LENTO mas memory-safe)
|
|
596
855
|
|
|
856
|
+
PERFORMANCE: O(n) - varre todo o CSV
|
|
857
|
+
Usar apenas em desenvolvimento ou quando índice não disponível
|
|
858
|
+
"""
|
|
597
859
|
# Normalizar parâmetros de busca
|
|
598
860
|
cultura_norm = normalizar_cultura(cultura)
|
|
599
861
|
uf_norm = normalizar_uf(uf) if uf else None
|
|
600
862
|
municipio_norm = normalizar_municipio(municipio) if municipio else None
|
|
601
863
|
solo_norm = normalizar_solo(solo) if solo else None
|
|
602
864
|
|
|
603
|
-
#
|
|
604
|
-
|
|
605
|
-
|
|
865
|
+
# Tentar obter arquivo ZARC
|
|
866
|
+
file_info = ensure_zarc_file(safra)
|
|
867
|
+
|
|
868
|
+
if file_info:
|
|
869
|
+
# Usar arquivo oficial/cache com streaming
|
|
870
|
+
source = file_info["source"]
|
|
606
871
|
melhor_match = None
|
|
607
872
|
melhor_score = 0
|
|
608
873
|
|
|
609
|
-
|
|
874
|
+
# Processar CSV em streaming (linha por linha)
|
|
875
|
+
for registro in iter_zarc_records(file_info["file_path"]):
|
|
610
876
|
score = 0
|
|
611
877
|
|
|
612
878
|
# Cultura deve bater
|
|
@@ -628,9 +894,10 @@ def buscar_zarc(
|
|
|
628
894
|
if normalizar_solo(solo_registro) == solo_norm:
|
|
629
895
|
score += 2
|
|
630
896
|
|
|
897
|
+
# Manter apenas o melhor match (não acumula lista)
|
|
631
898
|
if score > melhor_score:
|
|
632
899
|
melhor_score = score
|
|
633
|
-
melhor_match = registro
|
|
900
|
+
melhor_match = registro.copy() # Copia apenas este registro
|
|
634
901
|
|
|
635
902
|
if melhor_match:
|
|
636
903
|
# Extrair janelas de plantio dos decêndios
|
|
@@ -684,11 +951,31 @@ def buscar_zarc(
|
|
|
684
951
|
"message": "Nenhuma recomendação ZARC encontrada para os parâmetros informados."
|
|
685
952
|
}
|
|
686
953
|
|
|
687
|
-
#
|
|
954
|
+
# Arquivo não disponível, usar fallback
|
|
955
|
+
return buscar_zarc_fallback(cultura, uf, municipio, solo, safra)
|
|
956
|
+
|
|
957
|
+
def buscar_zarc_fallback(
|
|
958
|
+
cultura: str,
|
|
959
|
+
uf: Optional[str] = None,
|
|
960
|
+
municipio: Optional[str] = None,
|
|
961
|
+
solo: Optional[str] = None,
|
|
962
|
+
safra: str = ZARC_SAFRA_DEFAULT
|
|
963
|
+
) -> Optional[Dict[str, Any]]:
|
|
964
|
+
"""
|
|
965
|
+
Busca ZARC em dados simplificados (fallback)
|
|
966
|
+
"""
|
|
967
|
+
# Normalizar parâmetros
|
|
968
|
+
cultura_norm = normalizar_cultura(cultura)
|
|
969
|
+
uf_norm = normalizar_uf(uf) if uf else None
|
|
970
|
+
municipio_norm = normalizar_municipio(municipio) if municipio else None
|
|
971
|
+
solo_norm = normalizar_solo(solo) if solo else None
|
|
972
|
+
|
|
973
|
+
# Fallback: usar dados simplificados (lista pequena em memória)
|
|
974
|
+
fallback_data = get_zarc_fallback()
|
|
688
975
|
melhor_match = None
|
|
689
976
|
melhor_score = 0
|
|
690
977
|
|
|
691
|
-
for registro in
|
|
978
|
+
for registro in fallback_data:
|
|
692
979
|
score = 0
|
|
693
980
|
|
|
694
981
|
# Cultura deve bater
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Script para construir índice ZARC compacto
|
|
3
|
+
|
|
4
|
+
Processa o CSV oficial ZARC e gera um índice JSON pequeno
|
|
5
|
+
contendo apenas as regiões e culturas de interesse do AgroPlan.
|
|
6
|
+
|
|
7
|
+
Uso:
|
|
8
|
+
python scripts/build_zarc_index.py
|
|
9
|
+
"""
|
|
10
|
+
import sys
|
|
11
|
+
import os
|
|
12
|
+
import json
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
|
|
15
|
+
# Adicionar diretório pai ao path para importar providers
|
|
16
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
|
17
|
+
|
|
18
|
+
from providers.zarc_provider import (
|
|
19
|
+
ensure_zarc_file,
|
|
20
|
+
iter_zarc_records,
|
|
21
|
+
normalizar_cultura,
|
|
22
|
+
normalizar_municipio,
|
|
23
|
+
normalizar_uf,
|
|
24
|
+
normalizar_solo,
|
|
25
|
+
mapear_codigo_solo,
|
|
26
|
+
extrair_janelas_plantio,
|
|
27
|
+
escolher_melhor_janela,
|
|
28
|
+
ZARC_CACHE_DIR
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Regiões de interesse
|
|
32
|
+
REGIOES_INTERESSE = [
|
|
33
|
+
{"uf": "SP", "municipio": "Clementina"},
|
|
34
|
+
{"uf": "SP", "municipio": "São Paulo"},
|
|
35
|
+
{"uf": "SP", "municipio": "Ribeirão Preto"},
|
|
36
|
+
{"uf": "MS", "municipio": "Campo Grande"},
|
|
37
|
+
{"uf": "PR", "municipio": "Londrina"},
|
|
38
|
+
{"uf": "DF", "municipio": "Brasília"},
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
# Culturas de interesse
|
|
42
|
+
CULTURAS_INTERESSE = [
|
|
43
|
+
"soja",
|
|
44
|
+
"milho",
|
|
45
|
+
"feijao",
|
|
46
|
+
"trigo",
|
|
47
|
+
"algodao",
|
|
48
|
+
"cafe",
|
|
49
|
+
"cana",
|
|
50
|
+
"arroz",
|
|
51
|
+
"sorgo",
|
|
52
|
+
"mandioca"
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
# Solos de interesse
|
|
56
|
+
SOLOS_INTERESSE = ["arenoso", "medio", "argiloso", "misto"]
|
|
57
|
+
|
|
58
|
+
def build_zarc_index(safra: str = "2025/2026"):
|
|
59
|
+
"""
|
|
60
|
+
Constrói índice ZARC compacto
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
safra: Safra para processar
|
|
64
|
+
"""
|
|
65
|
+
print(f"🌾 Construindo índice ZARC para safra {safra}...")
|
|
66
|
+
print()
|
|
67
|
+
|
|
68
|
+
# Garantir que arquivo ZARC existe
|
|
69
|
+
file_info = ensure_zarc_file(safra)
|
|
70
|
+
|
|
71
|
+
if not file_info:
|
|
72
|
+
print("❌ Erro: Arquivo ZARC não disponível")
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
print(f"✅ Arquivo ZARC: {file_info['file_path']}")
|
|
76
|
+
print(f" Fonte: {file_info['source']}")
|
|
77
|
+
print()
|
|
78
|
+
|
|
79
|
+
# Normalizar regiões de interesse
|
|
80
|
+
regioes_norm = []
|
|
81
|
+
for regiao in REGIOES_INTERESSE:
|
|
82
|
+
regioes_norm.append({
|
|
83
|
+
"uf": normalizar_uf(regiao["uf"]),
|
|
84
|
+
"municipio": normalizar_municipio(regiao["municipio"]),
|
|
85
|
+
"municipio_original": regiao["municipio"]
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
# Normalizar culturas de interesse
|
|
89
|
+
culturas_norm = [normalizar_cultura(c) for c in CULTURAS_INTERESSE]
|
|
90
|
+
|
|
91
|
+
print("📍 Regiões de interesse:")
|
|
92
|
+
for r in regioes_norm:
|
|
93
|
+
print(f" - {r['municipio_original']}/{r['uf']}")
|
|
94
|
+
print()
|
|
95
|
+
|
|
96
|
+
print("🌱 Culturas de interesse:")
|
|
97
|
+
for c in CULTURAS_INTERESSE:
|
|
98
|
+
print(f" - {c}")
|
|
99
|
+
print()
|
|
100
|
+
|
|
101
|
+
# Processar CSV em streaming
|
|
102
|
+
print("🔄 Processando CSV oficial...")
|
|
103
|
+
|
|
104
|
+
index_records = {}
|
|
105
|
+
registros_processados = 0
|
|
106
|
+
registros_incluidos = 0
|
|
107
|
+
|
|
108
|
+
for registro in iter_zarc_records(file_info['file_path']):
|
|
109
|
+
registros_processados += 1
|
|
110
|
+
|
|
111
|
+
# Mostrar progresso a cada 100k registros
|
|
112
|
+
if registros_processados % 100000 == 0:
|
|
113
|
+
print(f" Processados: {registros_processados:,} registros...")
|
|
114
|
+
|
|
115
|
+
# Verificar se é cultura de interesse
|
|
116
|
+
cultura_csv = normalizar_cultura(registro.get("Nome_cultura", ""))
|
|
117
|
+
if cultura_csv not in culturas_norm:
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
# Verificar se é região de interesse
|
|
121
|
+
uf_csv = normalizar_uf(registro.get("UF", ""))
|
|
122
|
+
municipio_csv = normalizar_municipio(registro.get("municipio", ""))
|
|
123
|
+
|
|
124
|
+
regiao_match = None
|
|
125
|
+
for regiao in regioes_norm:
|
|
126
|
+
if uf_csv == regiao["uf"] and municipio_csv == regiao["municipio"]:
|
|
127
|
+
regiao_match = regiao
|
|
128
|
+
break
|
|
129
|
+
|
|
130
|
+
if not regiao_match:
|
|
131
|
+
continue
|
|
132
|
+
|
|
133
|
+
# Solo
|
|
134
|
+
solo_codigo = registro.get("Cod_Solo", "")
|
|
135
|
+
solo_nome = mapear_codigo_solo(solo_codigo)
|
|
136
|
+
solo_norm = normalizar_solo(solo_nome)
|
|
137
|
+
|
|
138
|
+
if solo_norm not in SOLOS_INTERESSE and solo_norm != "desconhecido":
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
# Extrair janelas de plantio
|
|
142
|
+
janelas = extrair_janelas_plantio(registro)
|
|
143
|
+
melhor_janela = escolher_melhor_janela(janelas)
|
|
144
|
+
|
|
145
|
+
if not melhor_janela:
|
|
146
|
+
# Sem janelas válidas, pular
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
# Criar chave: UF|municipio|cultura|solo
|
|
150
|
+
chave = f"{uf_csv}|{municipio_csv}|{cultura_csv}|{solo_norm}"
|
|
151
|
+
|
|
152
|
+
# Se já existe, manter o de menor risco
|
|
153
|
+
if chave in index_records:
|
|
154
|
+
risco_atual = index_records[chave]["risco"]
|
|
155
|
+
risco_novo = melhor_janela["risco_predominante"]
|
|
156
|
+
|
|
157
|
+
ordem_risco = {"baixo": 1, "medio": 2, "alto": 3}
|
|
158
|
+
|
|
159
|
+
if ordem_risco.get(risco_novo, 999) < ordem_risco.get(risco_atual, 999):
|
|
160
|
+
# Novo tem risco menor, substituir
|
|
161
|
+
pass
|
|
162
|
+
else:
|
|
163
|
+
# Manter o atual
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
# Adicionar ao índice
|
|
167
|
+
index_records[chave] = {
|
|
168
|
+
"source": "zarc-oficial-derived",
|
|
169
|
+
"fallback": False,
|
|
170
|
+
"cultura": registro.get("Nome_cultura"),
|
|
171
|
+
"uf": uf_csv.upper(),
|
|
172
|
+
"municipio": regiao_match["municipio_original"],
|
|
173
|
+
"solo": solo_nome,
|
|
174
|
+
"safra": safra,
|
|
175
|
+
"janela_plantio": {
|
|
176
|
+
"inicio": melhor_janela["inicio"],
|
|
177
|
+
"fim": melhor_janela["fim"]
|
|
178
|
+
},
|
|
179
|
+
"risco": melhor_janela["risco_predominante"],
|
|
180
|
+
"decendios_recomendados": melhor_janela["decendios"],
|
|
181
|
+
"geocodigo": registro.get("geocodigo", ""),
|
|
182
|
+
"encontrado": True,
|
|
183
|
+
"observacao": "Dados derivados da Tábua de Risco oficial do ZARC."
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
registros_incluidos += 1
|
|
187
|
+
|
|
188
|
+
print(f"✅ Processamento concluído!")
|
|
189
|
+
print(f" Total processado: {registros_processados:,} registros")
|
|
190
|
+
print(f" Incluídos no índice: {registros_incluidos} registros")
|
|
191
|
+
print()
|
|
192
|
+
|
|
193
|
+
# Criar estrutura do índice
|
|
194
|
+
index = {
|
|
195
|
+
"metadata": {
|
|
196
|
+
"source": "zarc-oficial-derived",
|
|
197
|
+
"safra": safra,
|
|
198
|
+
"generated_at": datetime.now().isoformat(),
|
|
199
|
+
"generated_from": file_info["source"],
|
|
200
|
+
"regions": [f"{r['municipio_original']}/{r['uf']}" for r in regioes_norm],
|
|
201
|
+
"cultures": CULTURAS_INTERESSE,
|
|
202
|
+
"soils": SOLOS_INTERESSE,
|
|
203
|
+
"total_records": registros_incluidos
|
|
204
|
+
},
|
|
205
|
+
"records": index_records
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
# Salvar índice
|
|
209
|
+
safra_filename = safra.replace("/", "-")
|
|
210
|
+
index_path = os.path.join(ZARC_CACHE_DIR, f"zarc_index_{safra_filename}.json")
|
|
211
|
+
|
|
212
|
+
with open(index_path, 'w', encoding='utf-8') as f:
|
|
213
|
+
json.dump(index, f, ensure_ascii=False, indent=2)
|
|
214
|
+
|
|
215
|
+
# Calcular tamanho
|
|
216
|
+
size_bytes = os.path.getsize(index_path)
|
|
217
|
+
size_kb = size_bytes / 1024
|
|
218
|
+
|
|
219
|
+
print(f"💾 Índice salvo em: {index_path}")
|
|
220
|
+
print(f" Tamanho: {size_kb:.2f} KB")
|
|
221
|
+
print()
|
|
222
|
+
|
|
223
|
+
# Estatísticas por região
|
|
224
|
+
print("📊 Estatísticas por região:")
|
|
225
|
+
for regiao in regioes_norm:
|
|
226
|
+
count = sum(1 for k in index_records.keys()
|
|
227
|
+
if k.startswith(f"{regiao['uf']}|{regiao['municipio']}|"))
|
|
228
|
+
print(f" {regiao['municipio_original']}/{regiao['uf']}: {count} registros")
|
|
229
|
+
print()
|
|
230
|
+
|
|
231
|
+
# Estatísticas por cultura
|
|
232
|
+
print("📊 Estatísticas por cultura:")
|
|
233
|
+
for cultura in culturas_norm:
|
|
234
|
+
count = sum(1 for k in index_records.keys()
|
|
235
|
+
if f"|{cultura}|" in k)
|
|
236
|
+
print(f" {cultura}: {count} registros")
|
|
237
|
+
print()
|
|
238
|
+
|
|
239
|
+
print("✅ Índice ZARC construído com sucesso!")
|
|
240
|
+
return True
|
|
241
|
+
|
|
242
|
+
if __name__ == "__main__":
|
|
243
|
+
import argparse
|
|
244
|
+
|
|
245
|
+
parser = argparse.ArgumentParser(description="Construir índice ZARC compacto")
|
|
246
|
+
parser.add_argument(
|
|
247
|
+
"--safra",
|
|
248
|
+
default="2025/2026",
|
|
249
|
+
help="Safra para processar (padrão: 2025/2026)"
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
args = parser.parse_args()
|
|
253
|
+
|
|
254
|
+
success = build_zarc_index(args.safra)
|
|
255
|
+
|
|
256
|
+
sys.exit(0 if success else 1)
|