nakas commited on
Commit
d98ecc1
·
1 Parent(s): 6c5b7a3

Add global download workflow and fix wave variable mapping

Browse files
Files changed (1) hide show
  1. app.py +332 -120
app.py CHANGED
@@ -1,5 +1,7 @@
1
  import os
 
2
  import json
 
3
  import datetime as dt
4
  from typing import List, Optional, Tuple, Union
5
 
@@ -19,11 +21,11 @@ WAVE_MODELS = {
19
  }
20
 
21
  WAVE_VARIABLES = {
22
- "SWH": ("Significant wave height", "m"),
23
- "MWD": ("Mean wave direction", "° (true)"),
24
- "MWP": ("Mean wave period", "s"),
25
- "MP2": ("Mean zero-crossing wave period", "s"),
26
- "PP1D": ("Peak wave period", "s"),
27
  }
28
 
29
 
@@ -80,6 +82,25 @@ def build_members_list(raw: str, model: str) -> Optional[List[int]]:
80
  return members or None
81
 
82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  def decode_json_response(response: requests.Response) -> List[dict]:
84
  """Parse GribStream JSON/NDJSON responses into a list of dictionaries."""
85
  try:
@@ -102,49 +123,74 @@ def decode_json_response(response: requests.Response) -> List[dict]:
102
  def fetch_wave_history(
103
  token: str,
104
  model: str,
105
- variable: str,
106
- latitude: float,
107
- longitude: float,
108
- from_time: str,
109
- until_time: str,
110
  min_horizon: int,
111
  max_horizon: int,
 
 
112
  members: Optional[List[int]] = None,
113
- ) -> Tuple[pd.DataFrame, str]:
114
- """Call GribStream's history endpoint and return a dataframe + alias."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  url = f"{API_BASE_URL}/{model}/history"
116
  headers = {
117
  "Authorization": f"Bearer {token}",
118
  "Content-Type": "application/json",
119
- "Accept": "application/ndjson",
120
  }
121
 
122
- alias = f"{variable}_value"
123
- payload = {
124
- "fromTime": from_time,
125
- "untilTime": until_time,
126
  "minHorizon": int(min_horizon),
127
  "maxHorizon": int(max_horizon),
128
- "coordinates": [{"lat": latitude, "lon": longitude}],
129
- "variables": [{"name": variable, "level": DEFAULT_LEVEL, "alias": alias}],
130
  }
131
 
 
 
 
 
 
 
 
 
 
132
  if members:
133
  payload["members"] = members
134
 
135
- response = requests.post(url, headers=headers, json=payload, timeout=60)
136
  try:
137
  response.raise_for_status()
138
  except requests.HTTPError as exc:
139
  detail = response.text[:500] # Trim to keep message readable.
140
  raise RuntimeError(f"API request failed: {response.status_code} {detail}") from exc
141
 
 
 
 
 
 
142
  records = decode_json_response(response)
143
  if not records:
144
- return pd.DataFrame(), alias
145
 
146
- df = pd.DataFrame(records)
147
- return df, alias
148
 
149
 
150
  def prepare_results(
@@ -175,7 +221,7 @@ def prepare_results(
175
  (df["forecasted_time"] - df["forecasted_at"]).dt.total_seconds() / 3600.0
176
  )
177
 
178
- variable_label, unit = WAVE_VARIABLES.get(variable.upper(), (variable, ""))
179
  label = f"{variable_label} ({unit})" if unit else variable_label
180
 
181
  fig = go.Figure()
@@ -242,7 +288,9 @@ def run_query(
242
  ) -> Tuple[str, Optional[pd.DataFrame], Optional[go.Figure]]:
243
  try:
244
  token = get_token()
245
- variable_name = (custom_variable or variable).strip()
 
 
246
  if not variable_name:
247
  raise ValueError("Select a variable or provide a custom variable name.")
248
 
@@ -262,17 +310,18 @@ def run_query(
262
 
263
  members = build_members_list(raw_members, model)
264
 
265
- df, alias = fetch_wave_history(
 
266
  token=token,
267
  model=model,
268
- variable=variable_name,
269
- latitude=float(latitude),
270
- longitude=float(longitude),
271
  from_time=window_start,
272
  until_time=window_end,
273
  min_horizon=lower,
274
  max_horizon=upper,
275
  members=members,
 
 
276
  )
277
 
278
  display_df, fig, status = prepare_results(df, alias, variable_name)
@@ -284,6 +333,88 @@ def run_query(
284
  return f"❌ {exc}", None, None
285
 
286
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
287
  def default_time_window(hours_back: int = 6, hours_forward: int = 24) -> Tuple[str, str]:
288
  now = dt.datetime.utcnow().replace(minute=0, second=0, microsecond=0)
289
  start = (now - dt.timedelta(hours=hours_back)).isoformat() + "Z"
@@ -291,111 +422,192 @@ def default_time_window(hours_back: int = 6, hours_forward: int = 24) -> Tuple[s
291
  return start, end
292
 
293
 
 
 
 
 
 
294
  def build_interface() -> gr.Blocks:
295
  start_default, end_default = default_time_window()
 
296
 
297
  with gr.Blocks(title="GribStream IFS Wave Explorer") as demo:
298
  gr.Markdown(
299
  """
