agroplan-ai-cli 1.0.29 → 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.
- package/backend-template/VERSION.json +7 -4
- package/backend-template/api.py +292 -0
- package/backend-template/core/crop_calendar_engine.py +426 -0
- package/backend-template/core/field_storage.py +175 -0
- package/backend-template/core/planning_models.py +484 -0
- package/backend-template/data/user_fields/fields.json +0 -0
- package/package.json +1 -1
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Engine de Calendário Agrícola
|
|
3
|
+
|
|
4
|
+
Gera calendários de tarefas para culturas baseado em:
|
|
5
|
+
- Ciclo da cultura
|
|
6
|
+
- Data de plantio
|
|
7
|
+
- Características do talhão
|
|
8
|
+
- Contexto climático (opcional)
|
|
9
|
+
- Contexto ZARC (opcional)
|
|
10
|
+
|
|
11
|
+
Fase inicial: Base local para soja, milho e feijão
|
|
12
|
+
Fase futura: Integração com clima real e replanejamento
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from datetime import date, timedelta
|
|
16
|
+
from typing import Dict, List, Optional
|
|
17
|
+
import uuid
|
|
18
|
+
|
|
19
|
+
from .planning_models import (
|
|
20
|
+
Field, CropCycle, CropPhase, CalendarTask,
|
|
21
|
+
TaskType, TaskPriority, TaskStatus
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Base de Conhecimento Local - Fase 10.1
|
|
26
|
+
|
|
27
|
+
CROP_CYCLES: Dict[str, Dict] = {
|
|
28
|
+
"soja": {
|
|
29
|
+
"cycle_days": 120,
|
|
30
|
+
"phases": [
|
|
31
|
+
{
|
|
32
|
+
"name": "germinacao",
|
|
33
|
+
"days": 10,
|
|
34
|
+
"description": "Emergência das plântulas",
|
|
35
|
+
"critical_water": True,
|
|
36
|
+
"tasks": [
|
|
37
|
+
{"type": "irrigate", "title": "Irrigar se não houver chuva", "priority": "high", "weather_sensitive": True},
|
|
38
|
+
{"type": "inspect_pests", "title": "Inspecionar germinação", "priority": "medium", "weather_sensitive": False}
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"name": "vegetativa",
|
|
43
|
+
"days": 40,
|
|
44
|
+
"description": "Crescimento vegetativo",
|
|
45
|
+
"critical_water": False,
|
|
46
|
+
"tasks": [
|
|
47
|
+
{"type": "fertilize", "title": "Aplicar fertilizante de cobertura", "priority": "high", "weather_sensitive": True},
|
|
48
|
+
{"type": "inspect_pests", "title": "Inspecionar pragas", "priority": "medium", "weather_sensitive": False},
|
|
49
|
+
{"type": "irrigate", "title": "Irrigar moderadamente", "priority": "medium", "weather_sensitive": True}
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"name": "florescimento",
|
|
54
|
+
"days": 30,
|
|
55
|
+
"description": "Floração e formação de vagens",
|
|
56
|
+
"critical_water": True,
|
|
57
|
+
"tasks": [
|
|
58
|
+
{"type": "irrigate", "title": "Irrigar - fase crítica", "priority": "critical", "weather_sensitive": True},
|
|
59
|
+
{"type": "inspect_diseases", "title": "Inspecionar doenças", "priority": "high", "weather_sensitive": False},
|
|
60
|
+
{"type": "monitor_growth", "title": "Monitorar temperatura", "priority": "medium", "weather_sensitive": False}
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"name": "enchimento_graos",
|
|
65
|
+
"days": 30,
|
|
66
|
+
"description": "Enchimento de grãos",
|
|
67
|
+
"critical_water": True,
|
|
68
|
+
"tasks": [
|
|
69
|
+
{"type": "irrigate", "title": "Irrigar - fase crítica", "priority": "critical", "weather_sensitive": True},
|
|
70
|
+
{"type": "monitor_growth", "title": "Monitorar maturação", "priority": "high", "weather_sensitive": False}
|
|
71
|
+
]
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"name": "maturacao",
|
|
75
|
+
"days": 10,
|
|
76
|
+
"description": "Maturação e secagem",
|
|
77
|
+
"critical_water": False,
|
|
78
|
+
"tasks": [
|
|
79
|
+
{"type": "harvest", "title": "Preparar colheita", "priority": "high", "weather_sensitive": True},
|
|
80
|
+
{"type": "monitor_growth", "title": "Monitorar umidade dos grãos", "priority": "medium", "weather_sensitive": False}
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
],
|
|
84
|
+
"optimal_temp_min": 20,
|
|
85
|
+
"optimal_temp_max": 30,
|
|
86
|
+
"critical_water_phases": ["germinacao", "florescimento", "enchimento_graos"],
|
|
87
|
+
"harvest_window_days": 15
|
|
88
|
+
},
|
|
89
|
+
"milho": {
|
|
90
|
+
"cycle_days": 140,
|
|
91
|
+
"phases": [
|
|
92
|
+
{
|
|
93
|
+
"name": "germinacao",
|
|
94
|
+
"days": 12,
|
|
95
|
+
"description": "Emergência das plântulas",
|
|
96
|
+
"critical_water": True,
|
|
97
|
+
"tasks": [
|
|
98
|
+
{"type": "irrigate", "title": "Irrigar se não houver chuva", "priority": "high", "weather_sensitive": True},
|
|
99
|
+
{"type": "inspect_pests", "title": "Inspecionar germinação", "priority": "medium", "weather_sensitive": False}
|
|
100
|
+
]
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"name": "vegetativa",
|
|
104
|
+
"days": 50,
|
|
105
|
+
"description": "Crescimento vegetativo",
|
|
106
|
+
"critical_water": False,
|
|
107
|
+
"tasks": [
|
|
108
|
+
{"type": "fertilize", "title": "Aplicar fertilizante nitrogenado", "priority": "high", "weather_sensitive": True},
|
|
109
|
+
{"type": "inspect_pests", "title": "Inspecionar pragas (lagarta)", "priority": "high", "weather_sensitive": False},
|
|
110
|
+
{"type": "irrigate", "title": "Irrigar moderadamente", "priority": "medium", "weather_sensitive": True}
|
|
111
|
+
]
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"name": "florescimento",
|
|
115
|
+
"days": 28,
|
|
116
|
+
"description": "Floração e polinização",
|
|
117
|
+
"critical_water": True,
|
|
118
|
+
"tasks": [
|
|
119
|
+
{"type": "irrigate", "title": "Irrigar - fase crítica", "priority": "critical", "weather_sensitive": True},
|
|
120
|
+
{"type": "monitor_growth", "title": "Monitorar polinização", "priority": "high", "weather_sensitive": False}
|
|
121
|
+
]
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"name": "enchimento_graos",
|
|
125
|
+
"days": 40,
|
|
126
|
+
"description": "Enchimento de grãos",
|
|
127
|
+
"critical_water": True,
|
|
128
|
+
"tasks": [
|
|
129
|
+
{"type": "irrigate", "title": "Irrigar - fase crítica", "priority": "critical", "weather_sensitive": True},
|
|
130
|
+
{"type": "inspect_diseases", "title": "Inspecionar doenças foliares", "priority": "high", "weather_sensitive": False},
|
|
131
|
+
{"type": "monitor_growth", "title": "Monitorar desenvolvimento das espigas", "priority": "medium", "weather_sensitive": False}
|
|
132
|
+
]
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"name": "maturacao",
|
|
136
|
+
"days": 10,
|
|
137
|
+
"description": "Maturação fisiológica",
|
|
138
|
+
"critical_water": False,
|
|
139
|
+
"tasks": [
|
|
140
|
+
{"type": "harvest", "title": "Preparar colheita", "priority": "high", "weather_sensitive": True},
|
|
141
|
+
{"type": "monitor_growth", "title": "Monitorar umidade dos grãos", "priority": "medium", "weather_sensitive": False}
|
|
142
|
+
]
|
|
143
|
+
}
|
|
144
|
+
],
|
|
145
|
+
"optimal_temp_min": 18,
|
|
146
|
+
"optimal_temp_max": 32,
|
|
147
|
+
"critical_water_phases": ["germinacao", "florescimento", "enchimento_graos"],
|
|
148
|
+
"harvest_window_days": 20
|
|
149
|
+
},
|
|
150
|
+
"feijao": {
|
|
151
|
+
"cycle_days": 90,
|
|
152
|
+
"phases": [
|
|
153
|
+
{
|
|
154
|
+
"name": "germinacao",
|
|
155
|
+
"days": 8,
|
|
156
|
+
"description": "Emergência das plântulas",
|
|
157
|
+
"critical_water": True,
|
|
158
|
+
"tasks": [
|
|
159
|
+
{"type": "irrigate", "title": "Irrigar se não houver chuva", "priority": "high", "weather_sensitive": True},
|
|
160
|
+
{"type": "inspect_pests", "title": "Inspecionar germinação", "priority": "medium", "weather_sensitive": False}
|
|
161
|
+
]
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
"name": "vegetativa",
|
|
165
|
+
"days": 30,
|
|
166
|
+
"description": "Crescimento vegetativo",
|
|
167
|
+
"critical_water": False,
|
|
168
|
+
"tasks": [
|
|
169
|
+
{"type": "fertilize", "title": "Aplicar fertilizante de cobertura", "priority": "high", "weather_sensitive": True},
|
|
170
|
+
{"type": "inspect_pests", "title": "Inspecionar pragas (vaquinha, mosca-branca)", "priority": "high", "weather_sensitive": False},
|
|
171
|
+
{"type": "irrigate", "title": "Irrigar moderadamente", "priority": "medium", "weather_sensitive": True}
|
|
172
|
+
]
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"name": "florescimento",
|
|
176
|
+
"days": 22,
|
|
177
|
+
"description": "Floração e formação de vagens",
|
|
178
|
+
"critical_water": True,
|
|
179
|
+
"tasks": [
|
|
180
|
+
{"type": "irrigate", "title": "Irrigar - fase crítica", "priority": "critical", "weather_sensitive": True},
|
|
181
|
+
{"type": "inspect_diseases", "title": "Inspecionar doenças (antracnose, ferrugem)", "priority": "high", "weather_sensitive": False},
|
|
182
|
+
{"type": "monitor_growth", "title": "Monitorar formação de vagens", "priority": "medium", "weather_sensitive": False}
|
|
183
|
+
]
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"name": "enchimento_graos",
|
|
187
|
+
"days": 20,
|
|
188
|
+
"description": "Enchimento de grãos",
|
|
189
|
+
"critical_water": True,
|
|
190
|
+
"tasks": [
|
|
191
|
+
{"type": "irrigate", "title": "Irrigar - fase crítica", "priority": "critical", "weather_sensitive": True},
|
|
192
|
+
{"type": "monitor_growth", "title": "Monitorar desenvolvimento das vagens", "priority": "high", "weather_sensitive": False}
|
|
193
|
+
]
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"name": "maturacao",
|
|
197
|
+
"days": 10,
|
|
198
|
+
"description": "Maturação e secagem",
|
|
199
|
+
"critical_water": False,
|
|
200
|
+
"tasks": [
|
|
201
|
+
{"type": "harvest", "title": "Preparar colheita", "priority": "high", "weather_sensitive": True},
|
|
202
|
+
{"type": "monitor_growth", "title": "Monitorar umidade dos grãos", "priority": "medium", "weather_sensitive": False}
|
|
203
|
+
]
|
|
204
|
+
}
|
|
205
|
+
],
|
|
206
|
+
"optimal_temp_min": 18,
|
|
207
|
+
"optimal_temp_max": 29,
|
|
208
|
+
"critical_water_phases": ["germinacao", "florescimento", "enchimento_graos"],
|
|
209
|
+
"harvest_window_days": 10
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def get_crop_cycle(cultura: str) -> Optional[CropCycle]:
|
|
215
|
+
"""
|
|
216
|
+
Retorna o ciclo de uma cultura.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
cultura: Nome da cultura
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
CropCycle ou None se cultura não encontrada
|
|
223
|
+
"""
|
|
224
|
+
if cultura not in CROP_CYCLES:
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
cycle_data = CROP_CYCLES[cultura]
|
|
228
|
+
|
|
229
|
+
phases = [
|
|
230
|
+
CropPhase(
|
|
231
|
+
name=phase["name"],
|
|
232
|
+
days=phase["days"],
|
|
233
|
+
description=phase["description"],
|
|
234
|
+
critical_water=phase["critical_water"],
|
|
235
|
+
tasks=[task["type"] for task in phase["tasks"]]
|
|
236
|
+
)
|
|
237
|
+
for phase in cycle_data["phases"]
|
|
238
|
+
]
|
|
239
|
+
|
|
240
|
+
return CropCycle(
|
|
241
|
+
culture=cultura,
|
|
242
|
+
cycle_days=cycle_data["cycle_days"],
|
|
243
|
+
phases=phases,
|
|
244
|
+
critical_water_phases=cycle_data["critical_water_phases"],
|
|
245
|
+
optimal_temp_min=cycle_data["optimal_temp_min"],
|
|
246
|
+
optimal_temp_max=cycle_data["optimal_temp_max"],
|
|
247
|
+
harvest_window_days=cycle_data["harvest_window_days"]
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def gerar_calendario_cultura(
|
|
252
|
+
cultura: str,
|
|
253
|
+
planting_date: date,
|
|
254
|
+
field: Field,
|
|
255
|
+
crop_plan_id: Optional[str] = None,
|
|
256
|
+
weather_context: Optional[Dict] = None,
|
|
257
|
+
zarc_context: Optional[Dict] = None
|
|
258
|
+
) -> Dict:
|
|
259
|
+
"""
|
|
260
|
+
Gera calendário de tarefas para uma cultura.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
cultura: Nome da cultura
|
|
264
|
+
planting_date: Data de plantio
|
|
265
|
+
field: Dados do talhão
|
|
266
|
+
crop_plan_id: ID do plano de cultura (opcional)
|
|
267
|
+
weather_context: Contexto climático (opcional, fase futura)
|
|
268
|
+
zarc_context: Contexto ZARC (opcional, fase futura)
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
Dict com calendário e informações do ciclo
|
|
272
|
+
"""
|
|
273
|
+
|
|
274
|
+
# Verificar se cultura existe
|
|
275
|
+
if cultura not in CROP_CYCLES:
|
|
276
|
+
return {
|
|
277
|
+
"error": f"Cultura '{cultura}' não encontrada",
|
|
278
|
+
"culturas_disponiveis": list(CROP_CYCLES.keys())
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
cycle_data = CROP_CYCLES[cultura]
|
|
282
|
+
crop_plan_id = crop_plan_id or str(uuid.uuid4())
|
|
283
|
+
|
|
284
|
+
# Calcular data estimada de colheita
|
|
285
|
+
estimated_harvest_date = planting_date + timedelta(days=cycle_data["cycle_days"])
|
|
286
|
+
|
|
287
|
+
# Gerar tarefas
|
|
288
|
+
tasks = []
|
|
289
|
+
current_date = planting_date
|
|
290
|
+
|
|
291
|
+
# Tarefa de preparação do solo (7 dias antes do plantio)
|
|
292
|
+
prepare_date = planting_date - timedelta(days=7)
|
|
293
|
+
tasks.append(
|
|
294
|
+
CalendarTask(
|
|
295
|
+
id=str(uuid.uuid4()),
|
|
296
|
+
crop_plan_id=crop_plan_id,
|
|
297
|
+
date=prepare_date,
|
|
298
|
+
type=TaskType.PREPARE_SOIL,
|
|
299
|
+
title="Preparar solo para plantio",
|
|
300
|
+
description=f"Preparar solo {field.soil_type.value} para plantio de {cultura}",
|
|
301
|
+
priority=TaskPriority.HIGH,
|
|
302
|
+
source="system",
|
|
303
|
+
weather_sensitive=False
|
|
304
|
+
)
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
# Tarefa de plantio
|
|
308
|
+
tasks.append(
|
|
309
|
+
CalendarTask(
|
|
310
|
+
id=str(uuid.uuid4()),
|
|
311
|
+
crop_plan_id=crop_plan_id,
|
|
312
|
+
date=planting_date,
|
|
313
|
+
type=TaskType.PLANT,
|
|
314
|
+
title=f"Plantar {cultura}",
|
|
315
|
+
description=f"Plantio de {cultura} em {field.area_ha} ha",
|
|
316
|
+
priority=TaskPriority.CRITICAL,
|
|
317
|
+
source="system",
|
|
318
|
+
weather_sensitive=True
|
|
319
|
+
)
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# Tarefas por fase
|
|
323
|
+
for phase in cycle_data["phases"]:
|
|
324
|
+
phase_start = current_date
|
|
325
|
+
phase_end = current_date + timedelta(days=phase["days"])
|
|
326
|
+
|
|
327
|
+
# Distribuir tarefas ao longo da fase
|
|
328
|
+
for i, task_data in enumerate(phase["tasks"]):
|
|
329
|
+
# Espaçar tarefas uniformemente na fase
|
|
330
|
+
task_offset = (phase["days"] // (len(phase["tasks"]) + 1)) * (i + 1)
|
|
331
|
+
task_date = phase_start + timedelta(days=task_offset)
|
|
332
|
+
|
|
333
|
+
tasks.append(
|
|
334
|
+
CalendarTask(
|
|
335
|
+
id=str(uuid.uuid4()),
|
|
336
|
+
crop_plan_id=crop_plan_id,
|
|
337
|
+
date=task_date,
|
|
338
|
+
type=TaskType[task_data["type"].upper()],
|
|
339
|
+
title=task_data["title"],
|
|
340
|
+
description=f"{task_data['title']} - Fase: {phase['description']}",
|
|
341
|
+
priority=TaskPriority[task_data["priority"].upper()],
|
|
342
|
+
source="system",
|
|
343
|
+
weather_sensitive=task_data["weather_sensitive"]
|
|
344
|
+
)
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
current_date = phase_end
|
|
348
|
+
|
|
349
|
+
# Tarefa de colheita
|
|
350
|
+
tasks.append(
|
|
351
|
+
CalendarTask(
|
|
352
|
+
id=str(uuid.uuid4()),
|
|
353
|
+
crop_plan_id=crop_plan_id,
|
|
354
|
+
date=estimated_harvest_date,
|
|
355
|
+
type=TaskType.HARVEST,
|
|
356
|
+
title=f"Colher {cultura}",
|
|
357
|
+
description=f"Colheita de {cultura} - Janela de {cycle_data['harvest_window_days']} dias",
|
|
358
|
+
priority=TaskPriority.CRITICAL,
|
|
359
|
+
source="system",
|
|
360
|
+
weather_sensitive=True
|
|
361
|
+
)
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
# Ordenar tarefas por data
|
|
365
|
+
tasks.sort(key=lambda t: t.date)
|
|
366
|
+
|
|
367
|
+
# Montar resposta
|
|
368
|
+
return {
|
|
369
|
+
"cultura": cultura,
|
|
370
|
+
"planting_date": planting_date.isoformat(),
|
|
371
|
+
"estimated_harvest_date": estimated_harvest_date.isoformat(),
|
|
372
|
+
"cycle_days": cycle_data["cycle_days"],
|
|
373
|
+
"field": field.to_dict(),
|
|
374
|
+
"crop_plan_id": crop_plan_id,
|
|
375
|
+
"cycle_info": {
|
|
376
|
+
"optimal_temp_min": cycle_data["optimal_temp_min"],
|
|
377
|
+
"optimal_temp_max": cycle_data["optimal_temp_max"],
|
|
378
|
+
"critical_water_phases": cycle_data["critical_water_phases"],
|
|
379
|
+
"harvest_window_days": cycle_data["harvest_window_days"],
|
|
380
|
+
"phases": [
|
|
381
|
+
{
|
|
382
|
+
"name": phase["name"],
|
|
383
|
+
"days": phase["days"],
|
|
384
|
+
"description": phase["description"],
|
|
385
|
+
"critical_water": phase["critical_water"]
|
|
386
|
+
}
|
|
387
|
+
for phase in cycle_data["phases"]
|
|
388
|
+
]
|
|
389
|
+
},
|
|
390
|
+
"tasks": [task.to_dict() for task in tasks],
|
|
391
|
+
"total_tasks": len(tasks),
|
|
392
|
+
"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)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def get_culturas_disponiveis() -> List[str]:
|
|
398
|
+
"""Retorna lista de culturas disponíveis no sistema"""
|
|
399
|
+
return list(CROP_CYCLES.keys())
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def get_cultura_info(cultura: str) -> Optional[Dict]:
|
|
403
|
+
"""
|
|
404
|
+
Retorna informações resumidas de uma cultura.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
cultura: Nome da cultura
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
Dict com informações ou None se não encontrada
|
|
411
|
+
"""
|
|
412
|
+
if cultura not in CROP_CYCLES:
|
|
413
|
+
return None
|
|
414
|
+
|
|
415
|
+
cycle_data = CROP_CYCLES[cultura]
|
|
416
|
+
|
|
417
|
+
return {
|
|
418
|
+
"cultura": cultura,
|
|
419
|
+
"cycle_days": cycle_data["cycle_days"],
|
|
420
|
+
"optimal_temp_min": cycle_data["optimal_temp_min"],
|
|
421
|
+
"optimal_temp_max": cycle_data["optimal_temp_max"],
|
|
422
|
+
"critical_water_phases": cycle_data["critical_water_phases"],
|
|
423
|
+
"harvest_window_days": cycle_data["harvest_window_days"],
|
|
424
|
+
"total_phases": len(cycle_data["phases"]),
|
|
425
|
+
"phases_names": [phase["name"] for phase in cycle_data["phases"]]
|
|
426
|
+
}
|
|
@@ -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
|