Skip to content

Commit 5d6d913

Browse files
SudipSinhaclaude
andcommitted
feat(datasource): implement get_dataframe_by_tag for drift metric reference data
All drift metric endpoints call data_source.get_dataframe_by_tag() to retrieve reference data filtered by tag (e.g. "TRAINING"), but the method did not exist on DataSource, causing AttributeError and HTTP 500 for every drift computation. Also converts metadata_names to list before calling .index() since column_names() returns numpy arrays which lack .index(). Test mocks use numpy arrays to match the real return type. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Sudip Sinha <Sudip.Sinha@RedHat.com>
1 parent eeca7e6 commit 5d6d913

2 files changed

Lines changed: 148 additions & 0 deletions

File tree

src/service/data/datasources/data_source.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,58 @@ async def get_organic_dataframe(
171171

172172
return df
173173

174+
async def get_dataframe_by_tag(self, model_id: str, tag: str) -> pd.DataFrame:
175+
"""Get a dataframe filtered to rows matching a specific tag.
176+
177+
Args:
178+
model_id: The model ID
179+
tag: The tag value to filter by (e.g. "TRAINING")
180+
181+
Returns:
182+
A pandas DataFrame containing only rows whose tags list includes the tag
183+
184+
"""
185+
try:
186+
model_data = ModelData(model_id)
187+
input_data, _, metadata = await model_data.data()
188+
input_names, _, metadata_names = await model_data.column_names()
189+
190+
if metadata is None or input_data is None:
191+
return pd.DataFrame()
192+
193+
tags_col = (
194+
list(metadata_names).index("tags") if "tags" in metadata_names else -1
195+
)
196+
if tags_col < 0:
197+
return pd.DataFrame()
198+
199+
mask = [
200+
tag in (row[tags_col] if isinstance(row[tags_col], list) else [])
201+
for row in metadata
202+
]
203+
filtered_input = input_data[mask]
204+
205+
df_data: dict[str, object] = {}
206+
for i, col_name in enumerate(input_names):
207+
if (
208+
len(filtered_input.shape) == ARRAY_DIM_2D
209+
and i < filtered_input.shape[1]
210+
):
211+
df_data[col_name] = filtered_input[:, i]
212+
elif len(filtered_input.shape) == 1 and i == 0:
213+
df_data[col_name] = filtered_input
214+
215+
return pd.DataFrame(df_data)
216+
217+
except Exception as e: # Broad catch intentional: dataframe creation involves dynamic storage operations
218+
logger.exception(
219+
"Error creating dataframe by tag for model=%s, tag=%s",
220+
model_id,
221+
tag,
222+
)
223+
msg = f"Error creating dataframe by tag for model={model_id}: {e!s}"
224+
raise DataframeCreateError(msg) from e
225+
174226
# METADATA READS
175227

176228
async def get_metadata(self, model_id: str) -> StorageMetadata:

tests/service/data/datasources/test_datasource.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,3 +428,99 @@ async def test_batch_size_calculation_with_limited_data(
428428
call_args = mock_model_data.data.call_args[1]
429429
assert call_args["start_row"] == 0 # Should start from beginning
430430
assert call_args["n_rows"] == EXPECTED_AVAILABLE_ROWS
431+
432+
433+
class TestGetDataframeByTag:
434+
"""Tests for DataSource.get_dataframe_by_tag."""
435+
436+
@pytest.fixture
437+
def data_source(self) -> DataSource:
438+
"""Create a DataSource instance."""
439+
return DataSource()
440+
441+
@patch("src.service.data.datasources.data_source.ModelData")
442+
@pytest.mark.asyncio
443+
async def test_returns_only_matching_rows(
444+
self, mock_model_data_class: Mock, data_source: DataSource
445+
) -> None:
446+
"""Rows tagged TRAINING are returned; unlabeled rows are excluded."""
447+
mock = Mock(spec=ModelData)
448+
mock.column_names = AsyncMock(
449+
return_value=(
450+
np.array(["feature1", "feature2"]),
451+
np.array(["output"]),
452+
np.array(["id", "iso_time", "unix_timestamp", "tags"]),
453+
),
454+
)
455+
mock.data = AsyncMock(
456+
return_value=(
457+
np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]),
458+
np.array([[0.0], [1.0], [0.0]]),
459+
np.array(
460+
[
461+
["id_0", "t0", 0.0, ["TRAINING"]],
462+
["id_1", "t1", 1.0, ["unlabeled"]],
463+
["id_2", "t2", 2.0, ["TRAINING"]],
464+
],
465+
dtype=object,
466+
),
467+
),
468+
)
469+
mock_model_data_class.return_value = mock
470+
471+
df = await data_source.get_dataframe_by_tag("test-model", "TRAINING")
472+
473+
assert len(df) == 2 # noqa: PLR2004
474+
assert list(df.columns) == ["feature1", "feature2"]
475+
assert df["feature1"].tolist() == [1.0, 5.0]
476+
assert df["feature2"].tolist() == [2.0, 6.0]
477+
478+
@patch("src.service.data.datasources.data_source.ModelData")
479+
@pytest.mark.asyncio
480+
async def test_returns_empty_for_nonexistent_tag(
481+
self, mock_model_data_class: Mock, data_source: DataSource
482+
) -> None:
483+
"""Non-existent tag returns empty DataFrame."""
484+
mock = Mock(spec=ModelData)
485+
mock.column_names = AsyncMock(
486+
return_value=(
487+
np.array(["feature1"]),
488+
np.array(["output"]),
489+
np.array(["id", "iso_time", "unix_timestamp", "tags"]),
490+
),
491+
)
492+
mock.data = AsyncMock(
493+
return_value=(
494+
np.array([[1.0], [2.0]]),
495+
np.array([[0.0], [1.0]]),
496+
np.array(
497+
[
498+
["id_0", "t0", 0.0, ["unlabeled"]],
499+
["id_1", "t1", 1.0, ["unlabeled"]],
500+
],
501+
dtype=object,
502+
),
503+
),
504+
)
505+
mock_model_data_class.return_value = mock
506+
507+
df = await data_source.get_dataframe_by_tag("test-model", "TRAINING")
508+
509+
assert len(df) == 0
510+
511+
@patch("src.service.data.datasources.data_source.ModelData")
512+
@pytest.mark.asyncio
513+
async def test_returns_empty_when_no_data(
514+
self, mock_model_data_class: Mock, data_source: DataSource
515+
) -> None:
516+
"""Missing data returns empty DataFrame."""
517+
mock = Mock(spec=ModelData)
518+
mock.column_names = AsyncMock(
519+
return_value=(np.array([]), np.array([]), np.array([])),
520+
)
521+
mock.data = AsyncMock(return_value=(None, None, None))
522+
mock_model_data_class.return_value = mock
523+
524+
df = await data_source.get_dataframe_by_tag("test-model", "TRAINING")
525+
526+
assert len(df) == 0

0 commit comments

Comments
 (0)