300
  # ECMWF Wave Data Explorer
301
- Use your GribStream API token (stored as the `GRIB_API` secret) to pull the latest ECMWF wave forecasts.
302
- Select the deterministic or ensemble wave model, choose a variable, and define the time window and horizons you
303
- care about. Results include a table and interactive plot.
304
  """
305
  )
306
 
307
- with gr.Row():
308
- with gr.Column(scale=1, min_width=320):
309
- model_input = gr.Dropdown(
310
- choices=list(WAVE_MODELS.keys()),
311
- value="ifswave",
312
- label="Wave model",
313
- info="ifswave = deterministic, ifswaef = ensemble",
314
- )
315
- variable_input = gr.Dropdown(
316
- choices=list(WAVE_VARIABLES.keys()),
317
- value="SWH",
318
- label="Variable",
319
- info="Common wave parameters: SWH (height), MWD (direction), MWP (period), MP2, PP1D.",
320
- )
321
- custom_variable_input = gr.Textbox(
322
- label="Custom variable (optional)",
323
- placeholder="Override with another parameter name, e.g. swh",
324
- info="Leave blank to use the dropdown selection.",
325
- )
326
- latitude_input = gr.Number(
327
- label="Latitude",
328
- value=32.0,
329
- precision=4,
330
- )
331
- longitude_input = gr.Number(
332
- label="Longitude",
333
- value=-64.0,
334
- precision=4,
335
- )
336
- from_time_input = gr.Textbox(
337
- label="From time (UTC)",
338
- value=start_default,
339
- info="ISO 8601 format, e.g. 2025-10-23T00:00:00Z",
340
- )
341
- until_time_input = gr.Textbox(
342
- label="Until time (UTC)",
343
- value=end_default,
344
- info="ISO 8601 format, must be after the start time.",
345
- )
346
- min_horizon_input = gr.Slider(
347
- label="Minimum forecast horizon (hours)",
348
- value=0,
349
- minimum=0,
350
- maximum=360,
351
- step=1,
352
- )
353
- max_horizon_input = gr.Slider(
354
- label="Maximum forecast horizon (hours)",
355
- value=72,
356
- minimum=0,
357
- maximum=360,
358
- step=1,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
359
  )
360
- members_input = gr.Textbox(
361
- label="Ensemble members (IFS Waef only)",
362
- placeholder="e.g. 0,1,2",
363
- info="Leave blank for control (0). Ignored for deterministic model.",
 
364
  )
365
- submit = gr.Button("Fetch wave data", variant="primary")
366
-
367
- with gr.Column(scale=2):
368
- status_output = gr.Markdown("Results will appear here once you hit **Fetch**.")
369
- table_output = gr.Dataframe(
370
- headers=[
371
- "valid_time",
372
- "forecast_issue_time",
373
- "lead_time_hours",
374
- "value",
375
- "lat",
376
- "lon",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
377
  ],
378
- interactive=False,
379
- wrap=False,
380
  )
381
- chart_output = gr.Plot(show_label=False)
382
-
383
- submit.click(
384
- fn=run_query,
385
- inputs=[
386
- model_input,
387
- variable_input,
388
- custom_variable_input,
389
- latitude_input,
390
- longitude_input,
391
- from_time_input,
392
- until_time_input,
393
- min_horizon_input,
394
- max_horizon_input,
395
- members_input,
396
- ],
397
- outputs=[status_output, table_output, chart_output],
398
- )
399
 
400
  demo.queue()
401
  return demo
 
1
  import os
2
+ import io
3
  import json
4
+ import tempfile
5
  import datetime as dt
6
  from typing import List, Optional, Tuple, Union
7
 
 
21
  }
22
 
23
  WAVE_VARIABLES = {
24
+ "swh": ("Significant wave height", "m"),
25
+ "mwd": ("Mean wave direction", "° (true)"),
26
+ "mwp": ("Mean wave period", "s"),
27
+ "mp2": ("Mean zero-crossing wave period", "s"),
28
+ "pp1d": ("Peak wave period", "s"),
29
  }
30
 
31
 
 
82
  return members or None
83
 
84
 
85
+ def make_alias(name: str) -> str:
86
+ """Create a lowercase alias compatible with the API response."""
87
+ cleaned = "".join(ch.lower() if ch.isalnum() else "_" for ch in name)
88
+ cleaned = "_".join(part for part in cleaned.split("_") if part)
89
+ return cleaned or "value"
90
+
91
+
92
+ def parse_variable_list(raw: str) -> List[str]:
93
+ """Split a comma/newline separated list of variable names."""
94
+ if not raw:
95
+ return []
96
+ parts = []
97
+ for chunk in raw.replace("\n", ",").split(","):
98
+ chunk = chunk.strip()
99
+ if chunk:
100
+ parts.append(chunk)
101
+ return parts
102
+
103
+
104
  def decode_json_response(response: requests.Response) -> List[dict]:
105
  """Parse GribStream JSON/NDJSON responses into a list of dictionaries."""
106
  try:
 
123
  def fetch_wave_history(
124
  token: str,
125
  model: str,
126
+ variables: List[dict],
127
+ *,
128
+ from_time: Optional[str] = None,
129
+ until_time: Optional[str] = None,
130
+ times_list: Optional[List[str]] = None,
131
  min_horizon: int,
132
  max_horizon: int,
133
+ coordinates: Optional[List[dict]] = None,
134
+ grid: Optional[dict] = None,
135
  members: Optional[List[int]] = None,
136
+ accept: str = "application/ndjson",
137
+ timeout: int = 120,
138
+ ) -> pd.DataFrame:
139
+ """Call GribStream's history endpoint and return a dataframe."""
140
+ if not variables:
141
+ raise ValueError("At least one variable must be specified.")
142
+
143
+ if not coordinates and not grid:
144
+ raise ValueError("Provide either coordinates or a grid definition.")
145
+
146
+ if from_time and until_time:
147
+ if until_time <= from_time:
148
+ raise ValueError("until_time must be after from_time.")
149
+ elif not times_list:
150
+ raise ValueError("Provide either a time range or an explicit times list.")
151
+
152
  url = f"{API_BASE_URL}/{model}/history"
