Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion libs/garf_core/garf_core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@
# See the License for the specific language governing permissions and
# limitations under the License.

__version__ = '0.0.9'
__version__ = '0.0.10'
62 changes: 57 additions & 5 deletions libs/garf_core/garf_core/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
import warnings
from collections import defaultdict
from collections.abc import MutableSequence, Sequence
from typing import Generator, Literal
from typing import Generator, Literal, get_args

from garf_core import exceptions, parsers, query_editor

Expand Down Expand Up @@ -414,8 +414,8 @@ def from_polars(cls, df: 'pl.DataFrame') -> GarfReport:
import polars as pl
except ImportError as e:
raise ImportError(
'Please install garf-io with Polars support '
'- `pip install garf-io[polars]`'
'Please install garf-core with Polars support '
'- `pip install garf-core[polars]`'
) from e
return cls(
results=df.to_numpy().tolist(), column_names=list(df.schema.keys())
Expand All @@ -438,11 +438,63 @@ def from_pandas(cls, df: 'pd.DataFrame') -> GarfReport:
import pandas as pd
except ImportError as e:
raise ImportError(
'Please install garf-io with Pandas support '
'- `pip install garf-io[pandas]`'
'Please install garf-core with Pandas support '
'- `pip install garf-core[pandas]`'
) from e
return cls(results=df.values.tolist(), column_names=list(df.columns.values))

@classmethod
def from_json(cls, json_str: str) -> GarfReport:
"""Creates a GarfReport object from a JSON string.

Args:
json_str: JSON string representation of the data.

Returns:
Report build from a json string.

Raises:
TypeError: If any value in the JSON data is not a supported type.
ValueError: If `data` is a list but not all dictionaries
have the same keys.
"""
data = json.loads(json_str)

def validate_value(value):
if not isinstance(value, get_args(parsers.ApiRowElement)):
raise TypeError(
f'Unsupported type {type(value)} for value {value}. '
'Expected types: int, float, str, bool, list, or None.'
)
return value

# Case 1: `data` is a dictionary
if isinstance(data, dict):
column_names = list(data.keys())
if not data.values():
results = []
else:
results = [[validate_value(value) for value in data.values()]]

# Case 2: `data` is a list of dictionaries, each representing a row
elif isinstance(data, list):
column_names = list(data[0].keys()) if data else []
for row in data:
if not isinstance(row, dict):
raise TypeError('All elements in the list must be dictionaries.')
if list(row.keys()) != column_names:
raise ValueError(
'All dictionaries must have consistent keys in the same order.'
)
results = [
[validate_value(value) for value in row.values()] for row in data
]
else:
raise TypeError(
'Input JSON must be a dictionary or a list of dictionaries.'
)
return cls(results=results, column_names=column_names)


class GarfRow:
"""Helper class to simplify iteration of GarfReport.
Expand Down
86 changes: 86 additions & 0 deletions libs/garf_core/tests/unit/test_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,92 @@ def test_conversion_from_polars(
)
assert report_from_df == expected_report

def test_from_json_with_single_row_dict_returns_gaarf_report(self):
json_str = '{"ad_group_id": 2, "campaign_id": 1}'
gaarf_report = report.GarfReport.from_json(json_str)
expected_report = report.GarfReport(
results=[[2, 1]], column_names=['ad_group_id', 'campaign_id']
)
assert gaarf_report == expected_report

def test_from_json_with_list_of_dicts_returns_gaarf_report(self):
json_str = (
'[{"ad_group_id": 2, "campaign_id": 1}, {"ad_group_id": 3, '
'"campaign_id": 2}]'
)
gaarf_report = report.GarfReport.from_json(json_str)
expected_report = report.GarfReport(
results=[[2, 1], [3, 2]], column_names=['ad_group_id', 'campaign_id']
)
assert gaarf_report == expected_report

def test_from_json_with_empty_list_returns_empty_report(self):
json_str = '[]'
gaarf_report = report.GarfReport.from_json(json_str)
expected_report = report.GarfReport(results=[], column_names=[])
assert gaarf_report == expected_report

def test_from_json_with_empty_dict_returns_empty_report(self):
json_str = '{}'
gaarf_report = report.GarfReport.from_json(json_str)
expected_report = report.GarfReport(results=[], column_names=[])
assert gaarf_report == expected_report

def test_from_json_with_inconsistent_keys_raises_value_error(self):
json_str = '[{"ad_group_id": 2}, {"campaign_id": 1}]'
with pytest.raises(
ValueError,
match='All dictionaries must have consistent keys in the same order.',
):
report.GarfReport.from_json(json_str)

def test_from_json_with_unsupported_type_in_dict_raises_type_error(self):
json_str = '{"ad_group_id": {"nested": "value"}, "campaign_id": 1}'
with pytest.raises(
TypeError, match=r"Unsupported type <class 'dict'> for value"
):
report.GarfReport.from_json(json_str)

def test_from_json_with_unsupported_type_in_list_raises_type_error(self):
json_str = (
'[{"ad_group_id": 2, "campaign_id": {"ad_group_id": 2, '
'"campaign_id": 1}}]'
)
with pytest.raises(
TypeError,
match=r"Unsupported type <class 'dict'> for value {'ad_group_id': 2, "
r"'campaign_id': 1}. Expected types: int, float, str, bool, list, or "
r'None.',
):
report.GarfReport.from_json(json_str)

def test_from_json_with_inconsistent_column_order_raises_value_error(self):
json_str = (
'[{"ad_group_id": 2, "campaign_id": 1}, {"campaign_id": 2, '
'"ad_group_id": 3}]'
)

with pytest.raises(
ValueError,
match='All dictionaries must have consistent keys in the same order.',
):
report.GarfReport.from_json(json_str)

def test_from_json_with_non_dict_or_list_raises_type_error(self):
json_str = '"invalid_data"'
with pytest.raises(
TypeError,
match='Input JSON must be a dictionary or a list of dictionaries.',
):
report.GarfReport.from_json(json_str)

def test_from_json_with_non_dict_elements_in_list_raises_type_error(self):
json_str = '[{"ad_group_id": 2}, 123]'
with pytest.raises(
TypeError, match='All elements in the list must be dictionaries.'
):
report.GarfReport.from_json(json_str)

def test_convert_report_to_pandas(self, multi_column_report):
expected = pd.DataFrame(
data=[[1, 2], [2, 3], [3, 4]], columns=['campaign_id', 'ad_group_id']
Expand Down
Loading