Skip to content

Commit 5bba9a9

Browse files
psbuildsSuraj Panickarraphael-intugle
authored
feat: auto-load datasets from directory in SemanticModel (#208)
Co-authored-by: Suraj Panickar <surajpanickar@Surajs-MacBook-Pro.local> Co-authored-by: raphael-intugle <raphael.tony@intugle.ai>
1 parent 5cbf391 commit 5bba9a9

2 files changed

Lines changed: 119 additions & 3 deletions

File tree

src/intugle/semantic_model.py

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from intugle.link_predictor.predictor import LinkPredictor
1212
from intugle.semantic_search import SemanticSearch
1313
from intugle.utils.files import update_relationship_file_mtime
14+
from pathlib import Path
1415

1516
if TYPE_CHECKING:
1617
from intugle.adapters.adapter import Adapter
@@ -20,7 +21,7 @@
2021

2122

2223
class SemanticModel:
23-
def __init__(self, data_input: Dict[str, Any] | List[DataSet], domain: str = ""):
24+
def __init__(self, data_input: Dict[str, Any] | List[DataSet] | str, domain: str = ""):
2425
"""
2526
Initialize a SemanticModel to build a semantic layer over your data.
2627
@@ -57,13 +58,15 @@ def __init__(self, data_input: Dict[str, Any] | List[DataSet], domain: str = "")
5758
self.domain = domain
5859
self._semantic_search_initialized = False
5960

60-
if isinstance(data_input, dict):
61+
if isinstance(data_input, str):
62+
self._initialize_from_folder(data_input)
63+
elif isinstance(data_input, dict):
6164
self._initialize_from_dict(data_input)
6265
elif isinstance(data_input, list):
6366
self._initialize_from_list(data_input)
6467
else:
6568
raise TypeError(
66-
"Input must be a dictionary of named dataframes or a list of DataSet objects."
69+
"Input must be a dictionary, a list of DataSet objects, or a folder path string."
6770
)
6871

6972
def _initialize_from_dict(self, data_dict: Dict[str, Any]):
@@ -80,6 +83,56 @@ def _initialize_from_list(self, data_list: List[DataSet]):
8083
"DataSet objects provided in a list must have a 'name' attribute."
8184
)
8285
self.datasets[dataset.name] = dataset
86+
def _initialize_from_folder(self, folder_path: str):
87+
"""
88+
Initialize datasets by scanning a folder (recursively) for supported data files.
89+
Supported formats: CSV, Parquet, Excel.
90+
"""
91+
path = Path(folder_path)
92+
93+
if not path.exists():
94+
raise ValueError(f"Provided path does not exist: {folder_path}")
95+
96+
if not path.is_dir():
97+
raise ValueError(f"Provided path is not a directory: {folder_path}")
98+
99+
extension_mapping = {
100+
".csv": "csv",
101+
".parquet": "parquet",
102+
".xlsx": "xlsx",
103+
}
104+
105+
found = False
106+
107+
for file_path in path.rglob("*"): # Recursive
108+
if not file_path.is_file():
109+
continue
110+
111+
ext = file_path.suffix.lower()
112+
if ext not in extension_mapping:
113+
continue
114+
115+
dataset_name = file_path.stem
116+
117+
if dataset_name in self.datasets:
118+
raise ValueError(
119+
f"Duplicate dataset name '{dataset_name}' found while scanning {folder_path}"
120+
)
121+
122+
config = {
123+
"path": str(file_path.resolve()),
124+
"type": extension_mapping[ext],
125+
}
126+
127+
dataset = DataSet(config, name=dataset_name)
128+
self.datasets[dataset_name] = dataset
129+
found = True
130+
131+
if not found:
132+
raise ValueError(
133+
f"No supported data files (.csv, .parquet, .xlsx) found in directory: {folder_path}"
134+
)
135+
83136

84137
def profile(self, force_recreate: bool = False):
85138
"""
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import pandas as pd
2+
import pytest
3+
4+
from intugle.semantic_model import SemanticModel
5+
6+
7+
def test_initialize_from_folder_loads_supported_files(tmp_path):
8+
"""
9+
Verify that passing a folder path correctly loads supported files.
10+
"""
11+
csv_file = tmp_path / "users.csv"
12+
parquet_file = tmp_path / "orders.parquet"
13+
14+
pd.DataFrame({"id": [1, 2]}).to_csv(csv_file, index=False)
15+
pd.DataFrame({"id": [10, 20]}).to_parquet(parquet_file)
16+
17+
sm = SemanticModel(str(tmp_path))
18+
19+
assert "users" in sm.datasets
20+
assert "orders" in sm.datasets
21+
assert len(sm.datasets) == 2
22+
23+
24+
def test_initialize_from_folder_ignores_unsupported_files(tmp_path):
25+
"""
26+
Verify that unsupported files are ignored.
27+
"""
28+
(tmp_path / "readme.txt").write_text("ignore me")
29+
30+
csv_file = tmp_path / "data.csv"
31+
pd.DataFrame({"x": [1]}).to_csv(csv_file, index=False)
32+
33+
sm = SemanticModel(str(tmp_path))
34+
35+
assert "data" in sm.datasets
36+
assert len(sm.datasets) == 1
37+
38+
39+
def test_initialize_from_folder_extension_mapping(tmp_path):
40+
"""
41+
Verify correct mapping of file extensions to DuckDB types.
42+
"""
43+
csv_file = tmp_path / "a.csv"
44+
parquet_file = tmp_path / "b.parquet"
45+
46+
pd.DataFrame({"x": [1]}).to_csv(csv_file, index=False)
47+
pd.DataFrame({"y": [2]}).to_parquet(parquet_file)
48+
49+
sm = SemanticModel(str(tmp_path))
50+
51+
csv_dataset = sm.datasets["a"]
52+
parquet_dataset = sm.datasets["b"]
53+
54+
assert csv_dataset.data["type"] == "csv"
55+
assert parquet_dataset.data["type"] == "parquet"
56+
57+
58+
def test_initialize_from_folder_empty_directory_raises(tmp_path):
59+
"""
60+
Verify that an empty directory raises a clear error.
61+
"""
62+
with pytest.raises(ValueError):
63+
SemanticModel(str(tmp_path))

0 commit comments

Comments
 (0)