153
  headers = {
154
  "Authorization": f"Bearer {token}",
155
  "Content-Type": "application/json",
156
+ "Accept": accept,
157
  }
158
 
159
+ payload: dict = {
 
 
 
160
  "minHorizon": int(min_horizon),
161
  "maxHorizon": int(max_horizon),
162
+ "variables": variables,
 
163
  }
164
 
165
+ if from_time and until_time:
166
+ payload["fromTime"] = from_time
167
+ payload["untilTime"] = until_time
168
+ if times_list:
169
+ payload["timesList"] = times_list
170
+ if coordinates:
171
+ payload["coordinates"] = coordinates
172
+ if grid:
173
+ payload["grid"] = grid
174
  if members:
175
  payload["members"] = members
176
 
177
+ response = requests.post(url, headers=headers, json=payload, timeout=timeout)
178
  try:
179
  response.raise_for_status()
180
  except requests.HTTPError as exc:
181
  detail = response.text[:500] # Trim to keep message readable.
182
  raise RuntimeError(f"API request failed: {response.status_code} {detail}") from exc
183
 
184
+ if accept == "text/csv":
185
+ buffer = io.BytesIO(response.content)
186
+ df = pd.read_csv(buffer)
187
+ return df
188
+
189
  records = decode_json_response(response)
