-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapi_web.py
More file actions
323 lines (271 loc) · 10.8 KB
/
Copy pathapi_web.py
File metadata and controls
323 lines (271 loc) · 10.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
"""
API Python expuesta a la interfaz web vía pywebview (OperaMar).
Puente JS ↔ motor_web: datos ERA5, análisis de ventanas operacionales y
exportación Excel/CSV/informe.
"""
import json
import queue
import threading
from pathlib import Path
import webview
import io_era5
import motor_web
import sistema
class Api:
"""Puente JS ↔ motor de OperaMar."""
def __init__(self):
self._window = None
self._busy = False
self._lock = threading.Lock()
self._eventos = queue.Queue()
def set_window(self, window):
self._window = window
# ------------------------------------------------------------------ eventos
def _emit(self, event, data):
"""Encola evento para el hilo GUI (poll_eventos desde JS)."""
self._eventos.put({"event": event, "data": data})
def poll_eventos(self):
"""Drena la cola de eventos hacia la UI (llamar periódicamente desde JS)."""
enviados = 0
while self._window:
try:
item = self._eventos.get_nowait()
except queue.Empty:
break
payload = json.dumps(item, ensure_ascii=False)
try:
self._window.evaluate_js(f"window.dispatchPyEvent({payload})")
enviados += 1
except Exception:
self._eventos.put(item)
break
return {"ok": True, "n": enviados}
def _parse_json(self, val, etiqueta="payload"):
if isinstance(val, dict):
return val
if isinstance(val, list):
return val
try:
return json.loads(val)
except (json.JSONDecodeError, TypeError) as exc:
raise ValueError(f"{etiqueta} JSON inválido.") from exc
def _error_tarea(self, exc):
"""Mensaje seguro para la UI (sin rutas ni trazas internas)."""
if isinstance(exc, ValueError):
return str(exc)
return exc.__class__.__name__
def _run_task(self, task_id, func):
with self._lock:
if self._busy:
return {"ok": False, "error": "Ya hay una tarea en curso."}
self._busy = True
def worker():
try:
result = func()
self._emit("task_done", {"id": task_id, "ok": True, "result": result})
except Exception as exc:
self._emit("task_done", {
"id": task_id, "ok": False,
"error": self._error_tarea(exc),
})
finally:
with self._lock:
self._busy = False
self._emit("task_start", {"id": task_id})
threading.Thread(target=worker, daemon=True).start()
return {"ok": True}
def _ventana(self):
return self._window or webview.active_window()
# ------------------------------------------------------------------ diálogos
def elegir_archivo(self, tipo="oleaje"):
win = self._ventana()
if not win:
return None
mapa = {
"oleaje": ("Datos marítimos (*.nc;*.mat;*.csv)",),
"serie": ("Serie (*.nc;*.mat;*.csv)",),
}
tipos = mapa.get(tipo, mapa["oleaje"]) + ("Todos (*.*)",)
r = win.create_file_dialog(webview.OPEN_DIALOG, file_types=tipos)
if r:
motor_web.guardar_config_carpeta("ultima_carpeta_datos", Path(r[0]).parent)
return r[0]
return None
def elegir_carpeta(self, tipo="datos"):
win = self._ventana()
if not win:
return None
inicial = motor_web.obtener_config_carpeta("ultima_carpeta_datos")
r = win.create_file_dialog(webview.FOLDER_DIALOG, directory=inicial or None)
if r:
motor_web.guardar_config_carpeta("ultima_carpeta_datos", r[0])
return r[0]
return None
def abrir_en_explorador(self, ruta):
try:
p = motor_web._ruta_usuario(ruta, "Ruta", debe_existir=True)
except ValueError as e:
return {"ok": False, "error": str(e)}
try:
if p.is_file():
sistema.abrir_carpeta(p.parent)
else:
sistema.abrir_carpeta(p)
except Exception as e:
return {"ok": False, "error": str(e)}
return {"ok": True}
def abrir_url_externa(self, url):
"""Abre un enlace HTTPS en el navegador del sistema."""
url = (url or "").strip()
if not url.startswith("https://"):
return {"ok": False, "error": "Solo se permiten URLs https."}
try:
import webbrowser
webbrowser.open(url)
except Exception as e:
return {"ok": False, "error": str(e)}
return {"ok": True}
def ruta_existe(self, ruta):
try:
p = motor_web._ruta_usuario(ruta, "Ruta", debe_existir=False)
return p.exists()
except ValueError:
return False
# ------------------------------------------------------------------ datos
def revision_datos(self, ruta):
return motor_web.revision_datos(ruta)
def revision_datos_ventanas(self, ruta):
return motor_web.revision_datos_ventanas(ruta)
def estado_cds_credenciales(self):
return {"ok": True, **io_era5.estado_credenciales_cds()}
def guardar_cds_credenciales(self, url, key=""):
try:
datos = io_era5.guardar_credenciales_cds(url, key or None)
return {"ok": True, **datos}
except ValueError as e:
return {"ok": False, "error": str(e)}
def probar_cds_credenciales(self, url="", key=""):
try:
datos = io_era5.probar_credenciales_cds(
url or None, key or None)
return {"ok": True, **datos}
except ValueError as e:
return {"ok": False, "error": str(e)}
def descargar_era5(self, lat, lon, inicio, fin, viento=True, espectro=False):
if not io_era5.leer_credenciales_cds():
return {"ok": False, "error": "Faltan credenciales ERA5"}
try:
motor_web._validar_era5(lat, lon, inicio, fin)
except ValueError as e:
return {"ok": False, "error": str(e)}
def log(msg):
self._emit("log", {"msg": msg})
def trabajo():
return motor_web.descargar_era5(
lat, lon, inicio, fin, con_viento=True, log_fn=log)
return self._run_task("era5", trabajo)
def listar_recientes(self):
return {"ok": True, "items": motor_web.listar_recientes()}
def listar_cache_era5(self):
return {"ok": True, "items": motor_web.listar_cache_era5()}
def eliminar_cache_era5(self, carpeta):
try:
return motor_web.eliminar_cache_era5(carpeta)
except ValueError as e:
return {"ok": False, "error": str(e)}
def guardar_preferencias(self, prefs_json):
try:
prefs = self._parse_json(prefs_json, "preferencias")
except ValueError as e:
return {"ok": False, "error": str(e)}
motor_web.guardar_preferencias(prefs)
return {"ok": True}
def obtener_preferencias(self):
return {"ok": True, "prefs": motor_web.obtener_preferencias()}
def preview_archivo(self, ruta):
try:
img = motor_web.preview_archivo(ruta)
return {"ok": True, "img": img}
except ValueError as e:
return {"ok": False, "error": str(e)}
def info_aplicacion(self):
return {"ok": True, **motor_web.info_aplicacion()}
def guardar_sesion_wizard(self, wizard, step, ctx_json):
try:
ctx = self._parse_json(ctx_json, "contexto wizard")
except ValueError as e:
return {"ok": False, "error": str(e)}
motor_web.guardar_sesion_wizard(wizard, int(step), ctx)
return {"ok": True}
def cargar_sesion_wizard(self):
ses = motor_web.cargar_sesion_wizard()
return {"ok": True, "sesion": ses}
def limpiar_sesion_wizard(self):
motor_web.limpiar_sesion_wizard()
return {"ok": True}
def abrir_archivo(self, ruta):
try:
p = motor_web._ruta_usuario(ruta, "Archivo", debe_existir=True)
except ValueError as e:
return {"ok": False, "error": str(e)}
try:
sistema.abrir_archivo(p)
except Exception as e:
return {"ok": False, "error": str(e)}
return {"ok": True}
def _ocupado(self):
"""Dict de error si hay una tarea en hilo corriendo; None si libre."""
with self._lock:
if self._busy:
return {"ok": False,
"error": "Espera a que termine la tarea en curso."}
return None
# ---------------------------------------------------------- ventanas (F4)
def listar_faenas(self):
return {"ok": True, **motor_web.listar_faenas()}
def analizar_faenas(self, ruta, faenas_json):
try:
faenas = self._parse_json(faenas_json, "faenas")
except ValueError as e:
return {"ok": False, "error": str(e)}
if not isinstance(faenas, list) or not faenas:
return {"ok": False, "error": "Selecciona al menos una faena."}
try:
motor_web._ruta_usuario(ruta, "Archivo de datos", debe_existir=True)
except ValueError as e:
return {"ok": False, "error": str(e)}
def log(msg):
self._emit("log", {"msg": msg})
def trabajo():
return motor_web.analizar_faenas(ruta, faenas, log_fn=log)
return self._run_task("analizar_faenas", trabajo)
def exportar_excel_ventanas(self, ruta, faena_json):
ocupado = self._ocupado()
if ocupado:
return ocupado
try:
faena = self._parse_json(faena_json, "faena")
return {"ok": True, **motor_web.exportar_excel_ventanas(ruta, faena)}
except (ValueError, TypeError) as e:
return {"ok": False, "error": str(e)}
def exportar_csv_serie(self, ruta, faena_json):
ocupado = self._ocupado()
if ocupado:
return ocupado
try:
faena = self._parse_json(faena_json, "faena")
return {"ok": True, **motor_web.exportar_csv_serie(ruta, faena)}
except (ValueError, TypeError) as e:
return {"ok": False, "error": str(e)}
def exportar_informe(self, ruta, faena_json, proyecto="", autor=""):
ocupado = self._ocupado()
if ocupado:
return ocupado
try:
faena = self._parse_json(faena_json, "faena")
return {"ok": True, **motor_web.exportar_informe_ventanas(
ruta, faena, proyecto=proyecto or "", autor=autor or "")}
except (ValueError, TypeError) as e:
return {"ok": False, "error": str(e)}
def ruta_ui():
return Path(__file__).resolve().parent / "ui" / "index.html"