Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
39eea3a
Add XArraySQLSource for N-dimensional scientific data
ghostiee-11 Mar 16, 2026
dce1606
Address review feedback: simplify code, add GRIB support and 2D coord…
ghostiee-11 Mar 19, 2026
5a91c7c
Merge origin/main, resolve pyproject.toml conflict
ghostiee-11 Mar 19, 2026
f2a3162
Fix isort: add blank line between stdlib imports in test file
ghostiee-11 Mar 19, 2026
9ed53cd
Address review: drop postgres dialect, add from_dataset, trim deps
ghostiee-11 Mar 20, 2026
f549d86
Address review: reduce nesting, prefer netcdf4 for HDF5, add xarray-g…
ghostiee-11 Mar 23, 2026
68e9d1e
Wire XArraySQLSource into Lumen AI (CLI, upload, SQL agent fixes)
ghostiee-11 Mar 23, 2026
ad250ff
Remove H5 engine detection, drop h5netcdf dep, add xarray-grib extra
ghostiee-11 Mar 24, 2026
fd44709
Merge origin/main, resolve test_ui.py conflict
ghostiee-11 Mar 24, 2026
fcb6316
Skip xarray tests when netCDF4 write backend unavailable in CI
ghostiee-11 Mar 24, 2026
612e589
Move imports to top of file per review
ghostiee-11 Mar 24, 2026
fd3a25a
Lazy-load xarray imports per review (philippjfr)
ghostiee-11 Mar 24, 2026
6788a9d
Merge PR1 lazy imports, inline numpy/xarray per review
ghostiee-11 Mar 24, 2026
56fc483
Merge main into feat/xarray-ai-integration
ghostiee-11 Mar 26, 2026
4136d12
Address review: move imports to top, extract xarray availability util…
ghostiee-11 Mar 26, 2026
c07277e
Fix isort: add blank line between import groups
ghostiee-11 Mar 26, 2026
71b7627
Address review: shared xarray util, remove duplicate logic, fix imports
ghostiee-11 Apr 4, 2026
30af4a5
Merge main; address review: unify xarray extensions, imports on top
ghostiee-11 Apr 17, 2026
67e25c6
Unify xarray extension tuples into single XARRAY_EXTENSIONS
ghostiee-11 Apr 17, 2026
3c85c61
Update test_upload_handler_returns_dict for unified extensions
ghostiee-11 Apr 17, 2026
dfa4689
Drop XARRAY_DOTTED_EXTENSIONS; check suffix against XARRAY_EXTENSIONS…
ghostiee-11 Apr 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions docs/configuration/sources.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ ui.servable()
|--------|---------|
| Files | CSV, Parquet, JSON (local or URL) |
| DuckDB | Local SQL queries on files |
| xarray | N-dimensional scientific data (NetCDF, Zarr, HDF5) |
| Snowflake | Cloud data warehouse |
| BigQuery | Google's data warehouse |
| PostgreSQL | PostgreSQL via SQLAlchemy |
Expand Down Expand Up @@ -173,6 +174,45 @@ source = DuckDBSource(

1. Required for HTTP/S3 access

### xarray for scientific data

Query N-dimensional scientific datasets (NetCDF, Zarr, HDF5) with SQL via Apache DataFusion:

``` bash title="Install dependencies"
pip install lumen[xarray]
```

``` py title="Query a NetCDF file"
from lumen.sources.xarray_sql import XArraySQLSource
import lumen.ai as lmai

source = XArraySQLSource(uri='air_temperature.nc')

ui = lmai.ExplorerUI(data=source)
ui.servable()
```

Each data variable in the dataset becomes a separate SQL table with coordinate columns:

``` py title="Direct SQL queries"
source = XArraySQLSource(uri='air_temperature.nc')
source.get_tables() # ['air']

df = source.execute(
"SELECT lat, lon, AVG(air) as avg_temp "
"FROM air GROUP BY lat, lon"
)
```

**Select specific variables:**

``` py hl_lines="3"
source = XArraySQLSource(
uri='climate_data.nc',
variables=['temperature', 'pressure']
)
```

### Multiple sources

``` py title="Mix sources"
Expand Down
2 changes: 1 addition & 1 deletion lumen/ai/controls/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from ..utils import log_debug
from .progress import Progress

TABLE_EXTENSIONS = ("csv", "parquet", "parq", "json", "xlsx", "geojson", "wkt", "zip")
TABLE_EXTENSIONS = ("csv", "parquet", "parq", "json", "xlsx", "geojson", "wkt", "zip", "nc", "nc4", "h5", "hdf5")
Comment thread
ghostiee-11 marked this conversation as resolved.
Outdated

METADATA_EXTENSIONS = ("md", "txt", "yaml", "yml", "json", "pdf", "docx", "doc", "pptx", "ppt")
METADATA_FILENAME_PATTERNS = ("_metadata", "metadata_", "readme", "schema")
Expand Down
56 changes: 55 additions & 1 deletion lumen/ai/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
)
from panel_splitjs import HSplit, MultiSplit, VSplit

XARRAY_FILE_EXTENSIONS = ('.nc', '.nc4', '.netcdf', '.h5', '.hdf5', '.he5', '.zarr', '.grib', '.grib2', '.grb', '.grb2')
XARRAY_UPLOAD_EXTENSIONS = ('nc', 'nc4', 'h5', 'hdf5')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are constants located between imports?

from lumen.ai.agents.deck_gl import DeckGLAgent

from ..pipeline import Pipeline
Expand Down Expand Up @@ -502,6 +505,40 @@ def __init__(
self._configure_session()
self._render_page()

@staticmethod
def _get_xarray_upload_handlers():
"""Build upload handlers for xarray file formats."""
import atexit
import os

_temp_files: list[str] = []

def _cleanup_temp_files():
for path in _temp_files:
try:
os.unlink(path)
except OSError:
pass

atexit.register(_cleanup_temp_files)

def _handle_xarray_upload(context, file_obj, alias, filename):
try:
from ..sources.xarray_sql import XArraySQLSource
except ImportError:
return None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should make this a util, rather than try/except Import everywhere.


        if not _check_xarray_available():
            raise ImportError(
                "xarray and xarray-sql are required for XArraySQLSource. "
                "Install them with: pip install lumen[xarray]"
            )

import tempfile
ext = filename.rsplit('.', 1)[-1] if '.' in filename else 'nc'
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=f'.{ext}')
file_obj.seek(0)
tmp.write(file_obj.read())
tmp.flush()
tmp.close()
_temp_files.append(tmp.name)
return XArraySQLSource(uri=tmp.name, name=alias)

return {ext: _handle_xarray_upload for ext in XARRAY_UPLOAD_EXTENSIONS}
Comment thread
ahuang11 marked this conversation as resolved.
Outdated

@classmethod
def _resolve_data(
cls, data: DataT | list[DataT] | dict[DataT] | None
Expand Down Expand Up @@ -591,6 +628,19 @@ def _resolve_data(
sources.append(source)
continue

# Handle xarray files (NetCDF, Zarr, HDF5, GRIB)
if src.endswith(XARRAY_FILE_EXTENSIONS):
try:
from ..sources.xarray_sql import XArraySQLSource
except ImportError as e:
raise ImportError(
"xarray and xarray-sql are required to load xarray files. "
"Install them with: pip install lumen[xarray]"
) from e
source = XArraySQLSource(uri=str(Path(src).absolute()))
sources.append(source)
continue

if src.startswith('http'):
remote = True
if src.endswith(('.parq', '.parquet', '.csv', '.json', '.tsv', '.jsonl', '.ndjson')):
Expand Down Expand Up @@ -1216,14 +1266,18 @@ async def _do_sync():
param.parameterized.async_executor(_do_sync)
self._source_catalog.param.watch(_schedule_visibility_change, 'visibility_changed')

# Register xarray upload handlers (if xarray-sql is available)
xarray_handlers = self._get_xarray_upload_handlers()
merged_upload_handlers = {**xarray_handlers, **self.upload_handlers}
Comment thread
ahuang11 marked this conversation as resolved.

# Initialize source controls
self._source_controls = []
control_tabs = []
for control in self.source_controls:
control_kwargs = {
'context': self.context,
'source_catalog': self._source_catalog,
'upload_handlers': self.upload_handlers
'upload_handlers': merged_upload_handlers
}
if control is UploadControls and self.filedropper_kwargs:
control_kwargs['filedropper_kwargs'] = self.filedropper_kwargs
Expand Down
11 changes: 8 additions & 3 deletions lumen/command/ai.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,10 +241,15 @@ def __init__(
continue

path = Path(table_path)
if not path.is_file():
if path.exists():
raise ValueError(f"Table path is not a file: {table_path}")
if not path.exists():
raise FileNotFoundError(f"Table file not found: {table_path}")
if path.is_dir() and not table_path.endswith('.zarr'):
raise ValueError(
f"Table path is a directory: {table_path}. "
"Only .zarr directories are supported."
)
if not path.is_file() and not path.is_dir():
raise ValueError(f"Table path is not a file: {table_path}")

source = self._build_source_code(
tables=tables,
Expand Down
5 changes: 5 additions & 0 deletions lumen/sources/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
from .base import *

try:
from .xarray_sql import XArraySQLSource # noqa: F401
except ImportError:
pass
Loading
Loading