diff --git a/libs/garf_core/garf_core/__init__.py b/libs/garf_core/garf_core/__init__.py index a2b243f..09af16b 100644 --- a/libs/garf_core/garf_core/__init__.py +++ b/libs/garf_core/garf_core/__init__.py @@ -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' diff --git a/libs/garf_core/garf_core/report.py b/libs/garf_core/garf_core/report.py index 0089e50..57b6fe4 100644 --- a/libs/garf_core/garf_core/report.py +++ b/libs/garf_core/garf_core/report.py @@ -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 @@ -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()) @@ -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. diff --git a/libs/garf_core/tests/unit/test_report.py b/libs/garf_core/tests/unit/test_report.py index 5f34f2d..da05b91 100644 --- a/libs/garf_core/tests/unit/test_report.py +++ b/libs/garf_core/tests/unit/test_report.py @@ -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 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 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']