190
  if not records:
191
+ return pd.DataFrame()
192
 
193
+ return pd.DataFrame(records)
 
194
 
195
 
196
  def prepare_results(
 
221
  (df["forecasted_time"] - df["forecasted_at"]).dt.total_seconds() / 3600.0
222
  )
223
 
224
+ variable_label, unit = WAVE_VARIABLES.get(variable.lower(), (variable, ""))
225
  label = f"{variable_label} ({unit})" if unit else variable_label
226
 
227
  fig = go.Figure()
 
288
  ) -> Tuple[str, Optional[pd.DataFrame], Optional[go.Figure]]:
289
  try:
290
  token = get_token()
291
+ dropdown_value = (variable or "").strip()
292
+ custom_value = (custom_variable or "").strip()
293
+ variable_name = custom_value or dropdown_value.lower()
294
  if not variable_name:
295
  raise ValueError("Select a variable or provide a custom variable name.")
296
 
 
310
 
311
  members = build_members_list(raw_members, model)
312
 
313
+ alias = make_alias(variable_name)
314
+ df = fetch_wave_history(
315
  token=token,
316
  model=model,
317
+ variables=[{"name": variable_name, "level": DEFAULT_LEVEL, "alias": alias}],
 
 
318
  from_time=window_start,
319
  until_time=window_end,
320
  min_horizon=lower,
321
  max_horizon=upper,
322
  members=members,
323
+ coordinates=[{"lat": float(latitude), "lon": float(longitude)}],
324
+ accept="application/ndjson",
325
  )
326
 
327
  display_df, fig, status = prepare_results(df, alias, variable_name)
 
333
  return f"❌ {exc}", None, None
334
 
335
 
336
+ def run_global_download(
337
+ model: str,
338
+ variables: List[str],
339
+ custom_variables: str,
340
+ valid_time: str,
341
+ min_horizon: int,
342
+ max_horizon: int,
343
+ grid_step: float,
344
+ raw_members: str,
345
+ ) -> Tuple[str, Optional[pd.DataFrame], Optional[str]]:
346
+ try:
347
+ token = get_token()
348
+
349
+ selected_from_dropdown = [name.lower() for name in (variables or [])]
350
+ custom_list = parse_variable_list(custom_variables)
351
+ variable_names = list(dict.fromkeys([*selected_from_dropdown, *custom_list]))
352
+ if not variable_names:
353
+ raise ValueError("Select at least one variable or enter custom variable names.")
354
+
355
+ valid_iso = to_iso_utc(valid_time, "Valid time")
356
+ lower = int(min(min_horizon, max_horizon))
357
+ upper = int(max(min_horizon, max_horizon))
358
+
359
+ if grid_step <= 0:
360
+ raise ValueError("Grid step must be a positive number of degrees.")
361
+
362
+ members = build_members_list(raw_members, model)
363
+
364
+ variables_payload = [
365
+ {"name": name, "level": DEFAULT_LEVEL, "alias": make_alias(name)}
366
+ for name in variable_names
367
+ ]
368
+
369
+ df = fetch_wave_history(
370
+ token=token,
371
+ model=model,
372
+ variables=variables_payload,
373
+ times_list=[valid_iso],
374
+ min_horizon=lower,
375
+ max_horizon=upper,
376
+ grid={
377
+ "minLatitude": -90,
378
+ "maxLatitude": 90,
379
+ "minLongitude": -180,
380
+ "maxLongitude": 180,
381
+ "step": float(grid_step),
382
+ },
383
+ members=members,
384
+ accept="text/csv",
385
+ timeout=240,
386
+ )
387
+
388
+ if df.empty:
389
+ raise ValueError("No data returned for the requested global configuration.")
390
+
391
+ df = df.copy()
392
+ for col in ("forecasted_time", "forecasted_at"):
393
+ if col in df.columns:
394
+ df[col] = pd.to_datetime(df[col], utc=True, errors="coerce")
395
+
396
+ preview = df.head(500).copy()
397
+ preview_rows = len(preview)
398
+
399
+ tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".csv", mode="w", newline="")
400
+ try:
401
+ df.to_csv(tmp.name, index=False)
402
+ finally:
403
+ tmp.close()
404
+
405
+ status = (
406
+ f"Fetched {len(df)} rows across {len(variable_names)} variable(s) "
407
+ f"for {valid_iso}. Showing the first {preview_rows} rows."
408
+ )
409
+
410
+ return status, preview, tmp.name
411
+
412
+ except ConfigurationError as exc:
413
+ return f"⚠️ {exc}", None, None
414
+ except Exception as exc: # noqa: BLE001
415
+ return f"❌ {exc}", None, None
416
+
417
+
418
  def default_time_window(hours_back: int = 6, hours_forward: int = 24) -> Tuple[str, str]:
