From 1b7fe584acf1c5174f84e38694f3690dae340a2b Mon Sep 17 00:00:00 2001 From: Scott Staniewicz Date: Tue, 19 May 2026 12:55:35 -0400 Subject: [PATCH] fix(download): run burst2stack off a running event loop burst2safe downloads via asyncio.run(), which raises RuntimeError when BurstSearch.download() is called from a thread that already owns a running loop (Jupyter/IPython kernel, jupyter execute). Add a guard that detects a running loop and runs burst2stack in a one-shot worker thread so its asyncio.run() gets a fresh loop; the script/CLI path is unchanged. Co-Authored-By: Claude Opus 4.7 --- src/sweets/download.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/sweets/download.py b/src/sweets/download.py index a5e4b97..d08f681 100644 --- a/src/sweets/download.py +++ b/src/sweets/download.py @@ -29,9 +29,11 @@ from __future__ import annotations +import asyncio +from concurrent.futures import ThreadPoolExecutor from datetime import date, datetime from pathlib import Path -from typing import Any, Literal, Optional +from typing import Any, Callable, Literal, Optional, TypeVar from dateutil.parser import parse as parse_date from dolphin.workflows.config import YamlModel @@ -43,6 +45,27 @@ from ._log import log_runtime +_T = TypeVar("_T") + + +def _call_off_running_loop(fn: Callable[..., _T], *args: Any, **kwargs: Any) -> _T: + """Call `fn` even when this thread already owns a running event loop. + + `burst2safe` downloads via `asyncio.run()`, which raises `RuntimeError` + when invoked from a thread that already has a running loop (a Jupyter or + IPython kernel, `jupyter execute`, etc.). In that case `fn` is run in a + dedicated worker thread so its `asyncio.run()` gets a fresh loop. With no + running loop (the normal script/CLI path) `fn` is called directly. + """ + try: + asyncio.get_running_loop() + except RuntimeError: + return fn(*args, **kwargs) + + with ThreadPoolExecutor(max_workers=1) as pool: + return pool.submit(fn, *args, **kwargs).result() + + FlightDirection = Literal["ASCENDING", "DESCENDING"] @@ -225,7 +248,8 @@ def download(self) -> list[Path]: self.out_dir.mkdir(parents=True, exist_ok=True) logger.info(self.summary()) - result = burst2stack( + result = _call_off_running_loop( + burst2stack, rel_orbit=self.track, start_date=self.start, end_date=self.end,