agroplan-ai-cli 1.0.30 → 1.0.32
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/VERSION.json +7 -4
- package/backend-template/api.py +183 -0
- package/backend-template/core/crop_calendar_engine.py +517 -1
- package/backend-template/core/field_storage.py +175 -0
- package/backend-template/core/planning_models.py +118 -0
- package/backend-template/data/user_fields/fields.json +0 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"cli_version": "1.0.
|
|
3
|
-
"backend_template_version": "1.0.
|
|
2
|
+
"cli_version": "1.0.32",
|
|
3
|
+
"backend_template_version": "1.0.32",
|
|
4
4
|
"zarc_index_version": "2025-2026-fast-index-v2",
|
|
5
5
|
"price_index_version": "2025-05-reference-v1",
|
|
6
6
|
"features": [
|
|
@@ -18,7 +18,10 @@
|
|
|
18
18
|
"market_profit_confidence_refinement",
|
|
19
19
|
"market_profit_comparative_evaluation",
|
|
20
20
|
"market_profit_experimental_optimizer",
|
|
21
|
-
"smart_crop_calendar_engine"
|
|
21
|
+
"smart_crop_calendar_engine",
|
|
22
|
+
"manual_field_registration",
|
|
23
|
+
"crop_calendar_from_manual_field",
|
|
24
|
+
"expanded_crop_calendar_10_cultures"
|
|
22
25
|
],
|
|
23
|
-
"generated_at": "2026-05-
|
|
26
|
+
"generated_at": "2026-05-10T18:30:00Z"
|
|
24
27
|
}
|
package/backend-template/api.py
CHANGED
|
@@ -1417,6 +1417,189 @@ def obter_cultura_info(cultura: str):
|
|
|
1417
1417
|
except Exception as e:
|
|
1418
1418
|
raise HTTPException(status_code=500, detail=str(e))
|
|
1419
1419
|
|
|
1420
|
+
# Endpoints de Talhões Manuais
|
|
1421
|
+
|
|
1422
|
+
@app.get("/planejamento/talhoes")
|
|
1423
|
+
def listar_talhoes():
|
|
1424
|
+
"""Lista todos os talhões cadastrados pelo usuário"""
|
|
1425
|
+
try:
|
|
1426
|
+
from core.field_storage import listar_talhoes_usuario
|
|
1427
|
+
|
|
1428
|
+
fields = listar_talhoes_usuario()
|
|
1429
|
+
|
|
1430
|
+
return {
|
|
1431
|
+
"total": len(fields),
|
|
1432
|
+
"talhoes": fields
|
|
1433
|
+
}
|
|
1434
|
+
except Exception as e:
|
|
1435
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
1436
|
+
|
|
1437
|
+
@app.post("/planejamento/talhoes")
|
|
1438
|
+
def criar_talhao(field_data: dict):
|
|
1439
|
+
"""Cria um novo talhão"""
|
|
1440
|
+
try:
|
|
1441
|
+
from core.field_storage import criar_talhao_usuario
|
|
1442
|
+
from core.planning_models import ManualFieldCreate
|
|
1443
|
+
|
|
1444
|
+
# Validar dados
|
|
1445
|
+
validated = ManualFieldCreate(**field_data)
|
|
1446
|
+
|
|
1447
|
+
# Criar talhão
|
|
1448
|
+
new_field = criar_talhao_usuario(validated.model_dump())
|
|
1449
|
+
|
|
1450
|
+
return new_field
|
|
1451
|
+
except ValueError as e:
|
|
1452
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
1453
|
+
except Exception as e:
|
|
1454
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
1455
|
+
|
|
1456
|
+
@app.get("/planejamento/talhoes/{field_id}")
|
|
1457
|
+
def obter_talhao(field_id: str):
|
|
1458
|
+
"""Obtém um talhão pelo ID"""
|
|
1459
|
+
try:
|
|
1460
|
+
from core.field_storage import obter_talhao_usuario
|
|
1461
|
+
|
|
1462
|
+
field = obter_talhao_usuario(field_id)
|
|
1463
|
+
|
|
1464
|
+
if not field:
|
|
1465
|
+
raise HTTPException(
|
|
1466
|
+
status_code=404,
|
|
1467
|
+
detail=f"Talhão '{field_id}' não encontrado"
|
|
1468
|
+
)
|
|
1469
|
+
|
|
1470
|
+
return field
|
|
1471
|
+
except HTTPException:
|
|
1472
|
+
raise
|
|
1473
|
+
except Exception as e:
|
|
1474
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
1475
|
+
|
|
1476
|
+
@app.put("/planejamento/talhoes/{field_id}")
|
|
1477
|
+
def atualizar_talhao(field_id: str, field_data: dict):
|
|
1478
|
+
"""Atualiza um talhão existente"""
|
|
1479
|
+
try:
|
|
1480
|
+
from core.field_storage import atualizar_talhao_usuario
|
|
1481
|
+
from core.planning_models import ManualFieldUpdate
|
|
1482
|
+
|
|
1483
|
+
# Validar dados
|
|
1484
|
+
validated = ManualFieldUpdate(**field_data)
|
|
1485
|
+
|
|
1486
|
+
# Atualizar apenas campos fornecidos
|
|
1487
|
+
update_data = {k: v for k, v in validated.model_dump().items() if v is not None}
|
|
1488
|
+
|
|
1489
|
+
# Atualizar talhão
|
|
1490
|
+
updated_field = atualizar_talhao_usuario(field_id, update_data)
|
|
1491
|
+
|
|
1492
|
+
if not updated_field:
|
|
1493
|
+
raise HTTPException(
|
|
1494
|
+
status_code=404,
|
|
1495
|
+
detail=f"Talhão '{field_id}' não encontrado"
|
|
1496
|
+
)
|
|
1497
|
+
|
|
1498
|
+
return updated_field
|
|
1499
|
+
except HTTPException:
|
|
1500
|
+
raise
|
|
1501
|
+
except ValueError as e:
|
|
1502
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
1503
|
+
except Exception as e:
|
|
1504
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
1505
|
+
|
|
1506
|
+
@app.delete("/planejamento/talhoes/{field_id}")
|
|
1507
|
+
def remover_talhao(field_id: str):
|
|
1508
|
+
"""Remove um talhão"""
|
|
1509
|
+
try:
|
|
1510
|
+
from core.field_storage import remover_talhao_usuario
|
|
1511
|
+
|
|
1512
|
+
removed = remover_talhao_usuario(field_id)
|
|
1513
|
+
|
|
1514
|
+
if not removed:
|
|
1515
|
+
raise HTTPException(
|
|
1516
|
+
status_code=404,
|
|
1517
|
+
detail=f"Talhão '{field_id}' não encontrado"
|
|
1518
|
+
)
|
|
1519
|
+
|
|
1520
|
+
return {
|
|
1521
|
+
"message": "Talhão removido com sucesso",
|
|
1522
|
+
"id": field_id
|
|
1523
|
+
}
|
|
1524
|
+
except HTTPException:
|
|
1525
|
+
raise
|
|
1526
|
+
except Exception as e:
|
|
1527
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
1528
|
+
|
|
1529
|
+
@app.post("/planejamento/talhoes/{field_id}/calendario")
|
|
1530
|
+
def gerar_calendario_talhao(field_id: str, request: dict):
|
|
1531
|
+
"""Gera calendário agrícola para um talhão cadastrado"""
|
|
1532
|
+
try:
|
|
1533
|
+
from core.field_storage import obter_talhao_usuario
|
|
1534
|
+
from core.crop_calendar_engine import gerar_calendario_cultura
|
|
1535
|
+
from core.planning_models import Field, SoilType, Slope, WaterAvailability, GenerateCalendarRequest
|
|
1536
|
+
from datetime import datetime
|
|
1537
|
+
|
|
1538
|
+
# Validar request
|
|
1539
|
+
validated = GenerateCalendarRequest(**request)
|
|
1540
|
+
|
|
1541
|
+
# Buscar talhão
|
|
1542
|
+
field_data = obter_talhao_usuario(field_id)
|
|
1543
|
+
|
|
1544
|
+
if not field_data:
|
|
1545
|
+
raise HTTPException(
|
|
1546
|
+
status_code=404,
|
|
1547
|
+
detail=f"Talhão '{field_id}' não encontrado"
|
|
1548
|
+
)
|
|
1549
|
+
|
|
1550
|
+
# Parsear data de plantio
|
|
1551
|
+
try:
|
|
1552
|
+
planting_date = datetime.fromisoformat(validated.planting_date).date()
|
|
1553
|
+
except Exception:
|
|
1554
|
+
raise HTTPException(
|
|
1555
|
+
status_code=400,
|
|
1556
|
+
detail="Formato de data inválido. Use ISO 8601 (YYYY-MM-DD)"
|
|
1557
|
+
)
|
|
1558
|
+
|
|
1559
|
+
# Criar objeto Field
|
|
1560
|
+
field = Field(
|
|
1561
|
+
id=field_data["id"],
|
|
1562
|
+
property_id="user",
|
|
1563
|
+
name=field_data["name"],
|
|
1564
|
+
area_ha=field_data["area_ha"],
|
|
1565
|
+
soil_type=SoilType(field_data["soil_type"]),
|
|
1566
|
+
slope=Slope(field_data["slope"]),
|
|
1567
|
+
water_availability=WaterAvailability(field_data["water_availability"])
|
|
1568
|
+
)
|
|
1569
|
+
|
|
1570
|
+
# Gerar calendário
|
|
1571
|
+
resultado = gerar_calendario_cultura(
|
|
1572
|
+
cultura=validated.cultura,
|
|
1573
|
+
planting_date=planting_date,
|
|
1574
|
+
field=field,
|
|
1575
|
+
crop_plan_id=None
|
|
1576
|
+
)
|
|
1577
|
+
|
|
1578
|
+
# Adicionar dados do talhão na resposta
|
|
1579
|
+
resultado["field_data"] = field_data
|
|
1580
|
+
|
|
1581
|
+
return converter_tipos_python(resultado)
|
|
1582
|
+
|
|
1583
|
+
except HTTPException:
|
|
1584
|
+
raise
|
|
1585
|
+
except ValueError as e:
|
|
1586
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
1587
|
+
except Exception as e:
|
|
1588
|
+
import traceback
|
|
1589
|
+
if DEBUG_ERRORS:
|
|
1590
|
+
raise HTTPException(
|
|
1591
|
+
status_code=500,
|
|
1592
|
+
detail={
|
|
1593
|
+
"error": str(e),
|
|
1594
|
+
"traceback": traceback.format_exc()
|
|
1595
|
+
}
|
|
1596
|
+
)
|
|
1597
|
+
else:
|
|
1598
|
+
raise HTTPException(
|
|
1599
|
+
status_code=500,
|
|
1600
|
+
detail="Erro ao gerar calendário para o talhão."
|
|
1601
|
+
)
|
|
1602
|
+
|
|
1420
1603
|
@app.post("/cache/limpar")
|
|
1421
1604
|
def limpar_cache(request: Request):
|
|
1422
1605
|
"""Limpa o cache de resultados pesados (protegido por token)"""
|
|
@@ -207,6 +207,517 @@ CROP_CYCLES: Dict[str, Dict] = {
|
|
|
207
207
|
"optimal_temp_max": 29,
|
|
208
208
|
"critical_water_phases": ["germinacao", "florescimento", "enchimento_graos"],
|
|
209
209
|
"harvest_window_days": 10
|
|
210
|
+
},
|
|
211
|
+
"cafe": {
|
|
212
|
+
"cycle_days": 730,
|
|
213
|
+
"phases": [
|
|
214
|
+
{
|
|
215
|
+
"name": "preparo",
|
|
216
|
+
"days": 30,
|
|
217
|
+
"description": "Preparo do solo e coveamento",
|
|
218
|
+
"critical_water": False,
|
|
219
|
+
"tasks": [
|
|
220
|
+
{"type": "prepare_soil", "title": "Preparar solo e covas", "priority": "high", "weather_sensitive": False},
|
|
221
|
+
{"type": "fertilize", "title": "Aplicar calcário e adubação de base", "priority": "high", "weather_sensitive": True}
|
|
222
|
+
]
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
"name": "plantio",
|
|
226
|
+
"days": 60,
|
|
227
|
+
"description": "Plantio de mudas e estabelecimento",
|
|
228
|
+
"critical_water": True,
|
|
229
|
+
"tasks": [
|
|
230
|
+
{"type": "plant", "title": "Plantar mudas de café", "priority": "critical", "weather_sensitive": True},
|
|
231
|
+
{"type": "irrigate", "title": "Irrigar mudas - fase crítica", "priority": "critical", "weather_sensitive": True},
|
|
232
|
+
{"type": "inspect_pests", "title": "Monitorar pegamento das mudas", "priority": "high", "weather_sensitive": False}
|
|
233
|
+
]
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
"name": "conducao",
|
|
237
|
+
"days": 365,
|
|
238
|
+
"description": "Condução e formação da lavoura",
|
|
239
|
+
"critical_water": False,
|
|
240
|
+
"tasks": [
|
|
241
|
+
{"type": "fertilize", "title": "Aplicar adubação de crescimento", "priority": "high", "weather_sensitive": True},
|
|
242
|
+
{"type": "inspect_pests", "title": "Monitorar pragas e doenças", "priority": "high", "weather_sensitive": False},
|
|
243
|
+
{"type": "monitor_growth", "title": "Avaliar desenvolvimento vegetativo", "priority": "medium", "weather_sensitive": False},
|
|
244
|
+
{"type": "irrigate", "title": "Irrigar conforme necessidade", "priority": "medium", "weather_sensitive": True}
|
|
245
|
+
]
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
"name": "pre_producao",
|
|
249
|
+
"days": 180,
|
|
250
|
+
"description": "Preparação para primeira produção",
|
|
251
|
+
"critical_water": False,
|
|
252
|
+
"tasks": [
|
|
253
|
+
{"type": "fertilize", "title": "Aplicar adubação de produção", "priority": "high", "weather_sensitive": True},
|
|
254
|
+
{"type": "inspect_diseases", "title": "Monitorar sanidade da lavoura", "priority": "high", "weather_sensitive": False},
|
|
255
|
+
{"type": "monitor_growth", "title": "Avaliar floração inicial", "priority": "medium", "weather_sensitive": False}
|
|
256
|
+
]
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
"name": "colheita",
|
|
260
|
+
"days": 95,
|
|
261
|
+
"description": "Primeira colheita",
|
|
262
|
+
"critical_water": False,
|
|
263
|
+
"tasks": [
|
|
264
|
+
{"type": "monitor_growth", "title": "Monitorar maturação dos frutos", "priority": "high", "weather_sensitive": False},
|
|
265
|
+
{"type": "harvest", "title": "Preparar colheita", "priority": "high", "weather_sensitive": True}
|
|
266
|
+
]
|
|
267
|
+
}
|
|
268
|
+
],
|
|
269
|
+
"optimal_temp_min": 18,
|
|
270
|
+
"optimal_temp_max": 28,
|
|
271
|
+
"critical_water_phases": ["plantio"],
|
|
272
|
+
"harvest_window_days": 60,
|
|
273
|
+
"category": "perene",
|
|
274
|
+
"water_need": "media",
|
|
275
|
+
"risk_notes": "Sensível a geadas e déficit hídrico em fases críticas.",
|
|
276
|
+
"calendar_notes": "Calendário simplificado para implantação e primeiros manejos. Cultura perene com ciclo longo."
|
|
277
|
+
},
|
|
278
|
+
"cana": {
|
|
279
|
+
"cycle_days": 365,
|
|
280
|
+
"phases": [
|
|
281
|
+
{
|
|
282
|
+
"name": "preparo",
|
|
283
|
+
"days": 15,
|
|
284
|
+
"description": "Preparo do solo",
|
|
285
|
+
"critical_water": False,
|
|
286
|
+
"tasks": [
|
|
287
|
+
{"type": "prepare_soil", "title": "Preparar solo para plantio", "priority": "high", "weather_sensitive": False},
|
|
288
|
+
{"type": "fertilize", "title": "Aplicar adubação de base", "priority": "high", "weather_sensitive": True}
|
|
289
|
+
]
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
"name": "plantio",
|
|
293
|
+
"days": 30,
|
|
294
|
+
"description": "Plantio de mudas e brotação",
|
|
295
|
+
"critical_water": True,
|
|
296
|
+
"tasks": [
|
|
297
|
+
{"type": "plant", "title": "Plantar mudas de cana", "priority": "critical", "weather_sensitive": True},
|
|
298
|
+
{"type": "irrigate", "title": "Irrigar para brotação", "priority": "high", "weather_sensitive": True},
|
|
299
|
+
{"type": "inspect_pests", "title": "Monitorar brotação", "priority": "medium", "weather_sensitive": False}
|
|
300
|
+
]
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
"name": "perfilhamento",
|
|
304
|
+
"days": 60,
|
|
305
|
+
"description": "Perfilhamento e estabelecimento",
|
|
306
|
+
"critical_water": False,
|
|
307
|
+
"tasks": [
|
|
308
|
+
{"type": "fertilize", "title": "Aplicar adubação de cobertura", "priority": "high", "weather_sensitive": True},
|
|
309
|
+
{"type": "inspect_pests", "title": "Monitorar plantas daninhas", "priority": "high", "weather_sensitive": False},
|
|
310
|
+
{"type": "irrigate", "title": "Irrigar moderadamente", "priority": "medium", "weather_sensitive": True}
|
|
311
|
+
]
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
"name": "crescimento",
|
|
315
|
+
"days": 180,
|
|
316
|
+
"description": "Crescimento vegetativo intenso",
|
|
317
|
+
"critical_water": False,
|
|
318
|
+
"tasks": [
|
|
319
|
+
{"type": "inspect_pests", "title": "Monitorar pragas (broca, cigarrinha)", "priority": "high", "weather_sensitive": False},
|
|
320
|
+
{"type": "monitor_growth", "title": "Avaliar desenvolvimento", "priority": "medium", "weather_sensitive": False},
|
|
321
|
+
{"type": "irrigate", "title": "Irrigar conforme necessidade", "priority": "medium", "weather_sensitive": True}
|
|
322
|
+
]
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
"name": "maturacao",
|
|
326
|
+
"days": 60,
|
|
327
|
+
"description": "Maturação e acúmulo de sacarose",
|
|
328
|
+
"critical_water": False,
|
|
329
|
+
"tasks": [
|
|
330
|
+
{"type": "monitor_growth", "title": "Monitorar maturação", "priority": "high", "weather_sensitive": False},
|
|
331
|
+
{"type": "harvest", "title": "Preparar colheita", "priority": "high", "weather_sensitive": True}
|
|
332
|
+
]
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
"name": "colheita",
|
|
336
|
+
"days": 20,
|
|
337
|
+
"description": "Colheita",
|
|
338
|
+
"critical_water": False,
|
|
339
|
+
"tasks": [
|
|
340
|
+
{"type": "harvest", "title": "Colher cana", "priority": "critical", "weather_sensitive": True}
|
|
341
|
+
]
|
|
342
|
+
}
|
|
343
|
+
],
|
|
344
|
+
"optimal_temp_min": 20,
|
|
345
|
+
"optimal_temp_max": 35,
|
|
346
|
+
"critical_water_phases": ["plantio"],
|
|
347
|
+
"harvest_window_days": 30,
|
|
348
|
+
"category": "semi-perene",
|
|
349
|
+
"water_need": "alta",
|
|
350
|
+
"risk_notes": "Sensível a geadas. Requer manejo adequado de plantas daninhas.",
|
|
351
|
+
"calendar_notes": "Calendário para cana-planta (primeiro ciclo). Soqueiras têm ciclo diferente."
|
|
352
|
+
},
|
|
353
|
+
"arroz": {
|
|
354
|
+
"cycle_days": 120,
|
|
355
|
+
"phases": [
|
|
356
|
+
{
|
|
357
|
+
"name": "preparo",
|
|
358
|
+
"days": 10,
|
|
359
|
+
"description": "Preparo do solo e sistematização",
|
|
360
|
+
"critical_water": False,
|
|
361
|
+
"tasks": [
|
|
362
|
+
{"type": "prepare_soil", "title": "Preparar solo para plantio", "priority": "high", "weather_sensitive": False},
|
|
363
|
+
{"type": "fertilize", "title": "Aplicar adubação de base", "priority": "high", "weather_sensitive": True}
|
|
364
|
+
]
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
"name": "germinacao",
|
|
368
|
+
"days": 15,
|
|
369
|
+
"description": "Germinação e emergência",
|
|
370
|
+
"critical_water": True,
|
|
371
|
+
"tasks": [
|
|
372
|
+
{"type": "plant", "title": "Semear arroz", "priority": "critical", "weather_sensitive": True},
|
|
373
|
+
{"type": "irrigate", "title": "Manter lâmina d'água", "priority": "critical", "weather_sensitive": True},
|
|
374
|
+
{"type": "inspect_pests", "title": "Monitorar emergência", "priority": "medium", "weather_sensitive": False}
|
|
375
|
+
]
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
"name": "vegetativa",
|
|
379
|
+
"days": 40,
|
|
380
|
+
"description": "Crescimento vegetativo e perfilhamento",
|
|
381
|
+
"critical_water": True,
|
|
382
|
+
"tasks": [
|
|
383
|
+
{"type": "fertilize", "title": "Aplicar adubação nitrogenada", "priority": "high", "weather_sensitive": True},
|
|
384
|
+
{"type": "irrigate", "title": "Manter manejo hídrico", "priority": "high", "weather_sensitive": True},
|
|
385
|
+
{"type": "inspect_pests", "title": "Monitorar pragas e plantas daninhas", "priority": "high", "weather_sensitive": False}
|
|
386
|
+
]
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
"name": "reproducao",
|
|
390
|
+
"days": 30,
|
|
391
|
+
"description": "Floração e formação de grãos",
|
|
392
|
+
"critical_water": True,
|
|
393
|
+
"tasks": [
|
|
394
|
+
{"type": "irrigate", "title": "Manter lâmina d'água - fase crítica", "priority": "critical", "weather_sensitive": True},
|
|
395
|
+
{"type": "inspect_diseases", "title": "Monitorar doenças (brusone)", "priority": "high", "weather_sensitive": False},
|
|
396
|
+
{"type": "monitor_growth", "title": "Avaliar floração", "priority": "medium", "weather_sensitive": False}
|
|
397
|
+
]
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
"name": "maturacao",
|
|
401
|
+
"days": 25,
|
|
402
|
+
"description": "Maturação dos grãos",
|
|
403
|
+
"critical_water": False,
|
|
404
|
+
"tasks": [
|
|
405
|
+
{"type": "monitor_growth", "title": "Monitorar maturação", "priority": "high", "weather_sensitive": False},
|
|
406
|
+
{"type": "harvest", "title": "Preparar colheita", "priority": "high", "weather_sensitive": True}
|
|
407
|
+
]
|
|
408
|
+
}
|
|
409
|
+
],
|
|
410
|
+
"optimal_temp_min": 20,
|
|
411
|
+
"optimal_temp_max": 35,
|
|
412
|
+
"critical_water_phases": ["germinacao", "vegetativa", "reproducao"],
|
|
413
|
+
"harvest_window_days": 15,
|
|
414
|
+
"category": "anual",
|
|
415
|
+
"water_need": "muito_alta",
|
|
416
|
+
"risk_notes": "Requer manejo hídrico intensivo. Sensível a déficit hídrico.",
|
|
417
|
+
"calendar_notes": "Calendário para arroz irrigado. Arroz de sequeiro tem manejo diferente."
|
|
418
|
+
},
|
|
419
|
+
"trigo": {
|
|
420
|
+
"cycle_days": 120,
|
|
421
|
+
"phases": [
|
|
422
|
+
{
|
|
423
|
+
"name": "preparo",
|
|
424
|
+
"days": 10,
|
|
425
|
+
"description": "Preparo do solo",
|
|
426
|
+
"critical_water": False,
|
|
427
|
+
"tasks": [
|
|
428
|
+
{"type": "prepare_soil", "title": "Preparar solo para plantio", "priority": "high", "weather_sensitive": False},
|
|
429
|
+
{"type": "fertilize", "title": "Aplicar adubação de base", "priority": "high", "weather_sensitive": True}
|
|
430
|
+
]
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
"name": "germinacao",
|
|
434
|
+
"days": 12,
|
|
435
|
+
"description": "Germinação e emergência",
|
|
436
|
+
"critical_water": True,
|
|
437
|
+
"tasks": [
|
|
438
|
+
{"type": "plant", "title": "Semear trigo", "priority": "critical", "weather_sensitive": True},
|
|
439
|
+
{"type": "irrigate", "title": "Irrigar se necessário", "priority": "high", "weather_sensitive": True},
|
|
440
|
+
{"type": "inspect_pests", "title": "Monitorar emergência", "priority": "medium", "weather_sensitive": False}
|
|
441
|
+
]
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
"name": "perfilhamento",
|
|
445
|
+
"days": 35,
|
|
446
|
+
"description": "Perfilhamento",
|
|
447
|
+
"critical_water": False,
|
|
448
|
+
"tasks": [
|
|
449
|
+
{"type": "fertilize", "title": "Aplicar adubação nitrogenada", "priority": "high", "weather_sensitive": True},
|
|
450
|
+
{"type": "inspect_pests", "title": "Monitorar pragas (pulgão)", "priority": "high", "weather_sensitive": False},
|
|
451
|
+
{"type": "irrigate", "title": "Irrigar moderadamente", "priority": "medium", "weather_sensitive": True}
|
|
452
|
+
]
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
"name": "espigamento",
|
|
456
|
+
"days": 28,
|
|
457
|
+
"description": "Espigamento e floração",
|
|
458
|
+
"critical_water": True,
|
|
459
|
+
"tasks": [
|
|
460
|
+
{"type": "irrigate", "title": "Irrigar - fase crítica", "priority": "critical", "weather_sensitive": True},
|
|
461
|
+
{"type": "inspect_diseases", "title": "Monitorar doenças foliares", "priority": "high", "weather_sensitive": False},
|
|
462
|
+
{"type": "monitor_growth", "title": "Avaliar floração", "priority": "medium", "weather_sensitive": False}
|
|
463
|
+
]
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
"name": "enchimento_graos",
|
|
467
|
+
"days": 25,
|
|
468
|
+
"description": "Enchimento de grãos",
|
|
469
|
+
"critical_water": True,
|
|
470
|
+
"tasks": [
|
|
471
|
+
{"type": "irrigate", "title": "Irrigar - fase crítica", "priority": "critical", "weather_sensitive": True},
|
|
472
|
+
{"type": "monitor_growth", "title": "Monitorar desenvolvimento dos grãos", "priority": "high", "weather_sensitive": False}
|
|
473
|
+
]
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
"name": "maturacao",
|
|
477
|
+
"days": 10,
|
|
478
|
+
"description": "Maturação",
|
|
479
|
+
"critical_water": False,
|
|
480
|
+
"tasks": [
|
|
481
|
+
{"type": "harvest", "title": "Preparar colheita", "priority": "high", "weather_sensitive": True},
|
|
482
|
+
{"type": "monitor_growth", "title": "Monitorar umidade dos grãos", "priority": "medium", "weather_sensitive": False}
|
|
483
|
+
]
|
|
484
|
+
}
|
|
485
|
+
],
|
|
486
|
+
"optimal_temp_min": 10,
|
|
487
|
+
"optimal_temp_max": 24,
|
|
488
|
+
"critical_water_phases": ["germinacao", "espigamento", "enchimento_graos"],
|
|
489
|
+
"harvest_window_days": 12,
|
|
490
|
+
"category": "anual",
|
|
491
|
+
"water_need": "media",
|
|
492
|
+
"risk_notes": "Sensível a chuvas excessivas na colheita. Requer clima ameno.",
|
|
493
|
+
"calendar_notes": "Calendário para trigo de inverno. Adaptar conforme região e cultivar."
|
|
494
|
+
},
|
|
495
|
+
"sorgo": {
|
|
496
|
+
"cycle_days": 110,
|
|
497
|
+
"phases": [
|
|
498
|
+
{
|
|
499
|
+
"name": "preparo",
|
|
500
|
+
"days": 8,
|
|
501
|
+
"description": "Preparo do solo",
|
|
502
|
+
"critical_water": False,
|
|
503
|
+
"tasks": [
|
|
504
|
+
{"type": "prepare_soil", "title": "Preparar solo para plantio", "priority": "high", "weather_sensitive": False},
|
|
505
|
+
{"type": "fertilize", "title": "Aplicar adubação de base", "priority": "high", "weather_sensitive": True}
|
|
506
|
+
]
|
|
507
|
+
},
|
|
508
|
+
{
|
|
509
|
+
"name": "germinacao",
|
|
510
|
+
"days": 10,
|
|
511
|
+
"description": "Germinação e emergência",
|
|
512
|
+
"critical_water": True,
|
|
513
|
+
"tasks": [
|
|
514
|
+
{"type": "plant", "title": "Semear sorgo", "priority": "critical", "weather_sensitive": True},
|
|
515
|
+
{"type": "irrigate", "title": "Irrigar se necessário", "priority": "high", "weather_sensitive": True},
|
|
516
|
+
{"type": "inspect_pests", "title": "Monitorar emergência", "priority": "medium", "weather_sensitive": False}
|
|
517
|
+
]
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
"name": "vegetativa",
|
|
521
|
+
"days": 40,
|
|
522
|
+
"description": "Crescimento vegetativo",
|
|
523
|
+
"critical_water": False,
|
|
524
|
+
"tasks": [
|
|
525
|
+
{"type": "fertilize", "title": "Aplicar adubação nitrogenada", "priority": "high", "weather_sensitive": True},
|
|
526
|
+
{"type": "inspect_pests", "title": "Monitorar pragas (pulgão, lagarta)", "priority": "high", "weather_sensitive": False},
|
|
527
|
+
{"type": "irrigate", "title": "Irrigar moderadamente", "priority": "medium", "weather_sensitive": True}
|
|
528
|
+
]
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
"name": "florescimento",
|
|
532
|
+
"days": 22,
|
|
533
|
+
"description": "Florescimento e polinização",
|
|
534
|
+
"critical_water": True,
|
|
535
|
+
"tasks": [
|
|
536
|
+
{"type": "irrigate", "title": "Irrigar - fase crítica", "priority": "critical", "weather_sensitive": True},
|
|
537
|
+
{"type": "monitor_growth", "title": "Avaliar floração", "priority": "medium", "weather_sensitive": False}
|
|
538
|
+
]
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
"name": "enchimento_graos",
|
|
542
|
+
"days": 20,
|
|
543
|
+
"description": "Enchimento de grãos",
|
|
544
|
+
"critical_water": True,
|
|
545
|
+
"tasks": [
|
|
546
|
+
{"type": "irrigate", "title": "Irrigar - fase crítica", "priority": "critical", "weather_sensitive": True},
|
|
547
|
+
{"type": "monitor_growth", "title": "Monitorar desenvolvimento dos grãos", "priority": "high", "weather_sensitive": False}
|
|
548
|
+
]
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
"name": "maturacao",
|
|
552
|
+
"days": 10,
|
|
553
|
+
"description": "Maturação",
|
|
554
|
+
"critical_water": False,
|
|
555
|
+
"tasks": [
|
|
556
|
+
{"type": "harvest", "title": "Preparar colheita", "priority": "high", "weather_sensitive": True},
|
|
557
|
+
{"type": "monitor_growth", "title": "Monitorar umidade dos grãos", "priority": "medium", "weather_sensitive": False}
|
|
558
|
+
]
|
|
559
|
+
}
|
|
560
|
+
],
|
|
561
|
+
"optimal_temp_min": 21,
|
|
562
|
+
"optimal_temp_max": 35,
|
|
563
|
+
"critical_water_phases": ["germinacao", "florescimento", "enchimento_graos"],
|
|
564
|
+
"harvest_window_days": 15,
|
|
565
|
+
"category": "anual",
|
|
566
|
+
"water_need": "baixa",
|
|
567
|
+
"risk_notes": "Tolerante à seca. Boa opção para regiões com déficit hídrico.",
|
|
568
|
+
"calendar_notes": "Calendário para sorgo granífero. Sorgo forrageiro tem manejo diferente."
|
|
569
|
+
},
|
|
570
|
+
"mandioca": {
|
|
571
|
+
"cycle_days": 300,
|
|
572
|
+
"phases": [
|
|
573
|
+
{
|
|
574
|
+
"name": "preparo",
|
|
575
|
+
"days": 15,
|
|
576
|
+
"description": "Preparo do solo e seleção de manivas",
|
|
577
|
+
"critical_water": False,
|
|
578
|
+
"tasks": [
|
|
579
|
+
{"type": "prepare_soil", "title": "Preparar solo para plantio", "priority": "high", "weather_sensitive": False},
|
|
580
|
+
{"type": "fertilize", "title": "Aplicar adubação de base", "priority": "high", "weather_sensitive": True}
|
|
581
|
+
]
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
"name": "plantio",
|
|
585
|
+
"days": 30,
|
|
586
|
+
"description": "Plantio de manivas e brotação",
|
|
587
|
+
"critical_water": True,
|
|
588
|
+
"tasks": [
|
|
589
|
+
{"type": "plant", "title": "Plantar manivas", "priority": "critical", "weather_sensitive": True},
|
|
590
|
+
{"type": "irrigate", "title": "Irrigar para brotação", "priority": "high", "weather_sensitive": True},
|
|
591
|
+
{"type": "inspect_pests", "title": "Monitorar brotação", "priority": "medium", "weather_sensitive": False}
|
|
592
|
+
]
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
"name": "estabelecimento",
|
|
596
|
+
"days": 60,
|
|
597
|
+
"description": "Estabelecimento e crescimento inicial",
|
|
598
|
+
"critical_water": False,
|
|
599
|
+
"tasks": [
|
|
600
|
+
{"type": "inspect_pests", "title": "Controlar plantas daninhas", "priority": "high", "weather_sensitive": False},
|
|
601
|
+
{"type": "fertilize", "title": "Aplicar adubação de cobertura", "priority": "medium", "weather_sensitive": True},
|
|
602
|
+
{"type": "irrigate", "title": "Irrigar se necessário", "priority": "medium", "weather_sensitive": True}
|
|
603
|
+
]
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
"name": "desenvolvimento",
|
|
607
|
+
"days": 120,
|
|
608
|
+
"description": "Desenvolvimento vegetativo e formação de raízes",
|
|
609
|
+
"critical_water": False,
|
|
610
|
+
"tasks": [
|
|
611
|
+
{"type": "inspect_pests", "title": "Monitorar pragas (mandarová, ácaros)", "priority": "high", "weather_sensitive": False},
|
|
612
|
+
{"type": "monitor_growth", "title": "Avaliar desenvolvimento", "priority": "medium", "weather_sensitive": False},
|
|
613
|
+
{"type": "irrigate", "title": "Irrigar conforme necessidade", "priority": "low", "weather_sensitive": True}
|
|
614
|
+
]
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
"name": "engrossamento",
|
|
618
|
+
"days": 60,
|
|
619
|
+
"description": "Engrossamento das raízes",
|
|
620
|
+
"critical_water": False,
|
|
621
|
+
"tasks": [
|
|
622
|
+
{"type": "monitor_growth", "title": "Avaliar desenvolvimento das raízes", "priority": "high", "weather_sensitive": False},
|
|
623
|
+
{"type": "inspect_pests", "title": "Monitorar sanidade", "priority": "medium", "weather_sensitive": False}
|
|
624
|
+
]
|
|
625
|
+
},
|
|
626
|
+
{
|
|
627
|
+
"name": "colheita",
|
|
628
|
+
"days": 15,
|
|
629
|
+
"description": "Colheita",
|
|
630
|
+
"critical_water": False,
|
|
631
|
+
"tasks": [
|
|
632
|
+
{"type": "harvest", "title": "Colher mandioca", "priority": "high", "weather_sensitive": True}
|
|
633
|
+
]
|
|
634
|
+
}
|
|
635
|
+
],
|
|
636
|
+
"optimal_temp_min": 20,
|
|
637
|
+
"optimal_temp_max": 35,
|
|
638
|
+
"critical_water_phases": ["plantio"],
|
|
639
|
+
"harvest_window_days": 60,
|
|
640
|
+
"category": "anual",
|
|
641
|
+
"water_need": "baixa",
|
|
642
|
+
"risk_notes": "Tolerante à seca após estabelecimento. Sensível a encharcamento.",
|
|
643
|
+
"calendar_notes": "Calendário para mandioca de mesa. Mandioca industrial pode ter ciclo mais longo."
|
|
644
|
+
},
|
|
645
|
+
"algodao": {
|
|
646
|
+
"cycle_days": 180,
|
|
647
|
+
"phases": [
|
|
648
|
+
{
|
|
649
|
+
"name": "preparo",
|
|
650
|
+
"days": 10,
|
|
651
|
+
"description": "Preparo do solo",
|
|
652
|
+
"critical_water": False,
|
|
653
|
+
"tasks": [
|
|
654
|
+
{"type": "prepare_soil", "title": "Preparar solo para plantio", "priority": "high", "weather_sensitive": False},
|
|
655
|
+
{"type": "fertilize", "title": "Aplicar adubação de base", "priority": "high", "weather_sensitive": True}
|
|
656
|
+
]
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
"name": "germinacao",
|
|
660
|
+
"days": 12,
|
|
661
|
+
"description": "Germinação e emergência",
|
|
662
|
+
"critical_water": True,
|
|
663
|
+
"tasks": [
|
|
664
|
+
{"type": "plant", "title": "Semear algodão", "priority": "critical", "weather_sensitive": True},
|
|
665
|
+
{"type": "irrigate", "title": "Irrigar para emergência", "priority": "high", "weather_sensitive": True},
|
|
666
|
+
{"type": "inspect_pests", "title": "Monitorar emergência", "priority": "medium", "weather_sensitive": False}
|
|
667
|
+
]
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
"name": "vegetativa",
|
|
671
|
+
"days": 50,
|
|
672
|
+
"description": "Crescimento vegetativo",
|
|
673
|
+
"critical_water": False,
|
|
674
|
+
"tasks": [
|
|
675
|
+
{"type": "fertilize", "title": "Aplicar adubação nitrogenada", "priority": "high", "weather_sensitive": True},
|
|
676
|
+
{"type": "inspect_pests", "title": "Monitorar pragas (bicudo, lagarta)", "priority": "high", "weather_sensitive": False},
|
|
677
|
+
{"type": "irrigate", "title": "Irrigar moderadamente", "priority": "medium", "weather_sensitive": True}
|
|
678
|
+
]
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
"name": "florescimento",
|
|
682
|
+
"days": 40,
|
|
683
|
+
"description": "Florescimento",
|
|
684
|
+
"critical_water": True,
|
|
685
|
+
"tasks": [
|
|
686
|
+
{"type": "irrigate", "title": "Irrigar - fase crítica", "priority": "critical", "weather_sensitive": True},
|
|
687
|
+
{"type": "inspect_diseases", "title": "Monitorar doenças (ramulária)", "priority": "high", "weather_sensitive": False},
|
|
688
|
+
{"type": "monitor_growth", "title": "Avaliar floração", "priority": "medium", "weather_sensitive": False}
|
|
689
|
+
]
|
|
690
|
+
},
|
|
691
|
+
{
|
|
692
|
+
"name": "formacao_macas",
|
|
693
|
+
"days": 48,
|
|
694
|
+
"description": "Formação e abertura de maçãs",
|
|
695
|
+
"critical_water": True,
|
|
696
|
+
"tasks": [
|
|
697
|
+
{"type": "irrigate", "title": "Irrigar - fase crítica", "priority": "critical", "weather_sensitive": True},
|
|
698
|
+
{"type": "inspect_pests", "title": "Monitorar pragas nas maçãs", "priority": "high", "weather_sensitive": False},
|
|
699
|
+
{"type": "monitor_growth", "title": "Avaliar desenvolvimento das maçãs", "priority": "high", "weather_sensitive": False}
|
|
700
|
+
]
|
|
701
|
+
},
|
|
702
|
+
{
|
|
703
|
+
"name": "maturacao",
|
|
704
|
+
"days": 20,
|
|
705
|
+
"description": "Maturação e abertura dos capulhos",
|
|
706
|
+
"critical_water": False,
|
|
707
|
+
"tasks": [
|
|
708
|
+
{"type": "monitor_growth", "title": "Monitorar abertura dos capulhos", "priority": "high", "weather_sensitive": False},
|
|
709
|
+
{"type": "harvest", "title": "Preparar colheita", "priority": "high", "weather_sensitive": True}
|
|
710
|
+
]
|
|
711
|
+
}
|
|
712
|
+
],
|
|
713
|
+
"optimal_temp_min": 20,
|
|
714
|
+
"optimal_temp_max": 30,
|
|
715
|
+
"critical_water_phases": ["germinacao", "florescimento", "formacao_macas"],
|
|
716
|
+
"harvest_window_days": 30,
|
|
717
|
+
"category": "anual",
|
|
718
|
+
"water_need": "media",
|
|
719
|
+
"risk_notes": "Sensível a pragas. Requer manejo fitossanitário intensivo.",
|
|
720
|
+
"calendar_notes": "Calendário para algodão herbáceo. Requer monitoramento constante de pragas."
|
|
210
721
|
}
|
|
211
722
|
}
|
|
212
723
|
|
|
@@ -377,6 +888,10 @@ def gerar_calendario_cultura(
|
|
|
377
888
|
"optimal_temp_max": cycle_data["optimal_temp_max"],
|
|
378
889
|
"critical_water_phases": cycle_data["critical_water_phases"],
|
|
379
890
|
"harvest_window_days": cycle_data["harvest_window_days"],
|
|
891
|
+
"category": cycle_data.get("category", "anual"),
|
|
892
|
+
"water_need": cycle_data.get("water_need", "media"),
|
|
893
|
+
"risk_notes": cycle_data.get("risk_notes", ""),
|
|
894
|
+
"calendar_notes": cycle_data.get("calendar_notes", ""),
|
|
380
895
|
"phases": [
|
|
381
896
|
{
|
|
382
897
|
"name": phase["name"],
|
|
@@ -390,7 +905,8 @@ def gerar_calendario_cultura(
|
|
|
390
905
|
"tasks": [task.to_dict() for task in tasks],
|
|
391
906
|
"total_tasks": len(tasks),
|
|
392
907
|
"weather_sensitive_tasks": sum(1 for task in tasks if task.weather_sensitive),
|
|
393
|
-
"critical_tasks": sum(1 for task in tasks if task.priority == TaskPriority.CRITICAL)
|
|
908
|
+
"critical_tasks": sum(1 for task in tasks if task.priority == TaskPriority.CRITICAL),
|
|
909
|
+
"cautela": "Este calendário é uma base inicial de planejamento. As datas e tarefas devem ser ajustadas conforme clima, solo, cultivar, manejo e orientação técnica."
|
|
394
910
|
}
|
|
395
911
|
|
|
396
912
|
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Storage simples para talhões do usuário
|
|
3
|
+
|
|
4
|
+
Persistência em JSON local:
|
|
5
|
+
- API Local: dados persistem no PC do usuário (~/.agroplan/backend/data/user_fields/)
|
|
6
|
+
- API Render: dados temporários/voláteis (perdem-se ao reiniciar)
|
|
7
|
+
|
|
8
|
+
Fase futura: Migrar para banco de dados PostgreSQL
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import uuid
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from typing import List, Dict, Optional
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Diretório de dados
|
|
20
|
+
DATA_DIR = Path(__file__).parent.parent / "data" / "user_fields"
|
|
21
|
+
FIELDS_FILE = DATA_DIR / "fields.json"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _ensure_data_dir():
|
|
25
|
+
"""Garante que o diretório de dados existe"""
|
|
26
|
+
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
|
|
28
|
+
# Criar arquivo vazio se não existir
|
|
29
|
+
if not FIELDS_FILE.exists():
|
|
30
|
+
with open(FIELDS_FILE, 'w', encoding='utf-8') as f:
|
|
31
|
+
json.dump([], f)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _load_fields() -> List[Dict]:
|
|
35
|
+
"""Carrega todos os talhões do arquivo JSON"""
|
|
36
|
+
_ensure_data_dir()
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
with open(FIELDS_FILE, 'r', encoding='utf-8') as f:
|
|
40
|
+
return json.load(f)
|
|
41
|
+
except (json.JSONDecodeError, FileNotFoundError):
|
|
42
|
+
return []
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _save_fields(fields: List[Dict]):
|
|
46
|
+
"""
|
|
47
|
+
Salva talhões no arquivo JSON com escrita segura.
|
|
48
|
+
|
|
49
|
+
Usa arquivo temporário + rename para evitar corrupção.
|
|
50
|
+
"""
|
|
51
|
+
_ensure_data_dir()
|
|
52
|
+
|
|
53
|
+
# Escrever em arquivo temporário
|
|
54
|
+
temp_file = FIELDS_FILE.with_suffix('.tmp')
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
with open(temp_file, 'w', encoding='utf-8') as f:
|
|
58
|
+
json.dump(fields, f, indent=2, ensure_ascii=False)
|
|
59
|
+
|
|
60
|
+
# Renomear atomicamente
|
|
61
|
+
temp_file.replace(FIELDS_FILE)
|
|
62
|
+
except Exception as e:
|
|
63
|
+
# Limpar arquivo temporário em caso de erro
|
|
64
|
+
if temp_file.exists():
|
|
65
|
+
temp_file.unlink()
|
|
66
|
+
raise e
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def listar_talhoes_usuario() -> List[Dict]:
|
|
70
|
+
"""
|
|
71
|
+
Lista todos os talhões do usuário.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Lista de talhões
|
|
75
|
+
"""
|
|
76
|
+
return _load_fields()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def criar_talhao_usuario(data: Dict) -> Dict:
|
|
80
|
+
"""
|
|
81
|
+
Cria um novo talhão.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
data: Dados do talhão (sem id, created_at, updated_at)
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Talhão criado com id e timestamps
|
|
88
|
+
"""
|
|
89
|
+
fields = _load_fields()
|
|
90
|
+
|
|
91
|
+
# Gerar novo talhão
|
|
92
|
+
now = datetime.now().isoformat()
|
|
93
|
+
new_field = {
|
|
94
|
+
"id": str(uuid.uuid4()),
|
|
95
|
+
**data,
|
|
96
|
+
"created_at": now,
|
|
97
|
+
"updated_at": now
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fields.append(new_field)
|
|
101
|
+
_save_fields(fields)
|
|
102
|
+
|
|
103
|
+
return new_field
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def obter_talhao_usuario(field_id: str) -> Optional[Dict]:
|
|
107
|
+
"""
|
|
108
|
+
Obtém um talhão pelo ID.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
field_id: ID do talhão
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Talhão ou None se não encontrado
|
|
115
|
+
"""
|
|
116
|
+
fields = _load_fields()
|
|
117
|
+
|
|
118
|
+
for field in fields:
|
|
119
|
+
if field.get("id") == field_id:
|
|
120
|
+
return field
|
|
121
|
+
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def atualizar_talhao_usuario(field_id: str, data: Dict) -> Optional[Dict]:
|
|
126
|
+
"""
|
|
127
|
+
Atualiza um talhão existente.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
field_id: ID do talhão
|
|
131
|
+
data: Novos dados (sem id, created_at, updated_at)
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Talhão atualizado ou None se não encontrado
|
|
135
|
+
"""
|
|
136
|
+
fields = _load_fields()
|
|
137
|
+
|
|
138
|
+
for i, field in enumerate(fields):
|
|
139
|
+
if field.get("id") == field_id:
|
|
140
|
+
# Preservar id e created_at, atualizar updated_at
|
|
141
|
+
updated_field = {
|
|
142
|
+
"id": field["id"],
|
|
143
|
+
**data,
|
|
144
|
+
"created_at": field["created_at"],
|
|
145
|
+
"updated_at": datetime.now().isoformat()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
fields[i] = updated_field
|
|
149
|
+
_save_fields(fields)
|
|
150
|
+
|
|
151
|
+
return updated_field
|
|
152
|
+
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def remover_talhao_usuario(field_id: str) -> bool:
|
|
157
|
+
"""
|
|
158
|
+
Remove um talhão.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
field_id: ID do talhão
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
True se removido, False se não encontrado
|
|
165
|
+
"""
|
|
166
|
+
fields = _load_fields()
|
|
167
|
+
|
|
168
|
+
original_length = len(fields)
|
|
169
|
+
fields = [f for f in fields if f.get("id") != field_id]
|
|
170
|
+
|
|
171
|
+
if len(fields) < original_length:
|
|
172
|
+
_save_fields(fields)
|
|
173
|
+
return True
|
|
174
|
+
|
|
175
|
+
return False
|
|
@@ -11,12 +11,18 @@ Define as entidades principais do sistema de planejamento agrícola:
|
|
|
11
11
|
- UserObservation: Observação do usuário
|
|
12
12
|
- Intervention: Intervenção/replanejamento
|
|
13
13
|
- PlanningSession: Sessão de planejamento
|
|
14
|
+
|
|
15
|
+
Modelos Pydantic para API:
|
|
16
|
+
- ManualFieldCreate: Criação de talhão manual
|
|
17
|
+
- ManualFieldUpdate: Atualização de talhão manual
|
|
18
|
+
- ManualFieldResponse: Resposta de talhão manual
|
|
14
19
|
"""
|
|
15
20
|
|
|
16
21
|
from dataclasses import dataclass, field
|
|
17
22
|
from datetime import date, datetime
|
|
18
23
|
from typing import Optional, List, Dict
|
|
19
24
|
from enum import Enum
|
|
25
|
+
from pydantic import BaseModel, Field as PydanticField, field_validator
|
|
20
26
|
|
|
21
27
|
|
|
22
28
|
# Enums para tipos padronizados
|
|
@@ -364,3 +370,115 @@ class PlanningSession:
|
|
|
364
370
|
"cultures_recommended": self.cultures_recommended,
|
|
365
371
|
"created_at": self.created_at.isoformat()
|
|
366
372
|
}
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
# Modelos Pydantic para API
|
|
376
|
+
|
|
377
|
+
class ManualFieldCreate(BaseModel):
|
|
378
|
+
"""Modelo para criação de talhão manual"""
|
|
379
|
+
name: str = PydanticField(..., min_length=1, max_length=100, description="Nome do talhão")
|
|
380
|
+
area_ha: float = PydanticField(..., gt=0, description="Área em hectares")
|
|
381
|
+
soil_type: str = PydanticField(..., description="Tipo de solo")
|
|
382
|
+
slope: str = PydanticField(..., description="Tipo de relevo")
|
|
383
|
+
water_availability: str = PydanticField(..., description="Disponibilidade de água")
|
|
384
|
+
uf: Optional[str] = PydanticField(None, min_length=2, max_length=2, description="UF")
|
|
385
|
+
municipio: Optional[str] = PydanticField(None, max_length=100, description="Município")
|
|
386
|
+
lat: Optional[float] = PydanticField(None, ge=-90, le=90, description="Latitude")
|
|
387
|
+
lon: Optional[float] = PydanticField(None, ge=-180, le=180, description="Longitude")
|
|
388
|
+
|
|
389
|
+
@field_validator('soil_type')
|
|
390
|
+
@classmethod
|
|
391
|
+
def validate_soil_type(cls, v: str) -> str:
|
|
392
|
+
allowed = ['argiloso', 'arenoso', 'misto', 'siltoso']
|
|
393
|
+
if v not in allowed:
|
|
394
|
+
raise ValueError(f'soil_type deve ser um de: {", ".join(allowed)}')
|
|
395
|
+
return v
|
|
396
|
+
|
|
397
|
+
@field_validator('slope')
|
|
398
|
+
@classmethod
|
|
399
|
+
def validate_slope(cls, v: str) -> str:
|
|
400
|
+
allowed = ['plano', 'suave', 'moderado', 'ingreme']
|
|
401
|
+
if v not in allowed:
|
|
402
|
+
raise ValueError(f'slope deve ser um de: {", ".join(allowed)}')
|
|
403
|
+
return v
|
|
404
|
+
|
|
405
|
+
@field_validator('water_availability')
|
|
406
|
+
@classmethod
|
|
407
|
+
def validate_water_availability(cls, v: str) -> str:
|
|
408
|
+
allowed = ['baixa', 'media', 'alta']
|
|
409
|
+
if v not in allowed:
|
|
410
|
+
raise ValueError(f'water_availability deve ser um de: {", ".join(allowed)}')
|
|
411
|
+
return v
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
class ManualFieldUpdate(BaseModel):
|
|
415
|
+
"""Modelo para atualização de talhão manual"""
|
|
416
|
+
name: Optional[str] = PydanticField(None, min_length=1, max_length=100)
|
|
417
|
+
area_ha: Optional[float] = PydanticField(None, gt=0)
|
|
418
|
+
soil_type: Optional[str] = None
|
|
419
|
+
slope: Optional[str] = None
|
|
420
|
+
water_availability: Optional[str] = None
|
|
421
|
+
uf: Optional[str] = PydanticField(None, min_length=2, max_length=2)
|
|
422
|
+
municipio: Optional[str] = PydanticField(None, max_length=100)
|
|
423
|
+
lat: Optional[float] = PydanticField(None, ge=-90, le=90)
|
|
424
|
+
lon: Optional[float] = PydanticField(None, ge=-180, le=180)
|
|
425
|
+
|
|
426
|
+
@field_validator('soil_type')
|
|
427
|
+
@classmethod
|
|
428
|
+
def validate_soil_type(cls, v: Optional[str]) -> Optional[str]:
|
|
429
|
+
if v is not None:
|
|
430
|
+
allowed = ['argiloso', 'arenoso', 'misto', 'siltoso']
|
|
431
|
+
if v not in allowed:
|
|
432
|
+
raise ValueError(f'soil_type deve ser um de: {", ".join(allowed)}')
|
|
433
|
+
return v
|
|
434
|
+
|
|
435
|
+
@field_validator('slope')
|
|
436
|
+
@classmethod
|
|
437
|
+
def validate_slope(cls, v: Optional[str]) -> Optional[str]:
|
|
438
|
+
if v is not None:
|
|
439
|
+
allowed = ['plano', 'suave', 'moderado', 'ingreme']
|
|
440
|
+
if v not in allowed:
|
|
441
|
+
raise ValueError(f'slope deve ser um de: {", ".join(allowed)}')
|
|
442
|
+
return v
|
|
443
|
+
|
|
444
|
+
@field_validator('water_availability')
|
|
445
|
+
@classmethod
|
|
446
|
+
def validate_water_availability(cls, v: Optional[str]) -> Optional[str]:
|
|
447
|
+
if v is not None:
|
|
448
|
+
allowed = ['baixa', 'media', 'alta']
|
|
449
|
+
if v not in allowed:
|
|
450
|
+
raise ValueError(f'water_availability deve ser um de: {", ".join(allowed)}')
|
|
451
|
+
return v
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
class ManualFieldResponse(BaseModel):
|
|
455
|
+
"""Modelo de resposta de talhão manual"""
|
|
456
|
+
id: str
|
|
457
|
+
name: str
|
|
458
|
+
area_ha: float
|
|
459
|
+
soil_type: str
|
|
460
|
+
slope: str
|
|
461
|
+
water_availability: str
|
|
462
|
+
uf: Optional[str] = None
|
|
463
|
+
municipio: Optional[str] = None
|
|
464
|
+
lat: Optional[float] = None
|
|
465
|
+
lon: Optional[float] = None
|
|
466
|
+
created_at: str
|
|
467
|
+
updated_at: str
|
|
468
|
+
|
|
469
|
+
class Config:
|
|
470
|
+
from_attributes = True
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
class GenerateCalendarRequest(BaseModel):
|
|
474
|
+
"""Modelo para geração de calendário"""
|
|
475
|
+
cultura: str = PydanticField(..., description="Nome da cultura")
|
|
476
|
+
planting_date: str = PydanticField(..., description="Data de plantio (YYYY-MM-DD)")
|
|
477
|
+
|
|
478
|
+
@field_validator('cultura')
|
|
479
|
+
@classmethod
|
|
480
|
+
def validate_cultura(cls, v: str) -> str:
|
|
481
|
+
allowed = ['soja', 'milho', 'feijao']
|
|
482
|
+
if v not in allowed:
|
|
483
|
+
raise ValueError(f'cultura deve ser uma de: {", ".join(allowed)}')
|
|
484
|
+
return v
|
|
Binary file
|