agroplan-ai-cli 1.0.30 → 1.0.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"cli_version": "1.0.
|
|
3
|
-
"backend_template_version": "1.0.
|
|
2
|
+
"cli_version": "1.0.31",
|
|
3
|
+
"backend_template_version": "1.0.31",
|
|
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,9 @@
|
|
|
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"
|
|
22
24
|
],
|
|
23
|
-
"generated_at": "2026-05-
|
|
25
|
+
"generated_at": "2026-05-10T16:15:00Z"
|
|
24
26
|
}
|
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)"""
|
|
@@ -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
|