419
  now = dt.datetime.utcnow().replace(minute=0, second=0, microsecond=0)
420
  start = (now - dt.timedelta(hours=hours_back)).isoformat() + "Z"
 
422
  return start, end
423
 
424
 
425
+ def default_valid_time(offset_hours: int = 0) -> str:
426
+ now = dt.datetime.utcnow().replace(minute=0, second=0, microsecond=0)
427
+ return (now + dt.timedelta(hours=offset_hours)).isoformat() + "Z"
428
+
429
+
430
  def build_interface() -> gr.Blocks:
431
  start_default, end_default = default_time_window()
432
+ valid_default = default_valid_time()
433
 
434
  with gr.Blocks(title="GribStream IFS Wave Explorer") as demo:
435
  gr.Markdown(
436
  """
437
  # ECMWF Wave Data Explorer
438
+ Use your GribStream API token (stored as the `GRIB_API` secret) to pull ECMWF IFS wave forecasts via GribStream.
439
+ Choose between a point time-series view or a global snapshot download of the latest wave fields.
 
440
  """
441
  )
442
 
443
+ with gr.Tabs():
444
+ with gr.Tab("Point time series"):
445
+ with gr.Row():
446
+ with gr.Column(scale=1, min_width=320):
447
+ series_model_input = gr.Dropdown(
448
+ choices=list(WAVE_MODELS.keys()),
449
+ value="ifswave",
450
+ label="Wave model",
451
+ info="Choose `ifswave` for the deterministic run or `ifswaef` for the ensemble.",
452
+ )
453
+ series_variable_input = gr.Dropdown(
454
+ choices=[code.upper() for code in WAVE_VARIABLES.keys()],
455
+ value="SWH",
456
+ label="Variable",
457
+ info="Wave parameters use ECMWF short names (e.g., SWH height, MWD direction, MWP period).",
458
+ )
459
+ series_custom_variable_input = gr.Textbox(
460
+ label="Custom variable (optional)",
461
+ placeholder="Override with another parameter, e.g. swh",
462
+ info="Leave blank to use the dropdown selection.",
463
+ )
464
+ series_latitude_input = gr.Number(
465
+ label="Latitude",
466
+ value=32.0,
467
+ precision=4,
468
+ )
469
+ series_longitude_input = gr.Number(
470
+ label="Longitude",
471
+ value=-64.0,
472
+ precision=4,
473
+ )
474
+ series_from_time_input = gr.Textbox(
475
+ label="From time (UTC)",
476
+ value=start_default,
477
+ info="ISO 8601 format, e.g. 2025-10-23T00:00:00Z",
478
+ )
479
+ series_until_time_input = gr.Textbox(
480
+ label="Until time (UTC)",
481
+ value=end_default,
482
+ info="ISO 8601 format, must be after the start time.",
483
+ )
484
+ series_min_horizon_input = gr.Slider(
485
+ label="Minimum forecast horizon (hours)",
486
+ value=0,
487
+ minimum=0,
488
+ maximum=360,
489
+ step=1,
490
+ )
491
+ series_max_horizon_input = gr.Slider(
492
+ label="Maximum forecast horizon (hours)",
493
+ value=72,
494
+ minimum=0,
495
+ maximum=360,
496
+ step=1,
497
+ )
498
+ series_members_input = gr.Textbox(
499
+ label="Ensemble members (IFS Waef only)",
500
+ placeholder="e.g. 0,1,2",
501
+ info="Leave blank for control (0). Ignored for deterministic model.",
502
+ )
503
+ series_submit = gr.Button("Fetch time series", variant="primary")
504
+
505
+ with gr.Column(scale=2):
506
+ series_status_output = gr.Markdown("Results will appear here once you hit **Fetch**.")
507
+ series_table_output = gr.Dataframe(
508
+ interactive=False,
509
+ wrap=False,
510
+ )
511
+ series_chart_output = gr.Plot(show_label=False)
512
+
513
+ series_submit.click(
514
+ fn=run_query,
515
+ inputs=[
516
+ series_model_input,
517
+ series_variable_input,
518
+ series_custom_variable_input,
519
+ series_latitude_input,
520
+ series_longitude_input,
521
+ series_from_time_input,
522
+ series_until_time_input,
523
+ series_min_horizon_input,
524
+ series_max_horizon_input,
525
+ series_members_input,
526
+ ],
527
+ outputs=[series_status_output, series_table_output, series_chart_output],
528
  )
