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
|
@@ -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)
|