Skip to content

Commit 93d97fc

Browse files
[core] feat: add empty report
1 parent 1e5288b commit 93d97fc

File tree

2 files changed

+91
-21
lines changed

2 files changed

+91
-21
lines changed

libs/garf_core/garf_core/report.py

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -45,25 +45,25 @@ class GarfReport:
4545

4646
def __init__(
4747
self,
48-
results: Sequence[Sequence[parsers.ApiRowElement]],
49-
column_names: Sequence[str],
48+
results: Sequence[Sequence[parsers.ApiRowElement]] | None = None,
49+
column_names: Sequence[str] | None = None,
5050
results_placeholder: Sequence[Sequence[parsers.ApiRowElement]]
5151
| None = None,
52-
query_specification: query_editor.BaseQuerySpecification | None = None,
52+
query_specification: query_editor.BaseQueryElements | None = None,
5353
auto_convert_to_scalars: bool = True,
5454
) -> None:
5555
"""Initializes GarfReport from API response.
5656
5757
Args:
58-
results: Contains data from Ads API in a form of nested list
59-
column_names: Maps in each element in sublist of results to name.
60-
results_placeholder: Optional placeholder values for missing results.
61-
query_specification: Specification used to get data from Ads API.
62-
auto_convert_to_scalars: Whether to simplify slicing operations.
58+
results: Contains data from Ads API in a form of nested list
59+
column_names: Maps in each element in sublist of results to name.
60+
results_placeholder: Optional placeholder values for missing results.
61+
query_specification: Specification used to get data from Ads API.
62+
auto_convert_to_scalars: Whether to simplify slicing operations.
6363
"""
64-
self.results = results
65-
self.column_names = column_names
66-
self._multi_column_report = len(column_names) > 1
64+
self.results = results or []
65+
self.column_names = column_names or []
66+
self._multi_column_report = len(column_names) > 1 if column_names else False
6767
if results_placeholder:
6868
self.results_placeholder = list(results_placeholder)
6969
else:
@@ -238,6 +238,8 @@ def get_value(
238238
Raises:
239239
GarfReportError: If row or column index are out of bounds.
240240
"""
241+
if not self:
242+
raise GarfReportError('Cannot get value from an empty report')
241243
if column_index >= len(self.column_names):
242244
raise GarfReportError(
243245
'Column %d of report is not found; report contains only %d columns.',
@@ -298,6 +300,10 @@ def __getitem__(
298300
Raises:
299301
GarfReportError: When incorrect column_name specified.
300302
"""
303+
if not self:
304+
if isinstance(key, (MutableSequence, str)):
305+
raise GarfReportError(f"Cannot get '{key}' from an empty report")
306+
raise GarfReportError('Cannot get element from an empty report')
301307
if isinstance(key, (MutableSequence, str)):
302308
return self._get_columns_slice(key)
303309
return self._get_rows_slice(key)
@@ -342,6 +348,8 @@ def _get_columns_slice(self, key: str | MutableSequence[str]) -> GarfReport:
342348
Raises:
343349
GarfReportError: When incorrect column_name specified.
344350
"""
351+
if not self:
352+
return self
345353
if isinstance(key, str):
346354
key = [key]
347355
if set(key).issubset(set(self.column_names)):
@@ -356,15 +364,13 @@ def _get_columns_slice(self, key: str | MutableSequence[str]) -> GarfReport:
356364
results.append(rows)
357365
# TODO: propagate placeholders and query specification to new report
358366
return GarfReport(results, key)
359-
non_existing_keys = set(key).intersection(set(self.column_names))
367+
non_existing_keys = set(key).difference(set(self.column_names))
360368
if len(non_existing_keys) > 1:
361-
message = (
362-
f"Columns '{', '.join(list(non_existing_keys))}' "
363-
'cannot be found in the report'
364-
)
365-
message = (
366-
f"Column '{non_existing_keys.pop()}' " 'cannot be found in the report'
367-
)
369+
missing_columns = ', '.join(list(non_existing_keys))
370+
else:
371+
missing_columns = non_existing_keys.pop()
372+
373+
message = f"Columns '{missing_columns}' cannot be found in the report"
368374
raise GarfReportError(message)
369375

370376
def __eq__(self, other) -> bool:

libs/garf_core/tests/unit/test_report.py

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@
2323
from garf_core import report
2424

2525

26+
@pytest.fixture
27+
def empty_report():
28+
return report.GarfReport()
29+
30+
2631
@pytest.fixture
2732
def single_element_report():
2833
return report.GarfReport(results=[[1]], column_names=['campaign_id'])
@@ -45,6 +50,9 @@ def multi_column_report():
4550

4651
class TestGarfReport:
4752
class TestGarfReportIteration:
53+
def test_empty_report_returns_empty_list(self, empty_report):
54+
assert list(empty_report) == []
55+
4856
def test_single_element_report_returns_garf_row(
4957
self, single_element_report
5058
):
@@ -118,6 +126,9 @@ def test_hasattr_return_false_for_missing_value(self, multi_column_report):
118126
]
119127

120128
class TestGarfReportMisc:
129+
def test_empty_report_length(self, empty_report):
130+
assert len(empty_report) == 0
131+
121132
def test_get_report_length(self, multi_column_report):
122133
assert len(multi_column_report) == len(multi_column_report.results)
123134

@@ -126,7 +137,14 @@ def test_report_bool(self, single_element_report):
126137
single_element_report.results = []
127138
assert not single_element_report
128139

140+
def test_empty_report_bool_return_false(self, empty_report):
141+
assert not empty_report
142+
129143
class TestGarfReportAddition:
144+
def test_add_two_empty_reports(self, empty_report):
145+
added_report = empty_report + empty_report
146+
assert len(added_report) == 0
147+
130148
def test_add_two_reports(self, multi_column_report):
131149
added_report = multi_column_report + multi_column_report
132150
assert len(added_report) == len(multi_column_report.results) * 2
@@ -150,6 +168,15 @@ def test_add_reports_with_different_columns_raises_exception(
150168
multi_column_report + single_element_report
151169

152170
class TestGarfReportSlicing:
171+
def test_slicing_empty_garf_report_returns_empty_list(
172+
self,
173+
empty_report,
174+
):
175+
with pytest.raises(
176+
report.GarfReportError, match='Cannot get element from an empty report'
177+
):
178+
empty_report[0:2]
179+
153180
def test_slicing_multi_column_garf_report_returns_garf_report(
154181
self,
155182
multi_column_report,
@@ -177,6 +204,16 @@ def test_indexing_multi_column_garf_report_by_multi_index_returns_garf_report(
177204
results=[[1, 2], [2, 3]], column_names=['campaign_id', 'ad_group_id']
178205
)
179206

207+
def test_indexing_empty_garf_report_by_one_column_returns_garf_report(
208+
self,
209+
empty_report,
210+
):
211+
with pytest.raises(
212+
report.GarfReportError,
213+
match="Cannot get 'campaign_id' from an empty report",
214+
):
215+
empty_report['campaign_id']
216+
180217
def test_indexing_multi_column_garf_report_by_one_column_returns_garf_report(
181218
self,
182219
multi_column_report,
@@ -193,11 +230,14 @@ def test_indexing_multi_column_garf_report_by_several_columns_returns_garf_repor
193230
new_report = multi_column_report[['campaign_id', 'ad_group_id']]
194231
assert new_report == multi_column_report
195232

196-
def test_indexing_multi_column_garf_report_by_non_existing_column_raises_exception(
233+
def test_indexing_multi_column_garf_report_by_non_existing_column_raises_garf_report_error(
197234
self,
198235
multi_column_report,
199236
):
200-
with pytest.raises(report.GarfReportError):
237+
with pytest.raises(
238+
report.GarfReportError,
239+
match="Columns 'ad_group' cannot be found in the report",
240+
):
201241
multi_column_report[['campaign_id', 'ad_group']]
202242

203243
def test_slicing_single_column_garf_report_returns_report(
@@ -551,18 +591,39 @@ def test_from_json_with_non_dict_elements_in_list_raises_type_error(self):
551591
):
552592
report.GarfReport.from_json(json_str)
553593

594+
def test_convert_empty_report_to_pandas_returns_empty_dataframe(
595+
self, empty_report
596+
):
597+
expected = pd.DataFrame()
598+
assert empty_report.to_pandas().equals(expected)
599+
554600
def test_convert_report_to_pandas(self, multi_column_report):
555601
expected = pd.DataFrame(
556602
data=[[1, 2], [2, 3], [3, 4]], columns=['campaign_id', 'ad_group_id']
557603
)
558604
assert multi_column_report.to_pandas().equals(expected)
559605

606+
def test_convert_empty_report_to_polars_returns_empty_dataframe(
607+
self, empty_report
608+
):
609+
expected = pl.DataFrame()
610+
assert empty_report.to_polars().equals(expected)
611+
560612
def test_convert_report_to_polars(self, multi_column_report):
561613
expected = pl.DataFrame(
562614
data=[[1, 2], [2, 3], [3, 4]], schema=['campaign_id', 'ad_group_id']
563615
)
564616
assert multi_column_report.to_polars().equals(expected)
565617

618+
def test_get_value_empty_report_raises_garf_report_error(
619+
self,
620+
empty_report,
621+
):
622+
with pytest.raises(
623+
report.GarfReportError, match='Cannot get value from an empty report'
624+
):
625+
empty_report.get_value()
626+
566627
def test_get_value_single_element_report_returns_correct_value(
567628
self,
568629
single_element_report,
@@ -593,6 +654,9 @@ def test_get_value_raises_exception_when_row_index_out_of_bound(
593654
single_element_report.get_value(row_index=1)
594655

595656
class TestGarfReportEquality:
657+
def test_empty_reports_are_equal(self):
658+
assert report.GarfReport() == report.GarfReport()
659+
596660
def test_report_with_different_columns_not_equal(
597661
self, single_element_report, multi_column_report
598662
):

0 commit comments

Comments
 (0)