529
+
530
+ with gr.Tab("Global snapshot download"):
531
+ gr.Markdown(
532
+ "Fetch the full global grid for a selected valid time, then download it as CSV. "
533
+ "Reduce the grid spacing if you need a lighter file."
534
  )
535
+ with gr.Row():
536
+ with gr.Column(scale=1, min_width=320):
537
+ global_model_input = gr.Dropdown(
538
+ choices=list(WAVE_MODELS.keys()),
539
+ value="ifswave",
540
+ label="Wave model",
541
+ info="`ifswave` deterministic or `ifswaef` ensemble.",
542
+ )
543
+ global_variables_input = gr.CheckboxGroup(
544
+ label="Variables",
545
+ choices=[code.upper() for code in WAVE_VARIABLES.keys()],
546
+ value=["SWH", "MWD", "MWP"],
547
+ info="Select one or more parameters to include in the download.",
548
+ )
549
+ global_custom_variables_input = gr.Textbox(
550
+ label="Additional variables (optional)",
551
+ placeholder="Comma-separated list, e.g. mp2,pp1d",
552
+ info="Use ECMWF short names. Combined with the selection above.",
553
+ )
554
+ global_valid_time_input = gr.Textbox(
555
+ label="Forecast valid time (UTC)",
556
+ value=valid_default,
557
+ info="ISO 8601 format corresponding to the wave field time you need.",
558
+ )
559
+ global_min_horizon_input = gr.Slider(
560
+ label="Minimum forecast horizon (hours)",
561
+ value=0,
562
+ minimum=0,
563
+ maximum=360,
564
+ step=1,
565
+ )
566
+ global_max_horizon_input = gr.Slider(
567
+ label="Maximum forecast horizon (hours)",
568
+ value=24,
569
+ minimum=0,
570
+ maximum=360,
571
+ step=1,
572
+ )
573
+ global_grid_step_input = gr.Slider(
574
+ label="Grid spacing (degrees)",
575
+ value=0.5,
576
+ minimum=0.25,
577
+ maximum=2.0,
578
+ step=0.25,
579
+ )
580
+ global_members_input = gr.Textbox(
581
+ label="Ensemble members (IFS Waef only)",
582
+ placeholder="e.g. 0,1,2,3",
583
+ info="Leave blank for default control member. Ignored for deterministic model.",
584
+ )
585
+ global_submit = gr.Button("Download global snapshot", variant="primary")
586
+
587
+ with gr.Column(scale=2):
588
+ global_status_output = gr.Markdown(
589
+ "The download link and preview will appear here after processing."
590
+ )
591
+ global_preview_output = gr.Dataframe(
592
+ interactive=False,
593
+ wrap=False,
594
+ )
595
+ global_file_output = gr.File(label="Download CSV")
596
+
597
+ global_submit.click(
598
+ fn=run_global_download,
599
+ inputs=[
600
+ global_model_input,
601
+ global_variables_input,
602
+ global_custom_variables_input,
603
+ global_valid_time_input,
604
+ global_min_horizon_input,
605
+ global_max_horizon_input,
606
+ global_grid_step_input,
607
+ global_members_input,
608
  ],
609
+ outputs=[global_status_output, global_preview_output, global_file_output],
 
610
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
611
 
612
  demo.queue()
613
  return demo