Files
OpenBB/cli/tests/test_controllers_utils_extract.py
Danglewood 9a30186174 [V5] CLI 2.0 - Pluggable Backends, Non-TTY Default, HTTP Dispatcher, Spec Files + Codegen (#7433)
* stash some changes

* add more robust testing

* mypy

* point PR at V5

* introduce spec file

* codespell

* test fix

* fix workflow environment setup

* fix workflow environment setup

* fix workflow environment setup

* add pyyaml to dependencies

* split lint jobs

* fix workflow environment setup

* fix workflow environment setup

* workflow env setup

* workflow env setup

* clean up code comments

* add auth hook entrypoints

* codespell

* add codegen feature

* codespell

* move _unpack into dispatchers for consistency with codegen packages

* surface nested models in the response

* fix missing coverage in CI

* socrata updates

* test fix

* detect plotly output

* add --include and --exclude flags from generate-extension command

* cap test matrix at python 3.14

* no useless comments

* platform controller command description split

* merge URL overloads from path params

* exclude none and unset from model dump

---------

Co-authored-by: deeleeramone <>
Co-authored-by: Copilot <copilot@github.com>
2026-06-01 19:14:38 +03:00

163 lines
5.5 KiB
Python

"""Tests for SQLiteTable and extract_dataframe in controllers/utils.py."""
import sqlite3
from unittest.mock import Mock
import pandas as pd
import pytest
from openbb_cli.controllers.utils import SQLiteTable, extract_dataframe
@pytest.fixture()
def sample_db(tmp_path):
"""Create a temporary SQLite database with a sample table."""
db_path = str(tmp_path / "test.db")
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
cursor.execute("CREATE TABLE prices (date TEXT, close REAL, volume INTEGER)")
cursor.executemany(
"INSERT INTO prices VALUES (?, ?, ?)",
[
("2024-01-01", 100.0, 1000),
("2024-01-02", 101.5, 1500),
("2024-01-03", 99.0, 1200),
],
)
conn.commit()
conn.close()
return db_path
class TestSQLiteTable:
"""Tests for the SQLiteTable lazy-loading wrapper."""
def test_init_stores_attributes(self, sample_db):
t = SQLiteTable(db_path=sample_db, table_name="prices", row_count=3)
assert t.db_path == sample_db
assert t.table_name == "prices"
assert t.row_count == 3
assert t._cached_df is None
def test_to_dataframe_returns_correct_data(self, sample_db):
t = SQLiteTable(db_path=sample_db, table_name="prices", row_count=3)
df = t.to_dataframe()
assert isinstance(df, pd.DataFrame)
assert len(df) == 3
assert list(df.columns) == ["date", "close", "volume"]
def test_to_dataframe_caches_by_default(self, sample_db):
t = SQLiteTable(db_path=sample_db, table_name="prices", row_count=3)
df1 = t.to_dataframe()
df2 = t.to_dataframe()
assert df1 is df2
def test_to_dataframe_no_cache(self, sample_db):
t = SQLiteTable(db_path=sample_db, table_name="prices", row_count=3)
df1 = t.to_dataframe(use_cache=False)
df2 = t.to_dataframe(use_cache=False)
assert df1 is not df2
assert t._cached_df is None
def test_get_schema(self, sample_db):
t = SQLiteTable(db_path=sample_db, table_name="prices")
schema = t.get_schema()
assert len(schema) == 3
col_names = [row[1] for row in schema]
assert col_names == ["date", "close", "volume"]
def test_query_with_where(self, sample_db):
t = SQLiteTable(db_path=sample_db, table_name="prices")
df = t.query(where="close > 100")
assert len(df) == 1
assert df.iloc[0]["close"] == 101.5
def test_query_with_limit(self, sample_db):
t = SQLiteTable(db_path=sample_db, table_name="prices")
df = t.query(limit=2)
assert len(df) == 2
def test_query_no_filters(self, sample_db):
t = SQLiteTable(db_path=sample_db, table_name="prices")
df = t.query()
assert len(df) == 3
def test_quoted_name_prevents_injection(self, tmp_path):
"""Table names with special chars are safely quoted."""
db_path = str(tmp_path / "special.db")
conn = sqlite3.connect(db_path)
conn.execute('CREATE TABLE "my table" (id INTEGER)')
conn.execute('INSERT INTO "my table" VALUES (1)')
conn.commit()
conn.close()
t = SQLiteTable(db_path=db_path, table_name="my table")
df = t.to_dataframe()
assert len(df) == 1
class TestExtractDataframe:
"""Tests for the extract_dataframe helper."""
def test_obbject_with_list_results(self):
mock_obj = Mock()
mock_obj.model_dump.return_value = {
"results": [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
}
df = extract_dataframe(mock_obj)
assert isinstance(df, pd.DataFrame)
assert len(df) == 2
assert list(df.columns) == ["a", "b"]
def test_obbject_with_dict_results(self):
mock_obj = Mock()
mock_obj.model_dump.return_value = {"results": {"x": 10, "y": 20}}
df = extract_dataframe(mock_obj)
assert isinstance(df, pd.DataFrame)
assert len(df) == 1
def test_obbject_with_dataframe_results(self):
expected = pd.DataFrame({"col": [1, 2, 3]})
mock_obj = Mock()
mock_obj.model_dump.return_value = {"results": expected}
df = extract_dataframe(mock_obj)
pd.testing.assert_frame_equal(df, expected)
def test_obbject_with_none_results(self):
mock_obj = Mock()
mock_obj.model_dump.return_value = {"results": None}
df = extract_dataframe(mock_obj)
assert isinstance(df, pd.DataFrame)
assert df.empty
def test_obbject_with_scalar_results(self):
mock_obj = Mock()
mock_obj.model_dump.return_value = {"results": 42}
df = extract_dataframe(mock_obj)
assert isinstance(df, pd.DataFrame)
assert df.iloc[0]["value"] == 42
def test_sqlite_table_results(self, sample_db):
sqlite_table = SQLiteTable(db_path=sample_db, table_name="prices")
mock_obj = Mock()
mock_obj.model_dump.return_value = {"results": sqlite_table}
df = extract_dataframe(mock_obj)
assert len(df) == 3
def test_non_obbject_passthrough_dict(self):
data = {"a": [1, 2], "b": [3, 4]}
df = extract_dataframe(data)
assert isinstance(df, pd.DataFrame)
assert len(df) == 1
def test_non_obbject_passthrough_list(self):
data = [{"a": 1}, {"a": 2}]
df = extract_dataframe(data)
assert isinstance(df, pd.DataFrame)
assert len(df) == 2
def test_non_obbject_passthrough_dataframe(self):
expected = pd.DataFrame({"z": [5]})
df = extract_dataframe(expected)
pd.testing.assert_frame_equal(df, expected)